Init kustom untuk UIViewController di Swift dengan penyiapan antarmuka di storyboard

89

Saya mengalami masalah untuk menulis init kustom untuk subclass UIViewController, pada dasarnya saya ingin meneruskan ketergantungan melalui metode init untuk viewController daripada mengatur properti secara langsung seperti viewControllerB.property = value

Jadi saya membuat init khusus untuk viewController saya dan memanggil init yang ditunjuk super

init(meme: Meme?) {
        self.meme = meme
        super.init(nibName: nil, bundle: nil)
    }

Antarmuka pengontrol tampilan berada di papan cerita, saya juga membuat antarmuka untuk kelas khusus menjadi pengontrol tampilan saya. Dan Swift perlu memanggil metode init ini meskipun Anda tidak melakukan apa pun dalam metode ini. Jika tidak, kompilator akan mengeluh ...

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

Masalahnya adalah ketika saya mencoba memanggil init kustom saya dengan MyViewController(meme: meme)itu tidak init properti di viewController saya sama sekali ...

Saya mencoba men-debug, saya menemukan di viewController saya, init(coder aDecoder: NSCoder)dipanggil terlebih dahulu, kemudian init kustom saya dipanggil nanti. Namun kedua metode init ini mengembalikan selfalamat memori yang berbeda .

Saya mencurigai ada yang salah dengan init untuk viewController saya, dan itu akan selalu kembali selfdengan init?(coder aDecoder: NSCoder), yang, tidak memiliki implementasi.

Apakah ada yang tahu cara membuat init kustom untuk viewController Anda dengan benar? Catatan: antarmuka viewController saya diatur di storyboard

berikut adalah kode viewController saya:

class MemeDetailVC : UIViewController {

    var meme : Meme!

    @IBOutlet weak var editedImage: UIImageView!

    // TODO: incorrect init
    init(meme: Meme?) {
        self.meme = meme
        super.init(nibName: nil, bundle: nil)
    }

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

    override func viewDidLoad() {
        /// setup nav title
        title = "Detail Meme"

        super.viewDidLoad()
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        editedImage = UIImageView(image: meme.editedImage)
    }

}
Pigfly
sumber
apakah Anda mendapatkan solusi untuk ini?
pengguna481610

Jawaban:

50

Seperti yang ditentukan dalam salah satu jawaban di atas, Anda tidak dapat menggunakan metode init kustom dan storyboard.

Tetapi Anda masih dapat menggunakan metode statis untuk membuat instance ViewControllerdari storyboard dan melakukan penyiapan tambahan di atasnya.

Ini akan terlihat seperti ini:

class MemeDetailVC : UIViewController {
    
    var meme : Meme!
    
    static func makeMemeDetailVC(meme: Meme) -> MemeDetailVC {
        let newViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "IdentifierOfYouViewController") as! MemeDetailVC
        
        newViewController.meme = meme
        
        return newViewController
    }
}

Jangan lupa untuk menentukan IdentifierOfYouViewController sebagai pengenal pengontrol tampilan di storyboard Anda. Anda mungkin juga perlu mengubah nama storyboard pada kode di atas.

Alexander Grushevoy
sumber
Setelah MemeDetailVC diinisialisasi, properti "meme" ada. Kemudian injeksi ketergantungan masuk untuk menetapkan nilai ke "meme". Saat ini, loadView () dan viewDidLoad belum dipanggil. Kedua metode tersebut akan dipanggil setelah MemeDetailVC menambahkan / mendorong untuk melihat hierarki.
Jim Yu
Jawaban terbaik disini!
PascalS
Masih diperlukan metode init, dan penyusun akan mengatakan bahwa properti yang disimpan meme belum diinisialisasi
Surendra Kumar
27

Anda tidak dapat menggunakan penginisialisasi khusus saat Anda menginisialisasi dari Storyboard, menggunakan init?(coder aDecoder: NSCoder)cara Apple merancang storyboard untuk menginisialisasi pengontrol. Namun, ada cara untuk mengirim data ke file UIViewController.

Nama pengontrol tampilan Anda ada detaildi dalamnya, jadi saya kira Anda sampai di sana dari pengontrol yang berbeda. Dalam hal ini Anda dapat menggunakan prepareForSeguemetode untuk mengirim data ke detail (Ini Swift 3):

