Статьи

iOS с нуля с Swift: создание приложения для списка покупок 2

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

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

  • удаление элемента из свойства items контроллера представления
  • обновление табличного представления
  • сохранение изменений на диск

Посмотрим, как это работает на практике. Сначала нам нужно добавить кнопку редактирования на панель навигации. В viewDidLoad() контроллера представления создайте экземпляр UIBarButtonItem и назначьте его rightBarButtonItem свойства navigationItem контроллера представления.

Как и в предыдущем уроке , мы создаем элемент кнопки панели, вызывая init(barButtonSystemItem:target:action:) , передавая .Edit , значение элемента UIBarButtonSystemItem , self в качестве цели и "editItems:" в качестве селектора ,

01
02
03
04
05
06
07
08
09
10
11
12
override func viewDidLoad() {
    super.viewDidLoad()
     
    // Register Class
    tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier)
     
    // Create Add Button
    navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Add, target: self, action: «addItem:»)
     
    // Create Edit Button
    navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Edit, target: self, action: «editItems:»)
}

Реализация editItems(_:) — это только одна строка кода, как вы можете видеть ниже. Всякий раз, когда пользователь нажимает кнопку редактирования, представление таблицы переключается в режим редактирования или выходит из него. Мы делаем это с помощью небольшой хитрости. Мы спрашиваем табличное представление, находится ли оно в режиме редактирования, которое возвращает значение Bool , и инвертирует возвращаемое значение ( true становится false и наоборот). Метод, который мы вызываем в табличном представлении, это setEditing(_:animated:) , специализированный установщик, который принимает параметр анимации.

1
2
3
func editItems(sender: UIBarButtonItem) {
    tableView.setEditing(!tableView.editing, animated: true)
}

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

Переключение просмотра таблицы в режим редактирования и из режима редактирования
Переключение просмотра таблицы в режим редактирования и из режима редактирования

Два метода протокола UITableViewDataSource важны для включения редактирования в табличном представлении:

  • tableView(_:canEditRowAtIndexPath:)
  • tableView(_:commitEditingStyle:forRowAtIndexPath:)

Если пользователь нажимает кнопку редактирования, табличное представление запрашивает источник данных, какие строки можно редактировать, отправляя источнику данных сообщение tableView(_:canEditRowAtIndexPath:) . Если для определенного пути индекса возвращается значение true , табличное представление указывает соответствующей ячейке табличного представления, что ему необходимо переключиться в режим редактирования или выйти из него, в зависимости от режима редактирования табличного представления. Это переводит в ячейку табличного представления, показывая или скрывая ее элементы редактирования. tableView(_:canEditRowAtIndexPath:) метод tableView(_:canEditRowAtIndexPath:) как показано ниже, чтобы увидеть, как это работает на практике.

1
2
3
4
5
6
7
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    if indexPath.row == 1 {
        return false
    }
     
    return true
}

Приведенная выше реализация tableView(_:canEditRowAtIndexPath:) позволяет пользователю редактировать каждую строку в табличном представлении, за исключением второй строки. Запустите приложение в симуляторе и попробуйте.

Добавление возможности редактировать табличное представление

Для приложения списка покупок пользователь должен иметь возможность редактировать каждую строку в табличном представлении. Это означает, что tableView(_:canEditRowAtIndexPath:) всегда должен возвращать true .

1
2
3
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
    return true
}

Возможно, вы заметили, что ничего не происходит, когда вы пытаетесь удалить строку в табличном представлении. Загадка еще не закончена. Всякий раз, когда пользователь нажимает кнопку удаления строки, табличное представление отправляет своему источнику данных сообщение tableView(_:commitEditingStyle:forRowAtIndexPath:) . Второй аргумент соответствующего метода указывает, какой тип действия пользователь выполнил, вставив или удалив строку. Для приложения списка покупок мы только реализуем поддержку удаления строк из табличного представления.

Удаление строк из табличного представления включает в себя:

  • удаление соответствующего элемента из свойства items контроллера представления
  • Обновление табличного представления путем удаления соответствующей строки

Давайте tableView(_:commitEditingStyle:forRowAtIndexPath:) реализацию tableView(_:commitEditingStyle:forRowAtIndexPath:) . Метод начинается с проверки того, равен ли стиль редактирования .Delete (значение элемента UITableViewCellEditingStyle ), потому что мы хотим только разрешить пользователю удалять строки из табличного представления.

