Спеціальний init для UIViewController в Swift з налаштуванням інтерфейсу в раскадровці


89

У мене проблема з написанням власного init для підкласу UIViewController, в основному я хочу передати залежність через метод init для viewController, а не встановлювати властивість безпосередньо, як viewControllerB.property = value

Тож я створив власний init для мого viewController і зателефонував супер призначеному init

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

Інтерфейс контролера перегляду знаходиться в раскадровці, я також зробив інтерфейс для користувацького класу моїм контролером перегляду. І Swift вимагає викликати цей метод init, навіть якщо ви нічого не робите в рамках цього методу. В іншому випадку компілятор скаржиться ...

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

Проблема полягає в тому, що я намагаюся зателефонувати моєму власному init, MyViewController(meme: meme)він взагалі не ініціює властивості в моєму viewController ...

Я намагався налагодити, я знайшов на моєму viewController, init(coder aDecoder: NSCoder)спочатку викликають мене, потім мою власну init викликають пізніше. Однак ці два методи init повертають різні selfадреси пам'яті.

Я підозрюю, що в init для мого viewController щось не так, і він завжди повертається selfз init?(coder aDecoder: NSCoder), який не має реалізації.

Хтось знає, як правильно зробити власний init для вашого viewController? Примітка: інтерфейс мого viewController налаштовано на розкадровці

ось мій viewController код:

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

}

ти отримав рішення для цього?
користувач481610

Відповіді:


49

Як було зазначено в одній із відповідей вище, ви не можете використовувати як власний метод init, так і розкадрування. Але ви все одно можете використовувати статичний метод для створення екземпляра ViewController із розкадрування та виконання додаткових налаштувань на ньому. Це буде виглядати так:

class MemeDetailVC : UIViewController {

    var meme : Meme!

    static func makeMemeDetailVC(meme: Meme) -> MemeDetailVC {
        let newViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("IdentifierOfYouViewController") as! MemeDetailVC

        newViewController.meme = meme

        return newViewController
    }
}

Не забудьте вказати IdentifierOfYouViewController як ідентифікатор контролера перегляду у вашій розкадровці. Можливо, вам також доведеться змінити назву розкадрування у наведеному вище коді.


Після ініціалізації MemeDetailVC властивість "мем" існує. Потім надходить ін’єкція залежностей, щоб присвоїти значення «мему». На даний момент loadView () та viewDidLoad не викликані. Ці два методи будуть викликані після додавання / натискання для перегляду ієрархії MemeDetailVC.
Jim Yu

Найкраща відповідь тут!
PascalS

Потрібен метод init, і компілятор скаже, що збережене мемом майно не ініціалізоване
Surendra Kumar

27

Ви не можете використовувати власний ініціалізатор під час ініціалізації з розкадрування, використовуючи init?(coder aDecoder: NSCoder)те, як Apple спроектувала розкадровку для ініціалізації контролера. Однак існують способи надсилання даних на UIViewController.

У ньому є ім’я вашого контролера перегляду detail, тому я припускаю, що ви потрапите туди з іншого контролера. У цьому випадку ви можете використовувати prepareForSegueметод для надсилання даних до деталей (Це Swift 3):

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

Я просто використовував властивість type, Stringа не Memeдля тестування. Також переконайтеся, що ви ввели правильний ідентифікатор сегмента ( "identifier"був лише заповнювачем).


1
Привіт, але як ми можемо позбутися зазначення як необов’язкового, і ми також не хочемо помилитися, не призначивши його. Ми просто хочемо, щоб в першу чергу існувала сувора залежність, яка має значення для контролера.
Amber K

1
@AmberK Можливо, ви захочете вивчити програмування свого інтерфейсу, а не використовувати Interface Builder.
Калеб Клеветер

Гаразд, я також шукаю проект kickstarter ios для довідки, я ще не можу сказати, як вони надсилають свої моделі перегляду.
Amber K

23

Як зазначав @Caleb Kleveter, ми не можемо використовувати власний ініціалізатор під час ініціалізації з Storyboard.

