欢迎访问我的博客原文地址。
译文:Fucking Swift UI - Cheat Sheet
译者的话:翻译过程中,发现了原文中的几个错误,我向作者@sarunw提出意见后,直接在译文中改掉了,如果您发现文中内容有误,欢迎与我联系。
关于 SwiftUI,您在下文中看到的所有答案并不是完整详细的,它只能充当一份备忘单,或是检索表。
关于 SwiftUI 的常见问题:
是否需要学 SwiftUI?
是
是否有必要现在就学 SwiftUI?
看情况,因为 SwiftUI 目前只能在 iOS 13、macOS 10.15、tvOS 13和 watchOS 6 上运行。如果您要开发的新应用计划仅针对前面提到的 OS 系统,我会说是。 但是,如果您打算找工作或是无法确保会在此 OS 版本的客户端项目上工作,则可能要等一两年,再考虑迁移成 SwiftUI,毕竟大多数客户端工作都希望支持尽可能多的用户,这意味着您的应用必须兼容多个 OS 系统。 因此,一年后再去体验优雅的 SwiftUI 也许是最好的时机。
是否需要学 UIKit/AppKit/WatchKit?
是的,就长时间来看,UIKit 仍将是 iOS 架构的重要组成部分。现在的 SwiftUI 并不成熟完善,我认为即使您打算用 SwiftUI 来开发,仍然不时需要用到 UIKit。
SwiftUI 能代替 UIKit/AppKit/WatchKit 吗?
现在不行,但将来也许会。SwiftUI 虽然是刚刚推出的,它看起来已经很不错。我希望两者能长期共存,SwiftUI 还很年轻,它还需要几年的打磨成长才能去代替 UIKit/AppKit/WatchKit。
如果我现在只能学习一种,那么应该选择 UIKit/AppKit/WatchKit 还是 SwiftUI?
UIKit。 您始终可以依赖 UIKit,它用起来一直不错,且未来一段时间仍然可用。如果您直接从 SwiftUI 开始学习,可能会遗漏了解一些功能。
SwiftUI 的控制器在哪里?
没有了。 如今页面间直接通过响应式编程框架 Combine 交互。Combine 也作为新的通信方式替代了 UIViewController。
想要体验 SwiftUI 画布,但不想在您的电脑上安装 macOS Catalina beta 系统 您可以与当前的 macOS 版本并行安装 Catalina。这里介绍了如何在单独的 APFS 卷上安装 macOS
UIKit | SwiftUI | 备注 |
---|---|---|
UIViewController | View | - |
UITableViewController | List | - |
UICollectionViewController | - | 目前,还没有 SwiftUI 的替代品,但是您可以像Composing Complex Interfaces's tutorial里那样,使用 List 的组成来模拟布局 |
UISplitViewController | NavigationView | Beta 5中有部分支持,但仍然无法使用。 |
UINavigationController | NavigationView | - |
UIPageViewController | - | - |
UITabBarController | TabView | - |
UISearchController | - | - |
UIImagePickerController | - | - |
UIVideoEditorController | - | - |
UIActivityViewController | - | - |
UIAlertController | Alert | - |
UIKit | SwiftUI | 备注 |
---|---|---|
UILabel | Text | - |
UITabBar | TabView | - |
UITabBarItem | TabView | TabView 里的 .tabItem |
UITextField | TextField | Beta 5中有部分支持,但仍然无法使用。 |
UITableView | List | VStack 和 Form 也可以 |
UINavigationBar | NavigationView | NavigationView 的一部分 |
UIBarButtonItem | NavigationView | NavigationView 里的 .navigationBarItems |
UICollectionView | - | - |
UIStackView | HStack | .axis == .Horizontal |
UIStackView | VStack | .axis == .Vertical |
UIScrollView | ScrollView | - |
UIActivityIndicatorView | - | - |
UIImageView | Image | - |
UIPickerView | Picker | - |
UIButton | Button | - |
UIDatePicker | DatePicker | - |
UIPageControl | - | - |
UISegmentedControl | Picker | Picker 中的一种样式 SegmentedPickerStyle |
UISlider | Slider | - |
UIStepper | Stepper | - |
UISwitch | Toggle | - |
UIToolBar | - | - |
将 SwiftUI 视图集成到现有应用程序中,并将 UIKit 视图和控制器嵌入 SwiftUI 视图层次结构中。
UIKit | SwiftUI | 备注 |
---|---|---|
UIView | UIViewRepresentable | - |
UIViewController | UIViewControllerRepresentable | - |
将 SwiftUI 视图集成到现有应用程序中,并将 UIKit 视图和控制器嵌入 SwiftUI 视图层次结构中。
UIKit | SwiftUI | 备注 |
---|---|---|
UIView (UIHostingController) | View | 没有直接转换为 UIView 的方法,但是您可以使用容器视图将 UIViewController 中的视图添加到视图层次结构中 |
UIViewController (UIHostingController) | View | - |
显示一行或多行只读文本的视图。
Text("Hello World") 复制代码
样式:
Text("Hello World") .bold() .italic() .underline() .lineLimit(2) 复制代码
Text
中填入的字符串也用作 LocalizedStringKey
,因此也会直接获得 NSLocalizedString
的特性。
Text("This text used as localized key") 复制代码
直接在文本视图里格式化文本。 实际上,这不是 SwiftUI 的功能,而是 Swift 5的字符串插入特性。
static let dateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .long return formatter }() var now = Date() var body: some View { Text("What time is it?: \(now, formatter: Self.dateFormatter)") } 复制代码
可以直接用 +
拼接 Text
文本:
Text("Hello ") + Text("World!").bold() 复制代码
文字对齐方式:
Text("Hello\nWorld!").multilineTextAlignment(.center) 复制代码
显示可编辑文本界面的控件。
@State var name: String = "John" var body: some View { TextField("Name's placeholder", text: $name) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding() } 复制代码
用户安全地输入私人文本的控件。
@State var password: String = "1234" var body: some View { SecureField($password) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding() } 复制代码
显示图像的视图。
Image("foo") //图像名字为 foo 复制代码
我们可以使用新的 SF Symbols:
Image(systemName: "clock.fill") 复制代码
您可以通过为系统图标添加样式,来匹配您使用的字体:
Image(systemName: "cloud.heavyrain.fill") .foregroundColor(.red) .font(.title) Image(systemName: "clock") .foregroundColor(.red) .font(Font.system(.largeTitle).bold()) 复制代码
为图片增加样式:
Image("foo") .resizable() // 调整大小,以便填充所有可用空间 .aspectRatio(contentMode: .fit) 复制代码
在触发时执行操作的控件。
Button( action: { // 点击事件 }, label: { Text("Click Me") } ) 复制代码
如果按钮的标签只有 Text
,则可以通过下面这种简单的方式进行初始化:
Button("Click Me") { // 点击事件 } 复制代码
您可以像这样给按钮添加属性:
Button(action: { }, label: { Image(systemName: "clock") Text("Click Me") Text("Subtitle") }) .foregroundColor(Color.white) .padding() .background(Color.blue) .cornerRadius(5) 复制代码
按下时会触发导航演示的按钮。它用作代替 pushViewController
。
NavigationView { NavigationLink(destination: Text("Detail") .navigationBarTitle(Text("Detail")) ) { Text("Push") }.navigationBarTitle(Text("Master")) } 复制代码
为了增强可读性,可以把 destination
包装成自定义视图 DetailView
的方式:
NavigationView { NavigationLink(destination: DetailView()) { Text("Push") }.navigationBarTitle(Text("Master")) } 复制代码
但不确定是 Bug 还是设计使然,上述代码 在 Beta 5 中的无法正常执行。尝试像这样把
NavigationLink
包装进列表中试一下:
NavigationView { List { NavigationLink(destination: Text("Detail")) { Text("Push") }.navigationBarTitle(Text("Master")) } } 复制代码
如果 NavigationLink
的标签只有 Text
,则可以用这样更简单的方式初始化:
NavigationLink("Detail", destination: Text("Detail").navigationBarTitle(Text("Detail"))) 复制代码
在开/关状态之间切换的控件。
@State var isShowing = true // toggle 状态值 Toggle(isOn: $isShowing) { Text("Hello World") } 复制代码
如果 Toggle
的标签只有 Text
,则可以用这样更简单的方式初始化:
Toggle("Hello World", isOn: $isShowing) 复制代码
从一组互斥值中进行选择的控件。
选择器样式根据其被父视图进行更改,在表单或列表下作为一个列表行显示,点击可以推出新界面展示所有的选项卡。
NavigationView { Form { Section { Picker(selection: $selection, label: Text("Picker Name") , content: { Text("Value 1").tag(0) Text("Value 2").tag(1) Text("Value 3").tag(2) Text("Value 4").tag(3) }) } } } 复制代码
您可以使用 .pickerStyle(WheelPickerStyle())
覆盖样式。
在 iOS 13 中, UISegmentedControl
也只是 Picker
的一种样式。
@State var mapChoioce = 0 var settings = ["Map", "Transit", "Satellite"] Picker("Options", selection: $mapChoioce) { ForEach(0 ..< settings.count) { index in Text(self.settings[index]) .tag(index) } }.pickerStyle(SegmentedPickerStyle()) 复制代码
分段控制器在iOS 13中也焕然一新了。
选择日期的控件。
日期选择器样式也会根据其父视图进行更改,在表单或列表下作为一个列表行显示,点击可以扩展到日期选择器(就像日历 App 一样)。
@State var selectedDate = Date() var dateClosedRange: ClosedRange<Date> { let min = Calendar.current.date(byAdding: .day, value: -1, to: Date())! let max = Calendar.current.date(byAdding: .day, value: 1, to: Date())! return min...max } NavigationView { Form { Section { DatePicker( selection: $selectedDate, in: dateClosedRange, displayedComponents: .date, label: { Text("Due Date") } ) } } } 复制代码
不在表单或列表里,它就可以作为普通的旋转选择器。
@State var selectedDate = Date() var dateClosedRange: ClosedRange<Date> { let min = Calendar.current.date(byAdding: .day, value: -1, to: Date())! let max = Calendar.current.date(byAdding: .day, value: 1, to: Date())! return min...max } DatePicker( selection: $selectedDate, in: dateClosedRange, displayedComponents: [.hourAndMinute, .date], label: { Text("Due Date") } ) 复制代码
如果 DatePicker
的标签只有 Text
,则可以用这样更简单的方式初始化:
DatePicker("Due Date", selection: $selectedDate, in: dateClosedRange, displayedComponents: [.hourAndMinute, .date]) 复制代码
可以使用 ClosedRange
、PartialRangeThrough
和 PartialRangeFrom
来设置 minimumDate
和 maximumDate
。
DatePicker("Minimum Date", selection: $selectedDate, in: Date()..., displayedComponents: [.date]) DatePicker("Maximum Date", selection: $selectedDate, in: ...Date(), displayedComponents: [.date]) 复制代码
从有界的线性范围中选择一个值的控件。
@State var progress: Float = 0 Slider(value: $progress, from: 0.0, through: 100.0, by: 5.0) 复制代码
Slider 虽然没有 minimumValueImage
和 maximumValueImage
属性, 但可以借助 HStack
实现。
@State var progress: Float = 0 HStack { Image(systemName: "sun.min") Slider(value: $progress, from: 0.0, through: 100.0, by: 5.0) Image(systemName: "sun.max.fill") }.padding() 复制代码
用于执行语义上递增和递减动作的控件。
@State var quantity: Int = 0 Stepper(value: $quantity, in: 0...10, label: { Text("Quantity \(quantity)")}) 复制代码
如果您的 Stepper
的标签只有 Text
,则可以用这样更简单的方式初始化:
Stepper("Quantity \(quantity)", value: $quantity, in: 0...10) 复制代码
如果您要一个自己管理的数据源的控件,可以这样写:
@State var quantity: Int = 0 Stepper(onIncrement: { self.quantity += 1 }, onDecrement: { self.quantity -= 1 }, label: { Text("Quantity \(quantity)") }) 复制代码
水平排列子元素的视图。
创建一个水平排列的静态列表:
HStack (alignment: .center, spacing: 20){ Text("Hello") Divider() Text("World") } 复制代码
垂直排列子元素的视图。
创建一个垂直排列的静态列表:
VStack (alignment: .center, spacing: 20){ Text("Hello") Divider() Text("World") } 复制代码
子元素会在 z轴方向上叠加,同时在垂直/水平轴上对齐的视图。
ZStack { Text("Hello") .padding(10) .background(Color.red) .opacity(0.8) Text("World") .padding(20) .background(Color.red) .offset(x: 0, y: 40) } 复制代码
用于显示排列一系列数据行的容器。
创建一个静态可滚动列表:
List { Text("Hello world") Text("Hello world") Text("Hello world") } 复制代码
表单里的内容可以混搭:
List { Text("Hello world") Image(systemName: "clock") } 复制代码
创建一个动态列表:
let names = ["John", "Apple", "Seed"] List(names) { name in Text(name) } 复制代码
加入分区:
List { Section(header: Text("UIKit"), footer: Text("We will miss you")) { Text("UITableView") } Section(header: Text("SwiftUI"), footer: Text("A lot to learn")) { Text("List") } } 复制代码
要使其成为分组列表,请添加 .listStyle(GroupedListStyle())
:
List { Section(header: Text("UIKit"), footer: Text("We will miss you")) { Text("UITableView") } Section(header: Text("SwiftUI"), footer: Text("A lot to learn")) { Text("List") } }.listStyle(GroupedListStyle()) 复制代码
滚动视图。
ScrollView(alwaysBounceVertical: true) { Image("foo") Text("Hello World") } 复制代码
对数据输入的控件进行分组的容器,例如在设置或检查器中。
您可以往表单中插入任何内容,它将为表单渲染适当的样式。
NavigationView { Form { Section { Text("Plain Text") Stepper(value: $quantity, in: 0...10, label: { Text("Quantity") }) } Section { DatePicker($date, label: { Text("Due Date") }) Picker(selection: $selection, label: Text("Picker Name") , content: { Text("Value 1").tag(0) Text("Value 2").tag(1) Text("Value 3").tag(2) Text("Value 4").tag(3) }) } } } 复制代码
一块既能在包含栈布局时沿主轴伸展,也能在不包含栈时沿两个轴展开的灵活空间。
HStack { Image(systemName: "clock") Spacer() Text("Time") } 复制代码
用于分隔其它内容的可视化元素。
HStack { Image(systemName: "clock") Divider() Text("Time") }.fixedSize() 复制代码
用于渲染视图堆栈的视图,这些视图会展示导航层次结构中的可见路径。
NavigationView { List { Text("Hello World") } .navigationBarTitle(Text("Navigation Title")) // 默认使用大标题样式 } 复制代码
对于旧样式标题:
NavigationView { List { Text("Hello World") } .navigationBarTitle(Text("Navigation Title"), displayMode: .inline) } 复制代码
增加 UIBarButtonItem
NavigationView { List { Text("Hello World") } .navigationBarItems(trailing: Button(action: { // Add action }, label: { Text("Add") }) ) .navigationBarTitle(Text("Navigation Title")) } 复制代码
用 NavigationLink 添加 show
/push
功能。
作为 UISplitViewController
:
NavigationView { List { NavigationLink("Go to detail", destination: Text("New Detail")) }.navigationBarTitle("Master") Text("Placeholder for Detail") } 复制代码
您可以使用两种新的样式属性:stack
和 doubleColumn
为 NavigationView 设置样式。默认情况下,iPhone 和 Apple TV 上的导航栏上显示导航堆栈,而在 iPad 和 Mac 上,显示的是拆分样式的导航视图。
您可以通过 .navigationViewStyle
重写样式:
NavigationView { MyMasterView() MyDetailView() } .navigationViewStyle(StackNavigationViewStyle()) 复制代码
在 beta 3中,NavigationView
支持拆分视图,但它仅支持非常基本的结构,其中主视图为列表,详细视图为叶视图,我期待在下一个 release 版本中能有优化补充。
使用交互式用户界面元素在多个子视图之间切换的视图。
TabView { Text("First View") .font(.title) .tabItem({ Text("First") }) .tag(0) Text("Second View") .font(.title) .tabItem({ Text("Second") }) .tag(1) } 复制代码
标签元素支持同时显示图像和文本, 您也可以使用 SF Symbols。
TabView { Text("First View") .font(.title) .tabItem({ Image(systemName: "circle") Text("First") }) .tag(0) Text("Second View") .font(.title) .tabItem(VStack { Image("second") Text("Second") }) .tag(1) } 复制代码
您也可以省略 VStack
:
TabView { Text("First View") .font(.title) .tabItem({ Image(systemName: "circle") Text("First") }) .tag(0) Text("Second View") .font(.title) .tabItem({ Image("second") Text("Second") }) .tag(1) } 复制代码
一个展示警告信息的容器。
我们可以根据布尔值显示 Alert
。
@State var isError: Bool = false Button("Alert") { self.isError = true }.alert(isPresented: $isError, content: { Alert(title: Text("Error"), message: Text("Error Reason"), dismissButton: .default(Text("OK"))) }) 复制代码
它也可与 Identifiable
项目绑定。
@State var error: AlertError? var body: some View { Button("Alert Error") { self.error = AlertError(reason: "Reason") }.alert(item: $error, content: { error in alert(reason: error.reason) }) } func alert(reason: String) -> Alert { Alert(title: Text("Error"), message: Text(reason), dismissButton: .default(Text("OK")) ) } struct AlertError: Identifiable { var id: String { return reason } let reason: String } 复制代码
模态视图的存储类型。
我们可以根据布尔值显示 Modal
。
@State var isModal: Bool = false var modal: some View { Text("Modal") } Button("Modal") { self.isModal = true }.sheet(isPresented: $isModal, content: { self.modal }) 复制代码
它也可与 Identifiable
项目绑定。
@State var detail: ModalDetail? var body: some View { Button("Modal") { self.detail = ModalDetail(body: "Detail") }.sheet(item: $detail, content: { detail in self.modal(detail: detail.body) }) } func modal(detail: String) -> some View { Text(detail) } struct ModalDetail: Identifiable { var id: String { return body } let body: String } 复制代码
操作表视图的存储类型。
我们可以根据布尔值显示 ActionSheet
。
@State var isSheet: Bool = false var actionSheet: ActionSheet { ActionSheet(title: Text("Action"), message: Text("Description"), buttons: [ .default(Text("OK"), action: { }), .destructive(Text("Delete"), action: { }) ] ) } Button("Action Sheet") { self.isSheet = true }.actionSheet(isPresented: $isSheet, content: { self.actionSheet }) 复制代码
它也可与 Identifiable
项目绑定。
@State var sheetDetail: SheetDetail? var body: some View { Button("Action Sheet") { self.sheetDetail = ModSheetDetail(body: "Detail") }.actionSheet(item: $sheetDetail, content: { detail in self.sheet(detail: detail.body) }) } func sheet(detail: String) -> ActionSheet { ActionSheet(title: Text("Action"), message: Text(detail), buttons: [ .default(Text("OK"), action: { }), .destructive(Text("Delete"), action: { }) ] ) } struct SheetDetail: Identifiable { var id: String { return body } let body: String } 复制代码
表示 UIKit 视图的视图,当您想在 SwiftUI 中使用 UIView 时,请使用它。
要使任何 UIView 在 SwiftUI 中可用,请创建一个符合 UIViewRepresentable 的包装器视图。
import UIKit import SwiftUI struct ActivityIndicator: UIViewRepresentable { @Binding var isAnimating: Bool func makeUIView(context: Context) -> UIActivityIndicatorView { let v = UIActivityIndicatorView() return v } func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) { if isAnimating { uiView.startAnimating() } else { uiView.stopAnimating() } } } 复制代码
如果您想要桥接 UIKit 里的数据绑定 (delegate, target/action) 就使用 Coordinator
, 具体见 SwiftUI 教程。
import SwiftUI import UIKit struct PageControl: UIViewRepresentable { var numberOfPages: Int @Binding var currentPage: Int func makeUIView(context: Context) -> UIPageControl { let control = UIPageControl() control.numberOfPages = numberOfPages control.addTarget( context.coordinator, action: #selector(Coordinator.updateCurrentPage(sender:)), for: .valueChanged) return control } func updateUIView(_ uiView: UIPageControl, context: Context) { uiView.currentPage = currentPage } func makeCoordinator() -> Coordinator { Coordinator(self) } // This is where old paradigm located class Coordinator: NSObject { var control: PageControl init(_ control: PageControl) { self.control = control } @objc func updateCurrentPage(sender: UIPageControl) { control.currentPage = sender.currentPage } } } 复制代码
表示 UIKit 视图控制器的视图。当您想在 SwiftUI 中使用 UIViewController 时,请使用它。
要使任何 UIViewController 在 SwiftUI 中可用,请创建一个符合 UIViewControllerRepresentable 的包装器视图,具体见 SwiftUI 教程。
import SwiftUI import UIKit struct PageViewController: UIViewControllerRepresentable { var controllers: [UIViewController] func makeUIViewController(context: Context) -> UIPageViewController { let pageViewController = UIPageViewController( transitionStyle: .scroll, navigationOrientation: .horizontal) return pageViewController } func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) { pageViewController.setViewControllers( [controllers[0]], direction: .forward, animated: true) } } 复制代码
表示 SwiftUI 视图的 UIViewController。
let vc = UIHostingController(rootView: Text("Hello World")) let vc = UIHostingController(rootView: ContentView()) 复制代码