Блог

Поради та лайфхаки: як отримати максимум користі від SwiftUI Property Wrappers

Всім привіт! Сьогодні хочу розповісти вам про SwiftUI — фреймворк для побудови інтерфейсів користувача на iOS та macOS. Він є доволі зручним, оскільки використовує декларативний підхід до програмування. За допомогою SwiftUI ви можете описати, що повинен робити та як виглядатиме ваш інтерфейс, а фреймворк подбає про решту.

Одним із ключових елементів SwiftUI є використання property wrappers. Це функціональні елементи, які дають змогу надавати додаткову логіку для властивостей. 

SwiftUI має п’ять основних property wrappers: @State, @Binding, @ObservedObject, @StateObject та @EnvironmentObject. Вони стануть вашими найкращими друзями в розробці.

@State

@State дає змогу створювати властивості, які можуть бути змінені, й, за необхідності, оновлювати інтерфейс на основі цих змін. Наприклад, якщо ви хочете створити кнопку, яка змінює свій колір під час натискання, можна використати змінну @State для зберігання кольору та додати її до кнопки:

struct MyButton: View {

    @State var buttonColor = Color.blue

    var body: some View {

        Button («Press me!») {

            buttonColor = Color.red

        }

        .background (buttonColor)

    }

}

@Binding

@Binding дає змогу використовувати значення, які зберігаються, в різних частинах коду. Зазвичай воно використовується у SwiftUI для передачі значення від одного view до іншого, надаючи змогу їм взаємодіяти один з одним. Уявіть, що у нас є два views — одне з текстовим полем, а інше з кнопкою. Ми хочемо, щоб текстове поле оновлювалося, коли користувач натискає кнопку. Для цього ми можемо використати @Binding:

struct ContentView: View {

    @State private var text = «"

    var body: some View {

        VStack {

            TextField («Enter text», text: $text)

            Button («Update text») {

                text = «New text»

            }

            SecondView (text: $text)

        }

    }

}

struct SecondView: View {

    @Binding var text: String

    var body: some View {

        Text (text)

    }

}

У цьому прикладі @Binding використовується для передачі значення з $text (який розташовується в ContentView) до text (який розташовується в SecondView). Отже, коли користувач натисне кнопку, текстове поле оновиться та відобразить новий текст.

@ObservedObject

@ObservedObject використовується для позначення властивостей, які спостерігаються та можуть змінюватися в залежності від змін зовнішніх даних. Ця property wrapper відповідає на зміни в об’єкті, що відповідає ObservableObject protocol і автоматично оновлює відповідні частини інтерфейсу, якщо дані змінилися. 

Ось швидкий приклад використання @ObservedObject:

class UserData: ObservableObject {

    @Published var name = «John»

}

struct ContentView: View {

    @ObservedObject var userData = UserData ()

    var body: some View {

        VStack {

            Text («Hello, \(userData.name)!»)

            TextField («Enter your name», text: $userData.name)

        }

    }

}

У цьому прикладі ми створюємо клас UserData, який містить ім’я @Published. У структурі ContentView створюємо властивість userData з типом UserData, використовуючи @ObservedObject. Відображаємо значення userData.name у текстовому полі та виводимо його на екран.

Коли користувач змінює значення в текстовому полі, SwiftUI автоматично оновлює відповідну частину інтерфейсу, оскільки властивість name публікується та спостерігається за допомогою @Published. Це означає, що нам не потрібен власний код для оновлення інтерфейсу, й ми дозволяємо SwiftUI робити це за нас.

Примітка: @Published — це property wrapper з фреймворку Combine, яку можна додати до властивості класу або структури. Вона автоматично надсилає сповіщення про будь-які зміни значень цієї властивості всім, хто на неї підписався. Іншими словами, це допоміжний атрибут для властивостей, які можна відстежувати на предмет змін.

@StateObject

@StateObject — це property wrapper, яка використовується для ініціалізації об’єкта класу та зберігання його у стані view в SwiftUI. Це означає, що об’єкт зберігається поки існує view, і знищується разом із ним. Як правило, використовувати @StateObject більш практично для об’єктів класу багаторазових views, а не тільки для одного. Наприклад:

class UserData: ObservableObject {

    @Published var name = «John»

    @Published var age = 30

}

struct ContentView: View {

    @StateObject var userData = UserData ()

    

    var body: some View {

        NavigationView {

            VStack {

                Text («Name: \(userData.name)»)

                Text («Age: \(userData.age)»)

                

                NavigationLink (

                    destination: ProfileView (userData: userData),

                    label: {

                        Text («Edit Profile»)

                    })

            }

            .navigationTitle («Home»)

        }

    }

}

