Универсальные шаблоны в Swift. Познаем через приложение.

12 ноября 2015

Если вы уже знакомы со Swift, то знаете уже базу и то, как создавать классы и структуры. Но Swift гораздо глубже, гораздо. Темой этого туторила является очень мощный инструмент Swift - особенность, популярная в ряде других языков: дженерики.

В типобезопасных языках существует общая проблема, что когда вы хотите написать код, воздействующий на один тип, то написанный код может также воздействовать и на другой тип. Представьте себе, например, функцию, добавляющую два Int. Функция добавления двух чисел с плавающей точкой будет выглядеть очень похоже, а по факту, идентично. Единственным отличием будет тип переменных.

В сторого-типизированных языках, вы должны определять отдельные функции, такие как addInts, addFloats, addDoubles и т.д., где у каждой функции есть правильный аргумент и возвращаемые типы.

Многие языки предлагают решение этой проблемы. С ++, например, использует шаблоны. Swift, как Java и C # использует универсальный шаблон программирования (generic programming), что и является темой этого туториала!

На протяжении этого туториала по дженерикам в Swift, вы рассмотрите существующие дженерики в языке, в том числе и некоторые, которые вы уже видели. Затем, вы создадите приложение поиска Flickr photo search с пользовательской универсальной структуры данных для отслеживания пользовательских запросов.

Знакомимся с дженериками

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

Разработчики на Objective-C привыкли к массивам и словарям, содержащим объекты многих типов в одной и той же коллекции. Это обеспечивает большую гибкость, но как вы поймете, какой тип будет держать массив, возвращенный из API? Вы можете быть уверены, только при чтении документации или имен переменных. Даже с документацией, нет ничего (кроме кода без багов!), что предотвратило бы появления чего-то неожиданного в коллекции во время исполнения.

C другой стороны, у Swift есть массивы с указанным типом, словари и множества. Массив Int может содержать только Int и никогда не может, например, содержать String. Это означает, что вы можете документировать код просто написав его, позволяя компилятору сделать проверку типа.

Например, в Objective-C UIKit метод, обрабатывающий прикосновения в custom view заключается в следующем:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;

Множества в этом методе содержат только экземпляры UITouch, но только потому, что так говорит документация. Ничто не мешает объектам быть в каком-то другом месте, и вам, как правило, нужно привести touches (прикосновения) во множестве, в качестве экземпляров UITouch для того, чтобы успешно обрабатывать их как объекты UITouch. В Swift этот метод будет выглядеть вот так:

func touchesBegan(touches: Set, withEvent event: UIEvent?)

В этом случае множество touches содержит только экземпляры UITouch, и компилятор выдаст ошибку, если код вызова этого метода попытается пройти что-то еще. Не только типы управления компилятором, помещенные в массив touch, но и вам больше не нужно приводить элементы к экземплярам UITouch!

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

Теперь у вас есть основное понимание о дженериках и их использовании, и вы готовы применить их для конкретной задачи.

Дженерики в действии

Чтобы протестировать дженерики, давайте создадим приложение поиска изображений Flickr.

Начните с того, что скачайте начальный проект этого туториала. Откройте его и бегло ознакомьтесь с основными классами. Класс Flickr управляет talking в Flickr API. Обратите внимание, что ключ API находится внутри этого класса- он сразу предусмотрен, но Вы можете использовать свой, в случае, если вы хотите расширить приложение. Вы можете подписаться под еще один тут.

Запустите приложение. Вы увидите это:

Пока еще не очень много! Но не бойтесь, вы скоро здесь будет много кошачьих картинок! (Ну а что вы хотели?)

Упорядоченные словари

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

Но что делать, если пользователь вводит один и тот же запрос дважды? Было бы неплохо, если бы приложение вернуло результаты старого поиска обратно в верхнюю часть списка и заменило его новыми результатам.

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

Во многих языках и фреймворках (в том числе и в Swift) множества и словари не гарантируют порядка, в отличие от массивов. Упорядоченный словарь - как обычный словарь, но ключи определены в определенном порядке. Вы будете использовать эту функцию для хранения результатов поиска в привязке к поисковому запросу, и сможете это делать быстро, а также для поддержания порядка в tableView.

Если вы хотите сделать на быструю руку, то можете создать структуру пользовательских данных для обработки упорядоченного словаря. Но лучше подумайте! Вы ведь хотите, чтобы то, что вы создадите, можно было использовать в приложениях долгие годы?! Это идеальный случай использования дженериков.

