文章源地址: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上关注我。