Pertanyaan Membuat UIView yang dapat digunakan kembali dengan xib (dan memuat dari storyboard)


OK, ada lusinan posting di StackOverflow tentang ini, tetapi tidak ada yang jelas pada solusinya. Saya ingin membuat suatu kebiasaan UIView dengan file xib yang menyertainya. Persyaratannya adalah:

  • Tidak terpisah UIViewController - kelas yang sepenuhnya mandiri
  • Outlet di kelas untuk memungkinkan saya mengatur / mendapatkan properti dari tampilan

Pendekatan saya saat ini untuk melakukan ini adalah:

  1. Mengesampingkan -(id)initWithFrame:

    -(id)initWithFrame:(CGRect)frame {
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:self
                                            options:nil] objectAtIndex:0];
        self.frame = frame;
        return self;
    }
    
  2. Instantiasikan menggunakan program -(id)initWithFrame: di controller pandangan saya

    MyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
    [self.view insertSubview:myCustomView atIndex:0];
    

Ini berfungsi dengan baik (meskipun tidak pernah menelepon [super init] dan hanya mengatur objek menggunakan isi dari nib yang dimuat tampaknya sedikit mencurigakan - ada saran di sini untuk tambahkan subview dalam kasus ini yang juga berfungsi dengan baik). Namun, saya ingin dapat memberi contoh pandangan dari storyboard juga. Jadi saya bisa:

  1. Letakkan sebuah UIView pada tampilan orang tua di papan cerita
  2. Setel kelas kustomnya ke MyCustomView
  3. Mengesampingkan -(id)initWithCoder: - kode yang paling sering saya lihat cocok dengan pola seperti berikut:

    -(id)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(id)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(void)initializeSubviews {
        typeof(view) view = [[[NSBundle mainBundle]
                             loadNibNamed:NSStringFromClass([self class])
                                    owner:self
                                  options:nil] objectAtIndex:0];
        [self addSubview:view];
    }
    

Tentu saja, ini tidak berhasil, karena apakah saya menggunakan pendekatan di atas, atau apakah saya memberi contoh programatis, keduanya berakhir dengan panggilan rekursif -(id)initWithCoder: setelah memasuki -(void)initializeSubviews dan memuat pena dari file.

Beberapa pertanyaan SO lainnya berhubungan dengan ini seperti sini, sini, sini dan sini. Namun, tidak ada jawaban yang diberikan secara memuaskan yang memperbaiki masalah:

  • Sebuah saran umum tampaknya untuk menanamkan seluruh kelas dalam UIViewController, dan melakukan pemuatan nib sana, tetapi ini tampaknya suboptimal bagi saya karena membutuhkan penambahan file lain hanya sebagai pembungkus

Adakah yang bisa memberikan saran tentang cara menyelesaikan masalah ini, dan mendapatkan saluran kerja dalam suatu kebiasaan UIView dengan rewel minim / tidak ada pengontrol pengontrol tipis? Atau apakah ada alternatif, cara yang lebih bersih dalam melakukan hal-hal dengan kode boilerplate minimum?


75
2018-02-20 04:33


asal


Jawaban:


Masalahmu memanggil loadNibNamed: dari (keturunan) initWithCoder:. loadNibNamed: panggilan internal initWithCoder:. Jika Anda ingin mengganti coder storyboard, dan selalu memuat implementasi xib Anda, saya sarankan teknik berikut. Tambahkan properti ke kelas tampilan Anda, dan di file xib, atur ke nilai yang telah ditentukan (di Atribut dengan Definisikasi Buatan Pengguna). Sekarang, setelah menelepon [super initWithCoder:aDecoder]; periksa nilai properti. Jika itu adalah nilai yang telah ditentukan, jangan panggil [self initializeSubviews];.

Jadi, sesuatu seperti ini:

-(instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];

    if (self && self._xibProperty != 666)
    {
        //We are in the storyboard code path. Initialize from the xib.
        self = [self initializeSubviews];

        //Here, you can load properties that you wish to expose to the user to set in a storyboard; e.g.:
        //self.backgroundColor = [aDecoder decodeObjectOfClass:[UIColor class] forKey:@"backgroundColor"];
    }

    return self;
}

-(instancetype)initializeSubviews {
    id view =   [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];

    return view;
}

12
2017-09-11 14:57



Perhatikan bahwa QA ini (seperti banyak) benar-benar hanya kepentingan bersejarah.

Sekarang Selama bertahun-tahun dan sekarang di iOS semuanya hanya tampilan kontainer. Tutorial lengkap di sini

(Memang akhirnya Apple menambahkan Referensi Papan Cerita, beberapa waktu lalu sekarang, membuatnya jauh lebih mudah.)

Inilah storyboard khas dengan penayangan kontainer di mana-mana. Semuanya adalah tampilan kontainer. Itu hanya cara Anda membuat aplikasi.

enter image description here

(Sebagai rasa ingin tahu, jawaban KenC menunjukkan dengan tepat bagaimana, biasanya dilakukan untuk memuat xib ke semacam tampilan pembungkus, karena Anda tidak dapat benar-benar "menetapkan diri".)


