オニオンコントローラー。 スクリーンをパーツに分割します

アトミックデザインとシステムデザインはデザインで一般的です。これは、コントロールから画面まで、すべてがコンポーネントで構成される場合です。 プログラマーが個別のコントロールを作成することは難しくありませんが、画面全体をどうするか?







新年の例を見てみましょう。















すべての束



この新年のスクリーンでは、ピッツェリアの特別営業時間について説明しています。 これは非常に単純なので、1つのコントローラーにすることは犯罪ではありません。













しかし。 次回、同様の画面が必要になったときは、すべてをもう一度繰り返し、すべての画面に同じ変更を加える必要があります。 まあ、それは編集なしでは起こりません。







したがって、それを部分に分割し、他の画面に使用する方が合理的です。 私は3つを強調しました:









独自のUIViewController



で各パーツを選択します。







コンテナナビゲーション



ナビゲーションコンテナの最も顕著な例は、 UINavigationController



とUITabBarController



です。 それぞれが独自のコントロールの下で画面上のストリップを占有し、残りのスペースを別のUIViewController



用にUIViewController



ます。







私たちの場合、閉じるボタンが1つだけのすべてのモーダル画面用のコンテナがあります。







ポイントは何ですか?

ボタンを右に移動する場合は、1つのコントローラーで変更するだけです。







または、AppStoreストーリーカードのように、すべてのモーダルウィンドウを特別なアニメーションで表示し、スワイプでインタラクティブに閉じることにした場合。 次に、 UIViewControllerTransitioningDelegate



をこのコントローラーに対してのみ設定する必要があります。













コントローラーを分離するには、 container view



使用できcontainer view



。これにより、親にUIView



が作成され、そこに子コントローラーのUIView



挿入されます。













container view



を画面の端まで引き伸ばします。 Safe area



は、自動的に子コントローラーに適用されます。













スクリーンパターン



コンテンツは画面上で明白です:写真、タイトル、テキスト。 ボタンはその一部のようですが、コンテンツはさまざまなiPhoneで動的であり、ボタンは固定されています。 異なるタスクを持つ2つのシステムが表示されます。1つはコンテンツを表示し、もう1つはコンテンツを埋め込み、調整します。 これらは2つのコントローラーに分割する必要があります。













1つ目は画面のレイアウトを担当します。コンテンツを中央に配置し、ボタンを画面の下部に固定する必要があります。 2番目はコンテンツを描画します。













テンプレートがなければ、すべてのコントローラーは似ていますが、要素は踊ります。

最後の画面のボタンは異なります-それは内容によって異なります。 委任は、問題の解決に役立ちます。コントローラーテンプレートは、コンテンツからコントロールを要求し、 UIStackView



表示します。







 // OnboardingViewController.swift protocol OnboardingViewControllerDatasource { var supportingViews: [UIView] { get } } // NewYearContentViewController.swift extension NewYearContentViewController: OnboardingViewControllerDatasource { var supportingViews: [UIView] { return [view().doneButton] } }
      
      





()を表示する理由

前回の記事ControllerでUIViewController



してUIView



を特化する方法について読むUIViewController



ができます。 UIViewでコードを取り出します。







ボタンは、関連オブジェクトを介してコントローラーに接続できます。 IBOutlet



およびIBAction



はコンテンツコントローラーに保存され、要素は階層に追加されません。













UIStoryboardSegue



準備段階で、コンテンツから要素を取得し、テンプレートに追加できます。







 // OnboardingViewController.swift override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let buttonsDatasource = segue.destination as? OnboardingViewControllerDatasource { view().supportingViews = buttonsDatasource.supportingViews } }
      
      





セッターでは、コントロールをUIStackView



追加します。







 // OnboardingView.swift var supportingViews: [UIView] = [] { didSet { for view in supportingViews { stackView.addArrangedSubview(view) } } }
      
      





その結果、コントローラーはナビゲーション、テンプレート、コンテンツの3つの部分に分割されました。 写真では、すべてのcontainer view



灰色container view



表示されています。













動的コントローラーサイズ



コンテンツコントローラーには独自の最大サイズがあり、内部のconstraints



によって制限されconstraints



。







Container view



は、 Autoresizing mask



基づいてコンストアを追加し、コンテンツの内部ディメンションと競合します。 この問題はコードで解決されます。コンテンツコントローラーでは、 Autoresizing mask



からのAutoresizing mask



ストアの影響を受けないことを示す必要があります。







 // NewYearContentViewController.swift override func loadView() { super.loadView() view.translatesAutoresizingMaskIntoConstraints = false }
      
      











