swift 如何编写一个“样式”视图修饰符来影响某个类型的所有嵌套视图?

fv2wmkja  于 2023-09-29  发布在  Swift
关注(0)|答案(2)|浏览(108)

我基本上试图创建类似于内置的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))
    }
}

我想到的一个想法是使用自定义的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的实际用例吗?

6ioyuze2

6ioyuze21#

我已经实现了几种样式和视图,可以使用与内置视图类似的API进行样式化,它们都遵循类似的模式。正如Benzy Neez发现的那样,您可以很快地陷入泛型的混乱之中。
您完全正确,因为这是AnyView的完美用例。在编译时不可能知道:

  • 对于给定的视图,当前有效的样式是什么
  • 什么内容视图被传递到视图的给定示例中(如果您允许使用视图构建器的任意内容)。

据我所知,您在内置配置类型中看到的类型擦除视图在功能上与AnyView相同。
创建样式和可设置样式的视图的步骤如下:

1.定义表示样式的协议:

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

}

这在任何地方都是一样的,只是把XX改成了你的真名。

2.定义配置类型

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

此类型应包含样式实现需要呈现的所有内容。如果你用视图构建器传入东西,你应该将它们存储为AnyView。如果你有字符串之类的东西,把它们存储为字符串。

3.环境和视图修改器杂务

样式存在于环境中,因为这就是如何将修改器应用于容器一次,以便该容器中的所有视图都可以检测到。为你的样式创建一个键,默认值为你的样式协议的自动或内置实现:

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.视图实现

按如下方式定义视图:

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样式,从中获取主体,然后将其 Package 在AnyView中,这样类型系统就不会爆炸。
这是一个相当复杂的过程,但它确实有效,我对在这里使用AnyView并不感到不好,因为这正是像Button这样的内置视图所做的,即使您没有应用样式。

njthzxwz

njthzxwz2#

在你的问题中,你给出了.buttonStyle.labelStyle作为修饰符的例子,这些修饰符可以应用在父级,并影响较低级别的视图。在这两种情况下,样式都适用于非常特定的视图类型,即ButtonLabel。在设计模式的世界里,我将这些风格描述为装饰器。值得注意的是,目标视图的类型在被修饰后不会改变,它们仍然是ButtonLabel
另一方面,我会说你在大纲中给出的代码更像是一个Builder的例子,因为它试图处理通用内容。我在想也许可以通过使用泛型来让它工作,但是当我尝试它时,我被some View的处理所束缚,甚至无法让它编译。
在与协议和泛型的斗争中筋疲力尽,我后退了一步,意识到如果你完全控制代码库,那么就有一个更简单的方法来解决这个问题。这是将不同风格的所有知识放入TwinTexts中,并在它们之间切换。这使得TwinTexts成为构建器。如果不同的样式很复杂,那么TwinTexts可以委托给特定的实现,但切换逻辑将保留在TwinTexts中。因此,识别不同样式所需的全部内容是enum

enum TwinTextsStyle {
    case any
    case horizontal
    case vertical
}

struct TwinTextsStyleKey: EnvironmentKey {
    static let defaultValue = TwinTextsStyle.any
}

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

extension View {

    // Makes the style available to sub-views
    func twinTextsStyle(_ style: TwinTextsStyle) -> some View {
        self.environment(\.twinTextsStyle, style)
    }
}

// The View TwinTexts combines two views together in the way
// defined by the environment value twinTextsStyle
struct TwinTexts<Text1: View, Text2: View>: View {
    let text1: Text1
    let text2: Text2
    @Environment(\.twinTextsStyle) var style

    private var horizontalStyle: some View {
        HStack {
            text1
            text2
        }
    }

    private var verticalStyle: some View {
        VStack {
            text1
            text2
        }
    }

    var body: some View {
        if style == .horizontal {
            horizontalStyle
        } else {
            verticalStyle
        }
    }
}

struct ContentView: View {
    var body: some View {
        VStack {
            VStack {
                VStack {
                    TwinTexts(
                        text1: Text("Foo").lineLimit(1),
                        text2: Text("Bar").lineLimit(1)
                    )
//                    ...
                }
//                ...
            }
//            ...
        }
        .twinTextsStyle(.horizontal) // this changes the style of TwinTexts, even if it is deeply nested
    }
}

正如您所看到的,您最初的要求,即能够在较高级别应用修改器以控制较低级别的样式设置,已经得到满足。但是,这种方法不能给予您无法控制生成器时添加新的自定义样式。

相关问题