Понимаем Property Wrappers в SwiftUI

Перевод статьи подготовлен специально для студентов курса «iOS Разработчик. Продвинутый курс v 2.0.»


На прошлой неделе мы начали новую серию постов о фреймворке SwiftUI. Сегодня я хочу продолжить эту тему, рассказав о Property Wrappers в SwiftUI. SwiftUI предоставляет нам обертки свойств @State, @Binding, @ObservedObject, @EnvironmentObject и @Environment. Итак, давайте попытаемся понять разницу между ними и когда, почему и какую из них мы должны использовать.

Property Wrappers

Property Wrappers (далее “обертки свойств”) описаны в предложении SE-0258. Основная идея — обернуть свойства логикой, которая может быть извлечена в отдельную структуру для повторного использования в кодовой базе.

State

@State — это обертка, которую мы можем использовать для обозначения состояния View. SwiftUI будет хранить ее в специальной внутренней памяти вне структуры View. Только связанный View может получить к ней доступ. Как только значение свойства @State изменяется, SwiftUI перестраивает View для учета изменений состояния. Вот простой пример.

struct ProductsView: View {     let products: [Product]      @State private var showFavorited: Bool = false      var body: some View {         List {             Button(                 action: { self.showFavorited.toggle() },                 label: { Text("Change filter") }             )              ForEach(products) { product in                 if !self.showFavorited || product.isFavorited {                     Text(product.title)                 }             }         }     } }

В приведенном выше примере у нас есть простой экран с кнопкой и списком продуктов. Как только мы нажимаем на кнопку, она меняет значение свойства state, и SwiftUI перестраивает View.

@Binding

@Binding предоставляет доступ по ссылке для типа-значения. Иногда нам нужно сделать состояние нашего View доступным для его детей. Но мы не можем просто взять и передать это значение, поскольку это тип-значение, и Swift передаст копию этого значения. Вот где приходит на помощь обертка свойства @Binding.

struct FilterView: View {     @Binding var showFavorited: Bool      var body: some View {         Toggle(isOn: $showFavorited) {             Text("Change filter")         }     } }  struct ProductsView: View {     let products: [Product]      @State private var showFavorited: Bool = false      var body: some View {         List {             FilterView(showFavorited: $showFavorited)              ForEach(products) { product in                 if !self.showFavorited || product.isFavorited {                     Text(product.title)                 }             }         }     } }

Мы используем @Binding чтобы отметить свойство showFavorited внутри FilterView. Мы также используем специальный символ $ для передачи привязываемой ссылки, потому что без $ Swift передаст копию значения вместо передачи самой привязываемой ссылки. FilterView может считывать и записывать значение свойства showFavorited в ProductsView, но не может следить за изменениями, используя эту привязку. Как только FilterView изменяет значение свойства showFavorited, SwiftUI воссоздает ProductsView и FilterView как его дочерний элемент.

@ObservedObject

@ObservedObject работает схоже со @State, но основное отличие состоит в том, что мы можем разделить его между несколькими независимыми View, которые могут подписываться и наблюдать за изменениями этого объекта, и как только изменения появляются, SwiftUI перестраивает все представления, связанные с этим объектом. Давайте рассмотрим пример.

import Combine  final class PodcastPlayer: ObservableObject {     @Published private(set) var isPlaying: Bool = false      func play() {         isPlaying = true     }      func pause() {         isPlaying = false     } }

Здесь у нас есть класс PodcastPlayer, который делят между собой экраны нашего приложения. На каждом экране должна отображаться плавающая кнопка паузы в случае, когда приложение воспроизводит эпизод подкаста. SwiftUI отслеживает изменения в ObservableObject с помощью обертки @Published, и как только свойство, помеченное как @Published изменится, SwiftUI перестраивает все View, связанные с этим объектом PodcastPlayer. Здесь мы используем обертку @ObservedObject для привязки нашего EpisodesView к классу PodcastPlayer

struct EpisodesView: View {     @ObservedObject var player: PodcastPlayer     let episodes: [Episode]      var body: some View {         List {             Button(                 action: {                     if self.player.isPlaying {                         self.player.pause()                     } else {                         self.player.play()                     }             }, label: {                     Text(player.isPlaying ? "Pause": "Play")                 }             )             ForEach(episodes) { episode in                 Text(episode.title)             }         }     } }

@EnvironmentObject

Вместо передачи ObservableObject через init-метод нашего View, мы можем неявно внедрить его в Environment нашей View-иерархии. Делая это, мы создаем возможность для всех дочерних представлений текущей Environment обращаться к этому ObservableObject.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {      var window: UIWindow?      func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {         let window = UIWindow(frame: UIScreen.main.bounds)         let episodes = [             Episode(id: 1, title: "First episode"),             Episode(id: 2, title: "Second episode")         ]          let player = PodcastPlayer()         window.rootViewController = UIHostingController(             rootView: EpisodesView(episodes: episodes)                 .environmentObject(player)         )         self.window = window         window.makeKeyAndVisible()     } }  struct EpisodesView: View {     @EnvironmentObject var player: PodcastPlayer     let episodes: [Episode]      var body: some View {         List {             Button(                 action: {                     if self.player.isPlaying {                         self.player.pause()                     } else {                         self.player.play()                     }             }, label: {                     Text(player.isPlaying ? "Pause": "Play")                 }             )             ForEach(episodes) { episode in                 Text(episode.title)             }         }     } }

Как видите, мы должны передать объект PodcastPlayer через модификатор environmentObject нашего View. Делая это, мы можем легко получить доступ к PodcastPlayer, определив его с помощью обертки @EnvironmentObject. @EnvironmentObject использует функцию динамического поиска членов, чтобы найти экземпляр класса PodcastPlayer в Environment, поэтому вам не нужно передавать его через init-метод EpisodesView. Environment является правильным способом внедрения зависимостей в SwiftUI.

@Environment

Как мы уже говорили в предыдущей главе, мы можем передавать пользовательские объекты в Environment View-иерархии внутри SwiftUI. Но SwiftUI уже имеет Environment, заполненную общесистемными настройками. Мы можем легко получить к ним доступ с помощью обертки @Environment.

struct CalendarView: View {     @Environment(\.calendar) var calendar: Calendar     @Environment(\.locale) var locale: Locale     @Environment(\.colorScheme) var colorScheme: ColorScheme      var body: some View {         return Text(locale.identifier)     } }

Помечая наши свойства оберткой @Environment, мы получаем доступ и подписываемся на изменения общесистемных настроек. Как только Locale, Calendar или ColorScheme системы меняются, SwiftUI воссоздает наш CalendarView.

Заключение

Сегодня мы поговорили о Property Wrappers, предоставляемых SwiftUI. @State, @Binding, @EnvironmentObject и @ObservedObject играют огромную роль в SwiftUI-разработке. Спасибо за внимание!

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *