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