ストーリーボードの外部xibファイルからビューを読み込む


131

ストーリーボードの複数のビューコントローラー全体でビューを使用したい。したがって、外部のxibでビューを設計して、すべてのビューコントローラーに変更が反映されるようにすることを考えました。しかし、どのようにしてストーリーボードの外部xibからビューをロードできますか?そうでない場合、上記の状況に合わせて他にどのような選択肢がありますか?



1
このビデオをご覧ください:youtube.com/watch
v

回答:


132

私の完全な例はここにありますが、以下に要約を提供します。

レイアウト

同じ名前の.swiftファイルと.xibファイルをプロジェクトに追加します。.xibファイルには、カスタムビューレイアウトが含まれます(自動レイアウト制約を使用することをお勧めします)。

swiftファイルをxibファイルの所有者にします。

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

次のコードを.swiftファイルに追加し、.xibファイルからアウトレットとアクションをフックします。

import UIKit
class ResuableCustomView: UIView {

    let nibName = "ReusableCustomView"
    var contentView: UIView?

    @IBOutlet weak var label: UILabel!
    @IBAction func buttonTap(_ sender: UIButton) {
        label.text = "Hi"
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        guard let view = loadViewFromNib() else { return }
        view.frame = self.bounds
        self.addSubview(view)
        contentView = view
    }

    func loadViewFromNib() -> UIView? {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: nibName, bundle: bundle)
        return nib.instantiate(withOwner: self, options: nil).first as? UIView
    }
}

これを使って

ストーリーボードの任意の場所でカスタムビューを使用します。を追加しUIView、クラス名をカスタムクラス名に設定します。

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


3
loadNibNamedがinit(coder :)を呼び出すのではないですか?私はあなたのアプローチを適応させようとしてクラッシュしました。
フィッシュマン'14年

@Fishman、(ストーリーボードからではなく)プログラムでビューをロードしようとすると、現在がないためクラッシュしinit(frame:)ます。詳細については、このチュートリアルを参照してください。
Suragch 2017年

7
クラッシュのもう1つの一般的な原因は、カスタムビューをファイルの所有者に設定していないことです。私の答えの赤い丸を見てください。
Suragch 2017年

5
ええ、私はファイル所有者の代わりにルートビューのクラスを設定しており、それが無限ループを引き起こしていました。
devios1 2017

2
self.addSubview(view)の前にview.autoresizingMask = [.flexibleWidth、.flexibleHeight]行を追加すると、完全に機能します。
Pramod Tapaniya

69

しばらくの間、クリストファー・スウェイジーのアプローチが私が見つけた最良のアプローチでした。私は私のチームの上級開発者の何人かにそれについて尋ねました、そして、彼らのうちの一人は完璧な解決策を持っていまし!Christopher Swaseyが雄弁に取り組んだすべての懸念を満たし、ボイラープレートサブクラスコードを必要としません(彼のアプローチに関する私の主な懸念)。1つの落とし穴がありますが、それ以外はかなり直感的で実装が簡単です。

  1. .swiftファイルにカスタムUIViewクラスを作成して、xibを制御します。すなわちMyCustomClass.swift
  2. .xibファイルを作成し、必要に応じてスタイルを設定します。すなわちMyCustomClass.xib
  3. File's Owner.xibファイルのをカスタムクラスに設定します(MyCustomClass
  4. GOTCHA: .xibファイルのカスタムビューのclass値(の下identity Inspector)を空白のままにします。したがって、カスタムビューには指定されたクラスはありませんが、指定されたファイルの所有者が含まれます。
  5. 通常使用するようにコンセントを接続しますAssistant Editor
    • 注:を見るConnections Inspectorと、参照元のアウトレットがカスタムクラス(つまりMyCustomClass)ではなく、を参照してFile's Ownerいることがわかります。File's Ownerはカスタムクラスとして指定されているため、アウトレットは接続して適切に動作します。
  6. カスタムステートメントのclassステートメントの前に@IBDesignableがあることを確認してください。
  7. カスタムクラスをNibLoadable以下で参照するプロトコルに準拠させます。
    • 注:カスタムクラス.swiftファイル名が.xibnibNameプロパティをファイルの名前に設定し.xibます。
  8. 実装する required init?(coder aDecoder: NSCoder)以下の例のようにしoverride init(frame: CGRect)て呼び出しsetupFromNib()ます。
  9. UIViewを目的のストーリーボードに追加し、クラスをカスタムクラス名(つまりMyCustomClass)に設定します。
  10. IBDesignableが実際に.xibをストーリーボードに描画する様子をご覧ください。