Если стиль редактирования равен .Delete , соответствующий элемент удаляется из свойства items , соответствующая строка удаляется из табличного представления, а обновленный список элементов сохраняется на диск.

01
02
03
04
05
06
07
08
09
10
11
12
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
    if editingStyle == .Delete {
        // Delete Item from Items
        items.removeAtIndex(indexPath.row)
         
        // Update Table View
        tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Right)
         
        // Save Changes
        saveItems()
    }
}

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

Мы могли бы повторно использовать класс AddItemViewController для редактирования элементов. Однако, поскольку использование одного контроллера представления для добавления и редактирования элементов часто может усложнить реализацию, я обычно заканчиваю тем, что создаю отдельный класс для редактирования элементов. Изначально это может привести к тому, что мы будем повторяться, но это даст нам больше гибкости в будущем.

Создайте новый подкласс UIViewController и назовите его EditItemViewController . Интерфейс классов AddItemViewController и EditItemViewController очень похожи.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
import UIKit
 
protocol EditItemViewControllerDelegate {
    func controller(controller: EditItemViewController, didUpdateItem item: Item)
}
 
class EditItemViewController: UIViewController {
 
    @IBOutlet var nameTextField: UITextField!
    @IBOutlet var priceTextField: UITextField!
     
    var item: Item!
     
    var delegate: EditItemViewControllerDelegate?
     
    …
 
}

Различия заключаются в добавлении свойства item для хранения ссылки на редактируемый элемент и определении метода протокола EditItemViewControllerDelegate . Обратите внимание, что item имеет тип Item! Принудительная распаковка необязательна. Поскольку контроллер представления редактирования элемента бесполезен без элемента для редактирования, мы ожидаем, что свойство item всегда будет иметь значение.

Откройте Main.storyboard , перетащите экземпляр UIViewController из библиотеки объектов , установите для его класса значение EditItemViewController и создайте последовательность показа вручную из контроллера представления списка в контроллер представления редактирования элемента. Установите идентификатор перехода в EditItemViewController .

Создание push-перехода к контроллеру редактирования элемента

Перетащите текстовые поля из библиотеки объектов в представление контроллера представления и расположите их, как показано на рисунке ниже. Выберите верхнее текстовое поле, откройте инспектор атрибутов и введите « Имя» в поле « Заполнитель» . Выделите нижнее текстовое поле и в инспекторе атрибутов установите для его текста-заполнителя значение « Цена» и установите для « Клавиатура» значение « Цифровая панель» . Выберите объект контроллера представления, откройте инспектор соединений и соедините nameTextField и priceTextField с соответствующим текстовым полем в пользовательском интерфейсе.

Настройка текстовых полей

В viewDidLoad() контроллера представления создайте кнопку сохранения, как мы делали в классе AddItemViewController .

1
2
3
4
5
6
7
8
// MARK: —
// MARK: View Life Cycle
override func viewDidLoad() {
    super.viewDidLoad()
     
    // Create Save Button
    navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Save, target: self, action: «save:»)
}

Реализация действия save(_:) очень похожа на ту, что мы реализовали в классе AddItemViewController . Однако есть несколько тонких различий, на которые я хочу обратить внимание.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
// MARK: —
// MARK: Actions
func save(sender: UIBarButtonItem) {
    if let name = nameTextField.text, let priceAsString = priceTextField.text, let price = Float(priceAsString) {
        // Update Item
        item.name = name
        item.price = price
         
        // Notify Delegate
        delegate?.controller(self, didUpdateItem: item)
         
        // Pop View Controller
        navigationController?.popViewControllerAnimated(true)
    }
}

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

Через несколько минут мы реализуем функциональность, которая позволяет пользователю выбирать элементы в контроллере представления списка, чтобы добавить их в список покупок. Пользователь сможет сделать это, нажав строку в представлении списка. Вопрос в том, как пользователь сможет редактировать товар, если постукивание по строке зарезервировано для добавления товара в список покупок?

