在 SwiftUI 中 shape 是非常常用的元素(比如 RoundedRectangle),但是如果在 view 中你想要把 shape 包装成一个属性却会遇到问题。
以下面的代码为例,某些条件下希望返回的是圆形,有的情况下返回的是圆角矩形。
struct ProblemView: View {
var flag = Bool.random()
var body: some View {
shape
.fill(Color.blue)
.frame(width: 100, height: 100)
}
var shape: some Shape {
if flag {
return Circle()
} else {
return RoundedRectangle(cornerRadius: 10)
}
}
}
你会得到下图的错误提示
因为 Shape 是一个协议,不是一个具体的类型,some 关键字只能返回一种透明类型。但是示例代码中因为有条件判断,所以编译期间不能确定唯一的具体类型,因此报错了。如果返回的两个 Shape 是同一个具体类型就不会报错了。但是我们今天要解决的就是不同 Shape 如何返回的问题。
转成 View
最懒的方式就是把 Shape 转成 View。转成 View 类型就可以使用 ViewBuilder。
@ViewBuilder
var shape: some View {
Group {
if flag {
Circle()
} else {
RoundedRectangle(cornerRadius: 10)
}
}
.foregroundColor(Color.blue)
}
使用了 foregroundColor 后类型转成是 View,因此就可以正常使用了。
但是这个方式有很大的限制,因为通常不是所有的场合都能在底层把 Shape 转成 View 类型,也许有的场合上层的的确确需要的是 Shape。
定义 AnyShape
我们可以学习 AnyView 的方式自定义一个 AnyShape,可以包含任意一种 Shape。
struct AnyShape: Shape {
init<S: Shape>(_ wrapped: S) {
_path = { rect in
let path = wrapped.path(in: rect)
return path
}
}
func path(in rect: CGRect) -> Path {
return _path(rect)
}
private let _path: (CGRect) -> Path
}
有了 AnyShape 后,就可以把返回的 Shape 全都包在 AnyShape 里。
var shape: some Shape {
if flag {
return AnyShape(Circle())
} else {
return AnyShape(RoundedRectangle(cornerRadius: 10))
}
}
这个方式的缺点首先写起来有一点小麻烦,其次是这种方式擦除了类型。擦除类型后性能会变差一点点,如果某些场景上层需要判断具体的 Shape 类型也做不到了。
学习 ViewBuilder:自定义 ShapeBuilder
有些聪明的宝宝可能会有一个疑惑了,为什么可以动态的返回 View?因为加了 @ViewBuilder 后,本质上储存的是一个闭包,一段代码(DSL),最后在 ViewBuilder 里会执行这个闭包,返回一个具体类型的 View。对于编译器而言就是一个具体类型的 View 了。
那么我们可不可以学习 ViewBuilder,自定义一个类似功能的 ShapeBuilder 呢?答案是可以的,Swift 提供了自定义 @resultBuilder 的方式。
在 GitHub 上已经有人实现了一个 ShapeBuilder,引入依赖或者拷贝源码到本地后就可以像 ViewBuilder 一样使用了:
@ShapeBuilder
var shape: some Shape {
if flag {
Circle()
} else {
RoundedRectangle(cornerRadius: 10)
}
}
非常丝滑!
我们看一下核心的源码:
@resultBuilder
public enum ShapeBuilder {
public static func buildBlock<S: Shape>(_ builder: S) -> some Shape {
builder
}
}
public extension ShapeBuilder {
static func buildOptional<S: Shape>(_ component: S?) -> EitherShape<S, EmptyShape> {
component.flatMap(EitherShape.first) ?? EitherShape.second(EmptyShape())
}
static func buildEither<First: Shape, Second: Shape>(first component: First) -> EitherShape<First, Second> {
.first(component)
}
static func buildEither<First: Shape, Second: Shape>(second component: Second) -> EitherShape<First, Second> {
.second(component)
}
}
public enum EitherShape<First: Shape, Second: Shape>: Shape {
case first(First)
case second(Second)
public func path(in rect: CGRect) -> Path {
switch self {
case let .first(first):
return first.path(in: rect)
case let .second(second):
return second.path(in: rect)
}
}
}
在自定义的 ShapeBuilder 中,实现了 buildOptional 和 buildEither 方法,因此支持在 ShapeBuilder 中返回 Optional,支持 if-else 条件语句。
大家都知道 SwiftUI 的 DSL 语法是受限的,如果要在 ShapeBuilder 中支持返回多个 Shape或者支持 for-in 这种语法就需要自己扩展了。
如果想深入了解 resultBuilder 可以看看这份文档 Apple 0289 Result Builder。