SwiftUI 2.0 备忘清单
该备忘单提供了使用 SwiftUI 的标签的一些示例等
入门
介绍
SwiftUI 提供用于声明应用程序用户界面的视图、控件和布局结构
- SwiftUI Document (apple.com)
- SwiftUI 2.0 备忘清单 (swiftui-example)
- Swift 备忘清单 (jaywcjlove.github.io)
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 效果一致
- Text & Label (UILabel)
- Image (UIImageView)
- TextField / SecureField (UITextField)
- TextEditor (UITextView)
- Toggle (UISwitch)
- Slider (UISlider)
- Button/ Link (UIButton)
- List (UITableView)
- LazyVGrid / LazyHGrid (UICollectionView)
- NavigationView (UINavigationController)
- TabView (UITabBarController)
- Alert (UIAlertController 带有样式 .alert)
- ActionSheet (UIAlertController 带有样式 .actionSheet)
- HStack / LazyHStack (UIStackView 带水平轴)
- VStack / LazyVStack (UIStackView 带垂直轴)
- Picker (UISegmentedControl)
- Stepper (UIStepper)
- DatePicker (UIDatePicker)
- Text (NSAttributedString)无等效项)
- Map (MapKit)
- ProgressView (UIProgressView)
- Shape / Rectangle / Circle
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
Link
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)")
    }
  }
}
- 懒加载:只有当子视图进入可视区域时,才会被创建和渲染
- 自适应:子视图的宽高可以自适应
- 性能优化:适用于大量子视图或动态内容的场景
- 文档 - LazyVStack
- 文档 - LazyHStack
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)的样式也会根据其祖先而改变。 在 Form 或 List 下,它显示为单个列表行,您可以点击以展开到日期选择器(就像日历应用程序一样)。
@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])
可以使用 ClosedRange,PartialRangeThrough 和 PartialRangeFrom 来设置 minimumDate 和 maximumDate。
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)
滑块缺少 minimumValueImage 和 maximumValueImage,但是我们可以通过 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 选择控件
用于从一组互斥值中进行选择的控件。
选择器样式的更改基于其祖先,在 Form 或 List 下,它显示为单个列表行,您可以点击以进入一个显示所有可能选项的新屏幕。
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 滚动视图
Containers(容器)
NavigationView
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") }
            )
    )
}
NavigationLink
按下时触发导航演示的按钮。这是 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 过渡。我们可以显示基于布尔的 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)
另见
- SwiftUI 2.0 Cheat Sheet (github.com)
- SwiftUI 2.0 备忘清单 (swiftui-example)
- Swift 备忘清单 (jaywcjlove.github.io)