Исходная структура данных

Добавить новый файл, нажав File\New\File… и выбираем iOS\Source\Swift File. Нажимаем Next и назваем файл OrderedDictionary. Наконец, нажимаем кнопку Create.

Вы увидите пустой файл Swift. Добавьте следующий код:

struct OrderedDictionary {  
}

Пока я думаю, что все понятно. Объект будет структурой, потому что у него должна быть семантика значения.

Заметка

 

Коротко, семантика значения- это скорее причудливый способ сказать "копировать/ вставить поведение", чем "общее ссылочное поведение". Семантика значения дает ряд преимуществ, таких как отсутствие беспокойства о каком-то другом куске кода, изменяющего неожиданно данные.

Теперь вы должны сделать его универсальным, для того, чтобы он мог хранить любой тип значения, какой вы захотите. Измените определение структуры следующим образом:

struct OrderedDictionary

Элементы внутри угловых скобок - это универсальные типы параметров. KeyType и ValueType -это не типы сами по себе, они становятся параметрами, которые можно использовать вместо типов внутри определения структуры. Все станет ясно в ближайшее время.

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

Добавьте следующий код в определение структуры:

typealias ArrayType = [KeyType]
typealias DictionaryType = [KeyType: ValueType]
 
var array = ArrayType()
var dictionary = DictionaryType()

Это объявляет два свойства, как описано, а также два псевдонима типов для того, чтобы дать новое имя уже существующему типу. Здесь вы даете псевдонимы массиву и типам словарей для дублирующего массива и словаря, соответственно. Типы псевдонимов - отличный способ, для того, чтобы принять сложный тип и дать ему гораздо более короткое имя.

Обратите внимание, как вы можете использовать типы параметров KeyType и KeyType из определения структуры вместо типов. Массив - это массив KeyType. Конечно, нет такого типа, как KeyType, вместо этого Swift рассматривает его как любой тип, который обозначает пользователь в OrderedDictionary при создании универсального экземпляра.

В этот момент, вы заметите ошибку компилятора:

Type 'Keytype' does not conform to protocol 'Hashable' (Тип 'Keytype' не подходит под протокол 'Hashable')

А это может быть сюрпризом. Посмотрите на реализацию Dictionary :

struct Dictionary

Это ужасно похоже на определение OrderedDictionary, за исключением только ": Hashable" после KeyType. Hashable после запятой объявляет, что тип перешел KeyType должен соответствовать протоколу Hashable. Потому, что Dictionary должен иметь возможность хэширования ключей для своей реализации.

В это случае обычно ограничивают универсальные параметры типа. Например, вы, возможно, захотите ограничить тип значения для соответствия протоколам Equatable или Printable, в зависимости от того, что ваше приложение должно делать с этими значениями.

Откройте OrderedDictionary.swift и замените определение структуры следующим:

struct OrderedDictionary

Здесь происходит объявление, что KeyType для OrderedDictionary.swift должен соответствовать Hashable. Это означает, что независимо от того, чем будет тип KeyType,он также будет приемлемым в качестве ключа для лежащего в основе словаря. Теперь файл будет компилироваться без каких-либо ошибок!

Ключи, значения и все такое прочее...

Какая польза в словаре, если вы не можете добавить в него значения? Откройте OrderedDictionary.swift и добавьте следующую функцию в определение структуры:

// 1
mutating func insert(value: ValueType, forKey key: KeyType, atIndex index: Int) -> ValueType?
{
  var adjustedIndex = index
 
  // 2
  let existingValue = self.dictionary[key]
  if existingValue != nil {
    // 3
    let existingIndex = find(self.array, key)!
 
    // 4
    if existingIndex < index {
      adjustedIndex--
    }
    self.array.removeAtIndex(existingIndex)
  }
 
  // 5
  self.array.insert(key, atIndex:adjustedIndex)
  self.dictionary[key] = value
 
  // 6
  return existingValue
}
  1. Метод для вставки новых объектов, insert(_:forKey:atIndex), должен принимать два параметра: значение для конкретного ключа и индекс, по которому вставляется пара «ключ-значение». Здесь мы используем ключевое слово, которое вы, возможно, не видели раньше: mutating.
  2. Структуры предназначены быть неизменными по умолчанию, что значит, что вы обычно не можете изменять переменные членов структуры в методе экземпляра. Так как это достаточное ограничение, вы можете добавить ключевое слово mutating, для того чтобы сказать компилятору, разрешается ли методу изменять (мутировать) состояние в структуре. Это помогает компилятору принимать решения о том, когда принимать копии структур (они копируются при записи), а также позволяет документировать API.
  1. Вы передаете ключ к индексации Dictionary, который возвращает существующее значение, если оно уже существует, для этого ключа. Этот метод insert эмулирует поведение аналогичного Dictionary updateValue и, следовательно, сохраняет существующее значение для ключа.
  2. Если есть существующее значение, тогда и только тогда метод находит индекс в массиве для этого ключа.
  3. Если существующий ключ появляется до индекса вставки, то вам нужно настроить индекс вставки, потому что иначе вы удалите существующий ключ.
  4. Обновите массив и словарь, в случае необходимости.
  5. Наконец, верните существующее значение. Может так случиться, что существующего значения не будет и функция вернет опциональное значение.