参照するプロトコルは次のとおりです。

public protocol NibLoadable {
    static var nibName: String { get }
}

public extension NibLoadable where Self: UIView {

    public static var nibName: String {
        return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
    }

    public static var nib: UINib {
        let bundle = Bundle(for: Self.self)
        return UINib(nibName: Self.nibName, bundle: bundle)
    }

    func setupFromNib() {
        guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }
        addSubview(view)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
        view.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
        view.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
        view.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
    }
}

そしてMyCustomClass、これはプロトコルを実装する例です(.xibファイルの名前はMyCustomClass.xib):

@IBDesignable
class MyCustomClass: UIView, NibLoadable {

    @IBOutlet weak var myLabel: UILabel!

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupFromNib()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupFromNib()
    }

}

注:Gotchaを見逃しclassて.xibファイル内の値をカスタムクラスに設定すると、ストーリーボードに描画されずEXC_BAD_ACCESS、アプリの実行時にエラーが発生します。init?(coder aDecoder: NSCoder)次に再度Self.nib.instantiate呼び出して呼び出すメソッドを使用してnibからクラスを初期化しようとしていますinit


2
ここではそれに別の偉大なアプローチがあるが、私は上記のものはまだ良いと感じる:medium.com/zenchef-tech-and-product/...
ベン・パッチ

4
上記のアプローチは完全に機能し、ストーリーボードでライブプレビューを適用します。とても便利で素晴らしいです。
Igor Leonovich 2017

1
参考までに、このソリューションは、で制約定義を使用してsetupFromNib()、XIBで作成されたビューを含むテーブルビューのセルの自動サイズ設定に関する特定の奇妙な自動レイアウトの問題を修正しているようです。
ゲイリー

1
断然最高のソリューションです!私は@IBDesignable互換性が大好きです。XViewまたはUIKitがUIViewファイルを追加するときにデフォルトでこのようなものを提供しない理由を信じられません。
fl034

1
これを機能させることができないようです。.xibファイル内でFile Ownerを設定するたびに、カスタムクラスも設定されます。
ドリュースター

32

使用したいxibを作成したと仮定します。

1)UIViewのカスタムサブクラスを作成します([ファイル]-> [新規]-> [ファイル...]-> [Cocoa Touch Class]に移動できます。「のサブクラス:」が「UIView」であることを確認してください)。

2)初期化時にこのビューに、xibに基づくビューをサブビューとして追加します。

Obj-C

-(id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super initWithCoder:aDecoder]) {
        UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:@"YourXIBFilename"
                                                              owner:self
                                                            options:nil] objectAtIndex:0];
        xibView.frame = self.bounds;
        xibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        [self addSubview: xibView];
    }
    return self;
}

Swift 2で

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    let xibView = NSBundle.mainBundle().loadNibNamed("YourXIBFilename", owner: self, options: nil)[0] as! UIView
    xibView.frame = self.bounds
    xibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
    self.addSubview(xibView)
}

Swift 3で

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    let xibView = Bundle.main.loadNibNamed("YourXIBFilename", owner: self, options: nil)!.first as! UIView
    xibView.frame = self.bounds
    xibView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    self.addSubview(xibView)
}

3)ストーリーボードで使用する場所に、通常どおりにUIViewを追加し、新しく追加したビューを選択して、IDインスペクターに移動します(右上の3番目のアイコンで、線が入った長方形のように見えます)。そして、「カスタムクラス」の下の「クラス」としてサブクラスの名前を入力します。


xibView.frame = self.frame;である必要がありxibView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);、そうでない場合xibViewは、ビューがストーリーボードに追加されたときにオフセットしています。
BabyPanda 2016年

パーティーに遅れましたが、彼はそれをxibView.frame = self.boundsに変更したようです。これはオフセットのないフレームです
Heavy_Bullets

20
無限再帰が原因でクラッシュが発生します。nibをロードすると、サブクラスの別のインスタンスが作成されます。
David

2
xibビューのクラスは、この新しいサブクラスと同じであってはなりません。xibがMyClassの場合、この新しいクラスをMyClassContainerにすることができます。
user1021430

上記のソリューションは無限再帰にクラッシュします。
JBarros35