override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "identifier" {
        if let controller = segue.destinationViewController as? MemeDetailVC {
            controller.meme = "Meme"
        }
    }
}

Saya baru saja menggunakan properti tipe Stringalih-alih Memeuntuk tujuan pengujian. Selain itu, pastikan Anda memasukkan pengenal segue yang benar ( "identifier"hanya sebagai placeholder).

Caleb Kleveter
sumber
1
Hai, tapi bagaimana kita bisa menghilangkan penspesifikasian sebagai opsional dan kita juga tidak ingin kesalahan tidak menugaskannya. Kami hanya ingin dependensi yang ketat bermakna bagi pengontrol yang ada di tempat pertama.
Amber K
1
@AmberK Anda mungkin ingin melihat pemrograman antarmuka Anda daripada menggunakan Pembuat Antarmuka.
Caleb Kleveter
Oke, saya juga mencari proyek kickstarter ios untuk referensi juga, belum bisa memberi tahu bagaimana mereka mengirim model tampilan mereka.
Amber K
23

Seperti yang ditunjukkan oleh @Caleb Kleveter, kita tidak dapat menggunakan penginisialisasi khusus saat memulai dari Storyboard.

Namun, kita dapat mengatasi masalah tersebut dengan menggunakan metode factory / class yang membuat instance objek view controller dari Storyboard dan mengembalikan objek view controller. Saya pikir ini cara yang cukup keren.

Catatan: Ini bukan jawaban yang tepat untuk pertanyaan melainkan solusi untuk memecahkan masalah.

Buat metode kelas, di kelas MemeDetailVC, sebagai berikut:

// Considering your view controller resides in Main.storyboard and it's identifier is set to "MemeDetailVC"
class func `init`(meme: Meme) -> MemeDetailVC? {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: "MemeDetailVC") as? MemeDetailVC
    vc?.meme = meme
    return vc
}

Pemakaian:

let memeDetailVC = MemeDetailVC.init(meme: Meme())
Nitesh Borad
sumber
5
Tapi tetap saja kekurangannya ada, properti itu harus opsional.
Amber K
saat menggunakan class func init (meme: Meme) -> MemeDetailVCXcode 10.2 bingung dan memberikan kesalahanIncorrect argument label in call (have 'meme:', expected 'coder:')
Samuël
21

Salah satu cara yang saya lakukan adalah dengan penginisialisasi praktis.

class MemeDetailVC : UIViewController {

    convenience init(meme: Meme) {
        self.init()
        self.meme = meme
    }
}

Kemudian Anda menginisialisasi MemeDetailVC Anda dengan let memeDetailVC = MemeDetailVC(theMeme)

Dokumentasi Apple tentang penginisialisasi cukup bagus, tetapi favorit pribadi saya adalah seri tutorial Ray Wenderlich: Inisialisasi dalam Kedalaman yang akan memberi Anda banyak penjelasan / contoh tentang berbagai pilihan init dan cara yang "tepat" untuk melakukan sesuatu.


EDIT : Meskipun Anda dapat menggunakan penginisialisasi praktis pada pengontrol tampilan kustom, semua orang benar dalam menyatakan bahwa Anda tidak dapat menggunakan penginisialisasi kustom saat menginisialisasi dari storyboard atau melalui segmen storyboard.

Jika antarmuka Anda diatur di storyboard dan Anda membuat pengontrol sepenuhnya secara terprogram, maka penginisialisasi praktis mungkin adalah cara termudah untuk melakukan apa yang Anda coba lakukan karena Anda tidak harus berurusan dengan init yang diperlukan NSCoder (yang masih belum saya mengerti).

Jika Anda mendapatkan pengontrol tampilan melalui storyboard, Anda harus mengikuti jawaban @Caleb Kleveter dan memasukkan pengontrol tampilan ke dalam subkelas yang Anda inginkan, lalu atur propertinya secara manual.