Але ми можемо вирішити проблему за допомогою методу factory / class, який створює екземпляр об'єкта контролера перегляду з Storyboard і повертає об'єкт контролера перегляду. Я думаю, що це досить крутий спосіб.

Примітка. Це не точна відповідь на запитання, а спосіб вирішення проблеми.

Зробіть метод класу в класі MemeDetailVC таким чином:

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

Використання:

let memeDetailVC = MemeDetailVC.init(meme: Meme())

5
Але все-таки недолік є, властивість має бути необов’язковою.
Amber K

при використанні class func init (meme: Meme) -> MemeDetailVCXcode 10.2 плутається і видає помилкуIncorrect argument label in call (have 'meme:', expected 'coder:')
Самуель

21

Я зробив це за допомогою зручного ініціалізатора.

class MemeDetailVC : UIViewController {

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

Потім ви ініціалізуєте ваш MemeDetailVC за допомогою let memeDetailVC = MemeDetailVC(theMeme)

Документація від Apple щодо ініціалізаторів є досить хорошою, але моїм улюбленим є серія підручників Ray Wenderlich: Initialization in Depth, яка повинна дати вам безліч пояснень / прикладів щодо різних варіантів ініціалізації та "правильного" способу зробити це.


РЕДАГУВАТИ : Хоча ви можете використовувати зручний ініціалізатор на користувацьких контролерах перегляду, всі правильно заявляють, що ви не можете використовувати власні ініціалізатори під час ініціалізації з розкадрування або через розкадрування.

Якщо ваш інтерфейс налаштований на раскадровці, і ви створюєте контролер повністю програмно, то зручний ініціалізатор - це, мабуть, найпростіший спосіб зробити те, що ви намагаєтеся зробити, оскільки вам не потрібно мати справу з необхідним ініціатором за допомогою NSCoder (який я досі не дуже розумію).

Якщо ви отримуєте свій контролер перегляду за допомогою розкадрування, то вам потрібно буде слідувати відповіді @Caleb Kleveter і передати контролер перегляду у бажаний підклас, а потім встановити властивість вручну.


1
Я не розумію, якщо мій інтерфейс налаштований на розкадрування, і я створюю його програмно, тоді я повинен викликати метод instantiate за допомогою розкадрування, щоб завантажити інтерфейс, чи не так? Як convenienceце допоможе тут.
Amber K

Класи в швидкому режимі не можна ініціалізувати, викликавши іншу initфункцію класу (хоча структури можуть). Отже, щоб зателефонувати self.init(), ви повинні позначити init(meme: Meme)як convenienceініціалізатор. В іншому випадку вам доведеться самостійно встановлювати всі необхідні властивості a UIViewControllerв ініціалізаторі, і я не впевнений, що це за всі ці властивості.
Ponyboy47

тоді це не підклас UIViewController, оскільки ви викликаєте self.init (), а не super.init (). ви все ще можете ініціалізувати MemeDetailVC, використовуючи init за замовчуванням, наприклад, MemeDetail()і в такому випадку код вийде з
Алі,

Це все ще вважається підкласифікацією, оскільки self.init () повинен викликати super.init () у своїй реалізації або, можливо, self.init () безпосередньо успадковується від батьків, і в цьому випадку вони функціонально еквівалентні.
Понібой47

6

Спочатку було кілька відповідей, які були проголосовані та видалені коровою, хоча вони були в основному правильними. Відповідь - не можна.

Під час роботи з визначенням розкадрування всі екземпляри контролера перегляду архівуються. Отже, для ініціювання їх потрібно init?(coder...використовувати. coderДе вся інформація настройки / вид приходить.

Отже, у цьому випадку неможливо також викликати деякі інші функції init із користувацьким параметром. Це слід або встановити як властивість під час підготовки серії, або ви можете скинути сегменти та завантажити екземпляри безпосередньо з розкадрування та налаштувати їх (в основному заводський шаблон за допомогою розкадрування).

У всіх випадках ви використовуєте SDK, необхідну функцію init, і надалі передаєте додаткові параметри.


4

Стрімкий 5

Ви можете написати власний ініціалізатор таким чином ->

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

3

UIViewControllerклас відповідає NSCodingпротоколу, який визначається як:

public protocol NSCoding {

   public func encode(with aCoder: NSCoder)

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

Так само UIViewControllerє два призначені ініціалізатори init?(coder aDecoder: NSCoder)і init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).

Storyborad дзвонить init?(coder aDecoder: NSCoder)безпосередньо до init UIViewControllerі UIView, Вам немає місця для передачі параметрів.

Одним громіздким обхідним шляхом є використання тимчасового кешу:

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
}


0

Застереження: Я не виступаю за це і не ретельно перевірив його стійкість, але це потенційне рішення, яке я знайшов під час роздумів.

Технічно, користувацьку ініціалізацію можна досягти, зберігаючи налаштований інтерфейс розкадрування, двічі ініціалізувавши контролер подання: перший раз за вашим замовленням init, а другий раз всередині, loadView()де ви берете вигляд із розкадрування.

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

Тепер десь у вашому додатку ви можете зателефонувати за власним ініціалізатором, як CustomViewController(someParameter: foo) і все одно отримувати конфігурацію подання з розкадрування.

Я не вважаю це чудовим рішенням з кількох причин:

