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¶
- Создание и управление объектом
@StateObject используется для создания и управления объектом в пределах представления, тогда как @ObservedObject** используется для отслеживания изменений в объекте, созданном где-то еще.
- Жизненный цикл
SwiftUI управляет жизненным циклом объекта, созданного с помощью @StateObject, гарантируя, что он будет существовать, пока существует представление. @ObservedObject не управляет жизненным циклом объекта, предполагая, что кто-то другой отвечает за его создание и удаление.
- Повторное использование объектов
@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"
Объяснение¶
- Определение обертки
Мы создаем структуру DefaultsStorage, которая принимает ключ для сохранения данных в UserDefaults и значение по умолчанию.
- Реализация геттера и сеттера
В геттере wrappedValue мы пытаемся получить значение из UserDefaults, если оно существует, или возвращаем значение по умолчанию. В сеттере мы сохраняем новое значение в UserDefaults.
- Использование обертки
В структуре Settings мы используем нашу обертку для управления настройками пользователя.