36

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,362
  • 3
  • 41
  • 56

3 Answers3

19

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,581
  • 5
  • 20
  • 35
Mojtaba Hosseini
  • 71,072
  • 19
  • 226
  • 225
  • 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
  • 11
    `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
  • 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
15

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
  • 8,356
  • 10
  • 70
  • 102
kontiki
  • 32,877
  • 11
  • 99
  • 118
  • Why about creating a view then `struct ColorChangeView: View`? – Timur Bernikovich Aug 08 '19 at 12:56
  • 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
  • 1
    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
  • 2
    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
  • 2
    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
3

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,357
  • 2
  • 43
  • 42