57

I'm trying to find out what is practical difference between these two approaches. For example:

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)
    }
}

Then we can use all of them following way:

Text(tech.title)
    .modifier(PrimaryLabel())
Text(tech.title)
    .makePrimaryLabel()
ModifiedContent(
    content: Text(tech.title),
    modifier: PrimaryLabel()
)
Timur Bernikovich
  • 5,660
  • 4
  • 45
  • 58

5 Answers5

26

I usually prefer extensions, as they get you a more readable code and they are generally shorter to write. I wrote an article about View extensions.

However, there are differences. At least one. With ViewModifier you can have @State variables, but not with View extensions. Here's an example:

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()
        }
    }
}
shim
  • 9,289
  • 12
  • 69
  • 108
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • Why about creating a view then `struct ColorChangeView: View`? – Timur Bernikovich Aug 08 '19 at 12:56
  • 2
    It is just an example. Of course this case does not need it. Its only purpose is showing you that `@State` variables are permitted inside a `ViewModifier`. Wasn't the original question what's the difference between View extensions and ViewModifier? There you have one ;-) – kontiki Aug 08 '19 at 13:00
  • 2
    Good point. But I'm just trying to find out why Apple developers created such a structure if it doesn't provide any opaque advantages. :) – Timur Bernikovich Aug 08 '19 at 13:04
  • 6
    Well, consider the example I posted. How would you write that logic in a way that can be applied to different views (without having to rewrite the same code for each view)? You cannot use View extensions, because you cannot keep track of an internal state. You cannot use a custom View, because you cannot reuse the logic to apply it to other views. You can only use a ViewModifier. (cont'd) – kontiki Aug 08 '19 at 13:08
  • 4
    As for what's the purpose of View extensions? Well probably two reasons: 1. View extensions are not something specific of SwiftUI. Extensions on any struct are part of the language. So how you prevent people from using them? And 2., view extensions look much nicer than modifier. So when given the option, I'll go for extensions ;-) Btw, nice question you posted! – kontiki Aug 08 '19 at 13:12
24

All of the approaches you mentioned are correct. The difference is how you use it and where you access it. Which one is better? is an opinion base question and you should take a look at clean code strategies and SOLID principles and etc to find what is the best practice for each case.

Since SwiftUI is very modifier chain base, The second option is the closest to the original modifiers. Also you can take arguments like the originals:

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")
            }
        }
    }
}

We should wait and see what is the BEST practices in new technologies like this. Anything we find now is just a GOOD practice.

Infinity James
  • 4,667
  • 5
  • 23
  • 36
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • 1
    As for me, `extension` looks good for almost all the cases I can imaging. That's the reason why I'm thinking ViewModifier+ModifiedContent are redundant. We can implement both this things with extensions and View structs. :) – Timur Bernikovich Aug 08 '19 at 12:02
  • As you say, **Almost**. – Mojtaba Hosseini Aug 08 '19 at 12:03
  • 16
    `ViewModifier` let you have `@State` variables, but View extensions do not. – kontiki Aug 08 '19 at 12:45
  • Extensions can never have stored variables. @States belongs to the view, not the modifier. – Mojtaba Hosseini Aug 08 '19 at 12:52
  • @kontiki What about just creating a custom View? I see no advantages of ViewModifier in this case too.) – Timur Bernikovich Aug 08 '19 at 12:55
  • 2
    @MojtabaHosseini I know extensions can never have store variables. That was my point! But ViewModifier can have them, I posted an example. – kontiki Aug 08 '19 at 13:01
  • 2
    To the question: What about just creating a custom View? They have different applications. ViewModifier (with a State variable) opens new possibilities with what you can do inside a ViewModifier. – kontiki Aug 08 '19 at 13:04
7

I believe the best approach is combining ViewModifiers and View extension. This will allow composition of @State within the ViewModifier and convenience of View extension.

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())
    }
}

Usage

Text(tech.title)
    .makePrimaryLabel()
Brody Robertson
  • 8,506
  • 2
  • 47
  • 42
  • 2
    This is btw also what Apple recommends in their own docs: https://developer.apple.com/documentation/swiftui/reducing-view-modifier-maintenance – alexkaessner Jun 22 '23 at 09:00
4

It may be that there is an advantage in the type signature of the resulting Views when using a ViewModifier. For example if we create the following TestView to display the types of the three variants using this:

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()
        )
    }
}

We can see it yields the following types:

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>

Even if there isn't an advantage during execution it might make debugging a little easier if nothing else.

Casey Fleser
  • 5,707
  • 1
  • 32
  • 43
  • this is quite good answer actually. It really helped me to understand the different between extending the view and using ViewModifier. Basically, using the ViewModifier helps to encapsulate any modification to the view but it keeps the original content easier to find when you kind of lost inspecting the SwiftUI view. – malxatra Nov 19 '22 at 05:09
0

There is another approach: using View extension and a generic custom view. Using generic custom view resolves the issue that @kontiki mentioned (how to apply it to other views). Below is the code:

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)
    }
}

While being different, the approach is very similar to the view modifier approach. I suspect this might be what the SwiftUI team originally had and, when they added more features to it, it evolved into view modifier.

rayx
  • 1,329
  • 10
  • 23