web-dev-qa-db-ja.com

iOSで大きくて不格好なUITableViewControllerを回避する方法は?

IOSにMVCパターンを実装するときに問題があります。私はインターネットを検索しましたが、この問題に対するニースの解決策は見つからないようです。

多くのUITableViewControllerの実装はかなり大きいようです。私が見たほとんどの例では、UITableViewController<UITableViewDelegate><UITableViewDataSource>を実装させています。これらの実装は、UITableViewControllerが大きくなる大きな理由です。 1つの解決策は、<UITableViewDelegate><UITableViewDataSource>を実装する個別のクラスを作成することです。もちろん、これらのクラスにはUITableViewControllerへの参照が必要です。このソリューションを使用して欠点はありますか?一般に、デリゲートパターンを使用して、機能を他の「ヘルパー」クラスなどに委任する必要があると思います。この問題を解決する確立された方法はありますか?

モデルに多くの機能やビューを含めたくありません。これはMVCパターンの土台の1つであるため、ロジックは本当にコントローラークラスにあるべきだと思います。しかし、大きな問題は次のとおりです。

MVC実装のコントローラーを管理しやすい小さな部分にどのように分割する必要がありますか?(この場合はiOSのMVCに適用されます)

これを解決するための一般的なパターンがあるかもしれませんが、私はiOSのソリューションを具体的に探しています。この問題を解決するための良いパターンの例を挙げてください。ソリューションが優れている理由を説明してください。

36
Johan Karlsson

UITableViewControllerは、1つのオブジェクトに多くの責任を負わせるため、使用を避けます。したがって、UIViewControllerサブクラスをデータソースとデリゲートから分離します。ビューコントローラーの役割は、テーブルビューを準備し、データを含むデータソースを作成し、それらを結合することです。 tableviewの表現方法の変更は、ビューコントローラーを変更せずに行うことができます。実際、同じビューコントローラーを、すべてこのパターンに従う複数のデータソースに使用できます。同様に、アプリのワークフローを変更すると、テーブルがどうなるかを気にすることなく、ビューコントローラーが変更されます。

UITableViewDataSourceUITableViewDelegateプロトコルを異なるオブジェクトに分離しようとしましたが、デリゲートのほとんどすべてのメソッドがデータソース(例: 、デリゲートは、選択された行によって表されるオブジェクトを知る必要があります)。したがって、データソースとデリゲートの両方である単一のオブジェクトができあがります。このオブジェクトは常にメソッド-(id)tableView: (UITableView *)tableView representedObjectAtIndexPath: (NSIndexPath *)indexPathを提供します。このメソッドは、データソースとデリゲートの両方の側面で、何が処理されているかを知る必要があります。

それが私の懸念の「レベル0」の分離です。同じテーブルビューで異なる種類のオブジェクトを表す必要がある場合、レベル1が使用されます。例として、連絡先アプリを作成する必要があったと想像してください。1つの連絡先について、電話番号を表す行、住所を表す行、メールアドレスを表す行などがあるとします。私はこのアプローチを避けたいです:

_- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  if ([object isKindOfClass: [PhoneNumber class]]) {
    //configure phone number cell
  }
  else if …
}
_

これまでに2つの解決策が提示されています。 1つは、セレクターを動的に作成することです。

_- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  NSString *cellSelectorName = [NSString stringWithFormat: @"tableView:cellFor%@AtIndexPath:", [object class]];
  SEL cellSelector = NSSelectorFromString(cellSelectorName);
  return [self performSelector: cellSelector withObject: tableView withObject: object];
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForPhoneNumberAtIndexPath: (NSIndexPath *)indexPath {
  // configure phone number cell
}
_

このアプローチでは、新しい型をサポートするためにエピックif()ツリーを編集する必要はありません。新しいクラスをサポートするメソッドを追加するだけです。このテーブルビューがこれらのオブジェクトを表す必要がある、または特別な方法で表示する必要がある唯一のビューである場合、これは優れたアプローチです。同じオブジェクトが異なるデータソースの異なるテーブルで表される場合、セル作成メソッドがデータソース間で共有する必要があるため、このアプローチは機能しません。これらのメソッドを提供する共通のスーパークラスを定義するか、これを行うことができます。

