在SwiftUI中创建ViewModifier和View扩展的区别

60

我试图找出这两种方法之间的实际区别。例如:

struct PrimaryLabel: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color.black)
            .foregroundColor(Color.white)
            .font(.largeTitle)
            .cornerRadius(10)
    }
}

extension View {
    func makePrimaryLabel() -> some View {
        self
            .padding()
            .background(Color.black)
            .foregroundColor(Color.white)
            .font(.largeTitle)
            .cornerRadius(10)
    }
}

然后我们可以按照以下方式使用它们:

Text(tech.title)
    .modifier(PrimaryLabel())
Text(tech.title)
    .makePrimaryLabel()
ModifiedContent(
    content: Text(tech.title),
    modifier: PrimaryLabel()
)
5个回答

28

通常我更喜欢使用扩展,因为它们可以让你的代码更易读,并且通常编写起来更短。我撰写了一篇关于 View 扩展的文章

然而,这里存在一个差异。至少有一个。使用 ViewModifier 可以拥有 @State 变量,但使用 View 扩展则不行。以下是一个示例:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, how are you?").modifier(ColorChangeOnTap())
        }
    }
}

struct ColorChangeOnTap: ViewModifier {
    @State private var tapped: Bool = false
    
    func body(content: Content) -> some View {
        return content.foregroundColor(tapped ? .red : .blue).onTapGesture {
            self.tapped.toggle()
        }
    }
}

为什么不创建一个视图,然后使用 struct ColorChangeView: View - Timur Bernikovich
2
这只是一个示例。当然,这种情况不需要它。它的唯一目的是向您展示 @State 变量在 ViewModifier 中是允许的。最初的问题不是视图扩展和 ViewModifier 之间的区别吗?你现在有了一个答案;) - kontiki
2
好观点。但我只是想弄清楚为什么苹果开发者会创建这样一个结构,如果它没有提供任何不透明的优势。 :) - Timur Bernikovich
6
考虑我发布的示例。你会如何以一种适用于不同视图的方式编写该逻辑(而不必为每个视图重新编写相同的代码)?你不能使用View扩展,因为你无法跟踪内部状态。你不能使用自定义View,因为你无法重复使用该逻辑以将其应用于其他视图。你只能使用一个ViewModifier。(续) - kontiki
4
至于View扩展的目的是什么?可能有两个原因:1. View扩展不是SwiftUI特定的东西。对任何结构体进行扩展都是语言的一部分。那么如何防止人们使用它们呢?2.视图扩展比修改器看起来更漂亮。所以在选择时,我会选择扩展;-) 顺便说一下,你发布的问题很好! - kontiki

24

你提到的所有方法都是正确的。区别在于你如何使用它以及从哪里访问它。 哪个更好? 是一个基于观点的问题,你应该查看清洁代码策略、SOLID原则等等,以找到每种情况的最佳实践。

由于SwiftUI非常依赖修饰器链,第二个选项是最接近原始修饰器的选项。此外,你可以像原始修饰器一样传递参数:

extension Text {
    enum Kind {
        case primary
        case secondary
    }

    func style(_ kind: Kind) -> some View {

        switch kind {
        case .primary:
            return self
                .padding()
                .background(Color.black)
                .foregroundColor(Color.white)
                .font(.largeTitle)
                .cornerRadius(10)

        case .secondary:
            return self
                .padding()
                .background(Color.blue)
                .foregroundColor(Color.red)
                .font(.largeTitle)
                .cornerRadius(20)
        }
    }
}

struct ContentView: View {
    @State var kind = Text.Kind.primary

    var body: some View {
        VStack {
        Text("Primary")
            .style(kind)
            Button(action: {
                self.kind = .secondary
            }) {
                Text("Change me to secondary")
            }
        }
    }
}

我们应该等待并观察像这样的新技术中的最佳实践是什么。现在我们发现的任何东西都只是一个好的实践。


