Bagaimana cara membuat kelas tampilan iOS kustom dan membuat instance banyak salinannya (dalam IB)?

114

Saat ini saya membuat aplikasi yang akan memiliki banyak timer, yang pada dasarnya semuanya sama.

Saya ingin membuat kelas khusus yang menggunakan semua kode untuk pengatur waktu serta tata letak / animasi, jadi saya dapat memiliki 5 pengatur waktu identik yang beroperasi secara independen satu sama lain.

Saya membuat tata letak menggunakan IB (xcode 4.2) dan semua kode untuk pengatur waktu saat ini hanya di kelas viewcontroller.

Saya mengalami kesulitan membungkus otak saya tentang cara merangkum semuanya ke dalam kelas khusus dan kemudian menambahkannya ke viewcontroller, bantuan apa pun akan sangat dihargai.

Michael Campsall
sumber
Bagi siapa pun yang mencari Google di sini ... sekarang sudah lima tahun sejak penayangan kontainer datang ke iOS! Tampilan wadah adalah ide dasar dan sentral tentang bagaimana Anda melakukan segala sesuatu di iOS sekarang. Mereka sangat mudah digunakan, dan ada sejumlah tutorial yang sangat baik (berusia lima tahun!) Tentang tampilan kontainer di sekitar, misalnya , contoh
Fattie
2
@JoeBlow Kecuali Anda tidak dapat menggunakan tampilan kontainer dalam UITableViewCellatau UICollectionViewCell. Dalam kasus saya, saya memerlukan tampilan kecil namun cukup kompleks yang dapat saya gunakan berkali-kali dalam tampilan pengontrol dan koleksi. Dan para desainer terus mengerjakannya kembali, jadi saya ingin satu tempat untuk menentukan tata letak. Oleh karena itu, sebuah ujung pena.
Eselon

Jawaban:

142

Nah untuk menjawab secara konseptual, pengatur waktu Anda kemungkinan besar harus menjadi subkelas, UIViewbukan NSObject.

Untuk membuat instance dari UIViewpengatur waktu Anda di IB cukup seret dan letakkan di tampilan pengontrol tampilan Anda, dan setel kelasnya ke nama kelas pengatur waktu Anda.

masukkan deskripsi gambar di sini

Ingat #importkelas pengatur waktu di pengontrol tampilan Anda.

Sunting: untuk desain IB (untuk contoh kode lihat riwayat revisi)

Saya sama sekali tidak begitu paham dengan storyboard, tetapi saya tahu bahwa Anda dapat membuat antarmuka Anda di IB menggunakan .xibfile yang hampir identik dengan versi storyboard; Anda bahkan harus dapat menyalin & menempel tampilan Anda secara keseluruhan dari antarmuka yang ada ke .xibfile.

Untuk menguji ini saya membuat kosong baru .xibbernama "MyCustomTimerView.xib". Kemudian saya menambahkan tampilan, dan untuk itu menambahkan label dan dua tombol. Seperti Jadi:

masukkan deskripsi gambar di sini

Saya membuat subclassing kelas tujuan-C baru UIViewbernama "MyCustomTimer". Di kelas saya, .xibsaya menetapkan kelas Pemilik File menjadi MyCustomTimer . Sekarang saya bebas untuk menghubungkan tindakan dan outlet seperti tampilan / pengontrol lainnya. File yang dihasilkan .hterlihat seperti ini:

@interface MyCustomTimer : UIView
@property (strong, nonatomic) IBOutlet UILabel *displayLabel;
@property (strong, nonatomic) IBOutlet UIButton *startButton;
@property (strong, nonatomic) IBOutlet UIButton *stopButton;
- (IBAction)startButtonPush:(id)sender;
- (IBAction)stopButtonPush:(id)sender;
@end

