【译】Fucking SwiftUI

原文: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])

可以使用 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())

文档

来源

  • API 文档
  • 官方教程
  • WWDC 2019
    • 介绍 SwiftUI: 创建您的第一个 App
    • SwiftUI 基础
    • SwiftUI 数据流
    • 使用 SwiftUI 构建自定义视图
    • 集成 SwiftUI
    • SwiftUI 中的可访问性
    • 所有设备上的 SwiftUI
    • watchOS 上的 SwiftUI
    • 掌握 Xcode 预览

我的博客原文地址

你可能感兴趣的:(iOS开发,swift,ios)