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()

Исходя из следующего поста

  1. Отметьте свой класс как @objc
  2. Унаследовать это от NSObject
  3. Добавьте категорию ObjC в свой класс
  4. Воплощать в жизнь 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, а затем использовать его по мостовой ссылке

Надеюсь, что лучший путь.....

Другие вопросы по тегам