struct ProfileView: View {

    @ObservedObject var userData: UserData

    

    var body: some View {

        Form {

            TextField («Name», text: $userData.name)

            Stepper («Age: \(userData.age)», value: $userData.age)

        }

        .navigationTitle («Profile»)

    }

}

У цьому прикладі, UserData — це об’єкт класу, який містить декілька властивостей, що можуть бути використані в декількох views. Клас позначено як ObservableObject, тому його можна використовувати із @StateObject та @ObservedObject.

У ContentView ми створюємо новий об’єкт UserData, використовуючи @StateObject для збереження стану між переходами для різних views. В цьому випадку ContentView відображає дані користувача, візуалізує їх та містить посилання на інший view (ProfileView), який можна використовувати для редагування даних користувача.

У ProfileView ми дістаємо доступ до того ж об’єкта UserData, використовуючи @ObservedObject для модифікації даних користувача. Коли користувач змінює дані, вони автоматично оновлюються у ContentView, оскільки використовується той самий об’єкт UserData.

Примітка: Використовуйте @ObservedObject, якщо вам потрібно спостерігати за змінами в об’єкті класу з одного view, й @StateObject, якщо вам потрібно зберегти стан об’єкта класу, який впливає на відображення декількох views.

Якщо ви використовуєте @ObservedObject замість @StateObject для об’єкта, необхідного в декількох views, кожен view матиме власний екземпляр об’єкта, що може призвести до проблем із синхронізацією даних між views. Тому в цьому випадку краще використовувати @StateObject.

@EnvironmentObject

@EnvironmentObject — це property wrapper для передачі об’єктів даних через ієрархію views SwiftUI. Вона дає змогу дістати доступ до об’єкта даних з будь-якого views в ієрархії SwiftUI, що належить до контейнера Environment (наприклад, Scene, View, App тощо). 

Уявіть, що у нас є додаток для керування списком завдань. Ми можемо мати корінь ContentView, який містить список завдань та можливість створювати нові завдання. Для цього ми створюємо окремий TaskListView, який відображає список завдань та кнопку для додавання нових завдань. Після додавання нового завдання користувач мусить бути перенаправлений на екран додавання завдання, тому ми створюємо окреме view AddTaskView.

Щоб передати об’єкт UserManager усім трьом views, ми можемо створити його екземпляр у ContentView, а потім передати його, як параметр до TaskListView та AddTaskView. Однак це може стати проблемою, якщо ми вирішимо додати ще більше вкладених view, оскільки нам доведеться передавати UserManager через багато проміжних views.

Натомість ми можемо використовувати @EnvironmentObject для передачі UserManager вниз по ієрархії views. Отже, всі views, яким потрібен доступ до UserManager, можуть просто оголосити його як @EnvironmentObject та використовувати його за потребою.

struct TaskManagerApp: App {

    @StateObject var userManager = UserManager ()

    

    var body: some Scene {

        WindowGroup {

            ContentView ()

                .environmentObject (userManager)

        }

    }

}

struct ContentView: View {

    var body: some View {

        NavigationView {

            TaskListView ()

        }

    }

}

struct TaskListView: View {

    @EnvironmentObject var userManager: UserManager

    

    var body: some View {

        List (userManager.tasks) { task in

            TaskRow (task: task)

        }

        .navigationBarTitle («Tasks»)

        .navigationBarItems (trailing:

            Button (action: {

                // Navigate to AddTaskView

            }) {

                Image (systemName: «plus»)

            }

        )

    }

}

struct AddTaskView: View {

    @EnvironmentObject var userManager: UserManager

    

    var body: some View {

        // Add new task using userManager

    }

}

Отже, тепер об’єкт UserManager буде автоматично передаватися до TaskListView та AddTaskView через @EnvironmentObject. Зверніть увагу, що ми можемо змінювати стан UserManager в одному view, і ці зміни будуть автоматично відображені в іншому.

У статті я розглянув основні property wrappers SwiftUI: @State, @Binding, @ObservedObject, @StateObject, @EnvironmentObject. Ці property wrappers формують основу роботи зі станом застосунку у SwiftUI.

Використовуйте цю статтю, як шпаргалку, щоб мати під рукою основні property wrappers, необхідні для розробки додатків за допомогою SwiftUI. Застосовуючи ці знання, ви зможете створювати складніші інтерфейси користувача з динамічно змінюваними станами та інтегрувати дані з ваших моделей у SwiftUI.