Satu-satunya rintangan yang tersisa untuk dilompati adalah mendapatkan ini .xibdi UIViewsubkelas saya . Menggunakan secara .xibdramatis mengurangi pengaturan yang diperlukan. Dan karena Anda menggunakan papan cerita untuk memuat pengatur waktu, kami tahu -(id)initWithCoder:bahwa satu-satunya penginisialisasi yang akan dipanggil. Jadi seperti inilah file implementasinya:

#import "MyCustomTimer.h"
@implementation MyCustomTimer
@synthesize displayLabel;
@synthesize startButton;
@synthesize stopButton;
-(id)initWithCoder:(NSCoder *)aDecoder{
    if ((self = [super initWithCoder:aDecoder])){
        [self addSubview:
         [[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" 
                                        owner:self 
                                      options:nil] objectAtIndex:0]];
    }
    return self;
}
- (IBAction)startButtonPush:(id)sender {
    self.displayLabel.backgroundColor = [UIColor greenColor];
}
- (IBAction)stopButtonPush:(id)sender {
    self.displayLabel.backgroundColor = [UIColor redColor];
}
@end

Metode bernama loadNibNamed:owner:options:melakukan persis seperti namanya. Ini memuat Nib dan menetapkan properti "Pemilik File" ke dirinya sendiri. Kami mengekstrak objek pertama dalam array dan itu adalah tampilan root dari Nib. Kami menambahkan tampilan sebagai subview dan Voila itu di layar.

Jelas ini hanya mengubah warna latar belakang label saat tombol ditekan, tetapi contoh ini akan membantu Anda.

Catatan berdasarkan komentar:

Perlu dicatat bahwa jika Anda mendapatkan masalah rekursi tak terbatas, Anda mungkin melewatkan trik halus dari solusi ini. Itu tidak melakukan apa yang Anda pikir dilakukannya. Tampilan yang diletakkan di storyboard tidak terlihat, melainkan memuat tampilan lain sebagai subview. Tampilan yang dimuatnya adalah tampilan yang ditentukan di ujung pena. "Pemilik file" di ujung pena adalah tampilan yang tak terlihat itu. Bagian yang keren adalah bahwa tampilan tak terlihat ini masih merupakan kelas Objective-C yang dapat digunakan sebagai semacam pengontrol tampilan untuk tampilan yang dibawa dari ujung pena. Misalnya, IBActionmetode di MyCustomTimerkelas adalah sesuatu yang Anda harapkan lebih banyak di pengontrol tampilan daripada di tampilan.

Sebagai catatan tambahan, beberapa orang mungkin berpendapat bahwa ini rusak MVCdan saya agak setuju. Dari sudut pandang saya, ini lebih terkait dengan kebiasaan UITableViewCell, yang terkadang juga harus menjadi pengontrol bagian.

Perlu juga dicatat bahwa jawaban ini memberikan solusi yang sangat spesifik; buat satu ujung yang bisa dibuat instance-nya beberapa kali pada tampilan yang sama seperti yang ditata di papan cerita. Misalnya, Anda dapat dengan mudah membayangkan enam dari pengatur waktu ini semuanya di layar iPad sekaligus. Jika Anda hanya perlu menentukan tampilan untuk pengontrol tampilan yang akan digunakan beberapa kali di seluruh aplikasi Anda, maka solusi yang diberikan oleh jyavenard untuk pertanyaan ini hampir pasti merupakan solusi yang lebih baik untuk Anda.