26
2017-09-18 11:00



Saya menambahkan ini sebagai posting terpisah untuk memperbarui situasi dengan rilis Swift. Pendekatan yang dijelaskan oleh LeoNatan bekerja dengan sempurna dalam Objective-C. Namun, pemeriksaan waktu kompilasi yang lebih ketat mencegah self ditugaskan ketika memuat dari file xib di Swift.

Akibatnya, tidak ada pilihan selain menambahkan tampilan yang dimuat dari file xib sebagai subkumpulan subkelas UIView khusus, alih-alih mengganti diri sepenuhnya. Ini analog dengan pendekatan kedua yang digariskan dalam pertanyaan awal. Garis besar kasar kelas di Swift menggunakan pendekatan ini adalah sebagai berikut:

@IBDesignable // <- to optionally enable live rendering in IB
class ExampleView: UIView {

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

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

    func initializeSubviews() {
        // below doesn't work as returned class name is normally in project module scope
        /*let viewName = NSStringFromClass(self.classForCoder)*/
        let viewName = "ExampleView"
        let view: UIView = NSBundle.mainBundle().loadNibNamed(viewName,
                               owner: self, options: nil)[0] as! UIView
        self.addSubview(view)
        view.frame = self.bounds
    }

}

Kelemahan dari pendekatan ini adalah pengenalan lapisan redundan tambahan dalam hierarki tampilan yang tidak ada ketika menggunakan pendekatan yang digariskan oleh LeoNatan di Objective-C. Namun, ini bisa dianggap sebagai kejahatan yang diperlukan dan produk dari cara mendasar hal-hal yang dirancang di Xcode (tampaknya masih gila bagi saya bahwa sangat sulit untuk menautkan kelas UIView khusus dengan tata letak UI dengan cara yang bekerja secara konsisten di atas papan cerita dan dari kode) - diganti self grosir di penginisialisasi sebelum tidak pernah tampak seperti cara yang sangat dapat diinterpretasikan dalam melakukan sesuatu, walaupun pada dasarnya memiliki dua kelas tampilan per tampilan juga tidak terlihat bagus.

Meskipun demikian, satu hasil senang dari pendekatan ini adalah bahwa kita tidak lagi perlu mengatur kelas kustom tampilan ke file kelas kami di pembuat antarmuka untuk memastikan perilaku yang benar saat menugaskan ke self, dan begitu panggilan rekursif ke init(coder aDecoder: NSCoder) saat menerbitkan loadNibNamed() rusak (dengan tidak mengatur kelas kustom dalam file xib, yang init(coder aDecoder: NSCoder) dari plain vanilla UIView daripada versi custom kami akan dipanggil sebagai gantinya).

Meskipun kami tidak dapat membuat penyesuaian kelas untuk tampilan yang disimpan di xib secara langsung, kami masih dapat menautkan tampilan ke subkelas UIView 'induk' kami menggunakan outlet / tindakan, dll. Setelah menetapkan pemilik file tampilan ke kelas khusus kami:

Setting the file owner property of the custom view

Sebuah video yang menunjukkan implementasi seperti kelas tampilan langkah demi langkah menggunakan pendekatan ini dapat ditemukan dalam video berikut.


22
2017-12-25 12:59



LANGKAH 1. Mengganti self dari Storyboard

Mengganti self di initWithCoder: metode akan gagal dengan kesalahan berikut.

'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'

Sebagai gantinya, Anda dapat mengganti objek yang di-decode dengan awakeAfterUsingCoder: (tidak awakeFromNib). seperti:

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

LANGKAH 2. Mencegah panggilan rekursif

Tentu saja, ini juga menyebabkan masalah panggilan rekursif. (decoding storyboard -> awakeAfterUsingCoder: -> loadNibNamed: -> awakeAfterUsingCoder: -> loadNibNamed: -> ...)
Jadi Anda harus memeriksa saat ini awakeAfterUsingCoder: disebut dalam proses decoding Storyboard atau proses decoding XIB. Anda memiliki beberapa cara untuk melakukannya:

a) Gunakan pribadi @property yang diatur hanya dalam NIB.

@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end

dan atur "Atribut Runtime Buatan Pengguna" hanya di 'MyCustomView.xib'.

Kelebihan:

  • Tidak ada

Cons:

  • Tidak berfungsi: setXib: akan dipanggil SETELAH  awakeAfterUsingCoder:

b) Periksa apakah self memiliki subview

Biasanya, Anda memiliki subview di xib, tetapi tidak di storyboard.

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(self.subviews.count > 0) {
        // loading xib
        return self;
    }
    else {
        // loading storyboard
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
}

Kelebihan:

  • Tidak ada trik di Interface Builder.

Cons:

  • Anda tidak dapat memiliki sub-gambar di Papan Cerita Anda.

c) Setel bendera statis selama loadNibNamed: panggilan

static BOOL _loadingXib = NO;

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(_loadingXib) {
        // xib
        return self;
    }
    else {
        // storyboard
        _loadingXib = YES;
        typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                           owner:nil
                                                         options:nil] objectAtIndex:0];
        _loadingXib = NO;
        return view;
    }
}