Anak kuda 47
sumber
1
Saya tidak mengerti, jika antarmuka saya diatur di storyboard dan ya saya membuatnya secara terprogram, maka saya harus memanggil metode instantiate menggunakan storyboard untuk memuat antarmuka, bukan? Bagaimana conveniencebantuannya di sini.
Amber K
Kelas dalam swift tidak dapat diinisialisasi dengan memanggil initfungsi lain dari kelas (meskipun struct dapat). Jadi untuk memanggil self.init(), Anda harus menandai init(meme: Meme)sebagai conveniencepenginisialisasi. Jika tidak, Anda harus menyetel sendiri semua properti yang diperlukan dari a UIViewControllerdi penginisialisasi, dan saya tidak yakin apa semua properti itu.
Ponyboy47
ini bukan subclass UIViewController kemudian, karena Anda memanggil self.init () dan bukan super.init (). Anda masih dapat menginisialisasi MemeDetailVC menggunakan init default seperti MemeDetail()dan dalam hal ini kode akan macet
Ali
Ini masih dianggap subclass karena self.init () harus memanggil super.init () dalam implementasinya atau mungkin self.init () secara langsung diwarisi dari induk dalam hal ini secara fungsional setara.
Ponyboy47
6

Awalnya ada beberapa jawaban, yaitu sapi dipilih dan dihapus meskipun pada dasarnya benar. Jawabannya adalah, Anda tidak bisa.

Saat bekerja dari definisi storyboard, semua instance pengontrol tampilan Anda diarsipkan. Jadi, untuk memulai mereka diperlukan yang init?(coder...digunakan. Dari codersinilah semua informasi pengaturan / tampilan berasal.

Jadi, dalam kasus ini, tidak mungkin juga memanggil beberapa fungsi init lainnya dengan parameter khusus. Ini harus ditetapkan sebagai properti saat menyiapkan segue, atau Anda dapat membuang segue dan memuat instance langsung dari storyboard dan mengonfigurasinya (pada dasarnya pola pabrik menggunakan storyboard).

Dalam semua kasus, Anda menggunakan fungsi init SDK yang diperlukan dan meneruskan parameter tambahan setelahnya.

Gerbong
sumber
4

Cepat 5

Anda dapat menulis penginisialisasi khusus seperti ini ->

class MyFooClass: UIViewController {

    var foo: Foo?

    init(with foo: Foo) {
        self.foo = foo
        super.init(nibName: nil, bundle: nil)
    }

    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.foo = nil
    }
}
emrcftci
sumber
3

UIViewControllerkelas sesuai dengan NSCodingprotokol yang didefinisikan sebagai:

public protocol NSCoding {

   public func encode(with aCoder: NSCoder)

   public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER
}    

Jadi UIViewControllermemiliki dua penginisialisasi yang ditunjuk init?(coder aDecoder: NSCoder)dan init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).

Storyborad memanggil init?(coder aDecoder: NSCoder)langsung ke init UIViewControllerdan UIView, Tidak ada ruang bagi Anda untuk menyampaikan parameter.

Salah satu solusi yang rumit adalah dengan menggunakan cache sementara:

class TempCache{
   static let sharedInstance = TempCache()

   var meme: Meme?
}

TempCache.sharedInstance.meme = meme // call this before init your ViewController    

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder);
    self.meme = TempCache.sharedInstance.meme
}
wj2061.dll
sumber
1

Alur yang benar adalah, panggil penginisialisasi yang ditunjuk yang dalam hal ini adalah init dengan nibName,

init(tap: UITapGestureRecognizer)
{
    // Initialise the variables here


    // Call the designated init of ViewController
    super.init(nibName: nil, bundle: nil)

    // Call your Viewcontroller custom methods here

}
Rahul Bansal
sumber
0

Penafian: Saya tidak menganjurkan hal ini dan belum menguji ketahanannya secara menyeluruh, tetapi ini adalah solusi potensial yang saya temukan saat bermain-main.

Secara teknis, inisialisasi kustom dapat dicapai sambil mempertahankan antarmuka yang dikonfigurasi storyboard dengan menginisialisasi pengontrol tampilan dua kali: pertama kali melalui kustom Anda init, dan kedua kalinya di dalam loadView()tempat Anda mengambil tampilan dari storyboard.

final class CustomViewController: UIViewController {
  @IBOutlet private weak var label: UILabel!
  @IBOutlet private weak var textField: UITextField!

  private let foo: Foo!

  init(someParameter: Foo) {
    self.foo = someParameter
    super.init(nibName: nil, bundle: nil)
  }

  override func loadView() {
    //Only proceed if we are not the storyboard instance
    guard self.nibName == nil else { return super.loadView() }

    //Initialize from storyboard
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let storyboardInstance = storyboard.instantiateViewController(withIdentifier: "CustomVC") as! CustomViewController

    //Remove view from storyboard instance before assigning to us
    let storyboardView = storyboardInstance.view
    storyboardInstance.view.removeFromSuperview()
    storyboardInstance.view = nil
    self.view = storyboardView

    //Receive outlet references from storyboard instance
    self.label = storyboardInstance.label
    self.textField = storyboardInstance.textField
  }

