こんにちは、Habr! 私の名前はBogdanです。Badooでは、iOS開発者としてモバイルチームで働いています。 記事は優れた実践を文書化するための最良の方法の1つですが、モバイル開発についてはほとんど何も語りません。 この記事では、作業で使用するいくつかの有用なアプローチについて説明します。
ここ数年、iOSコミュニティはUIKitと戦っています。 架空のアーキテクチャの抽象化レイヤーの下にUIKit内部を「埋める」複雑な方法を考え出す人がいます。他のチームは、エゴを楽しませながら、サポートする必要のある大量のコードを残して、それを書き直します。
UIKitをあなたのために機能させる
私は怠け者なので、必要なコードのみを記述しようとします。 製品の要件とチームが採用した品質基準を満たすコードを作成したいのですが、インフラストラクチャとアーキテクチャテンプレートの標準部分をサポートするためにコードの量を最小限に抑えます。 したがって、UIKitと戦うのではなく、UIKitを受け入れてできるだけ広く使用する必要があると思います。
UIKitに適したアーキテクチャの選択
問題を解決するには、別のレベルの抽象化を追加します。 したがって、多くの人がVIPERを選択します。仕事で使用できるレベル/エンティティは多数あります。 VIPERでアプリケーションを作成することは難しくありません。テンプレートコードを少なくして同じ利点を備えたMVCアプリケーションを作成することははるかに困難です。
プロジェクトをゼロから開始する場合、アーキテクチャテンプレートを選択し、最初からすべてを「正しく」行うことができます。 しかし、ほとんどの場合、このような贅沢品は利用できません。既存のコードベースを使用する必要があります。
思考実験をしましょう。
大規模なコードベースを開発したチームに参加します。 どのようなアプローチを期待していますか? 純粋なMVC? フローコントローラーを備えたMVVM / MVPはありますか? おそらく、FRPフレームワークでのVIPERアプローチまたはReduxベースのアプローチでしょうか? 個人的には、シンプルで実用的なアプローチを期待しています。 さらに、誰でも読んで修正できるようなコードを残したいです。
要するに、View Controllerに基づいて何かを行う方法を見てみましょう。それらを置き換えたり隠したりするのではありません。
一連の画面があり、それぞれが1つのコントローラーで表されているとします。 これらのView Controllerは、インターネットからデータを抽出して画面に表示します。 製品の観点から見ると、すべてが完全に機能しますが、コントローラーのコードをテストする方法がわからず、再使用の試みはコピーペーストで終わります。そのため、ビューコントローラーのサイズが大きくなります。
明らかに、コードの分割を開始する必要があります。 しかし、手間をかけずにそれを行う方法は? データを抽出するコードを別のオブジェクトにプルすると、コントローラーは画面に情報のみを表示します。 やってみましょう:
すべてがMVVMに非常によく似ているので、その用語を使用します。 したがって、ビューとプレゼンテーションモデルがあります。 このモデルを簡単にテストできます。 次に、ネットワークの操作やデータの保存など、反復的なタスクをサービスに転送しましょう。
その結果:
- コードを再利用できます。
- ユーザーインターフェイスレベルに縛られていない真実の情報源を入手してください。
これはすべてUIKitと関係がありますか? 説明させてください。
ビューモデルはView Controllerによって保存され、コントローラーが存在するかどうかにはまったく関心がありません。 したがって、メモリからコントローラーを削除すると、対応するモデルも削除されます。
一方、コントローラーが別のオブジェクト(プレゼンターなど)によってMVPに保存されている場合、何らかの理由でコントローラーがアンロードされると、コントローラーとプレゼンターの間の接続が切断されます。 また、誤って誤ったコントローラーをアンロードすることが難しいと思われる場合は、 UIViewController.dismiss(animated:completion:)
説明を注意深くお読みください。
したがって、View Controllerをキングとして認識することが最も安全であると考えています。したがって、UIに関連しないオブジェクトは2つのカテゴリに分類されます。
- ライフサイクルがUI要素のサイクルと等しいオブジェクト(プレゼンテーションモデルなど)。
- ライフサイクルがアプリケーションサイクルと等しいオブジェクト(サービスなど)。
View Controllerのライフサイクルを使用する
すべてのコードをView Controllerに配置するのはなぜ魅力的ですか? はい。コントローラーでは、すべてのデータとビューの現在の状態にアクセスできるためです。 モデルまたはプレゼンターでプレゼンテーションのライフサイクルにアクセスする必要がある場合は、手動で渡す必要があります。これは正常ですが、コードをさらに記述する必要があります。
しかし、別の解決策があります。 View Controllerは互いに連携できるため、Sorush Hanlowはこれを使用して小さなView Controller間で作業を分散することを提案しました。
さらに先に進み、View Controllerをライフサイクルに接続する普遍的な方法であるViewControllerLifecycleBehaviour
ます。
public protocol ViewControllerLifecycleBehaviour { func afterLoading(_ viewController: UIViewController) func beforeAppearing(_ viewController: UIViewController) func afterAppearing(_ viewController: UIViewController) func beforeDisappearing(_ viewController: UIViewController) func afterDisappearing(_ viewController: UIViewController) func beforeLayingOutSubviews(_ viewController: UIViewController) func afterLayingOutSubviews(_ viewController: UIViewController) }
例で説明します。 チャットビューコントローラーでスクリーンショットを定義する必要があるとしますが、それが表示されている場合のみです。 このタスクをVCLBehaviourに送信すると、すべてがかつてないほど簡単になります。
open override func viewDidLoad() { let screenshotDetector = ScreenshotDetector(notificationCenter: NotificationCenter.default) { // Screenshot was detected } self.add(behaviours: [screenshotDetector])}
動作の実装においても複雑なものはありません。
public final class ScreenshotDetector: NSObject, ViewControllerLifecycleBehaviour { public init(notificationCenter: NotificationCenter, didDetectScreenshot: @escaping () -> Void) { self.didDetectScreenshot = didDetectScreenshot self.notificationCenter = notificationCenter } deinit { self.notificationCenter.removeObserver(self) } public func afterAppearing(_ viewController: UIViewController) { self.notificationCenter.addObserver(self, selector: #selector(userDidTakeScreenshot), name: .UIApplicationUserDidTakeScreenshot, object: nil) } public func afterDisappearing(_ viewController: UIViewController) { self.notificationCenter.removeObserver(self) } @objc private func userDidTakeScreenshot() { self.didDetectScreenshot() } private let didDetectScreenshot: () -> Void private let notificationCenter: NotificationCenter }
また、 ViewControllerLifecycleBehaviour
プロトコルでカバーされているため、動作を単独でテストすることもできます。
実装の詳細: こちら 。
動作は、分析などのVCL固有のタスクで使用できます。
レスポンダーチェーンの使用
ビューの階層の奥にボタンがあり、新しいコントローラーのプレゼンテーションを作成するだけでよいとします。 通常、このためにプレゼンテーションコントローラーが実装され、そこからプレゼンテーションが行われます。 これは正しいアプローチです。 しかし、これが原因で、中間ではなく階層の深層にいる人々が使用する移行依存が現れることがあります。
おそらく既に推測したように、それを解決する別の方法があります。 レスポンダーチェーンを使用して、別のプレゼンテーションコントローラーを提示できるコントローラーを見つけることができます。
例:
public extension UIView { public func viewControllerForPresentation() -> UIViewController? { var next = self.next while let nextResponder = next { if let viewController = next as? UIViewController, viewController.presentedViewController == nil, !viewController.isDetached { return viewController } next = nextResponder.next } return nil } } public extension UIViewController { public var isDetached: Bool { if self.viewIfLoaded?.window?.rootViewController == self return false } return self.parent == nil && self.presentingViewController == nil } }
ビュー階層を使用する
エンティティ-コンポーネント-システムテンプレートは、分析をアプリケーションに組み込むための優れた方法です。 私の同僚はそのようなシステムを実装し、非常に便利であることが判明しました。
ここで、「エンティティ」はUIView、「コンポーネント」は追跡データの一部、「システム」は分析追跡サービスです。
考えは、関連する追跡データでUIビューを補完することです。 次に、分析追跡サービスは、プレゼンテーション階層の可視部分をN回/秒スキャンし、まだ記録されていない追跡データを記録します。
このようなシステムを使用する場合、開発者は画面名や要素などの追跡データを追加するだけで済みます。
class EditProfileViewController: UIViewController { override func viewDidLoad() { ... self.trackingScreen = TrackingScreen(screenName:.screenNameMyProfile) } } class SparkUIButton: UIButton { public override func awakeFromNib() { ... self.trackingElement = TrackingElement(elementType: .elementSparkButton) } }
ビュー階層のバイパスは、表示されていないビューを無視するBFSです。
let visibleElements = Class.visibleElements(inView: window) for view in visibleElements { guard let trackingElement = view.trackingElement else { continue } self.trackViewElement(view) }
明らかに、このシステムには無視できないパフォーマンスの制限があります。 メインスレッドの実行のオーバーロードを回避する方法はいくつかあります。
- ビューの階層をあまり頻繁にスキャンしないでください。
- スクロール時にビューの階層をスキャンしないでください(より適切な実行ループモードを使用してください)。
-
NSPostWhenIdle
を使用して通知がNSNotificationQueue
発行された場合にのみ、階層をスキャンします。
PS
UIKitをどのように活用できるかを示し、日々の仕事に役立つものを見つけていただければ幸いです。 または、少なくとも思考の糧を得ました。