在 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。