Пользовательский 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 требует вызова этого метода инициализации, даже если вы ничего не делаете в этом методе. Иначе компилятор пожалуется ...

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

Проблема в том, что когда я пытаюсь вызвать свой собственный init, MyViewController(meme: meme)он вообще не запускает свойства в моем viewController ...

Я пытался отладить, я обнаружил в своем viewController, init(coder aDecoder: NSCoder)сначала вызывается , а затем мой собственный init вызывается позже. Однако эти два метода инициализации возвращают разные selfадреса памяти.

Я заподозрив что - то не так с моей инициализации для 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)
    }

}
Свинка
источник
вы нашли решение для этого?
user481610 03

Ответы:

49

Как было указано в одном из ответов выше, вы не можете использовать как собственный метод инициализации, так и раскадровку. Но вы по-прежнему можете использовать статический метод для создания экземпляра 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 для просмотра иерархии.
Джим Ю
Лучший ответ здесь!
PascalS
По-прежнему требуется метод инициализации, и компилятор скажет, что хранимое свойство мем не было инициализировано
Сурендра Кумар
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"
        }
    }
}

Я просто использовал свойство типа, Stringа не Memeдля целей тестирования. Кроме того, убедитесь, что вы передаете правильный идентификатор перехода ( "identifier"был просто заполнителем).

Калеб Клветер
источник
1
Привет, но как мы можем избавиться от указания как необязательного, и мы также не хотим ошибиться, не назначая его. Мы просто хотим, чтобы в первую очередь существовала строгая зависимость, значимая для контроллера.
Amber K
1
@AmberK Возможно, вы захотите изучить программирование своего интерфейса вместо использования Interface Builder.
Калеб Клветер
Хорошо, я тоже смотрю проект kickstarter ios для справки, но пока не могу сказать, как они отправляют свои модели представления.
Amber K
23

Как отметил @Caleb Kleveter, мы не можем использовать настраиваемый инициализатор при инициализации из раскадровки.

Но мы можем решить проблему, используя метод фабрики / класса, который создает экземпляр объекта контроллера представления из раскадровки и возвращает объект контроллера представления. Думаю, это довольно крутой способ.

Примечание. Это не точный ответ на вопрос, а скорее способ решения проблемы.

Создайте метод класса в классе 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:')
Samuël
21

Я сделал это одним из способов - с помощью удобного инициализатора.

class MemeDetailVC : UIViewController {

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

Затем вы инициализируете свой MemeDetailVC с помощью let memeDetailVC = MemeDetailVC(theMeme)

Документация Apple по инициализаторам довольно хороша, но мне больше всего нравится серия руководств Ray Wenderlich: Initialization in Depth, которая должна дать вам множество объяснений / примеров по вашим различным параметрам инициализации и «правильному» способу работы.


РЕДАКТИРОВАТЬ : хотя вы можете использовать удобный инициализатор на настраиваемых контроллерах представления, все правы, заявляя, что вы не можете использовать настраиваемые инициализаторы при инициализации из раскадровки или через переход раскадровки.

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

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

Понибой47
источник
1
Я не понимаю, если мой интерфейс настроен в раскадровке, и да, я создаю его программно, тогда я должен вызвать метод создания экземпляра с помощью раскадровки, чтобы правильно загрузить интерфейс? Как convenienceздесь поможет.
Amber K
Классы в swift не могут быть инициализированы путем вызова другой initфункции класса (хотя структуры могут). Итак, чтобы вызвать self.init(), вы должны пометить init(meme: Meme)как convenienceинициализатор. В противном случае вам придется вручную установить все необходимые свойства a UIViewControllerв инициализаторе, и я не уверен, что это за свойства.
Ponyboy47
тогда это не подкласс UIViewController, потому что вы вызываете self.init (), а не super.init (). вы все еще можете инициализировать MemeDetailVC, используя инициализацию по умолчанию, MemeDetail()и в этом случае код выйдет из
Али
Он по-прежнему считается подклассом, потому что self.init () должен вызвать super.init () в своей реализации или, возможно, self.init () напрямую наследуется от родителя, и в этом случае они функционально эквивалентны.
Ponyboy47
6

Изначально было несколько ответов, которые были проголосованы и удалены, хотя в основном они были правильными. Ответ: вы не можете.

При работе с определением раскадровки все экземпляры контроллера представления архивируются. Итак, для их инициализации требуется, чтобы init?(coder...их использовали. coderГде вся информация настройки / вид приходит.

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

Во всех случаях вы используете обязательную функцию инициализации SDK и после этого передаете дополнительные параметры.

Wain
источник
4

Swift 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
    }
}
emrcftci
источник
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
}
wj2061
источник
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

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

Эта возможность была добавлена ​​в Xcode 11, и ниже приводится выдержка из примечаний к выпуску Xcode 11 :

Метод контроллера представления, аннотированный новым @IBSegueActionатрибутом, может использоваться для создания целевого контроллера представления перехода в коде с использованием настраиваемого инициализатора с любыми необходимыми значениями. Это позволяет использовать контроллеры представления с необязательными требованиями к инициализации в раскадровках. Создайте соединение от перехода к @IBSegueActionметоду на его контроллере исходного представления. В новых версиях ОС, поддерживающих действия destinationViewControllerперехода , этот метод будет вызываться, и возвращаемое им значение будет соответствовать переданному объекту перехода prepareForSegue:sender:. @IBSegueActionНа одном контроллере представления исходного кода может быть определено несколько методов, что может избавить от необходимости проверять строки идентификатора перехода в 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перед типом возвращаемого значения:

- (IBSegueAction ViewController *)makeDogController:(NSCoder *)coder
               sender:(id)sender
      segueIdentifier:(NSString *)segueIdentifier
{
   return [PetController initWithCoder:coder
                               petName:self.selectedPetName
                                  type:@"dog"];
}
Malhal
источник
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 и для него установлен идентификатор

Класс 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!
}

Класс А

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

 navigationController?.pushViewController(objB,animated: true)
Кашиш Маккар
источник