NJones
sumber
Ini sebenarnya tampak seperti pertanyaan terpisah. Tapi saya pikir itu bisa dijawab dengan cepat, jadi begini. Setiap kali Anda membuat pemberitahuan baru, itu adalah pemberitahuan baru tetapi jika Anda membutuhkan mereka untuk memiliki nama yang sama ingin menambahkan sesuatu untuk membedakan mereka kemudian gunakan userInfoproperti pada pemberitahuan@property(nonatomic,copy) NSDictionary *userInfo;
NJones
28
Saya mendapatkan rekursi tak terbatas dari initWithCoder. Ada ide?
menghilang
6
Maaf untuk menghidupkan kembali utas lama, memastikan File's Ownerdisetel ke nama kelas Anda memecahkan masalah rekursi tak terbatas saya.
themanatuf
20
Alasan lain untuk rekursi tak terbatas adalah menyetel tampilan root di xib ke kelas kustom Anda. Anda tidak ingin melakukan itu, biarkan sebagai "UIView" default
XY
11
Hanya ada satu hal yang menarik perhatian saya tentang pendekatan ini ... Tidakkah ini mengganggu siapa pun bahwa 2 Tampilan berada di kelas yang sama sekarang? File Asli .h / .m mewarisi dari UIView menjadi yang pertama dan dengan melakukan [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MyCustomTimerView" owner:self options:nil] objectAtIndex:0]]; Menambahkan UIView Kedua ... Ini agak terlihat aneh bagi saya tidak ada orang lain yang merasakan hal yang sama? Jika demikian, apakah ini sesuatu yang dibuat dengan sengaja oleh apel? Atau apakah solusi yang ditemukan seseorang dengan melakukan ini?
SH
137

Contoh cepat

Diperbarui untuk Xcode 10 dan Swift 4

Berikut ini panduan dasar. Saya awalnya belajar banyak tentang ini dari menonton serial video Youtube ini . Kemudian saya memperbarui jawaban saya berdasarkan artikel ini .

Tambahkan file tampilan kustom

Dua file berikut akan membentuk tampilan kustom Anda:

  • .xib untuk memuat tata letak
  • file .swift sebagai UIViewsubclass

Detail untuk menambahkannya ada di bawah.

File Xib

Tambahkan file .xib ke proyek Anda (File> New> File ...> User Interface> View) . Saya menelepon milik saya ReusableCustomView.xib.

Buat tata letak yang Anda inginkan untuk tampilan kustom Anda. Sebagai contoh, saya akan membuat layout dengan a UILabeldan a UIButton. Merupakan ide yang baik untuk menggunakan tata letak otomatis sehingga segala sesuatunya akan berubah ukuran secara otomatis tidak peduli berapa ukuran yang Anda atur nanti. (Saya menggunakan Bentuk Bebas untuk ukuran xib di inspektur Atribut sehingga saya dapat menyesuaikan metrik yang disimulasikan, tetapi ini tidak perlu.)

masukkan deskripsi gambar di sini

File cepat

Tambahkan file .swift ke proyek Anda (File> New> File ...> Source> Swift File) . Ini adalah subkelas dari UIViewdan saya memanggil milik saya ReusableCustomView.swift.

import UIKit
class ResuableCustomView: UIView {

}

Jadikan file Swift sebagai pemiliknya

Kembali ke file .xib Anda dan klik "Pemilik File" di Garis Besar Dokumen. Di Identity Inspector, tulis nama file .swift Anda sebagai nama kelas kustom.

masukkan deskripsi gambar di sini

Tambahkan Kode Tampilan Kustom

Ganti konten ReusableCustomView.swiftfile dengan kode berikut:

import UIKit

@IBDesignable
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)
        commonInit()
    }

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

    func commonInit() {
        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
    }
}

Pastikan untuk mendapatkan ejaan yang tepat untuk nama file .xib Anda.

Hubungkan Outlet dan Tindakan

Hubungkan outlet dan tindakan dengan kontrol menyeret dari label dan tombol di tata letak xib ke kode tampilan kustom cepat.

Gunakan tampilan kustom Anda

Tampilan kustom Anda sudah selesai sekarang. Yang harus Anda lakukan adalah menambahkan apa UIViewpun yang Anda inginkan di storyboard utama Anda. Tetapkan nama kelas tampilan ReusableCustomViewdi Identity Inspector.

masukkan deskripsi gambar di sini

