Skip to content

0x055-SwiftUI编程思想-笔记3

environment

环境 (environment) 是让 SwiftUI 代码能如此紧凑的⼀个关键机制。本质上,它是⼀种内置的依赖注⼊ (dependency injection) 技术。

下面的两种写法是等同的:

我们可以将环境想象为⼀个带有键和值的字典,它从视图树的根部⼀直向下传递到叶⼦节点。在视图树的任意位置上,我们都可以通过写⼊环境的⽅式来改变某个键的对应值 (就像我们使⽤ .font 修饰器时所做的那样)。之后,位于这个环境写⼊修饰器下⽅的⼦树将接受到修改后的环境。

SwiftUI 中的许多修饰器都使⽤环境值来对整棵⼦树产⽣影响,⽽不仅仅只作⽤于单⼀视图。例如,我们可以设定背景样式、⽂本⼤⼩写、字体以及⼀系列其他东⻄ (EnvironmentValues 的⽂档中包含了⼀个完整的列表)。环境值也会被⽤来进⾏“全局”设定,⽐如当前的区域设置或者时区等,这些设定对 app 中的每个视图来说都是可⽤的。

从环境中读取变量 @Environment

我们可以使⽤ @Environment 属性包装来从环境中读取⼀个值。这让我们可以访问环境中特定的值,也让我们可以观察这个值的变更

swift
struct EnvironmentView: View {
    @Environment(\.dynamicTypeSize) private var dynamicTypeSize

    var body: some View {
        VStack {
            Text("\(dynamicTypeSize)")
            if dynamicTypeSize < .large {
				Image(systemName: "hand.wave")
			}
        }
    }
}
struct EnvironmentView: View {
    @Environment(\.dynamicTypeSize) private var dynamicTypeSize

    var body: some View {
        VStack {
            Text("\(dynamicTypeSize)")
            if dynamicTypeSize < .large {
				Image(systemName: "hand.wave")
			}
        }
    }
}

和 @State 属性类似,我们没有任何理由将 @Environment 属性暴露给外界。因此,我们通常会将环境值标记为私有,并使⽤⼀个代码检查规则 (linting rule) 来验证它们的私有访问权限。和 State 属性相似的另⼀个点是,我们只能在视图的body 中从环境⾥读取值;在视图的初始化⽅法中,视图还不具有身份标识,因此我们不能在那⾥对环境进⾏读取。如果我们尝试在视图的初始化⽅法中读取环境属性,我们会得到⼀个运⾏时的警告。

示例:badge颜色定制

方案1:tint

如果没有指定颜色,回滚到系统默认的tint。

方案2: ⾃定义的 EnvironmentKey

  1. 必须实现一个自定义的 EnvironmentKey
  2. 必须为EnvironmentValues添加扩展,并提供属性
  3. (可选)在View上提供辅助函数。这可以让我们将⾃定义的键和扩展隐藏起来,同时为⽤户提供⼀个易于发现的 API
swift

// 步骤1
enum BadgeColorKey: EnvironmentKey {
    static var defaultValue: Color = .red
}

// 步骤2
extension EnvironmentValues {
    var badgeColor: Color {
        get { self[BadgeColorKey.self] }
        set { self[BadgeColorKey.self] = newValue }
    }
}

// 步骤3(可选)
extension View {
    func badgeColor(_ color: Color) -> some View {
        environment(\.badgeColor, color)
    }
}

struct Badge: ViewModifier {
    @Environment(\.badgeColor) private var badgeColor

    func body(content: Content) -> some View {
        content
            .font(.caption)
            .foregroundColor(.white)
            .padding(.horizontal, 5)
            .padding(.vertical, 2)
            .background {
                Capsule(style: .continuous)
                    .fill(badgeColor)
            }
    }
}

struct BdView: View {
    var body: some View {
        VStack {
            Text("999")
                .modifier(Badge())
                .badgeColor(.yellow)
            Text("999")
                .modifier(Badge())
        }
    }
}

// 步骤1
enum BadgeColorKey: EnvironmentKey {
    static var defaultValue: Color = .red
}

