## 入门

### 介绍

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

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

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

文档 - Label

### Link

可以设置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(布局)

### Background

将图像用作背景

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

### VStack

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

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

创建静态可滚动列表。文档 - VStack

### HStack

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

创建静态可滚动列表

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

文档 - HStack

### LazyVStack

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

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

文档 - LazyVStack

### LazyHStack

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

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

文档 - LazyHStack

### ZStack

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

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

文档 - ZStack

### 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

## 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 滚动视图

滚动视图。

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

文档 - 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

## 另见

- SwiftUI 2.0 Cheat Sheet (github.com)
- SwiftUI 2.0 备忘清单 (swiftui-example)
- Swift 备忘清单 (jaywcjlove.github.io)