На предыдущем уроке мы добавили возможность создавать задачи. Хотя это добавление сделало приложение немного более полезным, было бы также удобно добавить возможность отмечать элементы как выполненные и удалять элементы. Это то, на чем мы сосредоточимся в этом уроке.
Предпосылки
Если вы хотите следовать за мной, убедитесь, что на вашем компьютере установлен Xcode 8.3.2 или выше. Вы можете скачать Xcode 8.3.2 из Apple App Store .
1. Удаление элементов
Чтобы удалить элементы, нам нужно реализовать два дополнительных метода протокола UITableViewDataSource
. Сначала нам нужно указать табличному представлению, какие строки можно редактировать, реализовав метод tableView(_:canEditRowAt:)
. Как вы можете видеть в следующем фрагменте кода, реализация проста. Мы сообщаем табличному представлению, что каждая строка является редактируемой, возвращая true
.
1
2
3
|
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
|
Второй метод, который нас интересует, это tableView(_:commit:forRowAt:)
. Реализация немного сложнее, но достаточно проста для понимания.
1
2
3
4
5
6
7
8
9
|
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Update Items
items.remove(at: indexPath.row)
// Update Table View
tableView.deleteRows(at: [indexPath], with: .right)
}
}
|
Мы начнем с проверки значения editingStyle
, перечисления типа UITableViewCellEditingStyle
. Мы удаляем элемент, только если значение editingStyle
равно UITableViewCellEditingStyle.delete
.
Свифт умнее этого. Поскольку он знает, что editingStyle
имеет тип UITableViewCellEditingStyle
, мы можем опустить UITableViewCellEditingStyle
, имя перечисления, и написать .delete
, значение члена перечисления, в котором мы заинтересованы. Если вы новичок в перечислениях в Swift, то Я рекомендую вам прочитать этот быстрый совет о перечислениях в Swift.
Затем мы обновляем источник данных табличного представления items
, вызывая метод remove(at:)
для свойства items
, передавая правильный индекс. Мы также обновляем табличное представление, вызывая deleteRows(at:with:)
для tableView
, передавая массив с indexPath
и .right
чтобы указать тип анимации. Как мы видели ранее, мы можем опустить имя перечисления, UITableViewRowAnimation
, так как Swift знает, что тип второго аргумента — UITableViewRowAnimation
.
Теперь пользователь должен иметь возможность удалять элементы из списка. Создайте и запустите приложение, чтобы проверить это.
2. Отключение пунктов
Чтобы пометить элемент как выполненный, мы добавим галочку к соответствующей строке. Это означает, что нам необходимо отслеживать элементы, помеченные пользователем как выполненные. Для этого мы объявим новое свойство, которое управляет этим для нас. checkedItems
свойство переменной checkedItems
типа [String]
и инициализируйте его пустым массивом.
1
|
var checkedItems: [String] = []
|
В tableView(_:cellForRowAt:)
мы проверяем, содержит ли checkedItems
соответствующий элемент, вызывая метод contains(_:)
, передавая элемент, соответствующий текущей строке. Метод возвращает значение true
если checkedItems
содержит item
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Fetch Item
let item = items[indexPath.row]
// Dequeue Cell
let cell = tableView.dequeueReusableCell(withIdentifier: «TableViewCell», for: indexPath)
// Configure Cell
cell.textLabel?.text = item
if checkedItems.contains(item) {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
|
Если item
найден в checkedItems
, мы устанавливаем accessoryType
свойства accessoryType
ячейки значение .checkmark
, значение члена перечисления UITableViewCellAccessoryType
. Если item
не найден, мы возвращаемся к .none
как к типу аксессуара ячейки.
Следующим шагом является добавление возможности пометить элемент как выполненное путем реализации метода протокола UITableViewDelegate
, tableView(_:didSelectRowAt:)
. В этом методе делегата мы сначала вызываем deselectRow(at:animated:)
для tableView
чтобы отменить выбор строки, которую tableView
пользователь.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// MARK: — Table View Delegate Methods
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Fetch Item
let item = items[indexPath.row]
// Fetch Cell
let cell = tableView.cellForRow(at: indexPath)
// Find Index of Item
let index = checkedItems.index(of: item)
if let index = index {
checkedItems.remove(at: index)
cell?.accessoryType = .none
} else {
checkedItems.append(item)
cell?.accessoryType = .checkmark
}
}
|
Затем мы выбираем соответствующий элемент из items
и ссылку на ячейку, которая соответствует повернутой строке. Мы запрашиваем checkedItems
для индекса соответствующего элемента, вызывая index(of:)
. Этот метод возвращает необязательный Int
. Если checkedItems
содержит item
, мы удаляем его из checkedItems
и устанавливаем тип аксессуара ячейки .none
. Если checkedItems
не содержит item
, мы добавляем его в checkedItems
и устанавливаем тип аксессуара ячейки .checkmark
.
Благодаря этим дополнениям пользователь теперь может пометить элементы как выполненные. Создайте и запустите приложение, чтобы убедиться, что все работает как положено.
3. Сохранение государства
В настоящее время приложение не сохраняет состояние между запусками. Чтобы решить эту проблему, мы собираемся сохранить массивы items
и checkedItems
в пользовательской базе данных приложения по умолчанию.
Шаг 1: Состояние загрузки
Начните с создания двух вспомогательных методов loadItems()
и loadCheckedItems()
. Обратите внимание на private
ключевое слово с префиксом каждого вспомогательного метода. Ключевое слово private
сообщает Swift, что эти методы доступны только из класса ViewController
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
// MARK: Private Helper Methods
private func loadItems() {
let userDefaults = UserDefaults.standard
if let items = userDefaults.object(forKey: «items») as?
self.items = items
}
}
private func loadCheckedItems() {
let userDefaults = UserDefaults.standard
if let checkedItems = userDefaults.object(forKey: «checkedItems») as?
self.checkedItems = checkedItems
}
}
|
private
ключевое слово является частью контроля доступа Swift. Как следует из названия, контроль доступа определяет, какой код имеет доступ к какому коду. Уровни доступа применяются к методам, функциям, типам и т. Д. Apple просто ссылается на объекты . Существует пять уровней доступа: открытый, общедоступный, внутренний, частный файл и частный.
- Открытый / общедоступный: объекты, помеченные как открытые или открытые, доступны для объектов, определенных в том же модуле, а также в других модулях. Это идеально подходит для демонстрации интерфейса фреймворка. Существует несколько различий между уровнями открытого и публичного доступа. Вы можете прочитать больше об этих различиях в языке программирования Swift .
- Внутренний: это уровень доступа по умолчанию. Другими словами, если уровень доступа не указан, этот уровень доступа применяется. Объект с внутренним уровнем доступа доступен только объектам, определенным в том же модуле.
- File-Private: объект, объявленный как file-private, доступен только объектам, определенным в том же исходном файле. Например, частные вспомогательные методы, определенные в классе
ViewController
, доступны только классуViewController
. - Private: Private очень похож на file-private. Единственное отличие состоит в том, что объект, объявленный как частный, доступен только из декларации, к которой он приложен. Например, если мы создадим расширение для класса
ViewController
в ViewController.swift , любые объекты, помеченные как закрытые для файла, не будут доступны в расширении, но будут доступны частные объекты.
Реализация вспомогательных методов проста, если вы знакомы с классом UserDefaults
. Для простоты использования мы храним ссылку на стандартный объект по умолчанию пользователя в константе с именем userDefaults
. В случае loadItems()
мы запрашиваем у userDefaults
объект, связанный с ключом "items"
и понижаем его до необязательного массива строк. Мы безопасно разворачиваем необязательное значение, что означает, что мы сохраняем значение в постоянных items
если необязательное значение не равно nil
, и присваиваем это значение свойству items
контроллера представления.
Если оператор if
выглядит сбивающим с толку, взгляните на более простую версию метода loadItems()
в следующем примере. Результат идентичен; единственная разница — краткость.
1
2
3
4
5
6
7
8
|
private func loadItems() {
let userDefaults = UserDefaults.standard
let storedItems = userDefaults.object(forKey: «items») as?
if let items = storedItems {
self.items = items
}
}
|
Реализация loadCheckedItems()
идентична, за исключением ключа, используемого для загрузки объекта, хранящегося в базе данных пользователя по умолчанию. Давайте loadItems()
и loadCheckedItems()
, обновив метод viewDidLoad()
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
override func viewDidLoad() {
super.viewDidLoad()
// Set Title
title = «To Do»
// Populate Items
items = [«Buy Milk», «Finish Tutorial», «Play Minecraft»]
// Load State
loadItems()
loadCheckedItems()
// Register Class for Cell Reuse
tableView.register(UITableViewCell.self, forCellReuseIdentifier: «TableViewCell»)
}
|
Шаг 2: Сохранение состояния
Для сохранения состояния мы реализуем еще два закрытых вспомогательных метода: saveItems()
и saveCheckedItems()
. Логика похожа на loadItems()
и loadCheckedItems()
. Разница в том, что мы храним данные в базе данных пользователя по умолчанию. Убедитесь, что ключи, используемые в setObject(_:forKey:)
совпадают с loadItems()
используемыми в loadItems()
и loadCheckedItems()
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
private func saveItems() {
let userDefaults = UserDefaults.standard
// Update User Defaults
userDefaults.set(items, forKey: «items»)
userDefaults.synchronize()
}
private func saveCheckedItems() {
let userDefaults = UserDefaults.standard
// Update User Defaults
userDefaults.set(checkedItems, forKey: «checkedItems»)
userDefaults.synchronize()
}
|
Вызов synchronize()
не является строго обязательным. Операционная система позаботится о том, чтобы данные, которые вы храните в базе данных пользователя по умолчанию, были записаны на диск в какой-то момент . Однако, вызывая synchronize()
, вы явно указываете операционной системе записывать любые ожидающие изменения на диск. Это полезно во время разработки, потому что операционная система не будет записывать ваши изменения на диск, если вы убьете приложение. Тогда может показаться, что что-то не работает должным образом.
Нам нужно вызывать saveItems()
и saveCheckedItems()
в нескольких местах. Для начала вызовите saveItems()
когда новый элемент добавлен в список. Мы делаем это в методе AddItemViewControllerDelegate
протокола AddItemViewControllerDelegate
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
// MARK: Add Item View Controller Delegate Methods
func controller(_ controller: AddItemViewController, didAddItem: String) {
// Update Data Source
items.append(didAddItem)
// Save State
saveItems()
// Reload Table View
tableView.reloadData()
// Dismiss Add Item View Controller
dismiss(animated: true)
}
|
Когда состояние элемента изменяется в tableView(_:didSelectRowAt:)
, мы обновляем checkedItems
. Это хорошая идея, чтобы также вызвать saveCheckedItems()
на этом этапе.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
// MARK: — Table View Delegate Methods
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Fetch Item
let item = items[indexPath.row]
// Fetch Cell
let cell = tableView.cellForRow(at: indexPath)
// Find Index of Item
let index = checkedItems.index(of: item)
if let index = index {
checkedItems.remove(at: index)
cell?.accessoryType = .none
} else {
checkedItems.append(item)
cell?.accessoryType = .checkmark
}
// Save State
saveCheckedItems()
}
|
При удалении items
обновляются как items
и checkedItems
элементы. Чтобы сохранить это изменение, мы вызываем и saveItems()
и saveCheckedItems()
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// Fetch Item
let item = items[indexPath.row]
// Update Items
items.remove(at: indexPath.row)
if let index = checkedItems.index(of: item) {
checkedItems.remove(at: index)
}
// Update Table View
tableView.deleteRows(at: [indexPath], with: .right)
// Save State
saveItems()
saveCheckedItems()
}
}
|
Вот и все. Создайте и запустите приложение, чтобы проверить свою работу. Поиграйте с приложением и принудительно выйдите из него. При повторном запуске приложения последнее известное состояние должно быть загружено и отображено.
4. Наблюдатели за недвижимостью
Пользовательский опыт приложения немного не хватает на данный момент. Когда каждый элемент удаляется или когда приложение запускается впервые, пользователь видит пустое табличное представление. Это не здорово. Мы можем решить эту проблему, показывая сообщение, когда нет элементов. Это также даст мне возможность показать вам еще одну особенность Свифта, наблюдателей за недвижимостью .
Шаг 1: Добавление метки
Начнем с добавления метки в пользовательский интерфейс для отображения сообщения. messageLabel
выход с именем messageLabel
типа UILabel
в классе ViewController
, откройте Main.storyboard и добавьте метку в представление контроллера представления.
1
|
@IBOutlet var messageLabel: UILabel!
|
Добавьте необходимые ограничения макета к метке и соедините его с выходом messageLabel
контроллера представления в Инспекторе соединений . Установите текст метки на « У вас нет задач». и центрируйте текст метки в Инспекторе Атрибутов .
Шаг 2. Реализация наблюдателя свойства
Метка сообщения должна быть видимой, только если items
содержат элементов. Когда это происходит, мы также должны скрыть табличное представление. Мы могли бы решить эту проблему, добавив различные проверки в классе ViewController
, но более удобный и элегантный подход заключается в использовании наблюдателя свойства.
Как следует из названия, наблюдатели за имуществом наблюдают за имуществом. Наблюдатель свойства вызывается всякий раз, когда свойство изменяется, даже когда новое значение совпадает со старым значением. Существует два типа наблюдателей за недвижимостью.
-
willSet
: вызывается до изменения значения -
didSet
: вызывается после изменения значения
Для нашей цели мы реализуем обозреватель didSet
для свойства items
. Посмотрите на синтаксис в следующем фрагменте кода.
1
2
3
4
5
6
7
|
var items: [String] = [] {
didSet {
let hasItems = items.count > 0
tableView.isHidden = !hasItems
messageLabel.isHidden = hasItems
}
}
|
Поначалу конструкция может выглядеть немного странно, поэтому позвольте мне объяснить, что происходит. Когда didSet
наблюдатель свойства didSet
, после изменения свойства items
мы проверяем, содержит ли свойство items
какие-либо элементы. На основании значения константы hasItems
мы обновляем пользовательский интерфейс. Это так просто.
Наблюдателю didSet
передается постоянный параметр, который содержит значение старого значения свойства. В приведенном выше примере он опущен, потому что он не нужен в нашей реализации. В следующем примере показано, как его можно использовать.
1
2
3
4
5
6
7
8
9
|
var items: [String] = [] {
didSet(oldValue) {
if oldValue != items {
let hasItems = items.count > 0
tableView.isHidden = !hasItems
messageLabel.isHidden = hasItems
}
}
}
|
Параметр oldValue
в примере не имеет явного типа, потому что Swift знает тип свойства items
. В этом примере мы обновляем пользовательский интерфейс, только если старое значение отличается от нового.
Наблюдатель willSet
работает аналогичным образом. Основное отличие состоит в том, что параметр, передаваемый наблюдателю willSet
является константой, содержащей новое значение свойства. При использовании наблюдателей свойств имейте в виду, что они не вызываются при инициализации экземпляра.
Создайте и запустите приложение, чтобы убедиться, что все правильно подключено. Даже если приложение не идеально и может использовать еще несколько функций, вы создали свое первое приложение для iOS, используя Swift.
Вывод
В течение последних трех уроков этой серии вы создали функциональное приложение для iOS с использованием объектно-ориентированных функций Swift. Если у вас есть некоторый опыт в программировании и разработке приложений, то вы, должно быть, заметили, что текущая модель данных имеет несколько недостатков, мягко говоря.
Хранение элементов в виде строк и создание отдельного массива для хранения состояния элемента не очень хорошая идея, если вы создаете правильное приложение. Лучшим подходом было бы создать отдельный класс ToDo
для моделирования элементов и сохранить их в песочнице приложения. Это будет нашей целью на следующем уроке этой серии.
А пока ознакомьтесь с некоторыми другими нашими курсами и учебными пособиями по разработке iOS на языке Swift!