Swift 3.1 не поддерживает initialize(). Как я могу достичь того же?
Objective-C объявляет функцию класса, initialize()
, то есть запускается один раз для каждого класса, прежде чем его использовать. Он часто используется как точка входа для обмена реализациями методов (Swizzling), среди прочего.
Swift 3.1 отказывается от этой функции с предупреждением:
Метод 'initialize()' определяет метод класса 'initialize' класса Objective C, который не гарантированно будет вызван Swift и будет запрещен в будущих версиях.
Как это можно решить, сохраняя при этом то же поведение и функции, которые я в настоящее время реализую, используя initialize()
точка входа?
8 ответов
Простое / Простое Решение
Общей точкой входа в приложение является делегат приложения applicationDidFinishLaunching
, Мы могли бы просто добавить статическую функцию к каждому классу, который мы хотим уведомить об инициализации, и вызвать ее отсюда.
Это первое решение является простым и легким для понимания. В большинстве случаев это то, что я бы порекомендовал. Хотя следующее решение дает результаты, которые больше похожи на оригинал initialize()
Функция также приводит к немного более длительному времени запуска приложения. Я больше не думаю, что это стоит усилий, снижения производительности или сложности кода в большинстве случаев. Простой код - это хороший код.
Продолжайте читать для другого варианта. У вас может быть причина, чтобы нуждаться в этом (или возможно части этого).
Не очень простое решение
Первое решение не обязательно масштабируется так хорошо. А что, если вы создаете фреймворк, где вы хотите, чтобы ваш код выполнялся без необходимости вызывать его у делегата приложения?
Первый шаг
Определите следующий код Swift. Цель состоит в том, чтобы обеспечить простую точку входа для любого класса, который вы хотели бы наполнить поведением, сродни initialize()
- теперь это можно сделать, просто следуя SelfAware
, Он также предоставляет одну функцию для запуска этого поведения для каждого соответствующего класса.
protocol SelfAware: class {
static func awake()
}
class NothingToSeeHere {
static func harmlessFunction() {
let typeCount = Int(objc_getClassList(nil, 0))
let types = UnsafeMutablePointer<AnyClass?>.allocate(capacity: typeCount)
let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass?>(types)
objc_getClassList(autoreleasingTypes, Int32(typeCount))
for index in 0 ..< typeCount { (types[index] as? SelfAware.Type)?.awake() }
types.deallocate(capacity: typeCount)
}
}
Шаг второй
Это все хорошо, но нам все еще нужен способ, чтобы фактически запустить функцию, которую мы определили, т.е. NothingToSeeHere.harmlessFunction()
, при запуске приложения. Ранее в этом ответе предлагалось использовать код Objective-C для этого. Однако, похоже, что мы можем делать то, что нам нужно, используя только Swift. Для macOS или других платформ, где UIApplication недоступна, понадобятся следующие варианты.
extension UIApplication {
private static let runOnce: Void = {
NothingToSeeHere.harmlessFunction()
}()
override open var next: UIResponder? {
// Called before applicationDidFinishLaunching
UIApplication.runOnce
return super.next
}
}
Шаг третий
Теперь у нас есть точка входа при запуске приложения и способ подключиться к ней из классов по вашему выбору. Все, что осталось сделать: вместо реализации initialize()
, соответствуют SelfAware
и реализовать определенный метод, awake()
,
Мой подход по сути такой же, как и у Адиба. Вот пример из настольного приложения, которое использует Core Data; цель здесь состоит в том, чтобы зарегистрировать наш собственный преобразователь, прежде чем код упоминает его:
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
override init() {
super.init()
AppDelegate.doInitialize
}
static let doInitialize : Void = {
// set up transformer
ValueTransformer.setValueTransformer(DateToDayOfWeekTransformer(), forName: .DateToDayOfWeekTransformer)
}()
// ...
}
Приятно то, что это работает для любого класса, так же, как initialize
сделал, при условии, что вы покрываете все свои базы - то есть вы должны реализовать каждый инициализатор. Вот пример текстового представления, которое настраивает свой собственный прокси внешнего вида один раз, прежде чем какие-либо экземпляры смогут появиться на экране; пример искусственный, но инкапсуляция очень приятная:
class CustomTextView : UITextView {
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame:frame, textContainer: textContainer)
CustomTextView.doInitialize
}
required init?(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
CustomTextView.doInitialize
}
static let doInitialize : Void = {
CustomTextView.appearance().backgroundColor = .green
}()
}
Это демонстрирует преимущество этого подхода намного лучше, чем делегат приложения. Существует только один экземпляр делегата приложения, поэтому проблема не очень интересна; но может быть много экземпляров CustomTextView. Тем не менее, линия CustomTextView.appearance().backgroundColor = .green
будет выполнен только один раз при создании первого экземпляра, поскольку он является частью инициализатора для статического свойства. Это очень похоже на поведение метода класса initialize
,
Если вы хотите исправить свой метод Swizzling в режиме Pure Swift:
public protocol SwizzlingInjection: class {
static func inject()
}
class SwizzlingHelper {
private static let doOnce: Any? = {
UILabel.inject()
return nil
}()
static func enableInjection() {
_ = SwizzlingHelper.doOnce
}
}
extension UIApplication {
override open var next: UIResponder? {
// Called before applicationDidFinishLaunching
SwizzlingHelper.enableInjection()
return super.next
}
}
extension UILabel: SwizzlingInjection
{
public static func inject() {
// make sure this isn't a subclass
guard self === UILabel.self else { return }
// Do your own method_exchangeImplementations(originalMethod, swizzledMethod) here
}
}
Так как objc_getClassList
это Objective-C, и он не может получить суперкласс (например, UILabel), но только все подклассы, но для связанного с UIKit swizzling мы просто хотим запустить его один раз на суперклассе. Просто запустите inject() для каждого целевого класса вместо того, чтобы зацикливать все классы проекта.
Небольшое дополнение к отличному классу @JordanSmith, которое гарантирует, что каждый awake()
вызывается только один раз:
protocol SelfAware: class {
static func awake()
}
@objc class NothingToSeeHere: NSObject {
private static let doOnce: Any? = {
_harmlessFunction()
}()
static func harmlessFunction() {
_ = NothingToSeeHere.doOnce
}
private static func _harmlessFunction() {
let typeCount = Int(objc_getClassList(nil, 0))
let types = UnsafeMutablePointer<AnyClass?>.allocate(capacity: typeCount)
let autoreleasingTypes = AutoreleasingUnsafeMutablePointer<AnyClass?>(types)
objc_getClassList(autoreleasingTypes, Int32(typeCount))
for index in 0 ..< typeCount { (types[index] as? SelfAware.Type)?.awake() }
types.deallocate(capacity: typeCount)
}
}
Вы также можете использовать статические переменные, так как они уже ленивы и ссылаться на них в инициализаторах ваших объектов верхнего уровня. Это было бы полезно для расширений приложения и тому подобного, у которых нет делегата приложения.
class Foo {
static let classInit : () = {
// do your global initialization here
}()
init() {
// just reference it so that the variable is initialized
Foo.classInit
}
}
Если вы предпочитаете Pure Swift™! тогда мое решение для такого рода вещей работает на _UIApplicationMainPreparations
время начинать:
@UIApplicationMain
private final class OurAppDelegate: FunctionalApplicationDelegate {
// OurAppDelegate() constructs these at _UIApplicationMainPreparations time
private let allHandlers: [ApplicationDelegateHandler] = [
WindowHandler(),
FeedbackHandler(),
...
Образец здесь - я избегаю проблемы Massive Application Delegate, разлагая UIApplicationDelegate
в различные протоколы, которые могут принять отдельные обработчики, если вам интересно. Но важный момент заключается в том, что простой способ быстро приступить к работе - отправить +initialize
введите задачи в инициализации вашего @UIApplicationMain
класс, как строительство allHandlers
Вот. _UIApplicationMainPreparations
время должно быть достаточно рано для почти всех!
Еще один пример однократного выполнения чего-либо с использованием статического closure stored property
[Около]
extension MyClass {
static let shared: MyClass = {
//closure
//create an instance and setup it
let myClass = MyClass(parameter: "parameter")
myClass.initialize()
return myClass
}()
//() to execute the closure.
func initialize() {
//is called once
}
}
//using
let myClass = MyClass.shared
Вот решение, которое работает на Swift 3.1+
@objc func newViewWillAppear(_ animated: Bool) {
self.newViewWillAppear(animated) //Incase we need to override this method
let viewControllerName = String(describing: type(of: self)).replacingOccurrences(of: "ViewController", with: "", options: .literal, range: nil)
print("VIEW APPEAR", viewControllerName)
}
static func swizzleViewWillAppear() {
//Make sure This isn't a subclass of UIViewController, So that It applies to all UIViewController childs
if self != UIViewController.self {
return
}
let _: () = {
let originalSelector = #selector(UIViewController.viewWillAppear(_:))
let swizzledSelector = #selector(UIViewController.newViewWillAppear(_:))
let originalMethod = class_getInstanceMethod(self, originalSelector)
let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
method_exchangeImplementations(originalMethod!, swizzledMethod!);
}()
}
Затем на AppDelegate:
UIViewController.swizzleViewWillAppear()
- Отметьте свой класс как
@objc
- Унаследовать это от
NSObject
- Добавьте категорию ObjC в свой класс
- Воплощать в жизнь
initialize
в категории
пример
Swift файлы:
//MyClass.swift
@objc class MyClass : NSObject
{
}
Объектные файлы:
//MyClass+ObjC.h
#import "MyClass-Swift.h"
@interface MyClass (ObjC)
@end
//MyClass+ObjC.m
#import "MyClass+ObjC.h"
@implement MyClass (ObjC)
+ (void)initialize {
[super initialize];
}
@end
Я думаю, что это обходной путь.
Также мы можем написать initialize()
функция в коде target-c, а затем использовать его по мостовой ссылке
Надеюсь, что лучший путь.....