Как проверить, действительно ли общий класс представления реализует init (frame :)?

Скажем, у меня есть пользовательский подкласс UIView с назначенным инициализатором:

class MyView: UIView {

    init(custom: String) {
        super.init(frame: .zero)
    }

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

Как и ожидалось, я не могу вызывать MyView(frame:.zero) поскольку он не унаследован автоматически от UIView.

Тогда предположим, что у меня есть класс построителя вида:

class Builder<V: UIView> {
    func build() -> V? {
        // Check if can call init(frame:) or not
        if V.instancesRespond(to: #selector(V.init(frame:))) {
            return V.init(frame: .zero) // <-- Crash here, because the check doesn't work
        } else { 
            return nil 
        }
    }
}

Здесь я проверил сначала, если пользовательский вид V имеет init(frame:) или нет, если да, то вызовите его.

Однако это не сработает, Builder<MyView>().build() сбой:

Неустранимая ошибка: использование нереализованного инициализатора 'init (frame :)' для класса '__lldb_expr_14.MyView'

V.instancesRespond(to: #selector(V.init(frame:))) всегда возвращает true, делая эту проверку бесполезной. (Или я использовал его неправильно?)

Вопрос: Как проверить, действительно ли общий класс V класса отвечает на init(frame:)?


Обновлено 1: я также попробовал V.responds(to: #selector(V.init(frame:))) как отмечали @kamaldeep и @Pranav. Тем не менее, он всегда возвращает false независимо от того, я переопределяю init (frame :) в MyView или нет.

Обновлено 2: Что я пытаюсь сделать:

Чтобы быть более ясным, я создаю фреймворк, который автоматически инициализирует UIView из этого перечисления:

enum ViewBuildMethod {
    case nib
    case nibName(String)
    case frame(CGRect)
    case custom(() -> UIView)
}

и это представление, которое будет использоваться с каркасом, должно принять этот протокол и указать, как его построить:

protocol SomeViewProtocol where Self: UIView {

    // ... other funcs

    static func buildMethod() -> ViewBuildMethod
}

Проблема в том, что я хочу override init(frame:) как необязательный и MyView настраиваемый инициализатор (например, в MyView). Затем испустите fatalError с сообщением об ошибке, когда init(frame:) используется (косвенно) в представлении, которое еще не переопределило его. Это указывает на незаконное использование структуры (например, MyView buildMethod возвращает .frame(.zero)), который не может быть проверен во время компиляции:

class MyView: UIView, SomeViewProtocol {

    init(custom: String) {
        super.init(frame: .zero)
    }

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

    // ILLEGAL!!
    static func buildMethod() -> ViewBulidMethod {
        return .frame(.zero) // <-- Illegal, the framework will call V.init(frame: .zero) eventually
    }

    // LEGAL
    // static func buildMethod() -> ViewBuildMethod {
    //     return .custom({ () -> UIView in
    //         return MyView(custom: "hello")
    //     })
    // }
}

Сообщение об ошибке может быть похоже на V.init(frame:) is called indirectly but V doesn't override this designated initializer. Please override init(frame:) to fix this issue V.init(frame:) is called indirectly but V doesn't override this designated initializer. Please override init(frame:) to fix this issue. Я могу разрешить ему сбой, как описано выше, но будет яснее, если я смогу добавить какое-то содержательное сообщение, прежде чем это произойдет.

Ответы

Ответ 1

Так как Swift автоматически не наследует UIView будет невозможно проверить, реализует ли ваш класс init(frame:) constructor.

Я предполагаю, что instancesRespond(to:) возвращают true, потому что он проверяет, соответствует ли ваш класс этому сообщению диспетчером Message. Однако Swift использует диспетчер таблиц для метода, объявленного в классах. Таким образом, эта проверка будет работать с Objective-C, но не с Swift

Итак, чтобы достичь желаемого, вы можете использовать protocol

создать протокол Buildable который будет иметь метод init(frame: CGRect)

protocol Buildable {
    init(frame: CGRect)
}

совместите свой класс с этим протоколом

class MyView: UIView, Buildable {...}

теперь для вашего класса потребуется метод init(frame:). Изменить общий тип класса Builder для Buildable

Теперь ваш класс может быть таким

class Builder<V: Buildable>  {
    func build() -> V? {
        return V(frame: .zero)
    }
}

и теперь, когда вы сможете построить свой Builder<MyView>().build(), и если ваш класс не будет подтверждать протокол Buildable, вы получите ошибку compile-time компиляции:

class SecondView: UIView {
    init(custom: String) {
        super.init(frame: .zero)
    }

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

Builder<SecondView>().build()

будет выдавать ошибку времени компиляции:

error: type 'SecondView' does not conform to protocol 'Buildable'
Builder<SecondView>().build()

Ответ 2

он выглядит как одно из многих ограничений в дженериках Swift. Если это действительно необходимо, вы можете попытаться сделать все быстрые объекты objc совместимыми, тогда вы можете использовать время выполнения objc, чтобы понять это, но я лично не считаю, что это стоит усилий. Я бы предпочел переписать код, чтобы подойти другим, но чистым.

Ответ 3

Как ни странно, init(frame:) получает синтез компилятора, следующий код доказывает это:

var methodCount: UInt32 = 0
let methods = class_copyMethodList(MyView.self, &methodCount)!      
for i in 0..<methodCount {
    let method = method_getName(methods[Int(i)])
    print(method, method_getImplementation(methods[Int(i)]))
}
let method = class_getMethodImplementation(UIView.self,  #selector(UIView.init(frame:)))!
print("UIView init(frame:)",method_getImplementation(method))

Вывод вышеуказанного кода

initWithCoder: 0x00000001054125b0
initWithFrame: 0x0000000105412790
UIView init(frame:) 0xffee1be8ffffee20

Кажется, что так называемый требуемый инициализатор init(frame:) автоматически синхронизируется компилятором в результате ошибки времени выполнения, что означает, что нет способа определить, реализуется ли init(frame:) или нет в подклассе UIView.

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