歡迎訪問我的部落格原文地址。
譯文: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。
要求
- Xcode 11 Beta(從 Apple 官網下載)
- iOS 13 / macOS 10.15 / tvOS 13 / watchOS 6
- macOS Catalina,以便在畫布上呈現 SwiftUI(從 Apple 官網下載)
想要體驗 SwiftUI 畫布,但不想在您的電腦上安裝 macOS Catalina beta 系統 您可以與當前的 macOS 版本並行安裝 Catalina。這裡介紹了如何在單獨的 APFS 捲上安裝 macOS
SwiftUI 中等效的 UIKit
檢視控制器
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 檢視層次結構中。
UIKit | SwiftUI | 備註 |
---|---|---|
UIView | UIViewRepresentable | - |
UIViewController | UIViewControllerRepresentable | - |
框架整合 - UIKit 中的 SwiftUI
將 SwiftUI 檢視整合到現有應用程式中,並將 UIKit 檢視和控制器嵌入 SwiftUI 檢視層次結構中。
UIKit | SwiftUI | 備註 |
---|---|---|
UIView (UIHostingController) | View | 沒有直接轉換為 UIView 的方法,但是您可以使用容器檢視將 UIViewController 中的檢視新增到檢視層次結構中 |
UIViewController (UIHostingController) | View | - |
SwiftUI - 檢視和控制元件
Text
顯示一行或多行只讀文字的檢視。
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)
複製程式碼
TextField
顯示可編輯文字介面的控制元件。
@State var name: String = "John"
var body: some View {
TextField("Name's placeholder", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
複製程式碼
SecureField
使用者安全地輸入私人文字的控制元件。
@State var password: String = "1234"
var body: some View {
SecureField($password)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
複製程式碼
Image
顯示影像的檢視。
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
在觸發時執行操作的控制元件。
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)
複製程式碼
NavigationLink
按下時會觸發導航演示的按鈕。它用作代替 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")))
複製程式碼
Toggle
在開/關狀態之間切換的控制元件。
@State var isShowing = true // toggle 狀態值
Toggle(isOn: $isShowing) {
Text("Hello World")
}
複製程式碼
如果 Toggle
的標籤只有 Text
,則可以用這樣更簡單的方式初始化:
Toggle("Hello World", isOn: $isShowing)
複製程式碼
Picker
從一組互斥值中進行選擇的控制元件。
選擇器樣式根據其被父檢視進行更改,在表單或列表下作為一個列表行顯示,點選可以推出新介面展示所有的選項卡。
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中也煥然一新了。
DatePicker
選擇日期的控制元件。
日期選擇器樣式也會根據其父檢視進行更改,在表單或列表下作為一個列表行顯示,點選可以擴充套件到日期選擇器(就像日曆 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])
複製程式碼
Slider
從有界的線性範圍中選擇一個值的控制元件。
@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()
複製程式碼
Stepper
用於執行語義上遞增和遞減動作的控制元件。
@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)") })
複製程式碼
SwiftUI - 頁面佈局與演示
HStack
水平排列子元素的檢視。
建立一個水平排列的靜態列表:
HStack (alignment: .center, spacing: 20){
Text("Hello")
Divider()
Text("World")
}
複製程式碼
VStack
垂直排列子元素的檢視。
建立一個垂直排列的靜態列表:
VStack (alignment: .center, spacing: 20){
Text("Hello")
Divider()
Text("World")
}
複製程式碼
ZStack
子元素會在 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
用於顯示排列一系列資料行的容器。
建立一個靜態可滾動列表:
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
滾動檢視。
ScrollView(alwaysBounceVertical: true) {
Image("foo")
Text("Hello World")
}
複製程式碼
Form
對資料輸入的控制元件進行分組的容器,例如在設定或檢查器中。
您可以往表單中插入任何內容,它將為表單渲染適當的樣式。
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)
})
}
}
}
複製程式碼
Spacer
一塊既能在包含棧佈局時沿主軸伸展,也能在不包含棧時沿兩個軸展開的靈活空間。
HStack {
Image(systemName: "clock")
Spacer()
Text("Time")
}
複製程式碼
Divider
用於分隔其它內容的視覺化元素。
HStack {
Image(systemName: "clock")
Divider()
Text("Time")
}.fixedSize()
複製程式碼
NavigationView
用於渲染檢視堆疊的檢視,這些檢視會展示導航層次結構中的可見路徑。
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
使用互動式使用者介面元素在多個子檢視之間切換的檢視。
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
一個展示警告資訊的容器。
我們可以根據布林值顯示 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
模態檢視的儲存型別。
我們可以根據布林值顯示 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
操作表檢視的儲存型別。
我們可以根據布林值顯示 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
}
複製程式碼
框架整合 - SwiftUI 中的 UIKit
UIViewRepresentable
表示 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
}
}
}
複製程式碼
UIViewControllerRepresentable
表示 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)
}
}
複製程式碼
框架整合 - UIKit 中的 SwiftUI
UIHostingController
表示 SwiftUI 檢視的 UIViewController。
let vc = UIHostingController(rootView: Text("Hello World"))
let vc = UIHostingController(rootView: ContentView())
複製程式碼