SwiftUI 中常见的属性包装器(Property-Wrapper)概览

1,920 阅读3分钟

@State

由 SwiftUI 管理的可读写的属性包装器,当修饰的属性值改变的时候,界面会随之更新。由 @State 包装的属性通常用 private 修饰,在 body 内使用。

下面的实例是一个可以切换天气的界面,并且可以控制天气是否可变。

enum Weather: String, CaseIterable {
  case sun = "Sun"
  case cloud = "Cloud"
  case rain = "Rain"
  case snow = "Snow"
  
  var imageName: String {
    switch self {
      case .sun:
        return "sun.max"
      case .cloud:
        return "cloud"
      case .rain:
        return "cloud.rain"
      case .snow:
        return "snow"
    }
  }
}

struct StateView: View {
  @State private var weather: Weather = .sun
  @State private var mutableWeather = false
  
  var body: some View {
    VStack {
      Toggle(isOn: $mutableWeather) {
        Text(mutableWeather ? "Mutable" : "Immutable")
      }
      .padding()
      // add weather view
      Spacer()
    }
    .navigationBarTitle("Weather", displayMode: .inline)
    .navigationBarItems(trailing: Button(action: {
      if self.mutableWeather {
        let random = Int.random(in: 0..<Weather.allCases.count)
        self.weather = Weather.allCases[random]
      }
    }) {
      Text(weather.rawValue)
    })
  }
}

@Binding

接下来,我们用自定义的 WeatherView 去展示天气图片,图片会跟随父视图天气变化做相应改变,并且在 WeatherView 中可以通过点击改变天气。这个时候,我们就需要用到 @binding 来做数据的双向绑定。

struct WeatherView: View {
  @Binding var weather: Weather
  @Binding var mutableWeather: Bool
  
  var body: some View {
    Image(systemName: weather.imageName)
      .resizable()
      .scaledToFill()
      .frame(width: 150, height: 150)
      .onTapGesture {
        self.mutableWeather = true
        let random = Int.random(in: 0..<Weather.allCases.count)
        self.weather = Weather.allCases[random]
    }
  }
}

接下来我们在 // add weather view下面添加如下代码:

WeatherView(weather: $weather, mutableWeather: $mutableWeather)

@ObservedObject、@Published、ObservableObject

遵循 ObservableObject 协议的类的属性可以用 @Published 包装,在多个界面之间同步数据,只需要将需要监听的实例对象用 @ObservedObject 包装即可。注意:这里适用的对象类型是 class,因为 class 是在内存中共享数据的。

下面是一个可以编辑一个人姓名和年龄的实例,在 EditView 所做的更改,会同步至 Person 界面。

class Person: ObservableObject {
  @Published var name: String
  @Published var age: Int
  
  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }
}

struct EditView: View {
  @ObservedObject var person: Person
  
  var body: some View {
    // TextField 只能绑定 String,需要自定义 Binding
    let bindingAge = Binding<String>(get: {
      "\(self.person.age)" == "0" ? "" : "\(self.person.age)"
    }) { value in
      self.person.age = Int(value) ?? 0
    }
    return Form {
      TextField("Input name", text: $person.name)
      TextField("Input age", text: bindingAge)
    }
  }
}

struct ObservedObjectView: View {
  @ObservedObject private var person = Person(name: "Bruce", age: 30)
  
  var body: some View {
    List {
      Text(person.name)
      Text("\(person.age)")
    }
    .navigationBarTitle("Person", displayMode: .inline)
    .navigationBarItems(trailing:
      NavigationLink(destination: EditView(person: self.person)) {
        Text("Edit")
      }
    )
  }
}

@Environment、EnvironmentValues

@Environment 可以让我们在 View 中直接访问预设的环境变量,比如系统是否暗黑模式、系统日历、时区等。

下面是一个可以返回上一个页面的实例,@Environment.presentationMode绑定在当前的 View,我们可以直接调用presentationMode来获取它的值:

struct EnvironmentView: View {
  @Environment(\.presentationMode) private var presentationMode
  
  var body: some View {
    Button("Dismiss") {
      self.presentationMode.wrappedValue.dismiss()
    }
  }
}

系统为我们提供了许多有用的预设变量,详见 developer.apple.com/documentati…

我们也可以为预设值注入新值,比如我们将返回按钮标题改为 “Dismiss\nDismiss”,这时可以看见一个换行的按钮,而当我们给.lineLimit注入新值得时候,按钮标题就只有一行了:

Button("Dismiss\nDismiss") {
  self.presentationMode.wrappedValue.dismiss()
}
.environment(\.lineLimit, 1)

这里只是举个例子,我们使用.lineLimit(1)也能达到同样的效果。

我们也可以自定义 EnvironmentValues:

struct DismissColorKey: EnvironmentKey {
  public static let defaultValue = Color.red
}

extension EnvironmentValues {
  var dismissColor: Color {
    set { self[DismissColorKey.self] = newValue }
    get { self[DismissColorKey.self] }
  }
}

然后我们添加一个新的属性:

@Environment(\.dismissColor) private var dismissColor

再使用自定义的预设值添加一个红色的返回按钮:

Button(action: {
  self.presentationMode.wrappedValue.dismiss()
}) {
  Text("Red Dismiss")
  .foregroundColor(dismissColor)
}

@EnvironmentObject

@EnvironmentObject 和 @ObservedObject 很像,都需要遵循 ObservableObject 协议,都可以同步数据状态,但是它具备更强大的功能,那就是子视图可以自动获取父视图注入的环境变量。

比如我们有如下视图层级:A -> B -> C -> D -> E,后一个是前一个视图的子视图。如果我们使用 @ObservedObject 在 A 视图包装一个变量,我们需要在每个视图包装一个变量,将变量一层层传递到 E 视图。而使用 @EnvironmentObject 我们不需要这么复杂,我们在 A 视图声明一个变量后,在 E 视图用 @EnvironmentObject 包装一个变量后,就可以获取到 A 视图注入的环境变量了,而且可以同步数据的修改,这简直是太方便了。要注意的是,如果 E 视图找不到这个环境变量,程序会崩溃,所以要确保 E 视图能获取到注入的环境变量。

class User: ObservableObject {
  @Published var name = "Bruce"
}

struct ViewA: View {
  var body: some View {
    ViewB()
    .frame(width: 300, height: 300)
      .background(Color.red)
  }
}

struct ViewB: View {
  var body: some View {
    ViewC()
    .frame(width: 200, height: 200)
    .background(Color.black)
  }
}

struct ViewC: View {
  @EnvironmentObject var user: User
  
  var body: some View {
    Text(user.name)
    .frame(width: 100, height: 100)
      .background(Color.white)
  }
}

struct EnvironmentObjectView: View {
  private let user = User()
  
  var body: some View {
    ViewA().environmentObject(user)
  }
}

Eul 是一款 SwiftUI 教程类 App(iOS、macOS),以文章(文字、图片、代码)配合真机示例(Xcode 12+、iOS 14+,macOS 11+)的形式呈现给读者。笔者意在尽可能使用简洁明了的语言阐述 SwiftUI 相关的知识,使读者能快速掌握并在 iOS 开发中实践。

下载链接:AppStore/Eul