Теперь у вас есть возможность добавлять значения в словарь, но как насчет удаления значения?

Добавьте следующую функцию в определении структуры OrderedDictionary:

// 1
mutating func removeAtIndex(index: Int) -> (KeyType, ValueType)
{
  // 2
  precondition(index < self.array.count, "Index out-of-bounds")
 
  // 3
  let key = self.array.removeAtIndex(index)
 
  // 4
  let value = self.dictionary.removeValueForKey(key)!
 
  // 5
  return (key, value)
}
  • Еще раз, это функция, которая изменяет (мутирует) внутреннее состояние структуры, и поэтому вы должны ее так и отметить. Название removeAtIndex соответствует методу Array. Считается хорошей практикой, если вы при необходимости пользуетесь дублированием (mirroring) API системы библиотеки. Это помогает разработчикам, использующим свой API, чувствовать себя как дома.
  • Во-первых, вам нужно проверить индекс, для того чтобы увидеть находится ли он в пределах массива. Попытка удалить его за границы элемента из лежащего в основе массива, вызовет ошибку выполнения, так что проверка поймает эту ошибку немного раньше. Вы, возможно, использовали утверждения в Objective-C, функция assert также доступна в Swift, но есть так же precondition, которая проверяет соответствия условию.
  • Далее, вы получите ключ из массива для данного индекса, в то же время удаляя значения из массива.
  • Затем удалите значение для этого ключа из словаря, который также возвращает существовавшее значение. Словарь может не содержать значения для данного ключа, поэтому removeValueForKey возвращает опциональное значение. В этом случае, вы знаете, что словарь будет содержать значение для данного ключа, потому что единственный способ добавить в словарь это ваш собственный insert(_:forKey:atIndex:), которые вы написали. Таким образом, вы можете сразу же развернуть опционал, зная, что там будет значение.
  • Наконец, вы возвращаете ключ и значение в кортеже. Это соответствует поведению массива removeAtIndex и словаря removeValueForKey, которые возвращают существующие значения.

Доступ значений

Теперь Вы можете писать в словаре, но не можете прочитать из него-а это не хорошо для структуры данных! Сейчас мы добавим методы, которые позволят вам получать значения из словаря.

Откроем OrderedDictionary.swift и добавим следующий код к определению структуры, сразу под объявлениями array и dictionary:

Вычисляемые свойства для подсчета упорядоченного словаря очень необходимы для такой структуры данных. Свойство count массива всегда будет соответствовать свойству count упорядоченного словаря, так что это просто!

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

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

Добавьте следующий код в нижнюю часть определения структуры:

// 1
subscript(key: KeyType) -> ValueType? {
  // 2(a)
  get {
    // 3
    return self.dictionary[key]
  }
  // 2(b)
  set {
    // 4
    if let index = find(self.array, key) {
    } else {
      self.array.append(key)
    }
 
    // 5
    self.dictionary[key] = newValue
  }
}

