环境(三) – 自定义组件样式

温馨提示:本文最后更新于2024-07-10 11:03:32,某些文章具有时效性,若有错误或已失效,请在下方留言

本文以 badge 为例,实现自定义组件样式的具体步骤:

  1. 创建一个 BadgeStyle 协议为角标样式定义接口。
  2. 为角标样式创建一个环境变键。
  3. 在 badge 修饰符中使用这个自定义角标样式。

⻆标样式的协议和 ViewModi!er 协议⼗分相似:它要求⼀个 body ⽅法,来将已经存在的视图包装进⻆标⾥。

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)
            .foregroundStyle(.white)
            .padding(.horizontal, 5)
            .padding(.vertical, 2)
            .background(
                Capsule(style: .continuous)
                    .fill(color)
            )
    }
}

当我们想要创建⼀个环境键时,不能简单地使⽤ DefaultBadgeStyle 来作为键所对应的值的类型。那样做的话我们就不能使⽤类型不同的⾃定义⻆标样式了。我们会⽤⼀个存在体 (existential) 来把具体的类型隐藏起来。本质上来说,它会把具体类型包装到⼀个盒⼦中 (这和 AnyView 将⼀个具体的视图类型包装到盒⼦中的做法类似)。下⾯是环境键和对应的属性:

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


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

为了使⽤⻆标样式,我们需要创建⼀个⾃定义的视图修饰器来从环境中读取样式,对标签进⾏转换,然后根据指定的对⻬确定⻆标的位置。

struct OverlayBadge<BadgeLabel: View>: ViewModifier {
    var alignment: Alignment = .topTrailing
    var label: BadgeLabel
    
    @Environment(\.badgeStyle) private var badgeStyle
    
    func body(content: Content) -> some View {
        content
            .overlay(alignment: alignment) {
                AnyView(badgeStyle.makeBody(AnyView(label)))
                    .fixedSize()
                    .alignmentGuide(alignment.horizontal)  { 
						$0[HorizontalAlignment.center]
                    }
                    .alignmentGuide(alignment.vertical) {
                        $0[VerticalAlignment.center]
                    }
            }
    }
}

这个 OverlayBadge 修饰器不需要公开。我们可以在 View 协议上编写⼀个简短的辅助⽅法,来让本节⼀开始时提出的语法成真:

extension View {
    func badge<V: View>(alignment: Alignment = .topTrailing, @ViewBuilder _ content: () -> V) -> some View {
        modifier(OverlayBadge(alignment: alignment, label: content()))
    }
}

现在,我们可以这样使⽤ badge 修饰器了,它会⾃动使⽤环境中当前的⻆标样式:

Text(100, format: .number)
	.badge { Text(100, format: .number) }
默认 BadgeStyle 样式
默认 BadgeStyle 样式

作为演示,我们来创建⼀个⾃定义带光泽的⻆标样式,并实际在环境中看看这个样式的使⽤⽅式:

struct FancyBadgeStyle: BadgeStyle {
    var background: some View {
        ZStack {
            ContainerRelativeShape()
                .fill(.red)
                .overlay {
                    ContainerRelativeShape()
                        .fill(LinearGradient(colors: [.white, .clear], startPoint: .top, endPoint: .center))
                }
            
            ContainerRelativeShape()
                .strokeBorder(Color.white, lineWidth: 2)
                .shadow(radius: 2)
        }
    }
    
    func makeBody(_ label: AnyView) -> some View {
        label
            .foregroundStyle(.white)
            .font(.caption)
            .padding(.horizontal, 7)
            .padding(.vertical, 4)
            .background(background)
            .containerShape(Capsule(style: .continuous))
    }
}

为了能更容易地在环境中设置⻆标样式,我们再⼀次向 View 添加⼀个扩展:

extension View {
    func badgeStyle(_ style: any BadgeStyle) -> some View {
        environment(\.badgeStyle, style)
    }
}

现在,我们可以在视图层级的任何地⽅指定 .badgeStyle(FanceBadgeStyle()) 来使⽤我们的这个 fancy ⻆标样式,整棵⼦树在遇到⻆标时都将使⽤这个样式进⾏渲染。为了让语法更优雅⼀些,我们可以向 BadgeStyle 添加下⾯这样的扩展:

extension BadgeStyle where Self == FancyBadgeStyle {
    static var fancy: FancyBadgeStyle {
        FancyBadgeStyle()
    }
}

现在我们可以把代码写成 .badgeStyle(.fancy) 了:

HStack {
    Text("Inbox")
    Text("Spam")
        .badge {
            Text(3000, format: .number)
        }
}
.badgeStyle(.fancy)
fancy 角标样式
fancy 角标样式
© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容