Инфраструктура UIKit предоставляет кнопку подробного раскрытия информации именно для этого варианта использования. Кнопка раскрытия подробностей расположена справа от ячейки табличного представления. Чтобы добавить кнопку подробного раскрытия в ячейку табличного представления, нам нужно повторно просмотреть tableView(_:cellForRowAtIndexPath:) в контроллере представления списка и изменить его, как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    // Dequeue Reusable Cell
    let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath)
     
    // Fetch Item
    let item = items[indexPath.row]
     
    // Configure Table View Cell
    cell.textLabel?.text = item.name
    cell.accessoryType = .DetailDisclosureButton
     
    return cell
}

Каждая ячейка табличного представления имеет свойство accessoryType . В tableView(_:cellForRowAtIndexPath:) мы устанавливаем для него значение .DetailDisclosureButton , значение элемента UITableViewCellAccessoryType . Возможно, вы заметили, что инженеры Apple не любят короткие имена.

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

01
02
03
04
05
06
07
08
09
10
11
12
// MARK: —
// MARK: Table View Delegate Methods
override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
    // Fetch Item
    let item = items[indexPath.row]
     
    // Update Selection
    selection = item
     
    // Perform Segue
    performSegueWithIdentifier(«EditItemViewController», sender: self)
}

Мы выбираем правильный элемент из свойства items и сохраняем его в selection , свойстве контроллера представления списка, которое мы объявим через минуту. Затем мы выполняем переход с идентификатором EditItemViewController .

Прежде чем мы обновим реализацию prepareForSegue(_:sender:) , нам нужно объявить свойство для хранения выбранного элемента. Нам также необходимо согласовать класс ListViewController с протоколом EditItemViewControllerDelegate .

01
02
03
04
05
06
07
08
09
10
11
12
import UIKit
 
class ListViewController: UITableViewController, AddItemViewControllerDelegate, EditItemViewControllerDelegate {
 
    let CellIdentifier = «Cell Identifier»
     
    var items = [Item]()
    var selection: Item?
     
    …
 
}

Обновленная реализация prepareForSegue(_:sender:) довольно проста, как вы можете видеть ниже. Мы получаем ссылку на контроллер представления элемента edit и устанавливаем его delegate и свойства item . Поскольку свойство selection имеет тип Item? мы привязываем его к константе item чтобы безопасно развернуть его.

01
02
03
04
05
06
07
08
09
10
11
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == «AddItemViewController» {
      …
         
    } else if segue.identifier == «EditItemViewController» {
        if let editItemViewController = segue.destinationViewController as?
            editItemViewController.delegate = self
            editItemViewController.item = item
        }
    }
}

Реализация EditItemViewController почти завершена. В его viewDidLoad() мы заполняем текстовые поля данными свойства item . Поскольку свойство text текстового поля имеет тип String? Мы используем строковую интерполяцию для создания строки из значения Float свойства price элемента. Строковая интерполяция в Swift довольно мощная.

01
02
03
04
05
06
07
08
09
10
override func viewDidLoad() {
    super.viewDidLoad()
     
    // Create Save Button
    navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .Save, target: self, action: «save:»)
     
    // Populate Text Fields
    nameTextField.text = item.name
    priceTextField.text = «\(item.price)»
}

Принятие протокола EditItemViewControllerDelegate означает реализацию метода controller(_:didUpdateItem:) как показано ниже. Вас может удивить, что мы не обновляем источник данных табличного представления. Все, что мы делаем в методе делегата, это перезагрузим одну строку табличного представления.

01
02
03
04
05
06
07
08
09
10
11
12
// MARK: —
// MARK: Edit Item View Controller Delegate Methods
func controller(controller: EditItemViewController, didUpdateItem item: Item) {
    // Fetch Index for Item
    if let index = items.indexOf(item) {
        // Update Table View
        tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: index, inSection: 0)], withRowAnimation: .Fade)
    }
     
    // Save Items
    saveItems()
}

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

Не забудьте сохранить список элементов, чтобы убедиться, что изменения записаны на диск. Запустите приложение, чтобы проверить функциональность редактирования.

Прежде чем мы исследуем источник данных контроллера представления списка покупок, давайте создадим некоторые леса для работы с ними. Создайте новый подкласс UITableViewController и назовите его ShoppingListViewController .

Откройте ShoppingListViewController.swift и добавьте два свойства типа [Item] :

  • items , которые будут содержать полный список элементов
  • shoppingList , который будет содержать только элементы списка покупок