_@interface PhoneNumber (TableViewRepresentation)

- (UITableViewCell *)tableView: (UITableView *)tableView representationAsCellForRowAtIndexPath: (NSIndexPath *)indexPath;

@end

@interface Address (TableViewRepresentation)

//more of the same…

@end
_

次に、データソースクラスで:

_- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
  id object = [self tableView: tableView representedObjectAtIndexPath: indexPath];
  return [object tableView: tableView representationAsCellForRowAtIndexPath: indexPath];
}
_

これは、電話番号や住所などを表示する必要があるanyデータソースが何でもテーブルビューセルのオブジェクトが表されます。データソース自体は、表示されているオブジェクトについて何も知る必要がなくなりました。

「でも待ってください」という架空の対話者の挿入が聞こえます。「それはbreakMVCではありませんか?ビューの詳細をモデルクラスに入れていませんか? 」

いいえ、MVCを壊しません。この場合のカテゴリは Decorator の実装であると考えることができます。したがって、PhoneNumberはモデルクラスですが、PhoneNumber(TableViewRepresentation)はビューのカテゴリです。データソース(コントローラーオブジェクト)はモデルとビューの間を仲介するので、MVCアーキテクチャはそのまま維持されます。

このカテゴリの使用は、Appleのフレームワークの装飾としても見ることができます。 NSAttributedStringは、いくつかのテキストと属性を保持するモデルクラスです。 AppKitはNSAttributedString(AppKitAdditions)を提供し、UIKitはNSAttributedString(NSStringDrawing)、これらのモデルクラスに描画動作を追加するデコレータカテゴリを提供します。

43
user4051

人々はUIViewController/UITableViewControllerにたくさん詰め込む傾向があります。

別のクラス(ビューコントローラーではない)への委任は、通常はうまく機能します。すべてのデリゲートメソッドにはUITableViewへの参照が渡されるため、デリゲートは必ずしもビューコントローラーへの参照を返す必要はありませんが、委任先のデータに何らかの方法でアクセスする必要があります。

長さを減らすための再編成のいくつかのアイデア:

  • コードでテーブルビューセルを作成する場合は、nibファイルまたはストーリーボードからセルを読み込むことを検討してください。ストーリーボードでは、プロトタイプセルと静的テーブルセルを使用できます。詳しくない場合は、これらの機能を確認してください

  • デリゲートメソッドに多くの 'if'ステートメント(またはswitchステートメント)が含まれている場合、これはいくつかのリファクタリングを実行できるという典型的な兆候です

UITableViewDataSourceがデータの正しいビットのハンドルを取得する責任があることは常に私にとって少しおもしろいと感じましたandビューを構成して表示します。良いリファクタリングポイントの1つは、セルに表示する必要があるデータのハンドルを取得するようにcellForRowAtIndexPathを変更し、セルビューの作成を別のデリゲートに委任することです(たとえば、CellViewDelegateまたは同様)、適切なデータ項目で渡されます。

3
occulus

同様の問題に直面したときに現在私が現在行っていることは、おおよそ次のとおりです。

  • データ関連の操作をXXXDataSourceクラス(BaseDataSource:NSObjectから継承)に移動します。 BaseDataSourceは- (NSUInteger)rowsInSection:(NSUInteger)sectionNum;のようないくつかの便利なメソッドを提供し、サブクラスはデータ読み込みメソッドをオーバーライドします(通常、アプリにはある種のオフリーキャッシュ読み込みメソッドが- (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;のように見えるため、受信したキャッシュデータでUIを更新できますネットワークから情報を更新している間はLoadProgressBlockで、完了ブロックでは新しいデータでUIを更新し、進捗インジケータがあれば削除します)。これらのクラスはUITableViewDataSourceプロトコルに準拠していません。

  • BaseTableViewController(UITableViewDataSourceおよびUITableViewDelegateプロトコルに準拠)では、コントローラの初期化中に作成するBaseDataSourceへの参照があります。コントローラーのUITableViewDataSource部分では、単純にdataSourceから値を返します(- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.tableViewDataSource sectionsCount]; }など)。

