Swift Features

У цій статті хотілося розповісти про особливості і труднощі 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 return to 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", "Бета", "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()

//Model View 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()

//Model View 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 (і не тільки).
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.