【译】[SwiftUI-Lab]使用Extension让你的代码更具可读性

2,585 阅读5分钟

文章源地址:swiftui-lab.com/view-extens…

作者: Javier

翻译: Liaoworking

使用Extension让你的代码更具可读性

当我们创建视图时,都会想让自己的代码看起来更可能的简洁,好的缩进避免代码过长见名知意的命名等。没有拿捏好可能会让代码变的难理解。可以通过ViewModifier和自定义View的封装让代码看起来更简洁,不过今天要讲一些不一样的代码封装知识,通过extension View协议(在SwiftUI中View不是类,而是以协议的形式存在)使代码可读性更高。内容不难,那我们开始吧~

一个简单的例子

在Xcode11 Beta5 中,我们看到一些有用的修饰器(modifier)过期或者被取消掉。这些修饰符只不过是作为View协议的extension。我们可以试着自定义一些已经被废弃的修饰器。 例如:

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

在Beta5中标明我们应该使用overlay()或者background()来代替:

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

这代码第一眼看起来又长又难理解。。一开始我们可能会想到创建一个自定义ViewModifier然后用.modifier()方法来使用。

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

这看起来更好一些了,但是我们还可以再改进一下代码的可读性,可以去创建一个View 的Extension 来达到和 .border() 相同的效果,我们可以把这个方法叫做 .addBorder(),来防止和将来系统库的命名重名。

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

条件性的去添加 ViewModifier

当你打算使用修饰器的时候,你可能会遇到下面这种情况,只有满足特定的条件才会使用修饰器,但是下面的两种写法都不起作用:

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

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

编译器会报error,因为在三目运算中两个表达式必须具有相同的返回类型,但这里没有。

可以写一个很有用的View 的 extension,方法名叫.conditionalModifier(),当符合条件时才能使用具体的修饰器:

extension View {
    // 条件满足,使用修饰器,否则,不使用。
    public func conditionalModifier<T>(_ condition: Bool, _ modifier: T) -> some View where T: ViewModifier {
        Group {
            if condition {
                self.modifier(modifier)
            } else {
                self
            }
        }
    }

    // 不同的条件下使用不同的修饰器
    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)
            }
        }
    }
}

下面是使用例子:

    struct ContentView: View {
    @State private var tapped = false
    
    var body: some View {
        VStack {
            Spacer()
                        
            // 通过tapped在两种修饰器中选择
            Text("Hello World").conditionalModifier(tapped, PlainModifier(), CrazyModifier())
            Spacer()
            
            // 通过tapped来选择修饰器或者不用
            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))
    }
}

创造便利构造器

在extension中你不仅可以为View 添加修饰器,你还可以创造便利构造器。有一个很好的使用场景就是创建图片的时候,如果图片为空,让图片使用默认图:

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

简化View 的 Preference 的使用

如果你读了我之前的文章《View Preference》,就知道这些有多强大了。它可以把子视图的信息传递给父视图,在传递geometry信息的时候相当有用。不过当你真正使用的时候才发现代码会变的很混乱。 我们可以生成两个Extension,一个去设置视图的bounds,并为其设置一个Id。

.saveBounds(viewId: 3)

第二是是通过这个id去获取bounds,并由Bindingd绑定这个bounds。

.retrieveBounds(viewId: 3, $bounds)

正如你看到的,你的代码中就没有.preference(),.onPreferenceChange(), .transformPreference()

关于视图偏好(View Preferences)的一些

当使用视图偏好的时候,你可能会用到子视图的geometry信息来反过来影响父视图的布局。这个时候你可要注意了,子视图的布局影响到父视图,父视图的布局反过来又去作用于子视图,这个时候就会形成一个递归循环。更多的信息你可以看看之前的文章探究View树 part-1 PreferenceKey中的明智的使用偏好
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"))
    }
}

代码就变的简洁了很多

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

为什么要用ViewModifier?

你可能会思考ViewModifier的作用是什么?它可以通过声明@State变量来保留自己的内部状态, 这点 View 的extension 就无法做到。 看下面这个例子。

自定义一个modifier,当修饰到view上时会让view拥有能否选中的属性。当被修饰的view被点击的时候,就会有一个边框。

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

具体的使用如下:

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

view 的extension就无法完成这个。因为它无法去记录点击状态。如果用自定义的View,就无法达到代码复用的效果。如果我们在view 的 extension外用变量去记录点击状态,这就不好玩了啊,这样就把逻辑代码一部分放在了view 的 extension 外,一部分封装在调用view了。 这不得体。

不过我们这篇文章是关于 View 的 extension 来讨论的,如果你不介意多写一行代码的话,可以把ViewModifier 封装到 View 的 extension中。

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

使用起来就更清爽一些了:

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

如果你想要了解更多关于ViewModifier,Majid 有一篇不错的文章SwiftUI中的ViewModifiers

总结

本文我们了解了一些 view 的 extension 的骚操作,可以让你的代码具有更好的可读性提升开发速度减少潜在bug量。还会有更多的使用场景等着你去发现。欢迎评论或者再Twitter上关注我。