我基本上是在try 创建一些类似于内置buttonStylelabelStyle等修饰符的东西.它们会影响所有嵌套的按钮/标签,无论它们嵌套得有多深.例如:

VStack {
    VStack {
        VStack {
            Button("OK") {}
            ...
        }
        ...
    }
    ...
}
.buttonStyle(.bordered) // this changes the style of the button, even if it is deeply nested

我的目标是对我的自定义视图执行相同的操作.让我们称其为TwinTextsView:

VStack {
    VStack {
        VStack {
            TwinTexts(text1: "Foo", text2: "Bar")
            ...
        }
        ...
    }
    ...
}
.twinTextsStyle(CustomStyle()) // this changes the style of TwinTexts, even if it is deeply nested

其中CustomStyle可以像ButtonStyle一样实现,makeBody方法接受具有两个视图的配置,表示文本

struct TwinTextsStyleConfiguration {
    // not sure what types these should be. AnyView?
    let text1: Text1
    let text2: Text2
}

protocol TwinTextsStyle {
    associatedtype Body: View
    
    @ViewBuilder
    func makeBody(configuration: TwinTextsStyleConfiguration) -> Body
}

struct CustomStyle: TwinTextsStyle {
    func makeBody(configuration: TwinTextsStyleConfiguration) -> some View {
        // let's suppose I want to put one text on top of the other.
        VStack {
            configuration.text1
            configuration.text2
        }
    }
}

然后,TwinTexts人将使用此样式,如下所示:

struct TwinTexts: View {
    let text1: String
    let text2: String
    
    var body: some View {
        // note that these are not of type "Text". I don't know what type they are as I will be adding view modifiers to them
        // here I am using lineLimit as an example 
        let text1 = Text(text1).lineLimit(1)
        let text2 = Text(text2).lineLimit(1)
        // not sure how I would get the "style" here
        style.makeBody(.init(text1: text1, text2: text2))
    }
}

我想到的一个 idea 是使用定制的Environment,因为.environment适用于所有嵌套的视图.有两个问题:

  • style.makeBody将返回any View,它不能在body中使用
struct TwinTextsStyleKey: EnvironmentKey {
    static let defaultValue: any TwinTextsStyle = DefaultStyle()
}

extension EnvironmentValues {
    var twinTextsStyle: any TwinTextsStyle {
        get { self[TwinTextsStyleKey.self] }
        set { self[TwinTextsStyleKey.self] = newValue }
    }
}

extension View {
    func twinTextsStyle(_ style: some TwinTextsStyle) -> some View {
        self.environment(\.twinTextsStyle, style)
    }
}

struct TwinTexts: View {
    let text1: String
    let text2: String
    @Environment(\.twinTextsStyle) var style
    var body: some View {
        let text1 = Text(text1).lineLimit(1)
        let text2 = Text(text2).lineLimit(1)
        style.makeBody(.init(text1: text1, text2: text2)) // this returns "any View"
    }
}
  • 我不知道我应该为TwinTextsStyleConfiguration中的视图使用什么类型.

    我看了看ButtonStyleConfiguration.Label,根据文件,这是一个"类型擦除"的视图.这是AnyView的实际用例吗?

推荐答案

我已经实现了几种样式和视图,可以使用类似于内置视图的API来设置样式,它们都遵循类似的模式.正如Benzy Neez发现的那样,你可能很快就会陷入泛型的纠结中.

您完全正确,这是AnyView的一个完美用例.在编译时不可能知道:

  • 给定视图当前使用的样式是什么
  • 哪些内容视图(S)已传递到该视图的给定实例中(如果您使用视图构建器允许任意内容).

就我所知,您在内置配置类型中看到的类型擦除视图在功能上与AnyView相同.

创建样式和可以设置样式的视图的步骤如下:

1. Define a protocol representing the style:

protocol XXStyle {
    
    associatedtype Body: View
    
    @ViewBuilder 
    func makeBody(configuration: Self.Configuration) ->      
      Self.Body
    
    typealias Configuration = XXStyleConfiguration

}

这几乎在任何地方都是相同的,只是将XX更改为您的实际姓名.

2. Define a configuration type

struct XXStyleConfiguration {
    let viewContent: AnyView
    let modelContent: ??
}

此类型应包含样式实现呈现所需的所有内容.如果使用视图构建器传递内容,则会将它们存储为AnyView.如果您有字符串之类的内容,请将它们作为字符串存储在这里.

3. Environment and view modifier chores

样式存在于环境中,因为这就是如何将修饰符应用于容器以供该容器中的所有视图检测的方式.为您的样式创建一个键,默认值为您的样式协议的某个自动或内置实现:

struct XXStyleKey: EnvironmentKey {
    static let defaultValue: any XXStyle = DefaultXXStyle()
}

extension EnvironmentValues {
    var xxStyle: any XXStyle {
        get { self[XXStyleKey.self] }
        set { self[XXStyleKey.self] = newValue }
    }
}

添加视图修改器以自动应用它,这样您就可以像使用.buttonStyle()一样使用它.

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

extension XXStyle where Self == ThisXXStyle {
    static var this: Self { ThisXXStyle() }
}

4. View implementation

按如下方式定义您的视图:

struct XX<ViewContent: View>: View {

    @Environment(\.xxStyle) private var style
    let configuration: XXStyleConfiguration
    init(_ modelStuff: ??, @ViewBuilder content: () -> ViewContent) {
        configuration = .init(
            viewContent: .init(content()), 
            modelContent: modelStuff)
    }

    var body: some View {
        AnyView(style.makeBody(configuration: configuration))
    }
}

传递构成实现的数据和/或视图,并将它们存储在您在init上创建的配置中.然后从编译时为any样式的环境中获取样式,从中获取正文,然后将其包装在AnyView中,这样类型系统就不会爆炸.

这是一个相当长的过程,但它确实有效,我对在这里使用AnyView并不感到难过,因为这正是Button这样的内置视图正在做的事情,即使您不应用样式.

Swift相关问答推荐

数据不会被ARC删除在swift'

Swift UI在视图中排序不同的 struct

变量捕获:变量在函数闭包中的行为

如何禁用MacOS上SwiftUI中按钮周围的聚焦环(或高亮显示)?

为什么要在SWIFT RandomAccessCollection协议中重新定义元素类型?

从SwiftUI使用UIPageView时,同一页面连续显示两次的问题

如何避免切换视图递归onChange调用SwiftUI

expose 不透明的底层类型

ToolbarItem 包含在动画中但不应该包含在动画中

UIView.convert(_ point:to:)的意外行为

在Swift中如何用变量计算0.9的立方

暂停我的 AR 会话并清除变量和锚点的功能

避免 `(())` 显式指定 Void 类型的枚举关联值

如何将变量添加到不属于可解码部分的 struct ?

ForEach within List:模式的好处?

如何避免从模块导入函数

这是 Int64 Swift Decoding 多余的吗?

VStack SwiftUI 中的动态内容高度

Swift在十进制格式中失go 精度

无法停止 AVPlayer