27

(サブビューとして追加する)ソリューションは、(1)オートレイアウト、(2)@IBInspectable、(3)コンセントでねじ込まれるので、満足のいくものではありませんでした。代わりに、私はの魔法をご紹介しましょうawakeAfter:AN、NSObject方法。

awakeAfterNIB /ストーリーボードから実際に起こされたオブジェクトを、別のオブジェクトと完全に入れ替えることができます。その後、そのオブジェクトはハイドレーションプロセスを通過し、awakeFromNibそれを呼び出し、ビューとして追加されます。

これをビューの「厚紙カットアウト」サブクラスで使用できます。その唯一の目的は、NIBからビューをロードして、ストーリーボードで使用するために返すことです。埋め込み可能なサブクラスは、元のクラスではなく、ストーリーボードビューのIDインスペクターで指定されます。これが機能するために実際にサブクラスである必要はありませんが、サブクラスにすることで、IBがIBInspectable / IBOutletプロパティを参照できるようになります。

この余分なボイラープレートは最適とは思えないかもしれませんが、ある意味理想的にUIStoryboardはこれをシームレスに処理するためですが、元のNIBとUIViewサブクラスを完全に変更しないという利点があります。それが果たす役割は、基本的にはアダプタークラスまたはブリッジクラスの役割であり、残念ながら、追加のクラスとして、デザイン的には完全に有効です。反対に、クラスを節約したい場合は、@ BenPatchのソリューションは、他のいくつかの小さな変更を加えたプロトコルを実装することで機能します。どちらのソリューションが優れているかという問題は、プログラマースタイルの問題に要約されます。つまり、オブジェクトの構成と多重継承のどちらを好むかです。

注:NIBファイルのビューに設定されたクラスは同じままです。埋め込み可能なサブクラスは、ストーリーボードでのみ使用されます。サブクラスを使用してコードでビューをインスタンス化することはできないため、サブクラス自体に追加のロジックを含めることはできません。フックのみを含める必要がありawakeAfterます。

class MyCustomEmbeddableView: MyCustomView {
  override func awakeAfter(using aDecoder: NSCoder) -> Any? {
    return (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)! as Any
  }
}

here️ここでの1つの重要な欠点は、ストーリーボードで幅、高さ、またはアスペクト比の制約を定義し、別のビューに関連しない場合、それらを手動でコピーする必要があることです。2つのビューに関連する制約は最も近い共通の祖先にインストールされ、ビューはストーリーボードから裏返しに起こされるため、これらの制約がスーパービューでハイドレートされるときまでに、スワップはすでに発生しています。問題のビューのみを含む制約は、そのビューに直接インストールされるため、コピーが作成されない限り、スワップが発生すると破棄されます。

ここで起こっているのは、ストーリーボードのビューインストールされている制約が、新しくインスタンス化されたビューコピーされていることに注意してください。それらは影響を受けません。

class MyCustomEmbeddableView: MyCustomView {
  override func awakeAfter(using aDecoder: NSCoder) -> Any? {
    let newView = (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)!

    for constraint in constraints {
      if constraint.secondItem != nil {
        newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: newView, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
      } else {
        newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: constraint.constant))
      }
    }

    return newView as Any
  }
}  

instantiateViewFromNibはのタイプセーフな拡張UIViewです。タイプに一致するオブジェクトが見つかるまで、NIBのオブジェクトをループするだけです。ジェネリック型は戻り値なので、呼び出しサイトで型を指定する必要があることに注意してください。

extension UIView {
  public class func instantiateViewFromNib<T>(_ nibName: String, inBundle bundle: Bundle = Bundle.main) -> T? {
    if let objects = bundle.loadNibNamed(nibName, owner: nil) {
      for object in objects {
        if let object = object as? T {
          return object
        }
      }
    }

    return nil
  }
}

それは気が遠くなるようなものです。私が誤っていない場合、これは「ストーリーボード上」でのみ機能します。実行時にコードでそのようなクラスを作成しようとした場合、機能するとは思われません。私は信じている。
Fattie

サブクラスは、すべての意図と目的のために、元のクラスと同様にコードで機能する必要があります。コード内のnibからビューをロードする場合は、同じ手法を使用して直接インスタンス化するだけです。サブクラスが行うことは、コードを使用してnibからビューをインスタンス化し、ストーリーボードが使用するフックに配置することです。
Christopher Swasey 2017年