1
就我而言,“extension” 在我所能想象的几乎所有情况下都很好用。这就是为什么我认为 ViewModifier+ModifiedContent 是多余的原因。我们可以使用扩展和 View 结构来实现这两个东西。 :) - Timur Bernikovich
17
ViewModifier 允许您拥有 @State 变量,但视图扩展则不允许。 - kontiki
@kontiki 那么只创建一个自定义视图呢?在这种情况下,我也看不到使用ViewModifier的任何优势。 - Timur Bernikovich
2
@MojtabaHosseini 我知道扩展永远不能有存储变量。那就是我的观点!但是ViewModifier可以拥有它们,我发布了一个例子。 - kontiki
2
针对这个问题:创建一个自定义 View 怎么样?它们有不同的应用。使用 ViewModifier(配合 State 变量)可以在 ViewModifier 内部打开新的可能性。 - kontiki
显示剩余2条评论

7

我认为最好的方法是将ViewModifiers和View扩展结合起来。这样可以在ViewModifier中组合@State,并享受View扩展的便利。

struct PrimaryLabel: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color.black)
            .foregroundColor(Color.white)
            .font(.largeTitle)
            .cornerRadius(10)
    }
}

extension View {
    func makePrimaryLabel() -> some View {
        ModifiedContent(content: self, modifier: PrimaryLabel())
    }
}

使用方法

Text(tech.title)
    .makePrimaryLabel()

2
这也是苹果在他们自己的文档中推荐的:https://developer.apple.com/documentation/swiftui/reducing-view-modifier-maintenance - alexkaessner

4

当使用ViewModifier时,结果视图的类型签名可能具有优势。例如,如果我们创建以下TestView来显示使用此方法的三个变体的类型:

struct TestView: View {
    init() {
        print("body1: \(type(of: body))")
        print("body2: \(type(of: body2))")
        print("body3: \(type(of: body3))")
    }
    
    @ViewBuilder var body: some View {
        Text("Some Label")
            .modifier(PrimaryLabel())
    }
    
    @ViewBuilder var body2: some View {
        Text("Some Label")
            .makePrimaryLabel()
    }
    
    @ViewBuilder var body3: some View {
        ModifiedContent(
            content: Text("Some Label"),
            modifier: PrimaryLabel()
        )
    }
}

我们可以看到它产生了以下类型:

body1: ModifiedContent<Text, PrimaryLabel>
body2: ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<ModifiedContent<Text, _PaddingLayout>, _BackgroundStyleModifier<Color>>, _EnvironmentKeyWritingModifier<Optional<Color>>>, _EnvironmentKeyWritingModifier<Optional<Font>>>, _ClipEffect<RoundedRectangle>>
body3: ModifiedContent<Text, PrimaryLabel>

即使在执行过程中没有优势,如果仅仅为了更容易调试,也可以使用它。

这实际上是一个非常好的答案。它真正帮助我理解了扩展视图和使用ViewModifier之间的区别。基本上,使用ViewModifier有助于封装对视图的任何修改,但当您在检查SwiftUI视图时迷失方向时,它可以使原始内容更容易找到。 - malxatra

1

还有另一种方法:使用View扩展和通用自定义视图。使用通用自定义视图解决了@kontiki提到的问题(如何将其应用于其他视图)。以下是代码:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, how are you?").colorChangeOnTap()
        }
    }
}

struct ColorChangeOnTap<Content: View>: View {
    var content: Content
    @State private var tapped: Bool = false

    var body: some View {
        return content.foregroundColor(tapped ? .red : .blue).onTapGesture {
            self.tapped.toggle()
        }
    }
}

extension View {
    func colorChangeOnTap() -> some View {
        ColorChangeOnTap(content: self)
    }
}

虽然不同,但这种方法与视图修饰符方法非常相似。我怀疑这可能是SwiftUI团队最初拥有的东西,当他们为其添加更多功能时,它演变成了视图修饰符。


谢谢!我已经有了一个包装器样式的View,我希望也能够用修饰器样式的方法来创建它。这比需要创建一个额外的ViewModifier类型来连接View扩展方法和自定义View要好得多。请注意,您还可以将colorChangeOnTap()的返回类型指定为-> ColorChangeOnTap<Self>,以提供更多的类型信息给类型系统。(虽然我不确定这是否真的对任何事情有帮助。) - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接