Перейти к содержанию

2. Property Wrappers


Изучение данного блока предполагает предварительное знание синтаксиса языка Swift. Для успешного освоения этого материала, необходимо иметь базовое понимание синтаксиса языка Swift. Это включает в себя знание основных структур данных, операторов, циклов, функций, абстракций и других ключевых элементов языка. Без этих фундаментальных знаний будет сложно понять более сложные концепции и примеры, которые будут рассматриваться в данном блоке.


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

  • Управление состоянием

Property wrappers позволяют легко отслеживать изменения в данных и автоматически обновлять пользовательский интерфейс при этих изменениях.

  • Связывание данных

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

  • Инкапсуляция логики и упрощение синтаксиса

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

Как это работает?

Под капотом, компилятор Swift генерирует код, который преобразует аннотированные свойства в более сложные конструкции. В каждом случае генерируется скрытое свойство для хранения состояния обертки, а доступ к основному свойству осуществляется через геттеры и сеттеры wrappedValue.

Swift выполняет эту трансформацию на уровне компиляции. Это означает, что никакие дополнительные объекты или данные не создаются во время выполнения — все необходимое генерируется и оптимизируется компилятором во время сборки.

Основные Property Wrappers в SwiftUI

State

Он используется для управления локальным состоянием внутри представления. Когда значение свойства изменяется, представление автоматически перерисовывается.

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

Binding

Когда локальное состояние(@State) нужно передать в дочернее и позволить ему изменять его, используется @Binding. Таким образом, @Binding передает ссылку на значение, а не само значение. Это позволяет дочернему представлению напрямую изменять состояние родительского представления.

struct ToggleView: View {
    @Binding var isOn: Bool

    var body: some View {
        Toggle("Toggle", isOn: $isOn)
    }
}

Published

Используется внутри объектов, соответствующих ObservableObject, для автоматического уведомления всех подписчиков об изменениях в значении.

class CounterModel: ObservableObject {
    @Published var count = 0
}

StateObject

@StateObject используется для создания и управления объектом, который вы хотите отслеживать в пределах конкретного представления. Это новый объект, который SwiftUI создает и сохраняет для управления его жизненным циклом.

class ViewModel: ObservableObject {
    @Published var count = 0
}

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        VStack {
            Text("Count: \(viewModel.count)")
            Button("Increment") {
                viewModel.count += 1
            }
        }
    }
}

Основные характеристики

  • Создание объекта:

@StateObject создает новый экземпляр объекта и управляет его жизненным циклом. Объект создается один раз, и он будет сохраняться между перерисовками (re-renders) представления.

  • Жизненный цикл:

SwiftUI сохраняет объект, созданный с помощью @StateObject, пока представление существует. Это важно для предотвращения утечек памяти и для того, чтобы объект продолжал существовать, даже если представление перерисовывается.

ObservedObject

Используется для отслеживания объекта, который был создан и управляется вне текущего представления. @ObservedObject не создает новый объект, а принимает уже существующий объект и отслеживает его изменения.

class ViewModel: ObservableObject {
    @Published var count = 0
}

struct ParentView: View {
    @StateObject private var viewModel = ViewModel()

    var body: some View {
        ChildView(viewModel: viewModel)
    }
}

struct ChildView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        VStack {
            Text("Count: \(viewModel.count)")
            Button("Increment") {
                viewModel.count += 1
            }
        }
    }
}

Основные характеристики

  • Передача объекта

@ObservedObject используется для отслеживания изменений в объекте, который был создан и передан в представление извне. Этот объект управляется где-то еще (например, в родительском представлении или модели данных).

  • Жизненный цикл

SwiftUI не управляет жизненным циклом объекта, переданного через @ObservedObject. Ответственность за создание и управление объектом лежит на том, кто передал этот объект в представление.

Основные различия @StateObject и @ObservedObject

  1. Создание и управление объектом

@StateObject используется для создания и управления объектом в пределах представления, тогда как @ObservedObject** используется для отслеживания изменений в объекте, созданном где-то еще.

  1. Жизненный цикл

SwiftUI управляет жизненным циклом объекта, созданного с помощью @StateObject, гарантируя, что он будет существовать, пока существует представление. @ObservedObject не управляет жизненным циклом объекта, предполагая, что кто-то другой отвечает за его создание и удаление.

  1. Повторное использование объектов

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

EnvironmentObject

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

class CounterModel: ObservableObject {
    @Published var count = 0
}

struct ParentView: View {
    @StateObject var model = CounterModel()

    var body: some View {
        ChildView().environmentObject(model)
    }
}

struct ChildView: View {
    @EnvironmentObject var model: CounterModel

    var body: some View {
        Text("Count: \(model.count)")
    }
}

Environment

Используется для доступа к значениям среды, таким как настройки системы, размеры шрифтов, цветовые схемы и многое другое.

struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        Text("Current color scheme: \(colorScheme == .dark ? "Dark" : "Light")")
    }
}

AppStorage

Сохраняет данные в хранилище UserDefaults. Это удобно для хранения небольших настроек или пользовательских данных.

struct SettingsView: View {
    @AppStorage("username") var username: String = ""

    var body: some View {
        TextField("Username", text: $username)
    }
}

SceneStorage

Сохраняет данные, связанные с конкретной сценой приложения. Это позволяет сохранять состояние представления при закрытии и повторном открытии приложения.

struct DetailView: View {
    @SceneStorage("note") var note: String = ""

    var body: some View {
        TextEditor(text: $note)
    }
}

FetchRequest

Используется для получения данных из Core Data. Это обертка для выполнения запросов выборки (fetch requests) в SwiftUI.

struct ContentView: View {
    @FetchRequest(entity: Item.entity(), sortDescriptors: []) var items: FetchedResults<Item>

    var body: some View {
        List(items, id: \.self) { item in
            Text(item.name ?? "Unknown")
        }
    }
}

FocusedBinding

Используется для отслеживания и управления фокусом на элементах ввода, таких как текстовые поля.

struct ContentView: View {
    @FocusedBinding(\.isTextFieldFocused) var isTextFieldFocused

    var body: some View {
        VStack {
            TextField("Enter something", text: .constant(""))
                .focused($isTextFieldFocused, equals: true)
        }
    }
}

Специальные Property Wrappers

GestureState

Управляет временным состоянием жестов. В отличие от @State, значение @GestureState сбрасывается в начальное, как только жест завершается.

struct DraggableCircle: View {
    @GestureState private var dragOffset: CGSize = .zero

    var body: some View {
        Circle()
            .fill(Color.blue)
            .frame(width: 100, height: 100)
            .offset(dragOffset)
            .gesture(
                DragGesture()
                    .updating($dragOffset) { value, state, transaction in
                        state = value.translation
                    }
            )
    }
}

Namespace

Используется для создания уникальных идентификаторов, которые помогают управлять анимациями переходов между представлениями. Это полезно при использовании matchedGeometryEffect.

struct MatchedGeometryEffectExample: View {
    @Namespace private var namespace
    @State private var isExpanded = false

    var body: some View {
        VStack {
            if isExpanded {
                Rectangle()
                    .fill(Color.blue)
                    .matchedGeometryEffect(id: "rectangle", in: namespace)
                    .frame(width: 300, height: 300)
            } else {
                Circle()
                    .fill(Color.blue)
                    .matchedGeometryEffect(id: "rectangle", in: namespace)
                    .frame(width: 100, height: 100)
            }
        }
        .onTapGesture {
            withAnimation {
                isExpanded.toggle()
            }
        }
    }
}

NSApplicationDelegateAdaptor

Используется для интеграции делегата macOS-приложения с SwiftUI-приложением, позволяя использовать методы делегата для управления жизненным циклом приложения.

import SwiftUI

@main
struct MyApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class AppDelegate: NSObject, NSApplicationDelegate {
    func applicationDidFinishLaunching(_ notification: Notification) {
        print("App launched")
    }
}

ScaledMetric

Автоматически масштабирует числовые значения (например, размеры шрифтов) в зависимости от настроек системы.

struct ContentView: View {
    @ScaledMetric var fontSize: CGFloat = 16

    var body: some View {
        Text("Hello, World!")
            .font(.system(size: fontSize))
    }
}

UIApplicationDelegateAdaptor

Используется для интеграции UIApplicationDelegate iOS-приложения с SwiftUI-приложением.

import SwiftUI

@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        print("App launched")
        return true
    }
}

Как создать свой Property Wrapper?

@propertyWrapper
struct DefaultsStorage<T> {
    let key: String
    let defaultValue: T

    var wrappedValue: T {
        get {
            return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
        }
        set {
            UserDefaults.standard.set(newValue, forKey: key)
        }
    }
}

struct Settings {
    @DefaultsStorage(key: "username", defaultValue: "") var username: String
    @DefaultsStorage(key: "isLoggedIn", defaultValue: false) var isLoggedIn: Bool
}

var settings = Settings()
settings.username = "JohnDoe"
print(settings.username) // "JohnDoe"

Объяснение

  1. Определение обертки

Мы создаем структуру DefaultsStorage, которая принимает ключ для сохранения данных в UserDefaults и значение по умолчанию.

  1. Реализация геттера и сеттера

В геттере wrappedValue мы пытаемся получить значение из UserDefaults, если оно существует, или возвращаем значение по умолчанию. В сеттере мы сохраняем новое значение в UserDefaults.

  1. Использование обертки

В структуре Settings мы используем нашу обертку для управления настройками пользователя.