web-dev-qa-db-ja.com

SwiftにXIBを含むカスタムUIViewサブクラス

SwiftとXcode 6.4を使用しています。

したがって、UILabelsとUIImageViewsの複数のペアをいくつか含むビューコントローラがあります。 UILabelとUIImageViewのペアをカスタムUIViewに入れたいので、前述のビューコントローラー内で同じ構造を繰り返し再利用するだけで済みます。 (これがUITableViewに変換される可能性があることは承知していますが、〜学習〜のために私と一緒に我慢してください)。これは、想像していたよりも複雑なプロセスであることが判明しています。これをすべてIBで機能させるための「正しい」方法を理解するのに苦労しています。

現在、UIViewサブクラスと対応するXIBをいじくり回して、init(frame :)とinit(coder)をオーバーライドし、nibからビューをロードして、サブビューとして追加しています。これは私がこれまでインターネットで見た/読んだものです。 (これはおおよそのことです: http://iphonedev.tv/blog/2014/12/15/create-an-ibdesignable-uiview-subclass-with-code-from-an-xib-file-in- xcode-6 )。

これにより、init(coder)とバンドルからnibをロードする間に無限ループが発生するという問題が発生しました。不思議なことに、これらの記事やスタックオーバーフローに関する以前の回答のどれもこれについて言及していません!

では、サブビューがすでに追加されているかどうかを確認するために、init(coder)にチェックを入れます。一見それで「解決」した。しかし、私はカスタムビューアウトレットに値を割り当てようとしたときまでに、それらのビューアウトレットがnilであるという問題に遭遇し始めました。

DidSetを作成し、ブレークポイントを追加して確認しました...それらは確実に1点に設定されていますが、たとえば、ラベルのtextColorを変更しようとするときには、そのラベルはnilです。ここで髪をちぎりました。

再利用可能なコンポーネントはソフトウェアデザイン101のように見えますが、Appleは私に対して陰謀を感じています。ここでコンテナVCを使用する必要がありますか?ビューをネストして、大量の私のメインVCのアウトレット?なぜこれが複雑なのですか?なぜみんなの例が私にとってうまくいかないのですか?

望ましい結果(全体がVCであると仮定して、ボックスは必要なカスタムuiviewです):

enter image description here

読んでくれてありがとう。

以下は、私のカスタムUIViewサブクラスです。私のメインのストーリーボードには、サブクラスがクラスとして設定されたUIViewがあります。

class StageCardView: UIView {

@IBOutlet weak private var stageLabel: UILabel! {
    didSet {
        NSLog("I will murder you %@", stageLabel)
    }
}
@IBOutlet weak private var stageImage: UIImageView!

var stageName : String? {
    didSet {
        self.stageLabel.text = stageName
    }
}
var imageName : String? {
    didSet {
        self.stageImage.image = UIImage(named: imageName!)
    }
}
var textColor : UIColor? {
    didSet {
        self.stageLabel.textColor = textColor
    }
}
var splatColor : UIColor? {
    didSet {
        let splatImage = UIImage(named: "backsplat")?.tintedImageWithColor(splatColor!)
        self.backgroundColor = UIColor(patternImage: splatImage!)
    }
}

// MARK: init
required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    if self.subviews.count == 0 {
        setup()
    }
}

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

func setup() {
    if let view = NSBundle.mainBundle().loadNibNamed("StageCardView", owner: self, options: nil).first as? StageCardView {
        view.frame = bounds
        view.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight

        addSubview(view)
    }
}




/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
    // Drawing code
}
*/

}

編集:これが私がこれまでに得たものです...

XIB:

enter image description here

結果:

enter image description here

問題:ラベルまたは画像のアウトレットにアクセスしようとすると、それらはnilです。上記のアクセスのブレークポイントで確認すると、ラベルと画像のサブビューがあり、ビューの階層は予想どおりです。

それが必要な場合はコードでこれをすべて実行してもかまいませんが、コードでAutolayoutを実行するのは大していないので、それを回避する方法があるのではないでしょうか。

編集/質問シフト:

コンセントをゼロにする方法を見つけました。

enter image description here

これからのインスピレーションSO回答: ロードされたnibですが、ビューアウトレットが設定されていません-InterfaceBuilderの新機能 ビューアウトレットを割り当てる代わりに、個々のコンポーネントアウトレットを割り当てました。

さて、これはちょうど私が壁にたわごとを投げつけて、それがくっつくかどうかを見ているところでした。なぜ私がこれをしなければならなかったのか誰か知っていますか?これはどんなダークマジックですか?

9
Fozzle

私はそれを回避することができましたが、解決策は少しトリッキーです。利益が努力に値するかどうかは議論次第ですが、ここでは私が純粋にインターフェイスビルダーで実装した方法を示します

最初に、_P2View_という名前のカスタムUIViewサブクラスを定義しました

