前言
SwiftUI
编写更少的代码,打造更出色的 app。
SwiftUI 是一种创新、简洁的编程方式,通过 Swift 的强大功能,在所有 Apple 平台上构建用户界面。借助它,您只需一套工具和 API,即可创建面向任何 Apple 设备的用户界面。SwiftUI 采用简单易懂、编写方式自然的声明式 Swift 语法,可无缝支持新的 Xcode 设计工具,让您的代码与设计保持高度同步。 SwiftUI 原生支持“动态字体”、“深色模式”、本地化和辅助功能——第一行您写出的 SwiftUI 代码,就已经是您编写过的、功能最强大的 UI 代码。
developer.apple.com/xcode/swift…
Creating and Combining Views
关于 some View
新建一个 SwiftUI 的新项目,会出现如下代码:一个Text
展示在body
中。
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
复制代码
可能会对some
比较陌生,所以我们先从View
说起。
文档内可以看到View
是 SwiftUI 一个协议,这个协议里含有一个associatedtype
:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
associatedtype Body : View
/// The content and behavior of the view.
@ViewBuilder var body: Self.Body { get }
}
复制代码
带有这种修饰的协议不能作为类型来使用,只能当作约束来使用。通过some View
的修饰,会向编译器保证:每次闭包中返回的一定是一个确定,而且遵守View
协议的类型,不要去关心到底是哪种类型。
// Error
func createView() -> View {
}
// Correct
func createView<T: View>() -> T {
}
复制代码
这种写法使用了 Swift 5.1 的 Opaque return types特性。这样的设计,为开发者提供了一个灵活的开发模式,抹掉了具体的类型,不需要修改公共API来确定每次闭包的返回类型,也降低了代码书写难度。
Preview SwiftUI
SwiftUI 含有苹果对标 React Native 或 Flutter 的 Hot Reloading 工具,Xcode 将对代码进行静态分析,找到所有遵守PreviewProvider
协议的类型进行预览渲染。经过尝试,Preview SwiftUI 不需要运行 app 就能查看实时预览,不像其他 Hot Reloading 可能还需要重启 app 或进入对应界面,调整数据进行调试。
快捷键:Option + Command + P
关于 ViewBuilder
我们先来看一个最简单的 UI:
我们创建了一个横向布局组件Hstack
,里面有两个文字组件Text
。问题是这两个Text
不是以数组的形式放进content
的闭包里,那这种写法为什么还能成立?我们来看Hstack
的初始化方法:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct HStack<Content> : View where Content : View {
/// Creates a horizontal stack with the given spacing and vertical alignment.
///
/// - Parameters:
/// - alignment: The guide for aligning the subviews in this stack. This
/// guide has the same vertical screen coordinate for every child view.
/// - spacing: The distance between adjacent subviews, or `nil` if you
/// want the stack to choose a default distance for each pair of
/// subviews.
/// - content: A view builder that creates the content of this stack.
@inlinable public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)
/// The type of view representing the body of this view.
///
/// When you create a custom view, Swift infers this type from your
/// implementation of the required `body` property.
public typealias Body = Never
}
复制代码
看最后一个参数content
,只返回了一个() -> Content
类型,但我们在创建的时候只是列举了两个Text
,并没有返回一个可用的Content
。
这里使用的就是 Swift 5.1 的另一个特性 Function Builder,同时注意到content
前面有一个@ViewBuilder
标记。
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@_functionBuilder public struct ViewBuilder {
/// Builds an empty view from a block containing no statements.
public static func buildBlock() -> EmptyView
/// Passes a single view written as a child view through unmodified.
///
/// An example of a single view written as a child view is
/// `{ Text("Hello") }`.
public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}
复制代码
被@_functionBuilder
标记的类型可以被用来对其他类型进行标记,所以在这个结构体中,@ViewBuilder
可以对content
进行标记,所以被标记的content
会按照ViewBuilder
中合适的buildBlock
进行 build 后再使用。
有趣的是我们发现了文档中有这么多buildBlock
:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0 : View, C1 : View, C2 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3) -> TupleView<(C0, C1, C2, C3)> where C0 : View, C1 : View, C2 : View, C3 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4) -> TupleView<(C0, C1, C2, C3, C4)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5) -> TupleView<(C0, C1, C2, C3, C4, C5)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
}
复制代码
而事实上,刚才我所写的示例代码也有如下写法:
而基于 function builder 的构造方式是有一定限制的,文档里规定了最多 10 个参数。
Building Lists and Navigation
List
最简单的办法是创建一个静态List
,如下:
这里的List
和HStack
或VStack
很相似,接受一个 view builder 并采用 View DSL 的方式列举了几个 Row。这种方式构建了对应着 UITableView 的静态 cell 的组织方式。
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension List where SelectionValue == Never {
/// Creates a list with the given content.
///
/// - Parameter content: The content of the list.
public init(@ViewBuilder content: () -> Content)
}
复制代码
同样我们去查看List
的初始化方法,还有其他一些动态初始化方法:
public init<RowContent>(_ data: Range<Int>, @ViewBuilder rowContent: @escaping (Int) -> RowContent) where Content == ForEach<Range<Int>, Int, HStack<RowContent>>, RowContent : View
复制代码
NavigationView
可以通过NavigationView
来进行页面跳转:
Handling User Input
@State
在前端界面上,数据同步及时刷新比较重要。一般都是数据源数据更新了,界面 UI 同时更新。在 SwiftUI 里面,视图中声明的任何状态、内容和布局,源头一旦发生改变,会自动更新视图,因此,只需要一次布局。在属性前面加上@State
关键词,即可实现每次数据改动,UI 动态更新的效果。