  • Ініціалізація об’єкта повторюється, включаючи будь-які властивості попереднього запуску
  • Параметри, що передаються користувачеві, initповинні зберігатися як додаткові властивості
  • Додає типовий шаблон, який необхідно підтримувати при зміні торгових точок / властивостей

Можливо, ви можете прийняти ці компроміси, але використовуйте на свій страх і ризик .


0

Хоча тепер ми можемо робити спеціальний init для контролерів за замовчуванням у розкадруванні за допомогою instantiateInitialViewController(creator:) та для сег, включаючи відносини та шоу.

Цю можливість було додано до Xcode 11, а далі наведено уривок із приміток до випуску Xcode 11 :

Метод контролера перегляду, @IBSegueActionкотрий коментується новим атрибутом, може бути використаний для створення контролера перегляду місця призначення в коді за допомогою спеціального ініціалізатора з будь-якими необхідними значеннями. Це дає можливість використовувати контролери перегляду з необов’язковими вимогами до ініціалізації в раскадровках. Створіть підключення від сегмента до @IBSegueActionметоду на його контролері подання джерела. У нових версіях ОС, які підтримують Segue Action, буде викликаний цей метод, і значення, яке він повертає, буде значення destinationViewControllerпереданого об'єкта segue prepareForSegue:sender:. @IBSegueActionНа одному контролері подання джерела може бути визначено кілька методів, що може полегшити необхідність перевірки рядків ідентифікаторів segue у prepareForSegue:sender:. (47091566)

IBSegueActionМетод приймає до трьох параметрів: кодер, відправник і ідентифікатори SEGUE в. Перший параметр є обов’язковим, а інші параметри можна пропустити з підпису вашого методу, якщо потрібно. NSCoderПовинен бути переданий на ініціалізатор За думку контролера - адресата, щоб переконатися в правильності його налаштувати з допомогою значень , налаштованих в розкадруванні. Метод повертає контролер подання, який відповідає типу контролера призначення, визначеному в розкадруванні, або nilвикликає ініціалізацію контролера призначення за допомогою стандартного init(coder:)методу. Якщо ви знаєте nil, що повертати не потрібно , тип повернення може бути необов’язковим.

У Swift додайте @IBSegueActionатрибут:

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

У Objective-C додайте IBSegueActionперед типом return:

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

0

Правильний потік, викличте призначений ініціалізатор, який у цьому випадку є init з 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

}

-1

// Контролер перегляду знаходиться в Main.storyboard, і в ньому встановлено ідентифікатор

Клас В

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

Клас А

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

 navigationController?.pushViewController(objB,animated: true)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.