  required init?(coder: NSCoder) {
    //Must set all properties intended for custom init to nil here (or make them `var`s)
    self.foo = nil
    //Storyboard initialization requires the super implementation
    super.init(coder: coder)
  }
}

Sekarang di tempat lain di aplikasi Anda, Anda dapat memanggil penginisialisasi kustom Anda seperti CustomViewController(someParameter: foo)dan masih menerima konfigurasi tampilan dari storyboard.

Saya tidak menganggap ini solusi yang bagus karena beberapa alasan:

  • Inisialisasi objek digandakan, termasuk properti pra-init
  • Parameter yang diteruskan ke kustom initharus disimpan sebagai properti opsional
  • Menambahkan boilerplate yang harus dipertahankan saat outlet / properti diubah

Mungkin Anda dapat menerima pengorbanan ini, tetapi gunakan dengan risiko Anda sendiri .

Nathan Hosselton
sumber
0

Meskipun sekarang kita dapat melakukan init khusus untuk pengontrol default di storyboard menggunakan instantiateInitialViewController(creator:) dan untuk segues termasuk hubungan dan pertunjukan.

Kemampuan ini ditambahkan di Xcode 11 dan berikut ini adalah kutipan dari Catatan Rilis Xcode 11 :

Metode pengontrol tampilan yang dianotasi dengan @IBSegueActionatribut baru dapat digunakan untuk membuat pengontrol tampilan tujuan segue dalam kode, menggunakan penginisialisasi kustom dengan nilai yang diperlukan. Ini memungkinkan untuk menggunakan pengontrol tampilan dengan persyaratan inisialisasi non-opsional di storyboard. Buat koneksi dari segue ke @IBSegueActionmetode pada pengontrol tampilan sumbernya. Pada versi OS baru yang mendukung Segue Actions, metode itu akan dipanggil dan nilai yang dikembalikannya akan menjadi destinationViewControllerobjek segue yang diteruskan prepareForSegue:sender:. Beberapa @IBSegueActionmetode dapat didefinisikan pada pengontrol tampilan sumber tunggal, yang dapat mengurangi kebutuhan untuk memeriksa string pengenal segue di prepareForSegue:sender:. (47091566)

Sebuah IBSegueActionmetode membutuhkan hingga tiga parameter: pembuat kode, pengirim, dan pengenal segue. Parameter pertama diperlukan, dan parameter lainnya dapat dihilangkan dari tanda tangan metode Anda jika diinginkan. The NSCoderharus melewati ke initializer tujuan pandangan controller, untuk memastikan itu disesuaikan dengan nilai-nilai dikonfigurasi dalam storyboard. Metode ini mengembalikan pengontrol tampilan yang cocok dengan jenis pengontrol tujuan yang ditentukan di papan cerita, atau nilmenyebabkan pengontrol tujuan diinisialisasi dengan init(coder:)metode standar . Jika Anda tahu Anda tidak perlu mengembalikan nil, jenis pengembalian bisa non-opsional.

Di Swift, tambahkan @IBSegueActionatribut:

@IBSegueAction
func makeDogController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ViewController? {
    PetController(
        coder: coder,
        petName:  self.selectedPetName, type: .dog
    )
}

Di Objective-C, tambahkan IBSegueActiondi depan tipe pengembalian:

- (IBSegueAction ViewController *)makeDogController:(NSCoder *)coder
               sender:(id)sender
      segueIdentifier:(NSString *)segueIdentifier
{
   return [PetController initWithCoder:coder
                               petName:self.selectedPetName
                                  type:@"dog"];
}
malhal
sumber
-1

// View controller ada di Main.storyboard dan memiliki identifier set

Kelas B

class func customInit(carType:String) -> BViewController 

{

let storyboard = UIStoryboard(name: "Main", bundle: nil)

let objClassB = storyboard.instantiateViewController(withIdentifier: "BViewController") as? BViewController

    print(carType)
    return objClassB!
}

Kelas A

let objB = customInit(carType:"Any String")

 navigationController?.pushViewController(objB,animated: true)
kashish makkar
sumber