// 步骤2
extension EnvironmentValues {
    var badgeColor: Color {
        get { self[BadgeColorKey.self] }
        set { self[BadgeColorKey.self] = newValue }
    }
}

// 步骤3(可选)
extension View {
    func badgeColor(_ color: Color) -> some View {
        environment(\.badgeColor, color)
    }
}

struct Badge: ViewModifier {
    @Environment(\.badgeColor) private var badgeColor

    func body(content: Content) -> some View {
        content
            .font(.caption)
            .foregroundColor(.white)
            .padding(.horizontal, 5)
            .padding(.vertical, 2)
            .background {
                Capsule(style: .continuous)
                    .fill(badgeColor)
            }
    }
}

struct BdView: View {
    var body: some View {
        VStack {
            Text("999")
                .modifier(Badge())
                .badgeColor(.yellow)
            Text("999")
                .modifier(Badge())
        }
    }
}

效果如图:

示例:定制化的Badge View

实现如图的定制化效果:

不考虑@Environment的场景可以这么写:

swift
struct OverlayBadgeModifier<BadgeLabel: View>: ViewModifier {
    let xView: BadgeLabel

    init(@ViewBuilder _ view: () -> BadgeLabel) {
        self.xView = view()
    }

    var alignment: Alignment = .topTrailing

    func body(content: Content) -> some View {
        content
            .overlay(alignment: .topTrailing) {
                xView
                    .background(Color.green)
                    .font(.title3)
                    .background {
                        Capsule(style: .continuous)
                            .fill(.green)
                    }
                    .alignmentGuide(alignment.horizontal) { $0[HorizontalAlignment.center] }
                    .alignmentGuide(alignment.vertical) { $0[VerticalAlignment.center] }
            }
    }
}

extension View {
    func makeBadge<V: View>(@ViewBuilder _ view: () -> V) -> some View {
        modifier(OverlayBadgeModifier(view))
    }
}

#Preview {
    Text("hello world")
        .font(.title)
        .makeBadge {
            Text("100")
        }
//        .modifier(OverlayBadgeModifier {
//            Text("100")
//        })
}
struct OverlayBadgeModifier<BadgeLabel: View>: ViewModifier {
    let xView: BadgeLabel

    init(@ViewBuilder _ view: () -> BadgeLabel) {
        self.xView = view()
    }

    var alignment: Alignment = .topTrailing

    func body(content: Content) -> some View {
        content
            .overlay(alignment: .topTrailing) {
                xView
                    .background(Color.green)
                    .font(.title3)
                    .background {
                        Capsule(style: .continuous)
                            .fill(.green)
                    }
                    .alignmentGuide(alignment.horizontal) { $0[HorizontalAlignment.center] }
                    .alignmentGuide(alignment.vertical) { $0[VerticalAlignment.center] }
            }
    }
}

extension View {
    func makeBadge<V: View>(@ViewBuilder _ view: () -> V) -> some View {
        modifier(OverlayBadgeModifier(view))
    }
}

#Preview {
    Text("hello world")
        .font(.title)
        .makeBadge {
            Text("100")
        }
//        .modifier(OverlayBadgeModifier {
//            Text("100")
//        })
}

现在引入Environment设置不同的style:

1). 声明协议、以及默认style。 后面可以通过

swift
protocol BadgeStyle {
    associatedtype Body: View
    @ViewBuilder func makeBody(_ label: AnyView) -> Body
}

struct DefaultBadgeStyle: BadgeStyle {
    var color: Color = .red
    func makeBody(_ label: AnyView) -> some View {
        label
            .font(.caption)
            .foregroundColor(.white)
            .padding(.horizontal, 5)
            .padding(.vertical, 2)
            .background {
                Capsule(style: .continuous)
                    .fill(color)
            }
    }
}
protocol BadgeStyle {
    associatedtype Body: View
    @ViewBuilder func makeBody(_ label: AnyView) -> Body
}