Interface Builderにはさらに2つのステップがあります:







手順1. UIView



Intrinsic size



を指定します 。 実際の値は発売後に表示されますが、ここでは適切な値を入力します。













ステップ2.コンテンツコントローラーに対して、 Simulated Size



指定します。 過去のサイズと一致しない場合があります。







レイアウトエラーがありました。どうすればよいですか?

AutoLayout



が現在のサイズの要素を分解する方法を理解できない場合、エラーが発生しAutoLayout



。







ほとんどの場合、定数の優先順位を変更すると問題はなくなります。 UIView



1つが他のUIView



よりも拡大/縮小できるように、それらを配置する必要があります。







部分に分けてコードを書く



コントローラーをいくつかの部分に分割しましたが、今のところそれらを再利用することはできませんUIStoryboard



からのインターフェースを部分的に抽出することUIStoryboard



困難です。 一部のデータをコンテンツに転送する必要がある場合は、階層全体でそれをノックする必要があります。 それは他の方法であるべきです:最初にコンテンツを取得し、それを設定し、それから必要なコンテナでそれをラップします。 電球のように。







次の3つのタスクが表示されます。







  1. 各コントローラーを独自のUIStoryboard



    分離します。
  2. container view



    拒否し、コードでコンテナにコントローラを追加します。
  3. それをすべて結びます。


UIStoryboardの共有



2つの追加のUIStoryboard



を作成し、Navigation ControllerとTemplate Controllerをそれらにコピーアンドペーストする必要があります。 Embed segue



は壊れますが、制約が設定されたcontainer view



は転送されます。 制約を保存し、 container view



通常のUIView



置き換える必要があります。







最も簡単な方法は、UIStoryboardコードでコンテナビューのタイプを変更することです。
  • コードの形式でUIStoryboard



    を開きます(ファイルコンテキストメニュー→として開く...→ソースコード);
  • タイプをcontainerView



    からview



    変更しview



    。 開始タグと終了タグの両方を変更する必要があります。







    同様に、必要に応じて、たとえばUIView



    をUIScrollView



    に変更できます。 そしてその逆。















コントローラーをプロパティに設定します。プロパティis initial view controller



で、 UIStoryboard



をコントローラーとして呼び出します。







UIStoryboardからコントローラーをロードします。

コントローラーの名前がUIStoryboard



の名前と一致する場合、ダウンロードはそれ自体が目的のファイルを見つけるメソッドでラップできます。







 protocol Storyboardable { } extension Storyboardable where Self: UIViewController { static func instantiateInitialFromStoryboard() -> Self { let controller = storyboard().instantiateInitialViewController() return controller! as! Self } static func storyboard(fileName: String? = nil) -> UIStoryboard { let storyboard = UIStoryboard(name: fileName ?? storyboardIdentifier, bundle: nil) return storyboard } static var storyboardIdentifier: String { return String(describing: self) } static var storyboardName: String { return storyboardIdentifier } }
      
      





コントローラーが.xib



で記述されている場合、標準コンストラクターはそのようなダンスなしでロードされます。 .xib



、 .xib



ことができるコントローラーは1つだけです。多くの場合、これでは十分ではありません。良い場合には、1つの画面が複数の画面で構成されます。 したがって、 UIStoryborad



を使用します。画面を複数の部分に分割するのは簡単です。







コードでコントローラーを追加する



コントローラーが適切に機能するためには、そのライフサイクルのすべてのメソッド、 will/did-appear/disappear



です。







正しく表示するには、5つのステップを呼び出す必要があります。







  willMove(toParent parent: UIViewController?) addChild(_ childController: UIViewController) addSubview(_ subivew: UIView) layout didMove(toParent parent: UIViewController?)
      
      





AppleはaddChild()



自体がwillMove(toParent)



を呼び出すため、コードを4ステップに減らすことをおwillMove(toParent)



ます。 要約すると:







  addChild(_ childController: UIViewController) addSubview(_ subivew: UIView) layout didMove(toParent parent: UIViewController?)
      
      





簡単にするために、すべてをextension



ラップできます。 このケースでは、 insertSubview()



をinsertSubview()



バージョンが必要です。







 extension UIViewController { func insertFullframeChildController(_ childController: UIViewController, toView: UIView? = nil, index: Int) { let containerView: UIView = toView ?? view addChild(childController) containerView.insertSubview(childController.view, at: index) containerView.pinToBounds(childController.view) childController.didMove(toParent: self) } }
      
      





削除するには、同じ手順が必要です。親コントローラーの代わりにnil