01
02
03
04
05
06
07
08
09
10
import UIKit
 
class ShoppingListViewController: UITableViewController {
 
    var items = [Item]()
    var shoppingList = [Item]()
     
    …
 
}

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

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

Наблюдатели за недвижимостью — отличный способ реагировать на изменения стоимости имущества. Давайте посмотрим, как мы можем использовать наблюдатели свойства для обновления свойства shoppingList при каждом изменении значения items .

1
2
3
4
5
var items = [Item]() {
    didSet {
        buildShoppingList()
    }
}

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

  • willSet , который вызывается до установки нового значения свойства
  • didSet , который вызывается после установки нового значения свойства

В приведенном выше примере мы вызываем buildShoppingList() всякий раз, когда новым items присваивается новое значение. Вот как выглядит реализация buildShoppingList() .

1
2
3
4
5
6
7
// MARK: —
// MARK: Helper Methods
func buildShoppingList() {
    shoppingList = items.filter({ (item) -> Bool in
        return item.inShoppingList
    })
}

Мы фильтруем элементы массива items , включая только те элементы, для которых inShoppingList имеет значение true . Результат присваивается shoppingList .

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

1
2
3
4
5
var shoppingList = [Item]() {
    didSet {
        tableView.reloadData()
    }
}

Реализация методов протокола UITableViewDataSource должна быть детской игрой на данный момент. Посмотрите на реализацию ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
// MARK: —
// MARK: Table View Data Source Methods
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
}
 
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return shoppingList.count
}
 
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    // Dequeue Reusable Cell
    let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath)
     
    // Fetch Item
    let item = shoppingList[indexPath.row]
     
    // Configure Table View Cell
    cell.textLabel?.text = item.name
     
    return cell
}

Не забудьте объявить идентификатор повторного использования ячейки и зарегистрировать класс UITableViewCell как мы делали в классе ListViewController .

1
2
3
4
5
6
7
8
9
import UIKit
 
class ShoppingListViewController: UITableViewController {
 
    let CellIdentifier = «Cell Identifier»
     
    …
 
}
1
2
3
4
5
6
7
8
// MARK: —
// MARK: View Life Cycle
override func viewDidLoad() {
    super.viewDidLoad()
     
    // Register Class
    tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier)
}

Как я объяснил ранее, ключевое преимущество использования списка товаров для хранения и построения списка покупок состоит в том, что приложение хранит каждый товар только в одном месте. Это делает обновление элементов в списке и в списке покупок менее головной болью. loadItems() и pathForItems() идентичны тем, которые мы реализовали в классе ListViewController .

1
2
3
4
5
6
7
private func loadItems() {
    if let filePath = pathForItems() where NSFileManager.defaultManager().fileExistsAtPath(filePath) {
        if let archivedItems = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as?
            items = archivedItems
        }
    }
}
1
2
3
4
5
6
7
8
9
private func pathForItems() -> String?
    let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
     
    if let documents = paths.first, let documentsURL = NSURL(string: documents) {
        return documentsURL.URLByAppendingPathComponent(«items»).path
    }
     
    return nil
}

Когда вы обнаружите, что дублируете код, вы должны услышать сигнал тревоги. Дублирование кода не вызывает проблем, пока вы реализуете новую функцию. Однако после этого вам следует рассмотреть возможность рефакторинга вашего кода, чтобы минимизировать количество дублирования в кодовой базе приложения. Это очень важная концепция в разработке программного обеспечения, и ее часто называют СУХОЙ , не повторяйте себя . Крис Питерс написал отличную статью на Envato Tuts + о СУХОМ программировании.

Прежде чем мы используем класс ShoppingListViewController , нам нужно обновить метод viewDidLoad() класса. Помимо установки заголовка контроллера представления, мы также загружаем список предметов, который автоматически заполняет массив shoppingList как мы видели ранее.

01
02
03
04
05
06
07
08
09
10
11
12
override func viewDidLoad() {
    super.viewDidLoad()
     
    // Set Title
    title = «Shopping List»
     
    // Load Items
    loadItems()
     
    // Register Class
    tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier)
}

Последний шаг — инициализация контроллера представления списка покупок путем обновления раскадровки. Это включает в себя добавление экземпляра UITableViewController , установку его класса на ShoppingListViewController , встраивание его в контроллер навигации и создание перехода между контроллером панели вкладок и контроллером навигации. Выберите табличное представление контроллера представления списка покупок и установите число ячеек прототипа равным 0 .

