属性包装器(SwiftUI)-含例子
属性包装器的本质是对属性的访问逻辑(如getter/setter中的存储、计算、校验、通知)抽离并封装成一个独立的类型,从而在不同地方通过@属性包装器名,来实现代码的高级复用。
@MainActor
是全局执行者的修饰符,它确保修饰的代码、类或属性必须在主线程上运行。
就比如在ViewModel前面可以使用@MainActor确保刷新UI操作在主线程上。
swift
@MainActor
class StepCounterViewModel: ObservableObject {}
@State
@State修饰值类型(区别于引用类型)私有变量,当@State修饰的变量改变时,SwiftUI会重新绘制当前视图。一般都和@Binding一起使用,@State在父视图使用,@Binding在子视图使用。
*其实使用@State和@Binding的两个视图本质上是没有任何关系的,只是通过@State和@Binding把变量联系起来了。
swift
struct MyView: View {
@State private var text = "" // 你持有的数据源
@State private var isOn = false // 你持有的数据源
var body: some View {
VStack {
// TextField 内部用的就是 @Binding,你传进去 $text
TextField("请输入", text: $text)
// Toggle 内部用的也是 @Binding,你传进去 $isOn
Toggle("开关", isOn: $isOn)
}
}
}
@Binding
@Binding建立读写连接传递数据,在子视图修改@Binding 的值,父视图的数据也会随之改变(比如用@State修饰的变量)。
举例:把isChildOn和isOn沟通起来了
swift
import SwiftUI
// 父视图(拥有真正的存钱罐)
struct ParentView: View {
// 1️⃣ 用 @State 创建真正的存钱罐
@State private var isOn: Bool = false
var body: some View {
VStack(spacing: 20) {
// 父视图读取展示
Text("父视图显示:\(isOn ? "🟢 开" : "🔴 关")")
Divider()
// 2️⃣ 调用子视图,用 $isOn 把"操控手柄"传过去
ChildSwitchView(isChildOn: $isOn)
}
.padding()
}
}
// 子视图(只拿手柄,不存钱)
struct ChildSwitchView: View {
// 3️⃣ 用 @Binding 接住手柄
@Binding var isChildOn: Bool
var body: some View {
VStack {
Text("子视图控制区域")
Button("子视图控制开关") {
// 4️⃣ 改动顺着 @Binding 管道反向传回父视图,父视图的 @State 改变,整个 UI 刷新!
isChildOn.toggle()
}
.buttonStyle(.borderedProminent)
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(10)
}
}
@StateObject
@StateObject修饰ObservableObject的类实例的作用是让它始终被持有,保证UI刷新时不被销毁。(就是把它固定住)
让ViewModel始终被View持有
swift
struct ContentView: View {
// 1️⃣ @StateObject: 在 ViewModel 首次被创建的视图中持有它,绑定生命周期
@StateObject private var profileVM = UserProfileViewModel()
}
@ObservedObject
@ObservedObject和@StateObject不同,它只接收外部传进来的ViewModel,而不持有它的生命周期,只负责监听
举例:使用@ObservedObject来监听ViewModel的变化(数据存在ViewModel里)
然后就可以直接使用ViewModel中变量
swift
struct ChildDetailView: View {
// 4️⃣ @ObservedObject: 不持有生命周期,纯接收外部传进来的 ViewModel 引用
@ObservedObject var viewModel: UserProfileViewModel
var body: some View {
VStack(spacing: 20) {
Text("子详情页")
.font(.title2)
.bold()
Text("从父视图同步过来的积分: \(viewModel.score)")
.font(.title3)
.foregroundColor(.blue)
}
@AppStorage
@AppStorege的作用是快速把某个属性,如isDarkMode写入UserDefaults,Key为"isDarkMode",下次打开,或者去到其他页面也会保留这个属性,比如这里可以用来控制白天或者黑夜模式。
@AppStorage("isDarkMode") private var isDarkMode: Bool = false
设置整个View的主题偏好,将其与isDarkMode绑定,实现控制。
swift
.preferredColorScheme(isDarkMode ? .dark : .light)
然后写开关,将isDarkMode与开关绑定。$表示开关样式随着isDarkMode变化而变化(双向修改数据)。
swift
Toggle("开启深色模式", isOn: $isDarkMode)


@Environment
@Environment用于 读取系统预设与系统环境参数,就比如.colorScheme用来读取系统白天与黑夜模式,.dismiss提供关闭当前页面的方法。
可以使用@Environment读取系统的主题状态为colorScheme,再进行对应的操作(比如切换图标等)
然后再读取系统预设的关闭页面方法dismiss,就可以使用。
swift
struct ChildDetailView: View {
// 4️⃣ @ObservedObject: 不持有生命周期,纯接收外部传进来的 ViewModel 引用
@ObservedObject var viewModel: UserProfileViewModel
// 5️⃣ @Environment: 读取系统级的环境变量(如页面关闭动作 dismiss)
@Environment(\.dismiss) private var dismiss
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(spacing: 20) {
Text("子详情页")
.font(.title2)
.bold()
Text("从父视图同步过来的积分: \(viewModel.score)")
.font(.title3)
.foregroundColor(.blue)
Button("在子视图修改积分") {
viewModel.addScore()
}
.buttonStyle(.bordered)
Divider()
Text("当前系统暗黑/亮色状态 (@Environment): \(colorScheme == .dark ? "Dark" : "Light")")
.font(.caption)
.foregroundColor(.secondary)
Button("关闭当前页面 (@Environment 之 dismiss)") {
dismiss() // 调用系统环境提供的推出页面方法
}
.foregroundColor(.red)
}
.padding()
.navigationTitle("详情页")
}
}
@EnviromentObject
在APP的入口创建ViewModel的实例,并通过.environmentObject()方法把这个实例挂载到根视图上,在任意子视图里就可以通过@EnvironmentObject提取这个实例,并使用ViewModel里的方法,非常方便。
举例:
1.这是事先写好的ViewModel,继承ObservableObject 让其可以被@StateObject修饰,始终被ContentView持有
swift
class GlobalThemeState: ObservableObject {
@Published var isVipUser: Bool = true
func getVIP() {
isVipUser = !isVipUser
}
}
@main表示APP的入口
在APP入口创建实例并挂载到根视图
swift
@main
struct DemoApp: App {
// 创建全局状态实例
@StateObject private var globalTheme = GlobalThemeState()
var body: some Scene {
WindowGroup {
ContentView()
// 注入到根视图,这样任意子视图都能直接用 @EnvironmentObject 取到
.environmentObject(globalTheme)
}
}
}
在子视图声明就可以直接使用
swift
struct ContentView: View {
// 2️⃣ @EnvironmentObject: 接收注入的全局共享对象
@EnvironmentObject var globalState: GlobalThemeState
}
现在就可以写UI样式了
swift
Button("更改VIP激活状态 (@StateObject 触发)") {
globalState.getVIP()
}
.buttonStyle(.borderedProminent)
通过接收的全局对象globalState调用ViewModel中的方法
@Published属于Combine框架
@Published表示通知,使用@Published,对应的ViewModel必须遵循ObservableObject协议;表明这是一个可以被SwiftUI监听的对象。所有监听了这个ViewModel的View,收到通知后,就会自动刷新UI。
举例
swift
class UserProfileViewModel: ObservableObject {
@Published var name: String = "张三"
@Published var score: Int = 100
func addScore() {
score += 10
}
}