【譯】Fucking SwiftUI

FiTeen發表於2020-02-18

歡迎訪問我的部落格原文地址
譯文: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

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 VStackForm 也可以
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])
複製程式碼

可以使用 ClosedRangePartialRangeThroughPartialRangeFrom 來設定 minimumDatemaximumDate

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 雖然沒有 minimumValueImagemaximumValueImage 屬性, 但可以藉助 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")
}
複製程式碼

您可以使用兩種新的樣式屬性:stackdoubleColumn 為 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())
複製程式碼

文件

來源

相關文章