iOSで大きくて不器用なUITableViewControllerを避ける方法は?


36

iOSでMVCパターンを実装するときに問題があります。インターネットを検索しましたが、この問題の良い解決策を見つけられないようです。

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

モデルに含まれる機能やビューが多すぎないようにします。これはMVCパターンの基礎の1つであるため、ロジックは実際にはコントローラークラスにあるべきだと思います。しかし、大きな問題は次のとおりです。

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

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


1
「この解決策が素晴らしい理由も議論しています。」:)
オクルス

1
それはポイントの少し横にありますが、UITableViewControllerメカニズムは私には非常に奇妙に見えるので、問題に関連することができます。私が実際に使用してうれしいのはMonoTouchMonoTouch.Dialog特にiOSでテーブルを操作するのずっと簡単になるからです。一方で、私は...他の、より多くの知識のある人がここで提案するかもしれないもの好奇心が強い
はPatrykĆwiek

回答:


43

UITableViewController単一のオブジェクトに多くの責任を課すので、使用は避けます。したがって、UIViewControllerサブクラスをデータソースとデリゲートから分離します。View Controllerの役割は、Table Viewを準備し、データを含むデータソースを作成し、それらを一緒にフックすることです。TableViewの表現方法の変更は、View Controllerを変更せずに実行できます。実際、同じView Controllerを、このパターンに従う複数のデータソースに使用できます。同様に、アプリのワークフローを変更すると、テーブルに何が起こるか心配することなくView Controllerを変更できます。

プロトコル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];
}

これは、任意の表示電話番号、住所などへのニーズがちょうど頼むことができることをデータソースどんなテーブルビューセルのために表されているオブジェクト。データソース自体は、表示されているオブジェクトについて何も知る必要がなくなりました。

「しかし、待ってください」と、私は仮想の対談者が「MVC を壊さないのですか?モデルクラスにビューの詳細を入れていませんか?」と聞いています。

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

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


データソースおよびテーブルビューデリゲートとして機能しているクラスの適切な名前は何ですか?
ヨハンカールソン

1
@JohanKarlsson私はしばしば単にデータソースと呼んでいます。たぶん少しずさんですが、私は「データソース」がAppleのより制限された定義への適応であることを知るのに十分な頻度で2つを組み合わせます。

1
この記事:objc.io/issue-1/table-views.htmlcellForPhotoAtIndexPath、データソースのメソッドでセルクラスを処理し、適切なファクトリメソッドを呼び出すことにより、複数のセルタイプを処理する方法を提案しています。もちろん、特定のクラスが特定の行を予想通りに占有している場合にのみ可能です。モデル上のビューを生成するカテゴリのシステムは、実際にははるかにエレガントです。ただし、MVCへの非正統的なアプローチかもしれません。:)
ベンジーXVI 14年

1
github.com/yonglam/TableViewPatternでこのパターンのデモを試みました。それが誰かに役立つことを願っています。
アンドリュー14年

1
ダイナミックセレクターアプローチの決定的なnoを投票します。問題は実行時にのみ現れるため、非常に危険です。指定されたセレクターが存在、それが正しく入力されたことを確認する自動化された方法はなく、この種のアプローチは最終的に崩壊し、維持するのは悪夢です。ただし、他のアプローチは非常に賢い方法です。
mkko 14年

3

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

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

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

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

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

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


これはいい答えです。しかし、私の頭の中にはいくつかの質問があります。なぜ多くのif文(またはswitch文)が設計が悪いと思うのですか?実際には、多くのif文とswitch文がネストされていますか?ifまたはswitchステートメントを回避するためにどのようにリファクタリングしますか?
ヨハンカールソン

@JohanKarlssonの1つの手法は、ポリモーフィズムです。1つのタイプのオブジェクトで1つのことを行い、別のタイプの別の何かで何かを行う必要がある場合は、それらのオブジェクトを異なるクラスにして、作業を選択させます。

@GrahamLeeはい、私はポリモーフィズムを知っています;-)しかし、このコンテキストでそれをどのように適用するかはわかりません。これについて詳しく説明してください。
ヨハンカールソン

@JohanKarlsson完了;)

2

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

  • データ関連の操作をXXXDataSourceクラス(BaseDataSourceから継承:NSObject)に移動します。BaseDataSourceは、のようないくつかの便利な方法を提供- (NSUInteger)rowsInSection:(NSUInteger)sectionNum;アプリは通常の方法のルックスが好きofflieキャッシュ負荷のいくつかの並べ替え持っているとして(サブクラスのオーバーライドデータのロード方法を、- (void)loadDataWithUpdateBlock:(LoadProgressBlock)dataLoadBlock completion:(LoadCompletionBlock)completionBlock;我々は、ネットワークからの情報を更新している間、LoadProgressBlockで受信し、キャッシュされたデータとUIを更新できるようにして、完了ブロックで新しいデータでUIを更新し、進行状況インジケータがあれば削除します)。これらのクラスはUITableViewDataSourceプロトコルに準拠していません。

  • (に準拠BaseTableViewControllerにUITableViewDataSource及びUITableViewDelegateプロトコル)私は、コントローラの初期化時に作成BaseDataSourceへの参照を持っています。UITableViewDataSourceコントローラの部分Iは、単にデータソース(などから値を返します - (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サブクラスを記述します(2つのクラスAccountLanguageがあるため、サブクラスはAccountDataSourceとLanguageDataSourceになります)。

そして、それがテーブルビューの一部です。必要に応じて、GitHubにコードを投稿できます。

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


2

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

https://github.com/fastred/ConfigurableTableViewController

これにより、これに近づく方法が変わる可能性があります。


2

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

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

ここに画像の説明を入力してください


このリンクは機能していません。
koen

1

以下のSOLID原則は、これらのような問題の任意の種類を解決します。

あなたが持っているあなたのクラスにしたい場合はJUST A SINGLEの責任を、あなたは別の定義すべきであるDataSourceDelegateするクラスを単に注入にそれらをtableView(可能性があり、所有者UITableViewControllerまたはUIViewControllerまたは何か他のもの)。これが懸念の分離を克服する方法です。

あなただけのクリーンで読み取り可能なコードを持っていると思いますし、その大規模なのViewControllerを取り除きたい場合でも、ファイルとあなたがしているSWIF、あなたは使用することができextension、そのために秒。単一のクラスの拡張機能は異なるファイルに記述でき、それらはすべて相互にアクセスできます。しかし、これは私が言ったようにSoCの問題を本当に解決します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.