を設定する必要があります。 didMove(toParent: nil)



removeFromParent()



がdidMove(toParent: nil)



removeFromParent()



呼び出すdidMove(toParent: nil)



になり、レイアウトは不要になりました。 短縮バージョンは大きく異なります。







  willMove(toParent: nil) view.removeFromSuperview() removeFromParent()
      
      





レイアウト



制約を設定する



コントローラーのサイズを正しく設定するには、 AutoLayout



を使用しAutoLayout



。 すべての面をすべての面に釘付けする必要があります。







 extension UIView { func pinToBounds(_ view: UIView) { view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ view.topAnchor.constraint(equalTo: topAnchor), view.bottomAnchor.constraint(equalTo: bottomAnchor), view.leadingAnchor.constraint(equalTo: leadingAnchor), view.trailingAnchor.constraint(equalTo: trailingAnchor) ]) } }
      
      





コードで子コントローラーを追加する



これですべてを組み合わせることができます:







 // ModalContainerViewController.swift public func embedController(_ controller: UIViewController) { insertFullframeChildController(controller, index: 0) }
      
      





使用頻度のため、これらすべてをextension



ラップできます。







 // ModalContainerViewController.swift extension UIViewController { func wrapInModalContainer() -> ModalContainerViewController { let modalController = ModalContainerViewController.instantiateInitialFromStoryboard() modalController.embedController(self) return modalController } }
      
      





テンプレートコントローラーにも同様の方法が必要です。 prepare(for segue:)



以前はprepare(for segue:)



で設定されていましたが、コントローラーの埋め込みメソッドでバインドできるようになりました:







 // OnboardingViewController.swift public func embedController(_ controller: UIViewController, actionsDatasource: OnboardingViewControllerDatasource) { insertFullframeChildController(controller, toView: view().contentContainerView, index: 0) view().supportingViews = actionsDatasource.supportingViews }
      
      





コントローラーの作成は次のようになります。







 // MainViewController.swift @IBAction func showModalControllerDidPress(_ sender: UIButton) { let content = NewYearContentViewController.instantiateInitialFromStoryboard() //     let onboarding = OnboardingViewController.instantiateInitialFromStoryboard() onboarding.embedController(contentController, actionsDatasource: contentController) let modalController = onboarding.wrapInModalContainer() present(modalController, animated: true) }
      
      





新しい画面をテンプレートに接続するのは簡単です:









コンテナの詳細



ステータスバー



多くの場合、 status bar



可視性は、コンテナではなく、コンテンツを持つコントローラーによって制御される必要がありstatus bar



。 これにはいくつかのproperty



があります。







 // UIView.swift var childForStatusBarStyle: UIViewController? var childForStatusBarHidden: UIViewController?
      
      





これらのproperty



を使用して、コントローラーのチェーンを作成できます。後者は、 status bar



表示を担当しstatus bar



。







安全なエリア



コンテナのボタンがコンテンツに重なる場合、 safeArea



ゾーンを増やす必要があります。 これはコードで実行できます。子コントローラーのadditinalSafeAreaInsets



を設定します。 embedController()



から呼び出すことができます:







 private func addSafeArea(to controller: UIViewController) { if #available(iOS 11.0, *) { let buttonHeight = CGFloat(30) let topInset = UIEdgeInsets(top: buttonHeight, left: 0, bottom: 0, right: 0) controller.additionalSafeAreaInsets = topInset } }
      
      





上に30ポイントを追加すると、ボタンはコンテンツの重複を停止し、 safeArea



は緑の領域を占有します。













マージン。 スーパービューのマージンを保持



コントローラーには標準のmargins



ます。 通常、それらは画面の各側から16ポイントに等しく、プラスサイズでのみ20ポイントです。







margins



基づいてmargins



定数を作成できます。エッジへのインデントはiPhoneごとに異なります。













あるUIView



を別のUIView



に配置すると、 margins



は半分になり、8ポイントになります。 これを防ぐには、 Preserve superview margins



するを有効にする必要があります。 子UIView



のmargins



親のmargins



と等しくなります。 フルスクリーンコンテナーに適しています。







終わり



コンテナコントローラーは強力なツールです。 コードを簡素化し、タスクを分離し、再利用できます。 ネストされたコントローラーは、 UIStoryboard



、 .xib



または単にコードのいずれの方法でも作成できます。 最も重要なことは、作成が簡単で使いやすいことです。







→ GitHubの記事の例







テンプレートを作成する価値がある画面はありますか? コメントで共有してください!








All Articles