Статьи

Swift From Scratch: контроль доступа и свойства обозревателей

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

Если вы хотите следовать за мной, убедитесь, что на вашем компьютере установлен Xcode 8.3.2 или выше. Вы можете скачать Xcode 8.3.2 из Apple App Store .

Чтобы удалить элементы, нам нужно реализовать два дополнительных метода протокола 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 .

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

Чтобы пометить элемент как выполненный, мы добавим галочку к соответствующей строке. Это означает, что нам необходимо отслеживать элементы, помеченные пользователем как выполненные. Для этого мы объявим новое свойство, которое управляет этим для нас. 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 .

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

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

Начните с создания двух вспомогательных методов 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»)
}

Для сохранения состояния мы реализуем еще два закрытых вспомогательных метода: 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()
    }
}

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

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

Начнем с добавления метки в пользовательский интерфейс для отображения сообщения. messageLabel выход с именем messageLabel типа UILabel в классе ViewController , откройте Main.storyboard и добавьте метку в представление контроллера представления.

1
@IBOutlet var messageLabel: UILabel!

Добавьте необходимые ограничения макета к метке и соедините его с выходом messageLabel контроллера представления в Инспекторе соединений . Установите текст метки на « У вас нет задач». и центрируйте текст метки в Инспекторе Атрибутов .

Добавить метку сообщения

Метка сообщения должна быть видимой, только если 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!

  • стриж
    Создавайте приложения для iOS с помощью Swift
    Маркус Мюльбергер
  • IOS
    Идите дальше со Swift: анимация, работа в сети и пользовательские элементы управления
    Маркус Мюльбергер
  • стриж
    Запрограммируйте игру с боковой прокруткой с помощью Swift и SpriteKit
    Дерек Дженсен
  • iOS SDK
    Правильный способ поделиться состоянием между контроллерами Swift View
    Маттео Манфердини