SwiftUI 2.0 备忘清单

该备忘单提供了使用 SwiftUI 的标签的一些示例等

入门

介绍

SwiftUI 提供用于声明应用程序用户界面的视图、控件和布局结构

import SwiftUI

struct AlbumDetail: View {
  var album: Album
  var body: some View {
    List(album.songs) { song in 
      HStack {
        Image(album.cover)
        VStack(alignment: .leading) {
          Text(song.title)
        }
      }
    }
  }
}

SwiftUI 与 UIKit 效果一致

View(视图)

Text

要在UI中显示文本,只需编写:

Text("Hello World")

添加样式

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

Text 设置文本格式

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

文档 - Label

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

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

文档 - Label

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)

文档 - Image

Shape

创建矩形的步骤

Rectangle()
    .fill(Color.red)
    .frame(width: 200, height: 200)

创建圆的步骤

Circle()
  .fill(Color.blue)
  .frame(width: 50, height: 50)

文档 - Image

ProgressView 进度视图

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

@State private var progress = 0.5

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

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

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

文档 - ProgressView

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

文档 - Map

Layout(布局)

VStack

VStack垂直 堆栈布局,用于将子视图垂直排列。默认将子视图从上到下排列

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

文档 - VStack

HStack

HStack水平 堆栈布局,用于将子视图水平排列。默认将子视图从左到右排列

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

文档 - HStack

ZStack

ZStack层叠 堆栈布局,用于将子视图重叠在一起。按照添加的顺序从下到上排列子视图,即先添加的视图会在下面,后添加的视图会覆盖在上面

ZStack {
    Text("Hello")
    Text("World")
}

文档 - ZStack

懒加载 Lazy

iOS 14.0 之后新增的视图,仅在需要时才会创建和渲染

ScrollView {
  LazyVStack(alignment: .leading) {
    ForEach(1...100, id: \.self) {
        Text("Row \($0)")
    }
  }
}
  • 懒加载:只有当子视图进入可视区域时,才会被创建和渲染
  • 自适应:子视图的宽高可以自适应
  • 性能优化:适用于大量子视图或动态内容的场景

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

文档 - LazyVGrid

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

文档 - LazyHGrid

Spacer

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

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

文档 - Spacer

Divider

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

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

文档 - Divider

Background

将图像用作背景

Text("Hello World")
    .font(.largeTitle)
    .background(
        Image("hello_world")
            .resizable()
            .frame(width: 100, height: 100)
    )

Input(输入)

Toggle 开关选择器

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

@State var isShowing = true // toggle state

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

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

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

文档 - Toggle

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)

文档 - Button

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

文档 - TextField

SecureField 密码输入框

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

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

文档 - SecureField

TextEditor 多行可滚动文本编辑器

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

@State private var fullText: String = "这是一些可编辑的文本..."

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 = "输入您的简历"
  var body: some View {
    VSplitView(){
      TextEditor(text: $profileText)
        .background(Color.red)
    }
  }
}

文档 - TextEditor

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

文档 - DatePicker

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

文档 - Slider

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

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

SwiftUI 中,UISegmentedControl 只是 Picker的另一种样式。分段控制(SegmentedControl)在 iOS 13 中也焕然一新。文档 - 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)")
}

文档 - Stepper

Tap

对于单次敲击

Text("Tap me!").onTapGesture {
  print("Tapped!")
}

用于双击

Text("Tap me!").onTapGesture(count: 2) {
  print("Tapped!")
}

Gesture 手势

手势如轻敲手势、长按手势、拖拉手势

Text("Tap")
  .gesture(
      TapGesture()
          .onEnded { _ in
              // do something
          }
  )
Text("Drag Me")
  .gesture(
      DragGesture(minimumDistance: 50)
          .onEnded { _ in
              // do something
          }
  )
Text("Long Press")
  .gesture(
      LongPressGesture(minimumDuration: 2)
          .onEnded { _ in
              // do something
          }
  )

OnChange

onChange 是一个新的视图修改器,可用于所有 SwiftUI 视图。它允许您侦听状态更改并相应地对视图执行操作

TextEditor(text: $currentText)
  .onChange(of: clearText) { value in
      if clearText{
          currentText = ""
      }
  }

List(列表)

List 列表

一个容器,用于显示排列在单列中的数据行。创建静态可滚动列表

List {
    Text("Hello world")
    Text("Hello world")
    Text("Hello world")
}

创建动态列表

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

可混合的列表

List {
    Text("Hello world")
    Image(systemName: "clock")
}

使其分组

添加 .listStyle(GroupedListStyle())