Вот то, что делает этот код:

  1. Так вы добавляете поведение сабскрипта. Вместо func или var, вы используете ключевое слово subscript. Параметр, в данном случае ключ, определяет объект, который вы ожидаете, должен появиться в квадратных скобках.
  2. Сабскрипты могут содержать сеттеры и геттеры, так же как и вычисляется свойства. Обратите внимание, что в этом случае есть и (a) get и (b) set замыкания, определяющие геттер и сеттер, соответственно.
  3. Геттер - это просто: Необходимо запросить значение у словаря для данного ключа. Сабскрипт словаря уже возвращает опционал, позволяя указать, что для этого ключа не существует значения.
  4. Сеттер является более сложным. Во-первых, он проверяет, существует ли уже ключ в упорядоченном словаре. Если он не существует, то вы должны добавить его в массив. Это имеет смысл, когда нужно добавить новый ключ в конец массива, так что вы вносите значение в массив путем добавления.
  5. Наконец, вы добавляете новое значение в словарь для данного ключа, передавая новое значение с помощью неявного имени переменной newValue.
  6. Теперь вы можете индексировать (составлять указали) в упорядоченном словаре, как будто это обычный словарь. Вы можете получить значение для определенного ключа, но что по поводу доступа через индекс, как с массивом? Так как это упорядоченный словарь, было бы полезно иметь доступ к элементу через индекс.
  7. Классы и структуры могут иметь несколько определений сабскрипта для различных типов аргументов. Добавьте следующую функцию в нижней части определения структуры:
subscript(index: Int) -> (KeyType, ValueType) {
  // 1
  get {
    // 2
    precondition(index < self.array.count, "Index out-of-bounds")
 
    // 3
    let key = self.array[index]
 
    // 4
    let value = self.dictionary[key]!
 
    // 5
    return (key, value)
  }
}

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

Вот то, что делает этот код:

  1. У сабскрипта есть только геттер. Вы могли бы реализовать также и сеттер, проверив, что индексы находяться в диапазоне упорядоченного словаря.
  2. Индекс должен быть в пределах массива, который определяет длину упорядоченного словаря. Вы можете использовать предварительное условие (precondition), для того чтобы предупредить программистов, которые пытаются получить доступ за границами упорядоченного словаря.
  3. Вы получите ключ из массива.
  4. Вы получите значение из словаря для данного ключа. Заметьте, опять же, использование извлечение опционала не обязательно, потому что вы знаете, что словарь должен содержать значение для любого ключа, находящегося в массиве.
  5. Наконец, вы вернете кортеж, содержащий ключ и значение.

Тестирование в Playground

Давайте создадим площадку для того, чтобы вы могли экспериментировать с тем, как именно компилятор выбирает какой метод сабскрипта использовать, и для того, чтобы увидеть как будет работать ваш OrderedDictionary.

Создадим новую площадку, нажав File\New\File… , выбираем iOS\Source\Playground и нажимаем Next. Назовем это ODPlayground, а затем нажмем Create.

Скопируйте и вставьте весь OrderedDictionary.swift в новую песочницу. Вам придется это сделать, потому что, к сожалению, пока вы пишите код, плейграунд не может "видеть" код в модуле вашего приложения.

Теперь добавьте следующее в конце:

var dict = OrderedDictionary

Вы увидите на боковой панеле (или через View\Assistant Editor\Show Assistant Editor ) результат print():

В этом примере, словарь имеет ключ Int, так что компилятор будет смотреть на тип переменной, назначенной для определения того, какой именно сабскрипт будет использоваться. Так как byIndex является кортежем (Int, String), то компилятор понимает, что нужно использовать версию индекса «в стиле массив», который соответствует ожидаемому возвращаемому типу.

Попробуйте удалить определение типа одного из byIndex или переменные byKey. Вы увидите сообщение об ошибке компилятора, указывающее, что компилятор не знает явно, какой сабскрипт использовать.

Заметка

Для того, чтобы вывод типа сработал, компилятору нужно, чтобы тип выражения был однозначным. Когда существует несколько методов с одними и теми же типами аргументов, но с разными возвращаемыми типами, вызов должен быть конкретным (определенным). Добавление метода в Swift может внести коренные изменения, так что будьте внимательны!

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

Теперь вы можете читать и писать в своем упорядоченном словаре! Что убережет вашу структуру данных. Теперь вы можете взяться за приложение!

Добавление поиска изображений

Давайте вернемся к приложению. Откройте MasterViewController.swift и добавьте следующее определение переменной, чуть ниже двух @IBOutlets:

var searches = OrderedDictionary()

Это будет упорядоченным словарем, который содержит данные о запросах пользователя в Flickr. Как вы можете видеть, он отображает результаты поиска в виде массиве Flickr.Photo, или фотографии, возвращенные из API Flickr. Обратите внимание, что вы даете ключ и значение в угловых скобках так же, как в нормальном словаре. Они становятся параметрами типа KeyType и ValueType в реализации.

