原文: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())
文档
我的博客原文地址