実は私は間違っ-それがだったでしょう NIBのビューはとてもそのタイプとしてスーパークラスを持つことになりますので、することはできませんあなたがそれをインスタンス化することができれば、同じようにうまく動作しますが、instantiateViewFromNib何も返しません。IMOのどちらの方法でも大したことではありません。サブクラスはストーリーボードにフックするための単なる工夫であり、すべてのコードは元のクラスにある必要があります。
Christopher Swasey

1
すごい!私はxibsの経験がほとんどないので1つ問題がありました(私はストーリーボードとプログラムによるアプローチでしか作業しませんでした)。誰かに役立つ場合に備えて、ここに残します。.xibファイルで、トップレベルのビューを選択し、そのクラス型をにMyCustomView。私のxibでは、デフォルトで左側の内部サイドバーがありませんでした。オンにするには、「表示:iPhone 7」特性コントロールの横にあるボタンが下部/左側にあります。
xaphod 2017年

5
別のオブジェクトに置き換えられると、制約が解除されます。:(
invoodoo 2017

6

現在、最善の解決策は、xibで定義されたビューでカスタムビューコントローラーを使用し、ビューコントローラーを追加するときにストーリーボード内にXcodeが作成する「ビュー」プロパティを削除することです(名前を設定することを忘れないでください)ただし、カスタムクラス)。

これにより、ランタイムは自動的にxibを探してロードします。このトリックは、あらゆる種類のコンテナービュー、またはコンテンツビューに使用できます。


5

別の絵コンテalternativeで使用XIB viewsすることを考えています。View Controller

次に、メインストーリーボードで、カスタムビューの代わりに、このカスタムビューコントローラーを使用container viewEmbed Segueて、メインストーリーボードの他のビュー内に配置するビューを指定します。StoryboardReference

次に、segueの準備を通じて、この埋め込みViewControllerとメインView Controller間の委任と通信を設定できます。このアプローチはUIViewの表示とは異なりますが、(プログラミングの観点から)はるかにシンプルで効率的に同じ目的を達成できます。つまり、メインストーリーボードに表示される再利用可能なカスタムビューがあります。

追加の利点は、CustomViewControllerクラスにロジックを実装でき、個別の(プロジェクトでは見つけにくい)コントローラークラスを作成することなく、コンポーネントを使用してメインのUIViewControllerにボイラープレートコードを配置することなく、すべての委任とビューの準備を設定できることです。これは、再利用可能なコンポーネントの元に適していると思います。他のビューに埋め込むことができる音楽プレーヤーコンポーネント(ウィジェットのような)。


