Swift Features

в 11:59, , рубрики: iOS, swift, разработка, разработка под iOS

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

Generic протоколы

Под этим термином я подразумеваю любые протоколы, в которых есть открытые typealias (associatedtype в Swift 2.2). В моем первом приложении на Swift было два таких протокола: (для примера я немного упростил их)

public protocol DataObserver {
    typealias DataType
    func didDataChangedNotification(data: DataType)
}

public protocol DataObservable {
    typealias DataType
    func observeData<TObserver: DataObserver where TObserver.DataType == DataType> (observer: TObserver)
}

DataObservable отвечает за отслеживание изменения данных. При этом не важно, где эти данные хранятся (на сервере, локально или еще как). DataObserver получает оповещения о том, что данные изменились. В первую очередь нас будет интересовать протокол DataObservable, и вот его простейшая реализация.

public class SimpleDataObservable<TData> : DataObservable {
    public typealias DataType = TData
    
    private var observer: DataObserver?

    public var data: DataType {
        didSet {
            observer?.didDataChangedNotification(data)
        }
    }
    
    public init(data: TData) {
        self.data = data
    }
    
    public func observeData<TObserver : DataObserver where TObserver.DataType == DataType>(observer: TObserver) {
        self.observer = observer
    }
}

Тут все просто: сохраняем ссылку на последний observer, и вызываем у него метод didDataChangedNotification, когда данные по какой-то причине изменяются. Но погодите… этот код не компилируется. Компилятор выдает ошибку «Protocol 'DataObserver' can only be used as a generic constraint because it has Self or associated type requirements». Все потому, что generic-протоколы могут использоваться только для накладывания ограничений на generic-параметры. Т.е. объявить переменную типа DataObserver не получится. Меня такое положение дел не устроило. Немного покопавшись в сети, я нашел решение, которое помогает разобраться со сложившейся проблемой, и имя ему Type Erasure.

Это паттерн, который представляет собой небольшой обертку над заданным протоколом. Для начала введем новый класс AnyDataObserver, который реализует протокол DataObserver.

public class AnyDataObserver<TData> : DataObserver {
    public typealias DataType = TData
    
    public func didDataChangedNotification(data: DataType) {
        
    }
}

Тело метода didDataChangedNotification пока оставим пустым. Идем дальше. Вводим в класс generic init (для чего он нужен расскажу чуть ниже):

public class AnyDataObserver<TData> : DataObserver {
    public typealias DataType = TData
    
    public func didDataChangedNotification(data: DataType) {
        
    }
    
    public  init<TObserver : DataObserver where TObserver.DataType == DataType>(sourceObserver: TObserver) {
        
    }
}

В него передается параметр sourceObserver типа TObserver. Видно, что на TObserver накладываются ограничения: во-первых он должен реализовать протокол DataObserver, во-вторых его DataType должен в точности соответствовать DataType нашего класса. Собственно sourceObserver это и есть исходный observer-объект, который мы хотим обернуть. И наконец финальный код класса:

public class AnyDataObserver<TData> : DataObserver {
    public typealias DataType = TData
    
    private let observerHandler: TData -> Void
    
    public func didDataChangedNotification(data: DataType) {
        observerHandler(data)
    }
    
    public  init<TObserver : DataObserver where TObserver.DataType == DataType>(sourceObserver: TObserver) {
        observerHandler = sourceObserver.didDataChangedNotification
    }
}

Собственно тут и происходит вся «магия». В класс добавляется закрытое поле observerHandler, в котором хранится реализация метода didDataChangedNotification объекта sourceObserver. В самом методе didDataChangedNotification нашего класса мы просто вызываем эту реализацию.

Теперь перепишем SimpleDataObservable:

public class SimpleDataObservable<TData> : DataObservable {
    public typealias DataType = TData
    
    private var observer: AnyDataObserver<DataType>?

    public var data: DataType {
        didSet {
            observer?.didDataChangedNotification(data)
        }
    }
    
    public init(data: TData) {
        self.data = data
    }
    
    public func observeData<TObserver : DataObserver where TObserver.DataType == DataType>(observer: TObserver) {
        self.observer = AnyDataObserver(sourceObserver: observer)
    }
}

