Article source address :swiftui-lab.com/view-extens…

Author: Javier

Translation: Liaoworking

Use Extension to make your code more readable

When we create a view, we all want to make our code look as concise as possible, good indentation, avoiding long code, naming things by name, and so on. Not getting it right can make code difficult to understand. ViewModifier and custom View encapsulation can be used to make code look cleaner, but today I’ll talk about some different code encapsulation, using the Extension View protocol (in SwiftUI View is not a class, but in the form of a protocol) to make code more readable. The content is not difficult, so let’s begin ~

A simple example

In Xcode11 Beta5, we saw that some useful modifiers had expired or been cancelled. These modifiers are simply extensions to the View protocol. We can try to customize some of the deprecated modifiers. Such as:

Text("Hello World").border(Color.black, width: 3, cornerRadius: 5)
Copy the code

In Beta5 specify that we should use overlay() or background() instead:

Text("Hello World").overlay(MyRoundedBorder(cornerRadius: 5).strokeBorder(Color.black, lineWidth: 3))
Copy the code

The e code looks long and difficult to understand at first glance. At first we might want to create a custom ViewModifier and use the. Modifier () method.

Text("Hello World").modifier(RoundedBorder(Color.black, width: 3, cornerRadius: 5))


struct MyRoundedBorder<S>: ViewModifier where S : ShapeStyle {
    let shapeStyle: S
    var width: CGFloat = 1
    let cornerRadius: CGFloat
    
    init(_ shapeStyle: S, width: CGFloat, cornerRadius: CGFloat) {
        self.shapeStyle = shapeStyle
        self.width = width
        self.cornerRadius = cornerRadius
    }
    
    func body(content: Content) -> some View {
        return content.overlay(RoundedRectangle(cornerRadius: cornerRadius).strokeBorder(shapeStyle, lineWidth: width))
    }
}
Copy the code

This looks better, but we can improve the readability of the code by creating a View Extension that has the same effect as.border(). We can call this method.addborder () to prevent it from having the same name as future system library names.

Text("Hello World").addBorder(Color.blue, width: 3, cornerRadius: 5)


extension View {
    public func addBorder<S>(_ content: S, width: CGFloat = 1, cornerRadius: CGFloat) -> some View where S : ShapeStyle {
        return overlay(RoundedRectangle(cornerRadius: cornerRadius).strokeBorder(content, lineWidth: width))
    }
}
Copy the code

Conditional to add ViewModifier

When you try to use a decorator, you may encounter a situation where a decorator will only be used if certain conditions are met, but neither of the following methods will work:

Text("Hello world").modifier(flag ? ModifierOne() : EmptyModifier())

Text("Hello world").modifier(flag ? ModifierOne() : ModifierTwo())
Copy the code

The compiler raises an error because both expressions must have the same return type in a ternary operation, which they do not.

ConditionalModifier () can be used only when conditionalModifier is conditional. conditionalModifier()