Вы можете удивиться, почему тип Flickr.Photo содержит точку. Это потому, что Photo является классом, определяемым внутри класса Flickr. Такая иерархия является довольно полезной функцией в Swift, помогая содержать пространство имен, сохраняя имена классов короткими. Внутри класса Flickr, вы можете использовать Photo само по себе относящееся к классу «фото», потому что контекст сообщает компилятору, что это такое.

Далее, найдем метод, называемый tableView(_:numberOfRowsInSection:) и изменим его, чтобы он выглядел вот так:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return self.searches.count
  }

Этот метод в настоящее время использует упорядоченный словарь для того, чтобы сказать tableView сколько у него рядов.

Далее, найдем метод tableView(_:cellForRowAtIndexPath: ) и изменим его, чтобы он выглядел вот так:

Вот что мы делаем в этом методе:

  1. Во-первых, мы убираем из очереди ячейку из UITableView. Вам нужно перекинуть его непосредственно в UITableViewCell, потому что dequeueReusableCellWithIdentifierдо сих пор возвращает AnyObjectdequeueReusableCellWithIdentifier(id в Objective-C), а не UITableViewCell. Возможно, в будущем, компания Apple перепишет свой API для того, чтобы можно было воспользоваться дженериками и в этом случае!
  2. Затем, вы получаете ключ и значение для данной строки, используя сабскрипт по индексу, который вы написали.
  3. Наконец, вы ставите метку текста ячейки соответствующим образом и возвращаете ячейку.

Теперь суть. Найдите расширение UISearchBarDelegate и измените единственный метод так, чтобы он выглядел следующим образом:

func searchBarSearchButtonClicked(searchBar: UISearchBar) {
    // 1
    searchBar.resignFirstResponder()
    
    // 2
    let searchTerm = searchBar.text
    Flickr.search(searchTerm!) {
      switch ($0) {
      case .Error:
        // 3
        break
      case .Results(let results):
        // 4
        self.searches.insert(results,
          forKey: searchTerm!,
          atIndex: 0)
        
        // 5
        self.tableView.reloadData()
      }
    }
  }

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

  1. Вы отписываете вашу поисковую строку в качестве first responder и прочите интерактивную клавиатуру.
  2. Затем, вы обозначаете условия поиска, как текст в строке поиска, и используете класс Flickr для поиска этого термина. Метод поиска Flickr принимает и условие поиска и замыкание для успешного выполнения или провала поиска. Замыкание принимает один параметр: перечисление возможных Error или Results.
  3. В случае ошибки ничего не происходит. Если хотите, то возможен вариант создания оповещения, но пока нам этого не нужно. Код требует здесь оператора break, чтобы сказать компилятору о своем намерении во время ошибки ничего не делать.
  4. Если поиск работает, поиск возвращает результаты, как связанные значения типа перечисления SearchResults. Вы добавляете результаты в верхнюю часть упорядоченного словаря, с термином поиска в качестве ключа. Если поисковый запрос уже существует в словаре, то он перенесет его верхнюю часть списка и обновит его последними результатами.
  5. Наконец, вы перезагрузите внешний вид таблицы, поскольку теперь у вас есть новые данные.

Вот! Ваше приложение теперь будет искать изображения!

Запустите приложение, и создайте пару поисков. Вы увидите что-то вроде этого:

Теперь повторите запрос, находящейся не на верхней позиции. Вы увидите, что он поднялся в списке:

Нажмите на один из результатов поиска и обратите внимание, что он пока не показывает фотографии. Пришло время это исправить!

Покажи мне фотки!

Откройте MasterViewController.swift и найдите prepareForSegue. Измените его так, чтобы он выглядел следующим образом:

override func prepareForSegue(segue: UIStoryboardSegue,
    sender: AnyObject?)
  {
    if segue.identifier == "showDetail" {
      if let indexPath = self.tableView.indexPathForSelectedRow
      {
        let (_, photos) = self.searches[indexPath.row]
        (segue.destinationViewController
          as! DetailViewController).photos = photos
      }
    }
  }

При этом используется тот же метод доступа searches к упорядоченному словарю как и при создании ячеек. Он не использует ключ (поиск по ключевому слову), поэтому вы указали подчеркиванием, что этой части кортежа не нужно быть связанной с локальной переменной.

Запустите приложение, создайте поиск и затем нажмите на результат. Вы увидите что-то вроде этого:

Ну вот и все! Наше приложение готово!

Конечный проект урока вы можете скачать тут.

Урок подготовлен командой SwiftBook.ru

Исходная версия урока устарела, но если вам интересно, то она здесь:].

Содержание