Теперь код компилируется и прекрасно работает. Могу отметить, что некоторые классы из стандартной библиотеки Swift работают по схожему принципу (например AnySequence).

Тип Self

В определенный момент мне потребовалось ввести в проект протокол копирования:

public protocol CopyableType {
    func copy() -> ???
}

Но что же должен возвращать метод copy? Any? CopyableType? Тогда при каждом вызове пришлось бы писать let copyObject = someObject.copy as! SomeClass, что не очень хорошо. В добавок к тому же этот код небезопасен. На помощь приходит ключевое слово Self.

public protocol CopyableType {
func copy() -> Self
}

Таким образом мы сообщаем компилятору, что реализация этого метода обязана вернуть объект того же типа, что и объект, для которого он был вызван. Тут можно провести аналогию с instancetype из Objective-C.

Рассмотрим реализацию этого протокола:

public class CopyableClass: CopyableType {
    public var fieldA = 0
    public var fieldB = "Field"
    
    public required init() {
        
    }
    
    public func copy() -> Self {
        let copy = self.dynamicType.init()
        
        copy.fieldA = fieldA
        copy.fieldB = fieldB
        
        return copy
    }
}

Для создание нового экземпляра используется ключевое слово dynamicType (получение ссылки на динамический объект-тип) и вызывается метод init (для гарантии того, что init без параметров действительно есть в классе, мы вводим его с ключевым словом required). После чего копируем в созданный экземпляр все нужные поля и возвращаем его из нашей функции.

Как только я закончил с копированием, возникла необходимость использовать Self еще в одном месте. Мне потребовалось написать протокол для View Controller, в котором бы был статический метод создания нового экземпляра этого самого View Controller.

Так как этот протокол никак напрямую не был связан с классом UIViewController, то я его сделал достаточно общим и назвал AutofactoryType:

public protocol AutofactoryType {
    static func createInstance() -> Self
}

Попробуем использовать его для создания View Conotroller:

public class ViewController: UIViewController, AutofactoryType {
    public static func createInstance() -> Self {
        let newInstance = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("ViewController")
        return newInstance as! ViewController
    }
}

Все бы хорошо, но этот код не скомпилируется: “Cannot convert return expression of type ViewController to return type 'Self'” Дело в том, что компилятор не может преобразовать ViewController к Self. В данном случае ViewController и Self — это одно и то же, но в общем случае это не так (например, при использовании наследования).

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

public func unsafeCast<T, E>(sourceValue: T) -> E {
    if let castedValue = sourceValue as? E {
        return castedValue
    }
    
    fatalError("Unsafe casting value (sourceValue) to type (E.self) failed")
}

Ее назначение — это преобразование объекта одного типа к другому типу. Если преобразование не удается, то функция просто завершается с ошибкой.

Используем эту функцию в createInstance:

public class ViewController: UIViewController, AutofactoryType {
    public static func createInstance() -> Self {
        let newInstance = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("ViewController")
        return unsafeCast(newInstance)
    }
}

Благодаря автоматическому выводу типов, newInstance теперь преобразуется к Self (чего нельзя было сделать напрямую). Этот код компилируется и работает.

Специфичные расширения

Расширения типов в Swift не были бы такими полезными, если бы нельзя было писать специфичный код для разных типов. Возьмем, к примеру, протокол SequenceType из стандартной библиотеки и напишем для него такое расширение:

extension SequenceType where Generator.Element == String {
    
    public func concat() -> String {
        var result = String()
        
        for value in self {
            result += value
        }
        
        return result
    }
}

В расширении введено ограничение на элемент последовательности, он должен быть типа String. Таким образом для любой последовательности, состоящей из строк (и только для них), можно будет вызвать функцию concat.

func test() {
    let strings = [“Alpha”, “Beta”, “Gamma”]
    
    //printing “AlphaBetaGamma”
    print("Strings concat: (strings.concat())")
}

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

Реализация методов протокола по умолчанию.

Реализация методов протокола по умолчанию.

public protocol UniqueIdentifierProvider {
    static var uniqueId: String { get }
}