確かに残念ながら、カスタムビューxibを使用するよりもはるかにシンプルなソリューション:(
Wilson

1
カスタムUIViewでアップルのxibライフサイクル作成チェーンを使用してサークルに入った後、これが最終的に私が行った方法です。
LightningStryk

カスタムビューを動的に追加する場合でも、個別のビューコントローラー(最も好ましくはXIB)を使用する必要があります
Satyam

5

最も人気のある回答は問題なく機能しますが、概念的には間違っています。これらはすべてFile's owner、クラスのアウトレットとUIコンポーネント間の接続として使用されます。s File's ownerではなくトップレベルのオブジェクトにのみ使用されることになっていますUIViewApple開発者向けドキュメントを確認してください。UIViewを使用File's ownerすると、これらの望ましくない結果につながります。

  1. 使用するcontentViewはずの場所での使用を余儀なくされていますself。中間ビューではデータ構造がUI構造を伝達できないため、醜いだけでなく構造的にも誤りです。宣言型UIの逆のようなものです。
  2. Xibごとに1つのUIViewのみを使用できます。Xibは複数のUIViewを持っていることになっています。

を使用せずにそれを行うためのエレガントな方法がありFile's ownerます。こちらのブログ投稿をご確認ください。それを正しく行う方法を説明します。


それが悪い理由を説明しなかったとしても、それはまったく問題ではありません。理由がわかりません。副作用はありません。
JBarros35

1

ここにあなたがずっと望んでいた答えがあります。CustomViewクラスを作成し、すべてのサブビューとアウトレットを備えたxibにそのクラスのマスターインスタンスを置くことができます。次に、そのクラスをストーリーボードまたは他のxibのインスタンスに適用できます。

ファイルのオーナーをいじったり、アウトレットをプロキシに接続したり、xibを特別な方法で変更したり、カスタムビューのインスタンスをそれ自体のサブビューとして追加したりする必要はありません。

これを行うだけです:

  1. BFWControlsフレームワークのインポート
  2. あなたのスーパークラスを変更UIViewするNibView(またはからUITableViewCellNibTableViewCell

それでおしまい!

ストーリーボードでの設計時にカスタムビュー(xibからのサブビューを含む)を参照するためにIBDesignableとも連携します。

詳しくは、https//medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155をご覧ください。

また、オープンソースのBFWControlsフレームワークは、https://github.com/BareFeetWare/BFWControlsから入手でき ます。

そしてNibReplaceable、興味がある場合に備えて、それを駆動するコードの簡単な抜粋を次に示します。https:
 //gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4

トム👣


気の利いたソリューション、ありがとう!
avee

フレームワークをネイティブで提供する必要があるときに、フレームワークをインストールしたくありません。
JBarros35

0

このソリューションは、クラスにXIBと同じ名前がない場合でも使用できます。たとえば、XIB名がcontrollerA.xibであるベースビューコントローラークラスcontrollerAがあり、これをcontrollerBでサブクラス化し、ストーリーボードにcontrollerBのインスタンスを作成する場合、次のことができます。

  • ストーリーボードにビューコントローラを作成する
  • コントローラのクラスをコントローラBに設定します
  • ストーリーボード内のコントローラーBのビューを削除する
  • controllerAの負荷ビューを次のようにオーバーライドします。

*

- (void) loadView    
{
        //according to the documentation, if a nibName was passed in initWithNibName or
        //this controller was created from a storyboard (and the controller has a view), then nibname will be set
        //else it will be nil
        if (self.nibName)
        {
            //a nib was specified, respect that
            [super loadView];
        }
        else
        {
            //if no nib name, first try a nib which would have the same name as the class
            //if that fails, force to load from the base class nib
            //this is convenient for including a subclass of this controller
            //in a storyboard
            NSString *className = NSStringFromClass([self class]);
            NSString *pathToNIB = [[NSBundle bundleForClass:[self class]] pathForResource: className ofType:@"nib"];
            UINib *nib ;
            if (pathToNIB)
            {
                nib = [UINib nibWithNibName: className bundle: [NSBundle bundleForClass:[self class]]];
            }
            else
            {
                //force to load from nib so that all subclass will have the correct xib
                //this is convenient for including a subclass
                //in a storyboard
                nib = [UINib nibWithNibName: @"baseControllerXIB" bundle:[NSBundle bundleForClass:[self class]]];
            }

            self.view = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
       }
}

0

Ben Patchの応答に記載されている手順に従ったObjective-Cのソリューション。

UIViewの拡張機能を使用します。

@implementation UIView (NibLoadable)

- (UIView*)loadFromNib
{
    UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
    xibView.translatesAutoresizingMaskIntoConstraints = NO;
    [self addSubview:xibView];
    [xibView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
    [xibView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
    [xibView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES;
    [xibView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES;
    return xibView;
}

@end

ファイルを作成しMyView.hMyView.mそしてMyView.xib

最初にMyView.xibBen Patchの応答が言っているようMyViewに、このXIB内のメインビューではなく、Fileの所有者のクラスを設定してください。

MyView.h

#import <UIKit/UIKit.h>

IB_DESIGNABLE @interface MyView : UIView

@property (nonatomic, weak) IBOutlet UIView* someSubview;

@end

MyView.m

#import "MyView.h"
#import "UIView+NibLoadable.h"

@implementation MyView

#pragma mark - Initializers

- (id)init
{
    self = [super init];
    if (self) {
        [self loadFromNib];
        [self internalInit];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self loadFromNib];
        [self internalInit];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self loadFromNib];
    }
    return self;
}

- (void)awakeFromNib
{
    [super awakeFromNib];
    [self internalInit];
}

- (void)internalInit
{
    // Custom initialization.
}

@end

その後、プログラムでビューを作成します。

MyView* view = [[MyView alloc] init];

警告!XcodeのこのバグのためにWatchKit拡張機能を使用している場合、このビューのプレビューはストーリーボードに表示されません> = 9.2:https ://forums.developer.apple.com/thread/95616


Swiftバージョンを提供する必要があります。現時点では、ObjCを使用している人はごくわずかです
Satyam
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.