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

Original author: Majid
  • Translation
Перевод статьи подготовлен специально для студентов курса «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-разработке. Спасибо за внимание!
OTUS. Онлайн-образование
507.69
Цифровые навыки от ведущих экспертов
Share post

Comments 3

    –4
    Иногда нам нужно сделать состояние нашего View доступным для его детей.

    Обязательно бездумно перепечатывать чужой текст, оставляя все ошибки? Есть superview и есть subviews, «родительский» и «дочерний» применяются для обозначения наследования. Что вы будете делать, когда нужно будет рассказать про наследование и вложенность в одной статье? Придумывать свою терминологию?

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

    Что такое «Environment»? Похоже на попытку объяснить неизвестный термин через другой неизвестный термин. Добавляем к этому машинный перевод и получаем на выходе нечитаемый текст, созданный только для саморекламы на хабре
      –1
      ну и? уже минус 4, а никто даже на вопрос не ответил.
      Тоже не понимаете, что такое Environment, но при этом втихаря поддерживаете автора?
        0
        Спасибо. Теперь мне некоторые тонкости стали понятны.))

        Only users with full accounts can post comments. Log in, please.