Создание контроллера просмотра списка покупок

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

Как я писал ранее, идея состоит в том, чтобы добавить элемент в список покупок при его нажатии в контроллере представления списка. Чтобы улучшить взаимодействие с пользователем, если элемент присутствует в списке покупок, мы показываем зеленую галочку слева от названия элемента. Если элемент, который уже находится в списке покупок, коснулся, он удаляется из списка покупок, и зеленая галочка исчезает. Это означает, что нам нужно взглянуть на метод tableView(_:didSelectRowAtIndexPath:) протокола UITableViewDelegate .

Прежде чем мы реализуем tableView(_:didSelectRowAtIndexPath:) , загрузите исходные файлы этого урока. В папке с именем Resources найдите файлы с именами checkmark.png и [email protected] . Добавьте оба этих файла в проект, потому что они понадобятся нам через несколько минут.

В первой строке tableView(_:didSelectRowAtIndexPath:) мы отправляем табличному представлению сообщение deselectRowAtIndexPath(_:animated:) чтобы отменить выбор строки, которую коснулся пользователь. При каждом нажатии на строку она должна выделяться только на мгновение, отсюда и это добавление. Затем мы выбираем элемент, который соответствует выбору пользователя, и обновляем свойство inShoppingList элемента ( true становится false и наоборот).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
     
    // Fetch Item
    let item = items[indexPath.row]
     
    // Update Item
    item.inShoppingList = !item.inShoppingList
     
    // Update Cell
    let cell = tableView.cellForRowAtIndexPath(indexPath)
     
    if item.inShoppingList {
        cell?.imageView?.image = UIImage(named: «checkmark»)
    } else {
        cell?.imageView?.image = nil
    }
     
    // Save Items
    saveItems()
}

На основании значения свойства inShoppingList мы либо показываем, либо скрываем зеленую галочку. Мы показываем галочку, устанавливая свойство image свойства imageView ячейки табличного представления. Ячейка табличного представления включает в себя представление изображения слева (экземпляр класса UIImageView ). Если установить для свойства изображения представления image значение nil , представление изображения будет пустым, без изображения.

Реализация tableView(_:didSelectRowAtIndexPath:) завершается сохранением списка элементов на диске, чтобы убедиться, что изменения являются постоянными.

Как список покупок узнает, когда пользователь нажимает на элемент в контроллере представления списка? Контроллер представления списка покупок не будет автоматически обновлять свое представление таблицы, если было внесено изменение в список элементов в контроллере представления списка. Чтобы предотвратить тесную связь, мы не хотим, чтобы контроллер представления списка и контроллер представления списка покупок общались напрямую друг с другом.

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

Как все это работает? Есть три этапа:

  • контроллер представления списка покупок начинает с уведомления центра уведомлений о том, что он заинтересован в получении уведомлений с именем ShoppingListDidChangeNotification
  • контроллер представления списка отправляет уведомление в центр уведомлений всякий раз, когда он обновляет список элементов
  • когда контроллер представления списка покупок получает уведомление от центра уведомлений, он обновляет свой источник данных и представление таблицы в ответ

Прежде чем мы реализуем три шага, которые я только что описал, неплохо бы поближе познакомиться с классом NSNotificationCenter .

NSNotificationCenter говоря, NSNotificationCenter управляет NSNotificationCenter уведомлений. Объекты в приложении могут регистрироваться в центре уведомлений для получения уведомлений с использованием addObserver(_:selector:name:object:) где:

  • первый аргумент — это объект, который будет получать уведомления (наблюдатель)
  • selector — это действие, которое вызывается у наблюдателя при получении уведомления.
  • name это имя уведомления
  • object — это объект, который запускает отправку уведомления

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

Пересмотрите метод viewDidLoad() класса ShoppingListViewController и добавьте экземпляр контроллера представления в качестве наблюдателя для получения уведомлений с именем ShoppingListDidChangeNotification .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
override func viewDidLoad() {
    super.viewDidLoad()
     
    // Set Title
    title = «Shopping List»
     
    // Load Items
    loadItems()
     
    // Register Class
    tableView.registerClass(UITableViewCell.classForCoder(), forCellReuseIdentifier: CellIdentifier)
     
    // Add Observer
    NSNotificationCenter.defaultCenter().addObserver(self, selector: «updateShoppingList:», name: «ShoppingListDidChangeNotification», object: nil)
}

