SwiftUI 速查手册

SwiftUI 与 UIKit 效果一致
UIKitSwiftUI
UILabelText & Label
UIImageViewImage
UITextFieldTextField / SecureField
UITextViewTextEditor
UISwitchToggle
UISliderSlider
UIButtonButton
UITableViewList
UICollectionViewLazyVGrid / LazyHGrid
UINavigationControllerNavigationView
UITabBarControllerTabView
UIAlertController with style .alertAlert
UIAlertController with style .actionSheetActionSheet
UIStackView with horizontal axisHStack / LazyHStack
UIStackView with vertical axisVStack / LazyVStack
Aligning them in both axes.ZStack
UISegmentedControlPicker
UIStepperStepper
UIDatePickerDatePicker
NSAttributedStringNo equivalent (use Text)
MapKitMap
UIProgressViewProgressView
-Modal
-ScrollView
-Form
-Spacer
-Divider

View

Text 文本

Text("Hello World")

添加样式

Text("Hello World")
    .font(.largeTitle)
    .foregroundColor(Color.green)
    .lineSpacing(50)
    .lineLimit(nil)
    .padding()

在文本视图中格式化文本

static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .long
    return formatter
}()

var now = Date()
var body: some View {
    Text("Task due date: \(now, formatter: Self.dateFormatter)")
}

Label

可以使用以下代码行在文本旁边设置图标。

Label("SwiftUI CheatSheet", systemImage: "up.icloud")

可以设置URL,单击后将重定向到浏览器。

Link("Click me", destination: URL(string: "your_url")!)

Documentation - Label

TextEditor 多行可滚动文本编辑器

可以显示和编辑长格式文本的视图。

@State private var fullText: String = "This is some editable text..."

var body: some View {
    TextEditor(text: $fullText)
}

设置 TextEditor 背景颜色

extension NSTextView {
    open override var frame: CGRect {
        didSet {
            backgroundColor = .clear
//            drawsBackground = true
        }

    }
}

struct DetailContent: View {
    @State private var profileText: String = "Enter your bio"
    var body: some View {
        VSplitView(){
            TextEditor(text: $profileText)
                .background(Color.red)
        }
    }
}

Documentation - TextEditor

TextField 输入框

显示可编辑文本界面的控件。

@State var name: String = "John"    
var body: some View {
    TextField("Name's placeholder", text: $name)
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .padding()
}

取消编辑框焦点样式。

extension NSTextField { // << workaround !!!
    open override var focusRingType: NSFocusRingType {
        get { .none }
        set { }
    }
}

如何居中放置 TextField 的文本

struct ContentView: View {
    @State var text: String = "TextField Text"
    var body: some View {
        TextField("Placeholder Text", text: $text)
            .padding(.all, 20)
            .multilineTextAlignment(.center)
    }
}

Documentation - TextField

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)

Documentation - Image

SecureField 密码输入框

用户安全地输入私人文本的控件。

@State var password: String = "1234"    
var body: some View {
    SecureField($password)
        .textFieldStyle(RoundedBorderTextFieldStyle())
        .padding()
}

Documentation - SecureField

Toggle 开关选择器

在打开和关闭状态之间切换的控件。

@State var isShowing = true // toggle state

Toggle(isOn: $isShowing) {
    Text("Hello World")
}

如果您的 Toggle 的标签只有 Text,则可以使用此更简单的签名进行初始化。

Toggle("Hello World", isOn: $isShowing)

Documentation - Toggle

Slider 滑动输入条

用于从值的有界线性范围中选择一个值的控件。

@State var progress: Float = 0

Slider(value: $progress, from: 0.0, through: 100.0, by: 5.0)

滑块缺少 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()

Documentation - Slider

Button 按钮控件

在触发时执行操作的控件。

Button(
    action: {
        print("did tap")
    },
    label: { Text("Click Me") }
)

如果 Button 的标签仅为 Text,则可以使用此更简单的签名进行初始化。

Button("Click Me") {
    print("did tap")
}

您可以通过此按钮了解一下

Button(action: {
    // 退出应用
    NSApplication.shared.terminate(self)
}, label: {
    Image(systemName: "clock")
    Text("Click Me")
    Text("Subtitle")
})
.foregroundColor(Color.white)
.padding()
.background(Color.blue)
.cornerRadius(5)

Documentation - Button

Picker 选择控件

用于从一组互斥值中进行选择的控件。

选择器样式的更改基于其祖先,在 FormList 下,它显示为单个列表行,您可以点击以进入一个显示所有可能选项的新屏幕。

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()) 覆盖样式。

