Перевод статьи подготовлен специально для студентов курса «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-разработке. Спасибо за внимание!
Автор: MaxRokatansky