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

你会得到下图的错误提示

SwiftUI Tips:如何像 ViewBuilder 一样动态返回 Shape

由于 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 中,完成了 buildOptionalbuildEither 方法,因而支撑在 ShapeBuilder 中回来 Optional,支撑 if-else 条件句子

大家都知道 SwiftUI 的 DSL 语法是受限的,假如要在 ShapeBuilder 中支撑回来多个 Shape或者支撑 for-in 这种语法就需求自己扩展了。

SwiftUI Tips:如何像 ViewBuilder 一样动态返回 Shape

假如想深化了解 resultBuilder 能够看看这份文档 Apple 0289 Result Builder。