实现自定义 ViewModifier,其中输出以具体视图类型为条件 (SwiftUI)

Implementing a custom ViewModifier where output is conditional on concrete View type (SwiftUI)

我想创建一个 ViewModifier,其中的输出取决于它正在修改的内容类型。

我管理的概念的最佳测试(使用 Text 和 TextField 作为示例视图类型)如下:

struct CustomModifier<T: View>: ViewModifier {
    @ViewBuilder func body(content: Content) -> some View {
        if content is Text.Type {
            content.background(Color.red)
        } else {
            if content is TextField<T>.Type {
                content.background(Color.blue)
            }
        } 
            content
    }
}

上述修饰符的问题在于,当您使用修饰符时,您需要明确提供通用术语,因此看起来不正确(而且,在我看来,代码有味道),因为您随后需要在父视图,然后是其父视图的泛型,等等。

例如

struct ContentView<T: View>: View {
    var body: some View {
        VStack {
            Text("Hello world")
                .modifier(CustomModifier<Text>())
            
            TextField("Textfield", text: .constant(""))
                .modifier(CustomModifier<TextField<T>>())
        }
    }
}

我设法在视图上使用此扩展解决了这个问题(在 Cristik 的一些指导下):

extension View {
    func customModifier() -> some View where Self:View {
        modifier(CustomModifier<Self>())
    }
}

使用 iPadOS Playgrounds 使用以下方法测试修改器:

struct ContentView: View {
    var body: some View {
        Form {
            Text("Hello")
                .customModifier()
            
            TextField("Text", text: .constant(""))
                .customModifier()
        }
    }
}

这可以编译并运行,但输出不是我预期的。 Text 和 TextField 视图应该有不同的背景(分别为红色和蓝色),但它们显示不变。在修改器中覆盖视图类型检查(将类型检查硬编码为 'true')会导致背景颜色发生变化,因此正在应用修改器;是类型检查失败了。

我将代码拖入 Xcode 以尝试更好地了解为什么会发生这种情况,并立即收到编译器警告,提示类型检查总是失败(屏幕截图中的修饰符名称不同) - 请无视):

Xcode 编译器错误:

这解释了为什么代码没有按预期执行,但我无法确定我是否犯了错误,或者是否(实际上)没有办法检查发送到视图修改器。据我所知,发送到 ViewModifier 的内容参数似乎确实被类型擦除(基于 Xcode 中可访问的方法),但在这种情况下似乎确实有一种获取类型信息的方法,因为某些修饰符(例如.focused()) 仅对某些类型的视图(特别是交互式文本控件)进行操作,而忽略其他类型。这当然可以是我们无法访问的私有 API(还...?​​)

任何指导/解释?

你是对的,该实现中有一些代码味道,首先是你需要编写类型检查来实现目标。每当您开始编写 isas? 以及具体类型时,您应该考虑抽象为协议。

在你的情况下,你需要一个抽象来给你背景颜色,所以一个简单的协议如:

protocol CustomModifiable: View {
    var customProp: Color { get }
}

extension Text: CustomModifiable {
   var customProp: Color { .red }
}

extension TextField: CustomModifiable {
   var customProp: Color { .blue }
}

,应该是要走的路,修饰符应该可以简化为:

struct CustomModifier: ViewModifier {
    @ViewBuilder func body(content: Content) -> some View {
        if let customModifiable = content as? CustomModifiable {
            content.background(customModifiable.customProp)
        } else {
            content
        }
    }
}

问题是这种惯用的方法不适用于 SwiftUI 修饰符,因为作为 body() 函数的参数接收的 content 是包装原始视图的某种内部类型的 SwiftUI .这意味着您不能(轻松地)访问应用修饰符的实际视图。

这就是 is 检查总是失败的原因,正如编译器所说的那样。

然而,并不是所有的都丢失了,因为我们可以通过静态属性和泛型来解决这个限制。

protocol CustomModifiable: View {
    static var customProp: Color { get }
}

extension Text: CustomModifiable {
    static var customProp: Color { .red }
}

extension TextField: CustomModifiable {
    static var customProp: Color { .blue }
}

struct CustomModifier<T: CustomModifiable>: ViewModifier {
    @ViewBuilder func body(content: Content) -> some View {
        content.background(T.customProp)
    }
}

extension View {
    func customModifier() -> some View where Self: CustomModifiable {
        modifier(CustomModifier<Self>())
    }
}

上述实现具有编译时优势,因为只有 TextTextField 允许使用自定义修饰符进行修改。如果开发人员试图在 non-accepted 类型的视图上应用修饰符,they-ll 会得到一个不错的 Instance method 'customModifier()' requires that 'MyView' conform to 'CustomModifiable',IMO 这比欺骗修饰符的行为要好(即什么都不做)一些观点)。

如果您将来需要支持更多视图,只需添加符合您的协议的扩展即可。