_@IBDesignable class P2View: UIView
{
    @IBOutlet private weak var titleLabel: UILabel!
    @IBOutlet private weak var iconView: UIImageView!

    @IBInspectable var title: String? {
        didSet {
            if titleLabel != nil {
                titleLabel.text = title
            }
        }
    }

    @IBInspectable var image: UIImage? {
        didSet {
            if iconView != nil {
                iconView.image = image
            }
        }
    }

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

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

    override func awakeFromNib()
    {
        super.awakeFromNib()

        let bundle = Bundle(for: type(of: self))

        guard let view = bundle.loadNibNamed("P2View", owner: self, options: nil)?.first as? UIView else {
            return
        }

        view.translatesAutoresizingMaskIntoConstraints = false
        addSubview(view)

        let bindings = ["view": view]

        let verticalConstraints = NSLayoutConstraint.constraints(withVisualFormat:"V:|-0-[view]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: bindings)
        let horizontalConstraints = NSLayoutConstraint.constraints(withVisualFormat:"H:|-0-[view]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: bindings)

        addConstraints(verticalConstraints)
        addConstraints(horizontalConstraints)        
    }

    titleLabel.text = title
    iconView.image = image
}
_

これは、インターフェイスビルダーでは次のようになります。

Xib definition

これが、ストーリーボードで定義されたサンプルビューコントローラーにこのカスタムビューを埋め込む方法です。 _P2View_のプロパティは、属性インスペクターで設定されます。

Custom view embedded in the view controller

言及する価値がある3つのポイントがあります

最初:

NibをロードするときにBundle(for: type(of: self))を使用します。これは、インターフェースビルダーがメインバンドルとメインバンドルが異なる別のプロセスでデザイン可能オブジェクトをレンダリングするためです。

2番目:

_    @IBInspectable var title: String? {
        didSet {
            if titleLabel != nil {
                titleLabel.text = title
            }
        }
    }
_

IBInspectablesIBOutletsを組み合わせる場合は、didSet関数がawakeFromNibメソッドの前に呼び出されることを覚えておく必要があります。そのため、アウトレットは初期化されておらず、アプリはおそらくこの時点でクラッシュします。残念ながら、didSet関数を省略することはできません。インターフェイスビルダーはカスタムビューをレンダリングしないため、ここではこのままにしておく必要があります。

3番目:

_    titleLabel.text = title
    iconView.image = image
_

どういうわけか、コントロールを初期化する必要があります。 didSet関数が呼び出されたときにそれを行うことができなかったため、IBInspectableプロパティに格納された値を使用し、awakeFromNibメソッドの最後でそれらを初期化する必要があります。

これは、Xibにカスタムビューを実装し、ストーリーボードに埋め込み、ストーリーボードに構成し、レンダリングして、クラッシュしないアプリを作成する方法です。ハックが必要ですが、それは可能です。

3

ビューの再利用に関する一般的なアドバイス

そうです、再利用可能で構成可能な要素はソフトウェア101です。InterfaceBuilderはあまり得意ではありません。

特に、xibsとストーリーボードは、コードで定義されたビューを再利用してビューを定義する優れた方法です。ただし、xibやストーリーボード内で再利用したいビューを定義するのにはあまり適していません。 (それは可能ですが、高度な演習です。)

だから、ここに経験則があります。コードから再利用したいビューを定義する場合は、好きなように定義してください。ただし、ストーリーボード内から再利用できるようにするビューを定義している場合は、そのビューをコードで定義します。

したがって、あなたの場合、ストーリーボードから再利用したいカスタムビューを定義しようとしているのであれば、コードでそれを行います。 xibを介してビューを定義する準備ができていない場合は、コードでビューを定義し、そのイニシャライザでxib定義のビューを初期化してサブビューとして構成します。

この場合のアドバイス

おおまかにコードでビューを定義する方法は次のとおりです。

class StageCardView: UIView {
  var stageLabel = UILabel(frame:CGRectZero)
  var stageImage = UIImageView(frame:CGRectZero)
  override init(frame:CGRect) {
   super.init(frame:frame)
   setup()
  }
  required init(coder aDecoder:NSCoder) { 
   super.init(coder:aDecoder)  
   setup()
  }
  private func setup() {
    stageImage.image = UIImage(named:"backsplat")
    self.addSubview(stageLabel)
    self.addSubview(stageImage)
    // configure the initial layout of your subviews here.
  }
}

これで、これをコード内またはストーリーボード経由でインスタンス化できますが、Interface Builderでのライブプレビューはそのままでは得られません。

また、xibで定義されたビューをコード定義のビューに埋め込むことにより、基本的にxibに基づいて再利用可能なビューを定義する方法をおおまかに示します。

class StageCardView: UIView {
  var embeddedView:EmbeddedView!
  override init(frame:CGRect) {
   super.init(frame:frame)
   setup()
  }
  required init(coder aDecoder:NSCoder) { 
   super.init(coder:aDecoder)  
   setup()
  }
  private func setup() {
    self.embeddedView = NSBundle.mainBundle().loadNibNamed("EmbeddedView",owner:self,options:nil).lastObject as! UIView
    self.addSubview(self.embeddedView)
    self.embeddedView.frame = self.bounds
    self.embeddedView.autoresizingMask = .FlexibleHeight | .FlexibleWidth
  }
}

これで、ストーリーボードまたはコードからコード定義ビューを使用できるようになり、nib定義のサブビューが読み込まれます(IBにはまだライブプレビューがありません)。

12
algal