Suragch
sumber
31
Penting untuk tidak menyetel kelas tampilan Xib sebagai ResuableCustomView, tetapi Pemilik Xib ini. Jika tidak, Anda akan mengalami error.
RealNmae
5
Ya, pengingat yang bagus. Saya tidak tahu berapa banyak waktu yang saya buang untuk mencoba memecahkan masalah rekursi tak terbatas yang disebabkan oleh pengaturan tampilan Xib (bukan Pemilik File) ke nama kelas.
Suragch
2
@TomCalmon, saya membuat ulang proyek ini di Xcode 8 dengan Swift 3 dan Tata Letak Otomatis berfungsi dengan baik untuk saya.
Suragch
6
Jika ada orang lain yang memiliki masalah dengan tampilan rendering di IB, saya menemukan bahwa mengubah Bundle.mainke Bundle(for: ...)melakukan triknya. Rupanya, Bundle.maintidak tersedia pada waktu desain.
crizzis
4
Alasan mengapa ini tidak berfungsi dengan @IBDesignable adalah karena Anda tidak menerapkannya init(frame:). Setelah Anda menambahkan ini (dan menarik semua kode inisialisasi ke dalam beberapa commonInit()fungsi), ini berfungsi dengan baik dan ditampilkan di IB. Lihat info lebih lanjut di sini: stackoverflow.com/questions/31265906/…
Dimitris
39

Jawaban untuk pengontrol tampilan, bukan tampilan :

Ada cara yang lebih mudah untuk memuat xib dari storyboard. Katakanlah pengontrol Anda adalah jenis MyClassController yang diwarisi dari UIViewController.

Anda menambahkan UIViewControllermenggunakan IB di storyboard Anda; ubah tipe kelas menjadi MyClassController. Hapus tampilan yang telah ditambahkan secara otomatis di storyboard.

Pastikan XIB yang ingin Anda panggil disebut MyClassController.xib.

Saat kelas akan dibuat instance-nya selama pemuatan storyboard, xib akan dimuat secara otomatis. Alasan untuk ini adalah karena implementasi default UIViewControlleryang memanggil XIB yang dinamai dengan nama kelas.

jyavenard.dll
sumber
1
Saya mengembangkan tampilan kustom sepanjang waktu, tetapi tidak pernah tahu trik kecil ini untuk memuatnya ke storyboard. BANYAK dihargai!
MrTristan
38
Pertanyaannya adalah tentang pandangan, bukan tentang pengontrol tampilan.
Ricardo
Menambahkan judul untuk klarifikasi, "Jawaban untuk pengontrol tampilan, bukan tampilan:"
nacho4d
1
Tampaknya tidak berfungsi di iOS 9.1. Jika saya menghapus tampilan yang dibuat otomatis (default), itu macet setelah kembali dari viewDidLoad VC. Saya tidak berpikir tampilan di XIB terhubung di tempat tampilan default.
Jeff
1
Pengecualian yang saya dapatkan adalah 'Could not load NIB in bundle: 'NSBundle </Users/me/.../MyAppName.app> (loaded)' with name 'uB0-aR-WjG-view-4OA-9v-tyV''. Perhatikan whacko nibName. Saya menggunakan nama kelas yang benar untuk di ujung pena.
Jeff
16

Ini sebenarnya bukan jawaban, tetapi saya pikir akan sangat membantu untuk membagikan pendekatan ini.

Objective-C

  1. Impor CustomViewWithXib.h dan CustomViewWithXib.m ke proyek Anda
  2. Buat file tampilan kustom dengan nama yang sama (.h / .m / .xib)
  3. Mewarisi kelas khusus Anda dari CustomViewWithXib

Cepat

  1. Impor CustomViewWithXib.swift ke proyek Anda
  2. Buat file tampilan kustom dengan nama yang sama (.swift dan .xib)
  3. Mewarisi kelas khusus Anda dari CustomViewWithXib

