В этой статье хотелось рассказать об особенностях и трудностях 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