Kelebihan:

  • Sederhana
  • Tidak ada trik di Interface Builder.

Cons:

  • Tidak aman: bendera bersama statis berbahaya

d) Gunakan subkelas pribadi di XIB

Misalnya, nyatakan _NIB_MyCustomView sebagai subkelas dari MyCustomView. Dan, gunakan _NIB_MyCustomView dari pada MyCustomView hanya di XIB Anda.

MyCustomView.h:

@interface MyCustomView : UIView
@end

MyCustomView.m:

#import "MyCustomView.h"

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In Storyboard decoding path.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

@interface _NIB_MyCustomView : MyCustomView
@end

@implementation _NIB_MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In XIB decoding path.
    // Block recursive call.
    return self;
}
@end

Kelebihan:

  • Tidak eksplisit if di MyCustomView

Cons:

  • Awalan _NIB_ trik di Builder Antarmuka xib
  • kode yang relatif lebih banyak

e) Gunakan subclass sebagai placeholder di Storyboard

Mirip dengan d) tetapi gunakan subkelas di Storyboard, kelas asli di XIB.

Di sini, kami menyatakan MyCustomViewProto sebagai subkelas dari MyCustomView.

@interface MyCustomViewProto : MyCustomView
@end
@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In storyboard decoding
    // Returns MyCustomView loaded from NIB.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

Kelebihan:

  • Sangat aman
  • Bersih; Tidak ada kode tambahan di MyCustomView.
  • Tidak eksplisit if periksa sama dengan d)

Cons:

  • Perlu menggunakan subkelas di papan cerita.

kupikir e) adalah strategi yang paling aman dan bersih. Jadi kami mengadopsi itu di sini.

STEP3. Salin properti

Setelah loadNibNamed: di 'awakeAfterUsingCoder:', Anda harus menyalin beberapa properti dari self yang diterjemahkan contoh dari Storyboard. frame dan sifat autolayout / autoresize sangat penting.

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                       owner:nil
                                                     options:nil] objectAtIndex:0];
    // copy layout properities.
    view.frame = self.frame;
    view.autoresizingMask = self.autoresizingMask;
    view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;

    // copy autolayout constraints
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in self.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == self) firstItem = view;
        if(secondItem == self) secondItem = view;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }

    // move subviews
    for(UIView *subview in self.subviews) {
        [view addSubview:subview];
    }
    [view addConstraints:constraints];

    // Copy more properties you like to expose in Storyboard.

    return view;
}

SOLUSI AKHIR

Seperti yang Anda lihat, ini adalah sedikit kode boiler. Kita dapat menerapkannya sebagai 'kategori'. Di sini, saya memperpanjang yang biasa digunakan UIView+loadFromNib kode.

#import <UIKit/UIKit.h>

@interface UIView (loadFromNib)
@end

@implementation UIView (loadFromNib)

+ (id)loadFromNib {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}

- (void)copyPropertiesFromPrototype:(UIView *)proto {
    self.frame = proto.frame;
    self.autoresizingMask = proto.autoresizingMask;
    self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in proto.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == proto) firstItem = self;
        if(secondItem == proto) secondItem = self;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }
    for(UIView *subview in proto.subviews) {
        [self addSubview:subview];
    }
    [self addConstraints:constraints];
}

Menggunakan ini, Anda dapat menyatakan MyCustomViewProto seperti:

@interface MyCustomViewProto : MyCustomView
@end

@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    MyCustomView *view = [MyCustomView loadFromNib];
    [view copyPropertiesFromPrototype:self];

    // copy additional properties as you like.

    return view;
}
@end

XIB:

XIB screenshot

Papan cerita:

Storyboard

Hasil:

enter image description here


16
2017-09-14 18:41



Jangan lupa

Dua poin penting:

  1. Atur Pemilik File dari .xib ke nama kelas dari tampilan kustom Anda.
  2. Jangan atur nama kelas kustom di IB untuk tampilan akar .xib.

Saya datang ke halaman Tanya Jawab ini beberapa kali sambil belajar untuk membuat tampilan yang dapat digunakan kembali. Melupakan hal-hal di atas membuat saya menghabiskan banyak waktu untuk mencari tahu apa yang menyebabkan rekursi tak terbatas terjadi. Poin-poin ini disebutkan dalam jawaban lain di sini dan di tempat lain, tapi saya hanya ingin menekankan kembali di sini.

Jawaban Swift penuh saya dengan langkah-langkahnya adalah sini.


12
2018-01-02 05:38



Ada solusi yang jauh lebih bersih daripada solusi di atas: https://www.youtube.com/watch?v=xP7YvdlnHfA

Tidak ada properti Runtime, tidak ada masalah panggilan rekursif sama sekali. Saya mencobanya dan itu bekerja seperti pesona menggunakan dari storyboard dan dari XIB dengan properti IBOutlet (iOS8.1, XCode6).

Selamat mencoba coding!


2
2018-01-10 09:05