Вернуть тип экземпляра в Swift

Я пытаюсь сделать это расширение:

extension UIViewController
{
    class func initialize(storyboardName: String, storyboardId: String) -> Self
    {
        let storyboad = UIStoryboard(name: storyboardName, bundle: nil)
        let controller = storyboad.instantiateViewControllerWithIdentifier(storyboardId) as! Self

        return controller
    }
}

Но я получаю ошибку компиляции:

ошибка: невозможно преобразовать возвращаемое выражение типа «UIViewController» в возвращаемый тип «Self»

Является ли это возможным? Также я хочу сделать это как init(storyboardName: String, storyboardId: String)


person ChikabuZ    schedule 18.10.2015    source источник
comment
Пожалуйста, не размещайте ответы внутри вашего вопроса.   -  person Rizier123    schedule 07.11.2016


Ответы (4)


Подобно тому, как в Использование 'self' в функциях расширения класса в Swift, вы можете определить общий вспомогательный метод, который выводит тип self из контекста вызова:

extension UIViewController
{
    class func instantiateFromStoryboard(storyboardName: String, storyboardId: String) -> Self
    {
        return instantiateFromStoryboardHelper(storyboardName, storyboardId: storyboardId)
    }

    private class func instantiateFromStoryboardHelper<T>(storyboardName: String, storyboardId: String) -> T
    {
        let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
        let controller = storyboard.instantiateViewControllerWithIdentifier(storyboardId) as! T
        return controller
    }
}

Затем

let vc = MyViewController.instantiateFromStoryboard("name", storyboardId: "id")

компилируется, и тип выводится как MyViewController.


Обновление для Swift 3:

extension UIViewController
{
    class func instantiateFromStoryboard(storyboardName: String, storyboardId: String) -> Self
    {
        return instantiateFromStoryboardHelper(storyboardName: storyboardName, storyboardId: storyboardId)
    }

    private class func instantiateFromStoryboardHelper<T>(storyboardName: String, storyboardId: String) -> T
    {
        let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
        let controller = storyboard.instantiateViewController(withIdentifier: storyboardId) as! T
        return controller
    }
}

Другое возможное решение с использованием unsafeDowncast:

extension UIViewController
{
    class func instantiateFromStoryboard(storyboardName: String, storyboardId: String) -> Self
    {
        let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
        let controller = storyboard.instantiateViewController(withIdentifier: storyboardId)
        return unsafeDowncast(controller, to: self)
    }
}
person Martin R    schedule 18.10.2015
comment
Привет Мартин, не могли бы вы объяснить, как компилятор может идентифицировать общий тип T и иметь возможность привести его к Self? - person Adithya; 10.09.2016
comment
люди, которые говорят, что функция частного класса, а не статическая функция, имеют большой стиль. :) - person Fattie; 05.02.2017
comment
интересно, что если вы просто возвращаете базовый тип - не беспокойтесь об универсальном - он отлично работает во время runtime (возвращая фактический подкласс), но вам нужно будет преобразовать результаты в код, во время редактирования. пример: stackoverflow.com/a/42053648/294884 - person Fattie; 05.02.2017
comment
@JoeBlow: Да, цель здесь была просто избежать актерского состава. - static - это class + final и поэтому не связан с private. - person Martin R; 05.02.2017
comment
довольно. спасибо @MartinR. Мне потребовались дни, чтобы найти ответ на этот концептуально похожий вопрос stackoverflow.com/q/42041150/294884 - person Fattie; 05.02.2017
comment
whoa @MartinR - похоже, что: stackoverflow. ком/вопросы/42161710/ - person Fattie; 10.02.2017
comment
MartinR, спасибо за эту прекрасную реализацию. Поскольку комментарий @Adithya с просьбой о разъяснении получил 8 голосов, я взял на себя смелость немного расширить ваш ответ, добавив быстрое дополнительное объяснение вашего фрагмента. Не стесняйтесь игнорировать его или редактировать дальше. - person luizv; 30.12.2017
comment
Я обнаружил, что это компилируется с использованием static, а также class. Интересно, class что-нибудь добавит. - person ThomasW; 01.05.2018
comment
@ThomasW: static == class + final, то есть метод типа, который нельзя переопределить в подклассе. Так что это не будет иметь значения, если вы не собираетесь переопределять его в подклассе. - person Martin R; 01.05.2018
comment
@Adithya Adithya Я думаю, это потому, что вы возвращаете T во вспомогательном методе и возвращаете Self в методе создания экземпляра, поэтому компилятор автоматически понимает, что T - это Self. Это как цепь. - person kientux; 04.05.2018

Self определяется во время компиляции, а не во время выполнения. В вашем коде Self в точности эквивалентно UIViewController, а не "подклассу, который вызывает это". Это вернет UIViewController, и вызывающая сторона должна будет as ввести его в правильный подкласс. Я предполагаю, что это то, чего вы пытались избежать (хотя это «обычный способ Cocoa», поэтому простое возвращение UIViewController, вероятно, является лучшим решением).

Примечание. Ни в коем случае нельзя называть функцию initialize. Это существующая функция класса NSObject, которая в лучшем случае вызовет путаницу, а в худшем — ошибки.

Но если вы хотите избежать as вызывающей стороны, создание подклассов обычно не является инструментом для добавления функциональности в Swift. Вместо этого вам обычно нужны дженерики и протоколы. В этом случае дженерики — это все, что вам нужно.

func instantiateViewController<VC: UIViewController>(storyboardName: String, storyboardId: String) -> VC {
    let storyboad = UIStoryboard(name name: storyboardName, bundle: nil)
    let controller = storyboad.instantiateViewControllerWithIdentifier(storyboardId) as! VC

    return controller
}

Это не метод класса. Это просто функция. Здесь не нужен класс.

let tvc: UITableViewController = instantiateViewController(name: name, storyboardId: storyboardId)
person Rob Napier    schedule 18.10.2015
comment
Это отличный ответ. У меня была очень похожая проблема некоторое время назад. Я хотел протокол, который могли бы реализовать классы, которые могут быть созданы из раскадровки, чтобы вызывающей стороне не нужно было знать точный тип создаваемого контроллера представления. Я отказался от такого подхода. Может еще раз попробую, но с дженериком в протоколе. - person NRitH; 18.10.2015

Более чистое решение (по крайней мере, визуально более аккуратное):

Свифт 5.1

class func initialize(storyboardName: String, storyboardId: String) -> Self {
    return UIStoryboard(name: storyboardName, bundle: nil)
        .instantiateViewController(withIdentifier: storyboardId).view as! Self
}
person mojuba    schedule 08.06.2019

Другой способ — использовать протокол, который также позволяет возвращать Self.

protocol StoryboardGeneratable {

}

extension UIViewController: StoryboardGeneratable {

}

extension StoryboardGeneratable where Self: UIViewController
{
    static func initialize(storyboardName: String, storyboardId: String) -> Self
    {
        let storyboad = UIStoryboard(name: storyboardName, bundle: nil)
        let controller = storyboad.instantiateViewController(withIdentifier: storyboardId) as! Self
        return controller
    }
}
person ukim    schedule 12.08.2018