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

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


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




すべての束


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



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


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



独自のUIViewControllerで各パーツを選択します。


コンテナナビゲーション


ナビゲーションコンテナの最も顕著な例は、 UINavigationControllerUITabBarControllerです。 それぞれが独自のコントロールの下で画面上のストリップを占有し、残りのスペースを別の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] } } 

()を表示する理由

前回の記事ControllerUIViewControllerして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 。 開始タグと終了タグの両方を変更する必要があります。


    同様に、必要に応じて、たとえばUIViewUIScrollViewに変更できます。 そしてその逆。




コントローラーをプロパティに設定します。プロパティ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するを有効にする必要があります。 子UIViewmargins親のmarginsと等しくなります。 フルスクリーンコンテナーに適しています。


終わり


コンテナコントローラーは強力なツールです。 コードを簡素化し、タスクを分離し、再利用できます。 ネストされたコントローラーは、 UIStoryboard.xibまたは単にコードのいずれの方法でも作成できます。 最も重要なことは、作成が簡単で使いやすいことです。


GitHubの記事の例


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



Source: https://habr.com/ru/post/J434470/


All Articles