LoginSignup
4
8

More than 5 years have passed since last update.

Container利用時のViewControllerの生成順と、nibからViewを作るメモ

Last updated at Posted at 2017-02-23

概要

Swift3で、カスタムContainerViewControllerにViewControllerを子としてセットし、その子ViewControllerが、xibファイルから生成するViewを持ち、そのViewも、別のxibから生成されるviewをメンバーに持つ、といった階層構造を作ろうとしたら、各コントロールがいつ生成されるのかわからなくて躓いたので、メモする。

前提

以下のような階層構造を想定する。
ContainerViewController    // Main.storyboardでデザイン
  ┣ SubViewController    // Main.storyboardでデザイン
  ┃   ┗ CustomButton    // CustomButton.xibでデザイン
  ┃      ┗ ExtendLabel // ExtendLabel.xibでデザイン
  ┃         ┗ UILabel
  ┗ SubViewController
      ┗ 以下同様

結論

Swiftでは以下のような順序で各要素が初期化される。

  • ContainerViewController.init
    • ここでSubViewControllerのインスタンス生成をマニュアルで実施。
    • ただしこの段階では、IB上に配置されたUIはロードされてない
  • ContainerViewController.viewDidLoad
  • ContainerViewController.viewWillAppear
    • ここでContainerViewControllerのメソッドでSubViewControllerをContainerに追加する定番処理を実施
    • addChildViewController(nextVC)
    • nextVC.view.frame = view.frame 
      • // (コメントから)viewがロードされていないので、viewにアクセスのあるこのタイミングでViewとその要素の初期化が走る。
      • CustomButton.init()
      • ExtendLabel.init()
      • ExtendLabel.awakeFromNib()
      • CustomButton.awakeFromNib()
      • SubViewController.viewDidLoad()
    • view.addSubview(nextVC.view)
    • nextVC.didMove(toParentViewController: self)
      • // didMove()の呼び出し完了後じゃないと、SubViewControllerのメンバがnilのままなことがあるので、メンバにアクセスするならこれ以降
  • SubViewController.willViewAppear()
  • ContainerViewController.viewibDidAppear()
  • 表示完了

xibからカスタムUIViewを生成するためのステップ

  • File's Ownerをちゃんと指定する。
    • xiibを開いての左上のFile's Ownerを選択して、右上に表示されるclass指定欄にカスタムクラス名を指定
    • rootViewのclass名に自分のクラス名を入れるのはNG
  • ルートのviewをいつもの手順でBindする。
  • self.addSubview(rootView)で自分にViewをBind
  • 要するに以下のような感じ
Viewクラスの例
class ExtendedLabel: UIView {
    @IBOutlet var view: UIView!        // IBからBINDする
    @IBOutlet weak var label: UILabel! // IBからBINDする

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        Bundle.main.loadNibNamed("ExtendedLabel", owner: self, options: nil)
        self.addSubview(self.view);  // こんな感じで自分自身のRootViewをaddSubViewする
    }

    override func awakeFromNib() {
        super.awakeFromNib()
    }
}

なお、公式ドキュメントには、以下のような記述もある。

  • 子 View Controller のルートビューにのみアクセスする。コンテナは、各子のルートビューにのみアクセスする必要があります。つまり、子の view プロパティにより返されるビューです。子の他のビューにはアクセスしないようにしてください。
  • 子 View Controller が持つコンテナの情報は最低限にする必要がある。子 View Controller は、自身のコンテンツに焦点を当てる必要があります。子の影響を受ける動作がコンテナで許容される場合、デリゲーション設計パターンを使用してそれらの操作を管理する必要があります。
  • まず標準ビューを使用してコンテナを設計する。標準ビュー (子 View Controller のビューではなく) を使用すると、簡略化された環境でレイアウト制約とアニメーション化された遷移をテストするチャンスが得られます。標準ビューが正常に機能した場合、そのビューを子 View Controller のビューと入れ替えます。

以上、参考までに。


以下大体のサンプルコードを記載する。

サンプルコード
class ContainerViewController: UIViewController {
    fileprivate var vc1:SubViewController!
    fileprivate var vc2:SubViewController!
    fileprivate var viewing:Bool = 0;

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        let sb = UIStoryboard.init(name: "Main", bundle: nil)
        vc1 = sb.instantiateViewController(withIdentifier: "SubViewController") as! SubViewController
        vc2 = sb.instantiateViewController(withIdentifier: "SubViewController") as! SubViewController
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        changeView()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    func changeView() {
        viewing = !viewing
        displayViewController()
    }

    fileprivate func displayViewController() {
        if let displaying = childViewControllers.first {
            hideViewController(vc: displaying)
        }
        if (viewing ) {
            nextVC = vc1
        } else {
            nextVC = vc2
        }
        addChildViewController(nextVC)
        nextVC.view.frame = view.frame
        view.addSubview(nextVC.view)
        nextVC.didMove(toParentViewController: self)
        nextVC.setLabel(text: "VC:" + viewing.description)
    }

    fileprivate func hideViewController(vc: UIViewController) {
        vc.willMove(toParentViewController: nil)
        vc.view.removeFromSuperview()
        vc.removeFromParentViewController()
    }
}

class SubViewController: UIViewController {
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var button: CustomButton!

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @IBAction func buttonTapped(_ sender: Any) {
        if let p = self.parent as? ContainerViewController {
            p.changeView()
        }
    }
    func setLabel(text:String) {
        label.text! = text
    }
}

class CustomButton: UIView {
    @IBOutlet weak var view: UIView!
    @IBOutlet weak var label: ExtendLabel!

    override func awakeFromNib() {
        super.awakeFromNib();
    }

    required init?(coder:NSCoder) {
        super.init(coder: coder)
        Bundle.main.loadNibNamed("CustomButton", owner: self, options: nil)
        self.addSubview(self.view);
    }
}
class ExtendedLabel: UIView {
    @IBOutlet var view: UIView!
    @IBOutlet weak var label: UILabel!

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        Bundle.main.loadNibNamed("ExtendedLabel", owner: self, options: nil)
        self.addSubview(self.view);
    }

    override func awakeFromNib() {
        super.awakeFromNib()
    }
}
4
8
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
8