SwiftUI 中,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())

分段控制(SegmentedControl)在 iOS 13 中也焕然一新

Documentation - Picker

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)") })

如果您还为带有 step 的初始化程序的每个步骤指定了一个值的数量。

Stepper(value: $quantity, in: 0...10, step: 2) {
    Text("Quantity \(quantity)")
}

Documentation - Stepper

DatePicker 日期控件

日期选择器(DatePicker)的样式也会根据其祖先而改变。 在 FormList 下,它显示为单个列表行,您可以点击以展开到日期选择器(就像日历应用程序一样)。

@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 的标签仅是纯文本,则可以使用此更简单的签名进行初始化。

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])

Documentation - DatePicker

Map 地图界面的视图

显示指定区域的地图

import MapKit

@State var region = MKCoordinateRegion(center: .init(latitude: 37.334722, longitude: -122.008889), latitudinalMeters: 300, longitudinalMeters: 300)
    
Map(coordinateRegion: $region)

您可以通过指定 interactionModes(使用[]禁用所有交互)来控制地图的交互。

struct PinItem: Identifiable {
    let id = UUID()
    let coordinate: CLLocationCoordinate2D
}

Map(coordinateRegion: $region, 
    interactionModes: [], 
    showsUserLocation: true, 
    userTrackingMode: nil, 
    annotationItems: [PinItem(coordinate: .init(latitude: 37.334722, longitude: -122.008889))]) { item in                    
    MapMarker(coordinate: item.coordinate)
}

Documentation - Map

ProgressView 进度视图

显示任务完成进度的视图。

@State private var progress = 0.5

VStack {
    ProgressView(value: progress)
    Button("More", action: { progress += 0.05 })
}

通过应用 CircularProgressViewStyle,可以将其用作 UIActivityIndicatorView

ProgressView(value: progress)
    .progressViewStyle(CircularProgressViewStyle())

Documentation - ProgressView

查看布局和演示

HStack

将其子级排列在一条水平线上的视图。

创建静态可滚动列表

HStack (alignment: .center, spacing: 20){
    Text("Hello")
    Divider()
    Text("World")
}

Documentation - HStack

LazyHStack

将子项排列在水平增长的线中的视图,仅在需要时创建项。

ScrollView(.horizontal) {
    LazyHStack(alignment: .center, spacing: 20) {
        ForEach(1...100, id: \.self) {
            Text("Column \($0)")
        }
    }
}

Documentation - LazyHStack

VStack

以垂直线排列其子项的视图。

创建静态可滚动列表

VStack (alignment: .center, spacing: 20){
    Text("Hello")
    Divider()
    Text("World")
}

Documentation - VStack

LazyVStack

iOS 14

一种视图,将其子级排列在垂直增长的线中,仅在需要时创建项。

ScrollView {
    LazyVStack(alignment: .leading) {
        ForEach(1...100, id: \.self) {
            Text("Row \($0)")
        }
    }
}

Documentation - LazyVStack

ZStack

覆盖其子项的视图,使子项在两个轴上对齐。

ZStack {
    Text("Hello")
        .padding(10)
        .background(Color.red)
        .opacity(0.8)
    Text("World")
        .padding(20)
        .background(Color.red)
        .offset(x: 0, y: 40)
}

Documentation - ZStack

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)
}

添加 Section

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())

要使其插入分组(.insetGrouped),请添加 .listStyle(GroupedListStyle()) 并强制使用常规水平尺寸类 .environment(\.horizontalSizeClass, .regular)

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())
.environment(\.horizontalSizeClass, .regular)

插图分组已添加到 iOS 13.2 中的 SwiftUI

iOS 14

iOS 14 中,我们为此设置了专用样式。

.listStyle(InsetGroupedListStyle())

Documentation - List

ScrollView 滚动视图

滚动视图。

ScrollView(alwaysBounceVertical: true) {
    Image("foo")
    Text("Hello World")
}

Documentation - ScrollView

LazyHGrid

一种容器视图,将其子视图排列在水平增长的网格中,仅在需要时创建项目。

var rows: [GridItem] =
        Array(repeating: .init(.fixed(20)), count: 2)

ScrollView(.horizontal) {
    LazyHGrid(rows: rows, alignment: .top) {
        ForEach((0...100), id: \.self) {
            Text("\($0)").background(Color.pink)
        }
    }
}

Documentation - LazyHGrid

LazyVGrid

容器视图,将其子视图排列在垂直增长的网格中,仅在需要时创建项目。

var columns: [GridItem] = Array(repeating: .init(.fixed(20)), count: 5)