Как следует из описания, любой тип реализующий этот протокол, должен обладать уникальным идентификатором uniqueId типа String. Но если немного подумать, то становится понятно, что в рамках одного модуля для любого типа уникальным идентификатором является его название. Так давайте напишем расширение для нашего нового протокола:

extension UniqueIdentifierProvider where Self: UIViewController {
    static var uniqueId: String {
        get {
            return String(self)
        }
    }
}

В данном случае ключевое слово Self используется для того, чтобы накладывать ограничения на объект-тип. Логика этого кода примерно следующая: «если этот протокол будет реализован классом UIViewController (или его наследником), то можно использовать следующую реализацию uniqueId». Это и есть реализация протокола по-умолчанию. На самом деле можно написать это расширение и без каких-либо ограничений:

extension UniqueIdentifierProvider {
    static var uniqueId: String {
        get {
            return String(self)
        }
    }
}

И тогда все типы, реализующие UniqueIdentifierProvider, получат uniqueId “из коробки”.

extension ViewController: UniqueIdentifierProvider {
   //Nothing
}

func test() {
   //printing "ViewController"
   print(ViewController.uniqueId)
}

Прелесть в том, что в классе может быть своя реализация этого метода. И в этом случае реализация по-умолчанию будет игнорироваться:

extension ViewController: UniqueIdentifierProvider {
   static var uniqueId: String {
        get {
            return "I’m ViewController”
        }
    }
}

func test() {
   //printing "I’m ViewController"
   print(ViewController.uniqueId)
}

Явное указание Generic аргумента

В своем проекте я использовал MVVM, и за создание ViewModel отвечал метод:

public func createViewModel<TViewModel: ViewModelType>() -> TViewModel {
    let viewModel = TViewModel.createIntsance()

    //View model configurate
    
    return viewModel
}

Соответственно, так он использовался:

func test() {
   let viewModel: MyViewModel = createViewModel()
}

В данном случае в функцию createViewModel в качестве generic аргумента будет поставляться MyViewModel. Все благодаря тому, что Swift сам выводит типы из контекста. Но всегда ли это хорошо? На мой взгляд, это не так. В некоторых случаях может даже привести к ошибкам:

func test(mode: FactoryMode) -> ViewModelBase {
   switch mode {
   case NormalMode:
      return createViewModel() as NormalViewModel
   case PreviewMode:
      return createViewModel() //забыли as PreviewViewModel
   }
}

В первом case в метод createViewModel подставляется NormalViewModel.
Во втором мы забыли написать «as PreviewViewModel», из-за чего в метод createViewModel подставляется тип ViewModelBase (что в лучшем случае приведет к ошибке в runtime).

Значит, необходимо сделать указание типа явным. Для этого в createViewModel мы добавим новый параметр viewModelType типа TViewModel.Type. Type тут означает, что метод принимает в качестве параметра не экземпляр типа, а сам объект-тип.

public func createViewModel<TViewModel: ViewModelType>(viewModelType: TViewModel.Type) -> TViewModel {
    let viewModel = viewModelType.createIntsance()

    //View model configurate
    
    return viewModel
}

После этого наш switch-case выглядит так:

func test(mode: FactoryMode) {
   let viewModel: ViewModelBase?
    
   switch mode {
   case NormalMode:
       return createViewModel(NormalViewModel.self)
   case PreviewMode:
       return createViewModel(PreviewViewModel.self)
   }
}

Теперь В функцию createViewModel передается аргументы NormalViewModel.self и PreviewViewModel.self. Это объекты-типы NormalViewModel и PreviewViewModel. В Swift есть довольно странная особенность: если у функции один параметр, можно не писать self.

func test(mode: FactoryMode) {
   let viewModel: ViewModelBase?
    
   switch mode {
   case NormalMode:
       return createViewModel(NormalViewModel)
   case PreviewMode:
       return createViewModel(PreviewViewModel)
   }
}

Но если аргументов два или больше, ключевое слово self необходимо.

P. S.

Надеюсь что данная статья окажется кому-то полезной. Так же планируется продолжение про Swift (и не только).

Автор: iON1k

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js