Pilihan :

  1. Buka file xib Anda, atur pemilik dengan nama kelas kustom Anda jika Anda perlu menghubungkan beberapa elemen (untuk lebih jelasnya lihat bagian Menjadikan file Swift pemilik jawaban @Suragch)

Itu saja, sekarang Anda dapat menambahkan tampilan kustom Anda ke storyboard Anda dan itu akan ditampilkan :)

CustomViewWithXib.h :

 #import <UIKit/UIKit.h>

/**
 *  All classes inherit from CustomViewWithXib should have the same xib file name and class name (.h and .m)
 MyCustomView.h
 MyCustomView.m
 MyCustomView.xib
 */

// This allows seeing how your custom views will appear without building and running your app after each change.
IB_DESIGNABLE
@interface CustomViewWithXib : UIView

@end

CustomViewWithXib.m :

#import "CustomViewWithXib.h"

@implementation CustomViewWithXib

#pragma mark - init methods

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // load view frame XIB
        [self commonSetup];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        // load view frame XIB
        [self commonSetup];
    }
    return self;
}

#pragma mark - setup view

- (UIView *)loadViewFromNib {
    NSBundle *bundle = [NSBundle bundleForClass:[self class]];

    //  An exception will be thrown if the xib file with this class name not found,
    UIView *view = [[bundle loadNibNamed:NSStringFromClass([self class])  owner:self options:nil] firstObject];
    return view;
}

- (void)commonSetup {
    UIView *nibView = [self loadViewFromNib];
    nibView.frame = self.bounds;
    // the autoresizingMask will be converted to constraints, the frame will match the parent view frame
    nibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    // Adding nibView on the top of our view
    [self addSubview:nibView];
}

@end

CustomViewWithXib.swift :

import UIKit

@IBDesignable
class CustomViewWithXib: UIView {

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

        commonSetup()
    }

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

        commonSetup()
    }

    // MARK: setup view 

    private func loadViewFromNib() -> UIView {
        let viewBundle = NSBundle(forClass: self.dynamicType)
        //  An exception will be thrown if the xib file with this class name not found,
        let view = viewBundle.loadNibNamed(String(self.dynamicType), owner: self, options: nil)[0]
        return view as! UIView
    }

    private func commonSetup() {
        let nibView = loadViewFromNib()
        nibView.frame = bounds
        // the autoresizingMask will be converted to constraints, the frame will match the parent view frame
        nibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
        // Adding nibView on the top of our view
        addSubview(nibView)
    }
}

Anda dapat menemukan beberapa contoh di sini .

Semoga membantu.

HamzaGhazouani
sumber
jadi bagaimana kita dapat menambahkan iboutlet untuk tampilan uicontrols
Amr Angry
1
Hai @AmrAngry, untuk menghubungkan tampilan Anda harus melakukan 4 langkah: Buka file xib Anda, di "Pemilik File" atur kelas Anda, dan seret dan lepas tampilan Anda ke antarmuka dengan mengetuk sentuh "ctrl", saya memperbarui sumber kode dengan contoh ini, jangan ragu jika Anda memiliki pertanyaan, semoga membantu
HamzaGhazouani
terima kasih banyak untuk replay Anda, saya sudah melakukan ini tetapi saya rasa masalah saya tidak ada di bagian ini. Masalah saya adalah saya menambahkan UIDatePIcker dan mencoba menambahkan tindakan target untuk kontrol ini dari ViewController untuk mengatur dan acara uiControleer adalah nilai berubah, tetapi metode ini tidak pernah dipanggil / api
Amr Angry
1
@AmrAngry Saya memperbarui contoh dengan menambahkan tampilan pemetik, semoga bisa membantu Anda :) github.com/HamzaGhazouani/Stackoverflow/tree/master/…
HamzaGhazouani
1
Terima kasih saya sudah mencari ini untuk waktu yang lama.
MH175