ScrollView {
    LazyVGrid(columns: columns) {
        ForEach((0...100), id: \.self) {
            Text("\($0)").background(Color.pink)
        }
    }
}

Documentation - LazyVGrid

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)
            })
        }
    }
}

Documentation - Form

Spacer

沿其包含的堆栈布局的主轴或如果不包含在堆栈中的两个轴上扩展的灵活空间。

HStack {
    Image(systemName: "clock")
    Spacer()
    Text("Time")
}

Documentation - Spacer

Divider

可用于分隔其他内容的视觉元素。

HStack {
    Image(systemName: "clock")
    Divider()
    Text("Time")
}.fixedSize()

Documentation - Divider

用于呈现视图堆栈的视图,这些视图表示导航层次结构中的可见路径。

NavigationView {            
    List {
        Text("Hello World")
    }
    .navigationBarTitle(Text("Navigation Title")) // Default to large title style
}

对于旧样式标题

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 添加显示/推送

用作 UISplitViewController

NavigationView {
    List {
        NavigationLink("Go to detail", destination: Text("New Detail"))
    }.navigationBarTitle("Master")
    Text("Placeholder for Detail")
}

您可以使用两个新的样式属性为 NavigationView 设置样式:stack和doubleColumn。 默认情况下,iPhoneApple TV 上的导航视图在视觉上反映了导航堆栈,而在 iPadMac 上,显示的是拆分视图样式的导航视图。

您可以使用 .navigationViewStyle 覆盖它。

NavigationView {
    MyMasterView()
    MyDetailView()
}
.navigationViewStyle(StackNavigationViewStyle())

iOS 14

iOS 14 中,UISplitViewController 有新的侧边栏样式。 您也可以通过在 NavigationView 下放置三个视图来做到这一点。

NavigationView {
    Text("Sidebar")
    Text("Primary")
    Text("Detail")
}

Documentation - NavigationView

iOS 14

UIViewController 中添加 UIToolbar,如 toolbarItems

NavigationView {
    Text("SwiftUI").padding()
        .toolbar {
            ToolbarItem(placement: .bottomBar) {
                Button {

                } label: {
                    Image(systemName: "archivebox")
                }
            }
            
            ToolbarItem(placement: .bottomBar) {
                Spacer()
            }
            
            ToolbarItem(placement: .bottomBar) {                
                Button {
                    
                } label: {
                    Image(systemName: "square.and.pencil")
                }                
            }
        }
}

Documentation - ToolbarItem

TabView

一个视图,允许使用可交互的用户界面元素在多个子视图之间进行切换。

TabView {
    Text("First View")
        .font(.title)
        .tabItem({ Text("First") })
        .tag(0)
    Text("Second View")
        .font(.title)
        .tabItem({ Text("Second") })
        .tag(1)
}

图像和文本在一起。 您可以在此处使用 SF Symbol

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")))
})

它也与可识别项绑定。

@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
}

Documentation - Alert

Modal 过渡。

我们可以显示基于布尔的 Modal。

@State var isModal: Bool = false

var modal: some View {
    Text("Modal")
}

Button("Modal") {
    self.isModal = true
}.sheet(isPresented: $isModal, content: {
    self.modal
})

Documentation - Sheet

它也与可识别项绑定。

@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
}

Documentation - Sheet

iOS 14

如果要使用模式显示以全屏显示的旧模式显示,可以使用 .fullScreenCover 而不是 .sheet

由于全屏封面样式不允许用户使用手势来消除模态,因此您必须添加一种方法来手动消除呈现的视图。 在下面的示例中,我们通过将 isModal 设置为 false 来添加一个按钮来消除显示的视图。

@State var isModal: Bool = false

var modal: some View {
Text("Modal")
      Button("Dismiss") {
        self.isModal = false
      }
}

Button("Fullscreen") {
    self.isModal = true
}.fullScreenCover(isPresented: $isFullscreen, content: {
      self.modal
    })

如果您将自定义视图用作模式,则可以使用 presentationMode 环境键关闭显示的视图。

struct Modal: View {
  @Environment(\.presentationMode) var presentationMode

  var body: some View {
    Text("Modal")
    Button("Dismiss Modal") {
      presentationMode.wrappedValue.dismiss()
    }
  }
}

struct ContentView: View {
    @State private var isModal = false

    var body: some View {
        Button("Fullscreen") {
            isModal = true
        }
        .fullScreenCover(isPresented: $isFullscreen, content: {
      Modal()
    })
}

Documentation - fullScreenCover

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
})

它也与可识别项绑定。

@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
}

Documentation - ActionSheet