Extension View {// If the condition is met, use the decorator, otherwise, do not use it. public func conditionalModifier<T>(_ condition: Bool, _ modifier: T) -> some View where T: ViewModifier {Group {if condition {self.modifier()} else {self}}} // Use different modifiers in different conditions public func conditionalModifier<M1, M2>(_ condition: Bool, _ trueModifier: M1, _ falseModifier: M2) -> some View where M1: ViewModifier, M2: ViewModifier {Group {if condition {self.modifier(trueModifier)} else {self.modifier(falseModifier)}}}} The following is an example:  struct ContentView: View { @State private var tapped = false var body: Some View {VStack {Spacer() // Select Text("Hello World") from two modifiers by tapped. ConditionalModifier (tapped, PlainModifier(), CrazyModifier()) Spacer() // To choose modifiers by tapping or not to Text("Hello World"). ConditionalModifier (tapped, PlainModifier()) Spacer() Button("Tap Me!" ) { self.tapped.toggle() }.padding(.bottom, 40) } } } struct PlainModifier: ViewModifier { func body(content: Content) -> some View { return content .font(.largeTitle) .foregroundColor(.blue) .autocapitalization(.allCharacters) } } struct CrazyModifier: ViewModifier { var font: Font = .largeTitle func body(content: Content) -> some View { return content .font(.largeTitle) .foregroundColor(.red) .autocapitalization(.words) .font(Font.custom("Papyrus", size: 24)) .padding() .overlay(RoundedRectangle(cornerRadius: 10). Stroke (color.purple, lineWidth: 3.0))}}Copy the code

Create convenience constructors

In Extension you can not only add modifiers to views, you can also create convenience constructors. A good use scenario is to create an image and use the default image if the image is empty:

 Image("landscape", defaultSystemImage: "questionmark.circle.fill")

Image("landscape", defaultImage: "empty-photo")


extension Image {
    init(_ name: String, defaultImage: String) {
        if let img = UIImage(named: name) {
            self.init(uiImage: img)
        } else {
            self.init(defaultImage)
        }
    }
    
    init(_ name: String, defaultSystemImage: String) {
        if let img = UIImage(named: name) {
            self.init(uiImage: img)
        } else {
            self.init(systemName: defaultSystemImage)
        }
    }
}
Copy the code

Simplify the use of a View’s Preference

If you read my previous article, View Preference, you can see how powerful this is. It can pass information from the child view to the parent view, which is very useful for transferring geometry information. But the code gets messy when you actually use it. We can generate two extensions, one to set the bounds of the view and give it an Id.

.saveBounds(viewId: 3)
Copy the code

The second is to get the bounds by this ID and bind the bounds by Bindingd.

.retrieveBounds(viewId: 3, $bounds)
Copy the code

As you can see, there is none in your code. Preference(),.onpreferencechange (),.transformpreference ()

Some View Preferences

When using view preferences, you may use the geometry information of the child views to influence the layout of the parent view. Notice that the layout of the child view affects the parent view, which in turn affects the child view, creating a recursive loop. You can check out the previous article for more informationExplore the View tree part-1 PreferenceKeyIn theUse preferences wisely
struct ContentView: View {
    @State private var littleRect: CGRect = .zero
    @State private var bigRect: CGRect = .zero
    
    var body: some View {
        VStack {
            Text("Little = \(littleRect.debugDescription)")
            Text("Big = \(bigRect.debugDescription)")
            HStack {
                LittleView()
                BigView()
            }
            .coordinateSpace(name: "mySpace")
            .retrieveBounds(viewId: 1, $littleRect)
            .retrieveBounds(viewId: 2, $bigRect)
        }
    }
}

struct LittleView: View {
    var body: some View {
        Text("Little Text").font(.caption).padding(20).saveBounds(viewId: 1, coordinateSpace: .named("mySpace"))
    }
}

struct BigView: View {
    var body: some View {
        Text("Big Text").font(.largeTitle).padding(20).saveBounds(viewId: 2, coordinateSpace: .named("mySpace"))
    }
}
Copy the code

The code is much cleaner

extension View {
    public func saveBounds(viewId: Int, coordinateSpace: CoordinateSpace = .global) -> some View {
        background(GeometryReader { proxy in
            Color.clear.preference(key: SaveBoundsPrefKey.self, value: [SaveBoundsPrefData(viewId: viewId, bounds: proxy.frame(in: coordinateSpace))])
        })
    }
    
    public func retrieveBounds(viewId: Int, _ rect: Binding<CGRect>) -> some View {
        onPreferenceChange(SaveBoundsPrefKey.self) { preferences in
            DispatchQueue.main.async {
                // The async is used to prevent a possible blocking loop,
                // due to the child and the ancestor modifying each other.
                let p = preferences.first(where: { $0.viewId == viewId })
                rect.wrappedValue = p?.bounds ?? .zero
            }
        }
    }
}

struct SaveBoundsPrefData: Equatable {
    let viewId: Int
    let bounds: CGRect
}

struct SaveBoundsPrefKey: PreferenceKey {
    static var defaultValue: [SaveBoundsPrefData] = []
    
    static func reduce(value: inout [SaveBoundsPrefData], nextValue: () -> [SaveBoundsPrefData]) {
        value.append(contentsOf: nextValue())
    }
    
    typealias Value = [SaveBoundsPrefData]
}
Copy the code

Why use ViewModifier?

You may be wondering what the role of ViewModifier is? It can preserve its internal State by declaring the @state variable, something a View’s Extension cannot do. Look at the following example.

Customize a modifier that allows the view to have the properties of whether it can be selected when modified to the view. When the decorated view is clicked, it has a border.

struct SelectOnTap: ViewModifier { let color: Color @State private var tapped: Bool = false func body(content: Content) -> some View { return content .padding(20) .overlay(RoundedRectangle(cornerRadius: 10).stroke(tapped ? Color: color.clear, lineWidth: 3.0). OnTapGesture {withAnimation(.easeinout (duration: 0.3)) {self.tapped. Toggle ()}}}}Copy the code

Specific uses are as follows:

struct ContentView: View { var body: some View { VStack { Text("Hello World!" ).font(.largeTitle) .modifier(SelectOnTap(color: .purple)) Circle().frame(width: 50, height: 50) .modifier(SelectOnTap(color: .green)) LinearGradient(gradient: .init(colors: [Color.green, Color.blue]), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1)).frame(width: 50, height: 50) .modifier(SelectOnTap(color: .blue)) }.padding(40) } }Copy the code

The View extension cannot do this. Because it can’t record click status. If you use a custom View, you won’t be able to reuse your code. If we use a variable outside of the view’s extension to record the click status, it will not be fun. This will put part of the logic code outside of the view’s extension and part of the logic code inside the call view. It’s unseemly.

If you don’t mind writing one more line of code, you can wrap the ViewModifier into a View extension.

extension View {
    func selectOnTap(_ color: Color) -> some View { modifier(SelectOnTap(color: color)) }
}
Copy the code

It’s a little more refreshing to use:

struct ContentView: View { var body: some View { VStack { Text("Hello World!" ).font(.largeTitle) .selectOnTap(.purple) Circle().frame(width: 50, height: 50) .selectOnTap(.green) LinearGradient(gradient: .init(colors: [Color.green, Color.blue]), startPoint: UnitPoint(x: 0, y: 0), endPoint: UnitPoint(x: 1, y: 1)).frame(width: 50, height: 50) .selectOnTap(.blue) }.padding(40) } }Copy the code

If you’d like to learn more about ViewModifiers, Majid has a great article on ViewModifiers in SwiftUI

conclusion

In this article, we’ve seen how view extension works to make your code more readable, faster, and less buggy. There are many more usage scenarios to discover. Feel free to comment or follow me again on Twitter.