web-dev-qa-db-ja.com

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

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

110

私の完全な例は here ですが、以下に要約を示します。

レイアウト

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

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

enter image description hereコード

次のコードを.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を追加して、クラス名をカスタムクラス名に設定するだけです。

enter image description here

105
Suragch

しばらくの間 Christopher Swaseyのアプローチ は私が見つけた最高のアプローチでした。私のチームの数人の上級開発者にそれについて尋ねたところ、そのうちの一人は完璧な解決策! Christopher Swaseyが雄弁に対処した懸念事項をすべて満たし、定型的なサブクラスコードを必要としません(彼のアプローチに関する私の主な関心事)。 gochaがありますが、それ以外はかなり直感的で実装が簡単です。

  1. .SwiftファイルにカスタムUIViewクラスを作成して、xibを制御します。すなわちMyCustomClass.Swift
  2. .xibファイルを作成し、必要に応じてスタイルを設定します。すなわちMyCustomClass.xib
  3. .xibファイルのFile's Ownerをカスタムクラスに設定します(MyCustomClass
  4. GOTCHA: .xibファイルのカスタムビューのclass値(identity Inspectorの下)を空白のままにします。 したがって、カスタムビューには指定されたクラスはありませんが、指定されたファイルの所有者があります。
  5. 通常のAssistant Editor。を使用する場合と同様に、コンセントを接続します。
    • 注:Connections Inspectorを見ると、参照アウトレットはカスタムクラス(つまりMyCustomClass)ではなく、File's Ownerを参照していることがわかります。 File's Ownerがカスタムクラスとして指定されているため、アウトレットは接続して適切に動作します。
  6. クラス文の前にカスタムクラスに@IBDesignableがあることを確認してください。
  7. カスタムクラスを、以下で参照されるNibLoadableプロトコルに準拠させます。
    • 注:カスタムクラス.Swiftファイル名が.xibファイル名と異なる場合、nibNameプロパティを.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を見逃して、.xibファイル内のclass値をカスタムクラスに設定すると、ストーリーボードに描画されず、実行時にEXC_BAD_ACCESSエラーが発生しますinit?(coder aDecoder: NSCoder)メソッドを使用してnibからクラスを初期化しようとする無限ループに陥り、Self.nib.instantiateを呼び出してinitを再度呼び出すためです。

57
Ben Patch

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

1)UIViewのカスタムサブクラスを作成します([ファイル]-> [新規]-> [ファイル...]-> [Cocoa Touchクラス]に移動できます。[サブクラス:]が[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番目のアイコンに線が入った長方形のように見えます)に移動します。 「カスタムクラス」の下の「クラス」としてサブクラスの名前を入力します。

30
user1021430

(1)autolayout、(2)@IBInspectable、および(3)アウトレットをねじ込んでいるのを見ると、「サブビューとして追加」ソリューションが不十分であることが常にわかっています。代わりに、NSObjectメソッドであるawakeAfter:の魔法を紹介します。

awakeAfterを使用すると、NIB /ストーリーボードから実際に起動されたオブジェクトを別のオブジェクトと完全に交換できます。 Thatその後、オブジェクトはハイドレーションプロセスにかけられ、awakeFromNibが呼び出され、ビューとして追加されます。

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

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

注:NIBファイルのビューに設定されたクラスは同じままです。埋め込み可能なサブクラスは、ストーリーボードで使用されるonlyです。サブクラスを使用してコードでビューをインスタンス化することはできません。したがって、サブクラス自体に追加のロジックはありません。 のみには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のオブジェクトをループするだけです。ジェネリックタイプはreturn値であるため、コールサイトでタイプを指定する必要があることに注意してください。

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
  }
}
26

XIB viewsを使用するためのalternativeView Controllerを使用することを考えています個別のストーリーボードで

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

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

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

5
Michał Ziobro

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

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

5
Ben G

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

  • ストーリーボードにView Controllerを作成します
  • コントローラーのクラスをcontrollerBに設定します
  • ストーリーボードのcontrollerBのビューを削除します
  • 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
otusweb

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

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

これを行うだけです:

  1. BFWControlsフレームワークをインポートする
  2. スーパークラスをUIViewからNibViewに変更します(またはUITableViewCellからNibTableViewCellに変更します)

それでおしまい!

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

詳しくはこちらをご覧ください: 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

トム????

0
barefeettom

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の応答 として準備します。このXIB内のメインビューではなく、ファイルの所有者のクラスMyViewを設定します。

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> = 9.2のこのバグのためにWatchKit拡張機能を使用する場合、ストーリーボードに表示されません: https://forums.developer.Apple .com/thread/95616

0

最も人気のある回答はうまくいきますが、間違っています。それらはすべて、クラスのアウトレットとUIコンポーネント間の接続としてFile's ownerを使用しますが、これは非常に間違っています。 File's ownerは、UIViewsではなく、トップレベルのオブジェクトにのみ使用されることになっています。 Apple開発者ドキュメント をご覧ください。 UIViewをFile's ownerとして使用すると、これらの望ましくない結果につながります。

  1. contentViewのみを使用することになっている場合、selfを使用する必要があります。いです。
  2. Xibごとに1つのUIViewのみを持つことができます。 Xibには複数のUIViewがあると想定されています。

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

0
Ingun