Действие запускается, когда контроллер представления получает уведомление с таким именем updateShoppingList(_:) . Последний параметр, object , равен nil , потому что не имеет значения, какой объект отправил уведомление.

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

Объект уведомления содержит ссылку на объект, который разместил уведомление, и может также содержать словарь с дополнительной информацией. Реализация метода updateShoppingList(_:) довольно проста. Мы вызываем loadItems() на контроллере представления, что означает, что список элементов загружается с диска. Остальное происходит автоматически благодаря наблюдателям свойств, которые мы реализовали ранее.

1
2
3
4
5
// MARK: —
// MARK: Notification Handling
func updateShoppingList(notification: NSNotification) {
    loadItems()
}

Третий фрагмент головоломки — это публикация уведомления всякий раз, когда список элементов изменяется контроллером представления списка. Мы можем сделать это в saveItems() класса ListViewController .

1
2
3
4
5
6
7
8
private func saveItems() {
    if let filePath = pathForItems() {
        NSKeyedArchiver.archiveRootObject(items, toFile: filePath)
         
        // Post Notification
        NSNotificationCenter.defaultCenter().postNotificationName(«ShoppingListDidChangeNotification», object: self)
    }
}

Сначала мы запрашиваем ссылку на центр уведомлений по умолчанию, вызывая defaultCenter() класса NSNotificationCenter . Затем мы вызываем postNotificationName(_:object:) в центре уведомлений по умолчанию, передавая имя уведомления ShoppingListDidChangeNotification и объект, отправляющий уведомление.

Перед созданием проекта обязательно tableView(_:cellForRowAtIndexPath:) в ListViewController.swift, как показано ниже, чтобы отобразить зеленую галочку для элементов, которые уже присутствуют в списке покупок.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    // Dequeue Reusable Cell
    let cell = tableView.dequeueReusableCellWithIdentifier(CellIdentifier, forIndexPath: indexPath)
     
    // Fetch Item
    let item = items[indexPath.row]
     
    // Configure Table View Cell
    cell.textLabel?.text = item.name
    cell.accessoryType = .DetailDisclosureButton
     
    if item.inShoppingList {
        cell.imageView?.image = UIImage(named: «checkmark»)
    } else {
        cell.imageView?.image = nil
    }
     
    return cell
}

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

Завершение работы со списком покупок

Отлично. Где кнопка для публикации приложения со списком покупок в App Store? Мы еще не совсем закончили. Несмотря на то, что мы заложили основу приложения списка покупок, оно не готово к публикации. Есть также несколько вещей, чтобы рассмотреть.

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

Кроме того, пользователи могут захотеть сохранить более одного списка покупок. Как бы вы справились с этим? Один из вариантов — хранить каждый список покупок в отдельном файле, но как вы будете реагировать на изменения, внесенные в товары? Собираетесь ли вы обновить каждый список покупок, который содержит товар? Когда вы начинаете работать со связями, лучше выбрать хранилище данных SQLite.

Core Data — отличный компаньон, если вы решите пойти по этому пути. Это мощный фреймворк, обладающий множеством функций, которые делают большую часть кода в нашем приложении списка покупок устаревшим. Это правда, что Core Data приносит с собой немного больше накладных расходов, поэтому важно сначала рассмотреть вопрос о том, подходят ли Core Data для вашего приложения, другими словами, стоит ли это накладных расходов.

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

Даже если приложение списка покупок не совсем готово для App Store, вы не можете отрицать, что оно работает как запланировано, и оно продемонстрировало вам несколько новых аспектов разработки Cocoa, таких как уведомления и реализация пользовательского протокола делегата.

Теперь вы знаете, чего ожидать от iOS SDK и на что похожа разработка iOS. Вам решать, хотите ли вы продолжить свое путешествие и стать опытным разработчиком iOS. Если вы решите продолжить разработку для iOS, я предоставлю вам несколько полезных ресурсов в следующей и последней части этой серии.

Если у вас есть какие-либо вопросы или комментарии, вы можете оставить их в комментариях ниже или обратиться ко мне в Twitter .