struct DefaultBadgeStyle: BadgeStyle {
    var color: Color = .red
    func makeBody(_ label: AnyView) -> some View {
        label
            .font(.caption)
            .foregroundColor(.white)
            .padding(.horizontal, 5)
            .padding(.vertical, 2)
            .background {
                Capsule(style: .continuous)
                    .fill(color)
            }
    }
}

2). 固定写法: 定义EnvironmentKey

swift
enum BadgeStyleKey: EnvironmentKey {
    static var defaultValue: any BadgeStyle = DefaultBadgeStyle()
}

extension EnvironmentValues {
    var badgeStyle: any BadgeStyle {
        get { self[BadgeStyleKey.self] }
        set { self[BadgeStyleKey.self] = newValue }
    }
}
enum BadgeStyleKey: EnvironmentKey {
    static var defaultValue: any BadgeStyle = DefaultBadgeStyle()
}

extension EnvironmentValues {
    var badgeStyle: any BadgeStyle {
        get { self[BadgeStyleKey.self] }
        set { self[BadgeStyleKey.self] = newValue }
    }
}

3). 调整扩展OverlayBadgeModifier

swift
struct OverlayBadgeModifier<BadgeLabel: View>: ViewModifier {
    let xView: BadgeLabel
    var alignment: Alignment = .topTrailing

    @Environment(\.badgeStyle) private var badgeStyle

    init(@ViewBuilder _ view: () -> BadgeLabel) {
        self.xView = view()
    }

    func body(content: Content) -> some View {
        content
            .overlay(alignment: .topTrailing) {
                AnyView(badgeStyle.makeBody(AnyView(xView)))
                    // xView
                    // .background(Color.green)
                    // .font(.title3)
                    // .background {
                    //    Capsule(style: .continuous)
                    //        .fill(.green)
                    // }
                    .alignmentGuide(alignment.horizontal) { $0[HorizontalAlignment.center] }
                    .alignmentGuide(alignment.vertical) { $0[VerticalAlignment.center] }
            }
    }
}
struct OverlayBadgeModifier<BadgeLabel: View>: ViewModifier {
    let xView: BadgeLabel
    var alignment: Alignment = .topTrailing

    @Environment(\.badgeStyle) private var badgeStyle

    init(@ViewBuilder _ view: () -> BadgeLabel) {
        self.xView = view()
    }

    func body(content: Content) -> some View {
        content
            .overlay(alignment: .topTrailing) {
                AnyView(badgeStyle.makeBody(AnyView(xView)))
                    // xView
                    // .background(Color.green)
                    // .font(.title3)
                    // .background {
                    //    Capsule(style: .continuous)
                    //        .fill(.green)
                    // }
                    .alignmentGuide(alignment.horizontal) { $0[HorizontalAlignment.center] }
                    .alignmentGuide(alignment.vertical) { $0[VerticalAlignment.center] }
            }
    }
}

便于后期定制化、程序也更优雅。

EnvironmentObject

我们不仅可以⽤环境来传递值,也可以⽤它来传递对象。
不过这在 iOS 17/macOS 14 中的⼯作⽅式发⽣了变化,因此我们必须区分我们想要针对的平台。
在 iOS 17 中,环境对象都应该使⽤新的 @Observable 宏,并且对应的属性应该使⽤我们在本章中⼀直使⽤的 @Environment 属性包装器进⾏声明。唯⼀的区别是,我们可以使⽤环境对象的类型作为键,⽽⽆需声明⼀个单独的键类型来遵守EnvironmentKey 协议。

|300

如果在视图层次结构中更⾼级别的某个视图已经在环境中设置了⼀个UserModel,我们就可以在这⾥访问到它,⽽⽆需将对象通过视图树的所有层进⾏传递。

如果我们想要针对 iOS 17 之前的平台,我们必须使⽤两个不同的API:

@EnvironmentObject 属性包装器⽤于从环境中读取对象,⽽environmentObject修饰器⽤于将对象设置在环境中。这些API也依赖于对象的类型作为键。但是,对象必须遵循 ObservableObject 协议(就像状态对象或观察对象⼀样)。