これが基本クラスの私のcellForRowです(サブクラスでオーバーライドする必要はありません)。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *cellIdentifier = [NSString stringWithFormat:@"%@%@", NSStringFromClass([self class]), @"TableViewCell"];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [self createCellForIndexPath:indexPath withCellIdentifier:cellIdentifier];
    }
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

configureCellはサブクラスによってオーバーライドする必要があり、createCellはUITableViewCellを返すため、カスタムセルが必要な場合はオーバーライドします。

  • 基本的なものが構成された後(実際には、このようなスキームを使用する最初のプロジェクトでは、この部分は再利用できます)、BaseTableViewControllerサブクラスに残されるものは次のとおりです。

    • ConfigureCellをオーバーライドします(これは通常、インデックスパスのオブジェクトにdataSourceを要求し、それをセルのconfigureWithXXX:メソッドにフィードするか、user4051の回答のようにオブジェクトのUITableViewCell表現を取得するように変換されます)

    • DidSelectRowAtIndexPath :(明らかに)をオーバーライドする

    • Modelの必要な部分を処理するBaseDataSourceサブクラスを記述します(AccountLanguageの2つのクラスがあるため、サブクラスはAccountDataSourceとLanguageDataSourceになります)。

これで、テーブルビューの部分はすべて完了です。必要に応じて、GitHubにコードを投稿できます。

編集: http://www.objc.io/issue-1/lighter-view-controllers.html (この質問へのリンクがあります)およびtableviewcontrollersに関する関連記事でいくつかの推奨事項を見つけることができます。

2
Timur Kuchkarov

私は最近、UITableViewのデリゲートとデータソースを実装する方法に関する記事を書きました: http://gosuwachu.gitlab.io/2014/01/12/uitableview-controller/

主なアイデアは、責任をセルファクトリ、セクションファクトリなどの個別のクラスに分割し、UITableViewが表示するモデルにいくつかの汎用インターフェイスを提供することです。以下の図はすべてを説明しています:

enter image description here

2
Piotr Wach

これについての私の見解は、モデルは、ViewModelまたはcellConfiguratorにカプセル化されたviewDataと呼ばれるオブジェクトの配列を提供する必要があるということです。 CellConfiguratorは、それをデキューしてセルを構成するために必要なCellInfoを保持します。セルにデータを提供するため、セルは自己を構成できます。 CellConfiguratorsを保持するSectionConfiguratorオブジェクトを追加した場合、これはセクションでも機能します。私はしばらくの間これを使い始め、最初はセルにviewDataを与えるだけで、ViewControllerにセルのデキューを処理させました。しかし、私はこのgitHubリポジトリを指す記事を読みました。

https://github.com/fastred/ConfigurableTableViewController

これにより、これへの取り組み方が変わる場合があります。

2
Pascale Beaulac

[〜#〜] solid [〜#〜]の原則に従うと、これらのようなあらゆる種類の問題が解決されます。

クラスにJUST A SINGLEの責任を持たせたい場合は、DataSourceクラスとDelegateクラスを個別に定義し、単純にinjectそれらをtableView所有者に(UITableViewControllerまたはUIViewControllerにすることができます) 。これがあなたが克服する方法です 懸念の分離

しかし、クリーンで読みやすいコードが必要で、その巨大なviewControllerfileを削除したい場合、Swif、そのためにextensionsを使用できます。単一クラスの拡張機能は、異なるファイルに書き込むことができ、それらすべてが相互にアクセスできます。しかし、これは私が述べたように本当にSoCの問題を本当に解決します。

1