プログラムからUITableViewを行単位でスクロールさせたいときは、以下のようにします。

下へスクロールさせる

NSIndexPath *indexPath = [_tableView indexPathForRowAtPoint:CGPointMake(0, _tableView.contentOffset.y + 1)];
if (indexPath) {
  indexPath = [NSIndexPath indexPathForRow:indexPath.row+1 inSection:indexPath.section];
  [_tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
}

上へスクロールさせる

NSIndexPath *indexPath = [_tableView indexPathForRowAtPoint:CGPointMake(0, _tableView.contentOffset.y + 1)];
if (indexPath && indexPath.row > 0) {
  indexPath = [NSIndexPath indexPathForRow:indexPath.row-1 inSection:indexPath.section];
  [_tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
}

UITableView#indexPathForRowAtPointは指定位置を含むセルのindexPathを返します。UITableView#contentOffsetで、現在のスクロール位置を得られます。

NSIndexPath *indexPath = [_tableView indexPathForRowAtPoint:CGPointMake(0, _tableView.contentOffset.y + 1)];

このようにすることで、現在のスクロール位置すなわち一番上にあるセルを返します。contentOffset.yに+1してあるのは、そうしないと稀に一番上の上にあるセルを取得することがあるからです。正確なことは追いきれてませんが、どうもcontentOffset.yが小数点以下を含むときに、そうなることがあるようです。万全を期して+10くらいしたほうが良いのかもしれません。(もちろんセルの高さが10より大きくない場合、バグになりますが・・・)

ちなみに以下のようにしたときも、同様の理由でスクロールがうまくいかないことがあるようです。

NSArray *indexPathArray = _tableView.indexPathsForVisibleRows;
if (indexPathArray.count > 0) {
  NSIndexPath *indexPath = [indexPathArray objectAtIndex:0];
  indexPath = [NSIndexPath indexPathForRow:indexPath.row + 1 inSection:indexPath.section];
  [_tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
}

UITableView#indexPathsForVisibleRowsは、現在見えているセルの配列を返します。配列の先頭が一番上に見えているセルのはずなのですが、なぜかその1つ上のセルを指していることがあります・・・