List {
  Section(header: Text("UIKit"),
    footer: Text("我们会想念你的")) {
      Text("UITableView")
  }

  Section(header: Text("SwiftUI"),
    footer: Text("要学的东西很多")) {
      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 中,我们为此设置了专用样式。

.listStyle(InsetGroupedListStyle())

文档 - List

ScrollView 滚动视图

滚动视图。

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

文档 - ScrollView

Containers(容器)

NavigationView 或多或少类似于 UINavigationController,它处理视图之间的导航,显示标题,将导航栏放在顶部。

NavigationView {
    Text("Hello")
        .navigationBarTitle(Text("World"), displayMode: .inline)
}

大标题使用 .large 将条形图项添加到导航视图

NavigationView {
  Text("Hello")
      .navigationBarTitle(Text("World"), displayMode: .inline)
      .navigationBarItems(
        trailing:
            Button(
                action: { print("Going to Setting") },
                label: { Text("Setting") }
            )
    )
}

按下时触发导航演示的按钮。这是 pushViewController 的替代品

NavigationView {
    NavigationLink(destination:
        Text("Detail")
        .navigationBarTitle(Text("Detail"))
    ) {
        Text("Push")
    }.navigationBarTitle(Text("Master"))
}

或者通过将组目标添加到自己的视图 DetailView 中,使其更具可读性

NavigationView {
    NavigationLink(destination: DetailView()) {
        Text("Push")
    }.navigationBarTitle(Text("Master"))
}

Group

Group 创建多个视图作为一个视图,同时也避免了 Stack 的10视图最大限制

VStack {
    Group {
        Text("Hello")
        Text("Hello")
        Text("Hello")
    }
    Group {
        Text("Hello")
        Text("Hello")
    }
}

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

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

您几乎可以在此表单中放入任何内容,它将为表单呈现适当的样式。文档 - Form

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

@State var isModal: Bool = false

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

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

文档 - Sheet

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

Alert 也与可识别项绑定

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

文档 - Alert

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

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
}

文档 - ActionSheet

SwiftData

SwiftData声明

import SwiftData
// 通过@Model宏来定义模型schema
// 支持基础值类型String、Int、CGFloat等
// 支持复杂类型Struct、Enum、Codable、集合等
@Model
class Person {
    var id: String
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.id = UUID().uuidString
        self.name = name
        self.age = age
    }
}

声明@Attribute

@Model
class Person {
    // @Attribute(.unique)为id添加唯一约束
    @Attribute(.unique) var id: String
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.id = UUID().uuidString
        self.name = name
        self.age = age
    }
}

声明@Relationship

@Model
class Person {
    @Attribute(.unique) 
    var id: String
    var name: String
    var age: Int
    // @Relationship(deleteRule: .cascade) 
    // 使得Person在数据库里被删除时
    // 删除掉所有关联的students
    @Relationship(deleteRule: .cascade)
    var students: [Student]? = []
    init(name: String, age: Int) {
        self.id = UUID().uuidString
        self.name = name
        self.age = age
    }
}

声明Transient

@Model
class Person {
    @Attribute(.unique) 
    var id: String
    var name: String
    // @Transient表示不要持久化这个属性
    // 需要提供一个默认值
    @Transient
    var age: Int = 0
    init(name: String) {
        self.id = UUID().uuidString
        self.name = name
    }
}

@Query

struct ContentView: View  {
    // Query 可以高效地查询大型数据集,并自定义返回内容的方式,如排序、过滤
    @Query(sort: \.age, order: .reverse) var persons: [Person]
    @Environment(\.modelContext) var modelContext
    var body: some View {
       NavigationStack() {
          List {
             ForEach(trips) { trip in 
                 // ...
             }
          }
       }
    }
}

构建ModelContainer

// 用 Schema 进行初始化
let container = try ModelContainer(for: Person.self)
// 用配置(ModelConfiguration)初始化
let container = try ModelContainer(
    for: Person.self,
    configurations: ModelConfiguration(url: URL("path"))
)
// 通过View 和 Scene 的修饰器来快速关联一个 ModelContainer
struct SwiftDataDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: Person.self)
    }
}

构建ModelContext

// 在配置Model Container完成后
// 通过Environment 来访问到 modelContext
struct ContextView : View {
    @Environment(\.modelContext) 
    private var context
}
// 或者直接获取共享的主Actor context
let context = container.mainContext
// 或者直接初始化一个新的Context
let context = ModelContext(container)

增、删、改

let person = Person(name: "Lily", age: 10)
// Insert a new person
context.insert(person)
// Delete an existing person
context.delete(person)
// Manually save changes to the context
try context.save()

查询

let personPredicate = #Predicate<Person> {
    $0.name == "Lily" &&
    $0.age == 10
}

let descriptor = FetchDescriptor<Person>(predicate: personPredicate)
let persons = try? context.fetch(descriptor)

另见