【译】Fucking SwiftUI

4,484 阅读10分钟

欢迎访问我的博客原文地址
译文: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())

文档

来源