Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I check if a generic view class actually implements init(frame:)?

Let's say I have a custom UIView subclass with a designated initializer:

class MyView: UIView {

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

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

As expected I cannot call MyView(frame: .zero) as it's not automatically inherited from UIView.

Then suppose I have a view builder class:

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

Here I checked first if the custom view V has init(frame:) or not, if yes, then call it.

However, it doesn't work, Builder<MyView>().build() will crash with:

Fatal error: Use of unimplemented initializer 'init(frame:)' for class '__lldb_expr_14.MyView'

V.instancesRespond(to: #selector(V.init(frame:))) always returns true, making this check useless. (Or did I use it incorrectly?)

Question: How do I check if generic view class V actually responds to init(frame:)?


Updated 1: I also tried V.responds(to: #selector(V.init(frame:))) as @kamaldeep and @Pranav have pointed out. However, it always returns false no matter I override init(frame:) on MyView or not.

Updated 2: What I'm trying to do:

To be more clear, I'm building a framework that automatically initializes UIView from this enum:

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

and that view to be used with the framework must adopt this protocol and specify how to build:

protocol SomeViewProtocol where Self: UIView {

    // ... other funcs

    static func buildMethod() -> ViewBuildMethod
}

The issue is that I want the override init(frame:) to be optional and allows a custom designated initializer (like in MyView). Then, emit fatalError with an error message when the init(frame:) is used (indirectly) on a view that hasn't overridden it yet. This indicates an illegal use of the framework (e.g., MyView's buildMethod returns .frame(.zero)), that can't be checked in compile time:

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

The error message could be sth like V.init(frame:) is called indirectly but V doesn't override this designated initializer. Please override init(frame:) to fix this issue. I can let it crash like above, but it will be more clear if I can add some meaningful message before that happens.

like image 601
aunnnn Avatar asked Jul 10 '18 11:07

aunnnn


1 Answers

Since Swift doesn't automatically inherit from UIView it will not be possible to check if your your class implement init(frame:) constructor.

I suppose instancesRespond(to:) return true, because it's checking if your class conform to this message with Message dispatching. However Swift uses Table dispatching for method declared in classes. So this checking will work with Objective-C but not with Swift

So, to achieve what you want, you can use protocol

create protocol Buildable which will have init(frame: CGRect) method

protocol Buildable {
    init(frame: CGRect)
}

conform your class to this protocol

class MyView: UIView, Buildable {...}

now init(frame:) method will be required for your class. Change generic type of you Builder class to Buildable

Now your class could be like this

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

and now when you will be able to build your view Builder<MyView>().build(), and if you class will be not confirm to Buildable protocol you will get compile-time error:

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

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

Builder<SecondView>().build()

will throw compile-time error:

error: type 'SecondView' does not conform to protocol 'Buildable'
Builder<SecondView>().build()
like image 172
Taras Chernyshenko Avatar answered Oct 17 '22 00:10

Taras Chernyshenko