SwiftUI 官方文件已經有相當完整的元件用法和清單,可以自行查閱,這裡先簡單列出一些常用的。其實跟 UIKit 一樣每個元件都有預設的實作和樣式,都是符合 Human Interface Guidelines,但是如果產品需求就是無法用原生元件達成,就需要自己建立客製化元件了。

User Interface Elements

最基礎使用者介面的單元,可以直接給予用戶訊息的元件,像是文字圖片等等。

  • Text: 單行或多行的唯讀文字
      
    1
    Text("Hamlet").font(.title)
  • Label: 帶有圖片的文字
    1
    Label("Lightning", systemImage: "bolt.fill")
  • TextField: 一般輸入框
    1
    2
    3
    4
    TextField(
    "User name (email address)",
    text: $username
    )
  • SecureField: 密碼輸入框
    1
    2
    3
    4
    5
    6
    SecureField(
    "Password",
    text: $password
    ) {
    handleLogin(username: username, password: password)
    }
  • TextEditor: 長文字編輯器
    1
    TextEditor(text: $fullText)
  • Image: 圖片
    1
    2
    3
    Image("avatar")
    .resizable()
    .aspectRatio(contentMode: .fit)
  • Button: 按鈕
    1
    2
    3
    Button(action: signIn) {
    Text("Sign In")
    }
  • Link: 超連結文字
    1
    Link("abc", destination: URL(string: "https://www.abc.com/")!)
  • Menu: 選單
    1
    2
    3
    4
    5
    6
    7
    Menu("Actions") {
    Button("Copy", action: {})
    Button("Delete", action: {})
    Menu("More") {
    Button("12345", action: {})
    }
    }
  • Slider: 滑動的控制元件
    1
    2
    3
    4
    5
    6
    7
    Slider(
    value: $speed,
    in: 0...100,
    onEditingChanged: { editing in
    isEditing = editing
    }
    )
  • Stepper: 一加一減的控制元件
    1
    2
    3
    4
    5
    6
    7
    Stepper {
    Text("Value: \(value)")
    } onIncrement: {
    value += 1
    } onDecrement: {
    value -= 1
    }
  • Toggle: 開關的的控制元件
    1
    2
    3
    Toggle(isOn: $vibrateOnRing) {
    Text("Vibrate on Ring")
    }
  • Shape: 基於 Path 繪畫出的元件,都是 Conform Shape,而 Shape Conform View
    • 例如: Circle (圓形)、Rectangle (矩形)、RoundedRectangle (圓角矩形)
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      Circle()
      Rectangle()
      .fill(.pink)
      .frame(width: 100, height: 100, alignment: .center)
      RoundedRectangle(cornerRadius: 20)
      .fill(
      LinearGradient(
      gradient: Gradient(colors: [.white, .yellow]),
      startPoint: .leading,
      endPoint: .trailing
      )
      )
      .frame(width: 200, height: 200, alignment: .center)
  • Canvas: 支援即時繪畫的功能

Layout Containers

以前在 UIKit 的時代 AutoLayout 的實踐方式有兩種,第一種是 StackView 自動帶有排版功能的元件、第二種是大家最熟悉的 Constraint。

演化到 SwiftUI 的時代,強化了第一種做法,且不再使用第二種做法,因此使用 Layout & Container 用來排列佈局所有元件,算是實作 UI 的關鍵,關係到不同元件之間的排列方式、間距、留白等等的問題。

選擇不同的容器來裝你的元件 - Picking Container Views for Your Content

Alignment 進階的對齊的方式 - 文章1文章2

  • Stack 是堆疊元件用的容器,只是有方向上的不同,都是 Conform View。
  • 例如: HStack (水平)、VStack (垂直)、ZStack (深度),下面有官方提供的範例,同時用到三種 Stack
  • 注意: 一個 Stack 內部最多只能放 10 元件,不然會編譯錯誤!
  • 注意: 需要使用 Spacer 或 Alignment (.leading .top …) 來實現靠左靠右、靠上靠下的效果!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct ProfileView: View {
var body: some View {
ZStack(alignment: .bottom) {
Image("avatar")
.resizable()
.aspectRatio(contentMode: .fit)
HStack {
VStack(alignment: .leading) {
Text("PinkPika")
.font(.headline)
Text("iOS Developer")
.font(.subheadline)
}
Spacer()
}
.padding()
.foregroundColor(.primary)
.background(Color.primary
.colorInvert()
.opacity(0.75))
}
}
}
  • Lazy 效果讓 View 在還沒出現在畫面上是不會建立該 View 的實體,例如: LazyHStack (水平)、LazyVStack (垂直)
  • 當元件內容超過畫面的範圍,需要使用 ScrollView 把 Stack 包起來,來達到可拖拉的效果,同時也建議使用 LazyStack,可以節省資源
    1
    2
    3
    4
    5
    6
    7
    ScrollView(.horizontal) {
    LazyHStack {
    ForEach(0...10, id: \.self) { i in
    ProfileView()
    }
    }
    }.frame(maxHeight: 300.0)
  • Section 可來切分不同的區塊
  • 如果想要固定 Header 改成 LazyVStack(spacing: 1, pinnedViews: [.sectionHeaders]) 即可!
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    struct StackGroupView: View {
    let sections = [
    ColorData(color: .red, name: "Reds"),
    ColorData(color: .green, name: "Greens"),
    ColorData(color: .blue, name: "Blues")
    ]
    var body: some View {
    ScrollView {
    LazyVStack(spacing: 1) {
    ForEach(sections) { section in
    Section(header: SectionHeaderView(colorData: section)) {
    ForEach(section.variations) { variation in
    section.color
    .brightness(variation.brightness)
    .frame(height: 20)
    }
    }
    }
    }
    }
    }
    }

    struct SectionHeaderView: View {
    var colorData: ColorData
    var body: some View {
    HStack {
    Text(colorData.name)
    .font(.headline)
    .foregroundColor(colorData.color)
    Spacer()
    }
    .padding()
    .background(Color.primary
    .colorInvert()
    .opacity(0.75))
    }
    }

    struct ColorData: Identifiable {
    let id = UUID()
    let name: String
    let color: Color
    let variations: [ShadeData]
    struct ShadeData: Identifiable {
    let id = UUID()
    var brightness: Double
    }
    init(color: Color, name: String) {
    self.name = name
    self.color = color
    self.variations = stride(from: 0.0, to: 0.5, by: 0.1)
    .map { ShadeData(brightness: $0) }
    }
    }
  • 自動填滿成網格形式的排版
    GridUnicodeView.swift
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    struct GridUnicodeView: View {
    let rows: [GridItem] = Array(repeating: .init(.fixed(20)), count: 2)
    let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)
    var body: some View {
    VStack(spacing: 10){
    ScrollView(.horizontal) {
    LazyHGrid(rows: rows, alignment: .top) {
    ForEach((0...79), id: \.self) {
    let codepoint = $0 + 0x1f600
    let codepointString = String(format: "%02X", codepoint)
    Text("\(codepointString)")
    .font(.footnote)
    let emoji = String(Character(UnicodeScalar(codepoint)!))
    Text("\(emoji)")
    .font(.largeTitle)
    }
    }
    }.frame(height: 100)
    ScrollView(.vertical){
    LazyVGrid(columns: columns) {
    ForEach((0...79), id: \.self) {
    let codepoint = $0 + 0x1f600
    let codepointString = String(format: "%02X", codepoint)
    Text("\(codepointString)")
    let emoji = String(Character(UnicodeScalar(codepoint)!))
    Text("\(emoji)")
    }
    }.font(.largeTitle)
    }
    }
    }
    }
    GridProfileView.swift
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct GridProfileView: View {
    var columes = [
    GridItem(spacing: 0),
    GridItem(spacing: 0),
    GridItem(spacing: 0)
    ]
    var body: some View {
    ScrollView{
    LazyVGrid(columns: columes, spacing: 0){
    ForEach(0...10, id: \.self) { i in
    ProfileView()
    }
    }
    }
    }
    }

Collection Containers

專門處理集合資料的容器,
可以用更少的程式碼和設定完成清單的顯示,提供一些預設行為和UI(例如顯示更多、可選功能、排序等等),
如果想要客製化行為或是UI(例如分隔線),請使用上面提到的 LazyStack + ScrollView

  • List: 清單
  • Table: 可選可排序的表格
  • Form: 對輸入或有控制功能的元件進行分類
  • Group: 當作可將多個 View 拉成一群的功能(不會實際影響內部子 View 的排序方式和間距)
  • GroupBox: 帶有文字標籤的分群
  • ScrollView: 可滾動的元件

Presentation Containers

專門處理不同層級頁面的容器(ex.navigation、tab…)

  • NavigationView: 就是 Navigation
    1
    2
    3
    4
    5
    6
    7
    8
    9
    NavigationView {
    List {
    NavigationLink("Purple", destination: ColorDetail(color: .purple))
    NavigationLink("Pink", destination: ColorDetail(color: .pink))
    NavigationLink("Orange", destination: ColorDetail(color: .orange))
    }
    .navigationTitle("Colors")
    Text("Select a Color") // A placeholder to show before selection.
    }
  • OutlineGroup: 專門顯示大綱或樹狀資料夾層級的資料
  • DisclosureGroup: 可用於顯示或隱藏Group
  • TabView: 就是 Tab (僅支援 Text, Image, Text+Image),iOS15 有 badge 可用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    TabView {
    Text("The First Tab")
    .tabItem {
    Image(systemName: "1.square.fill")
    Text("First")
    }
    Text("Another Tab")
    .tabItem {
    Image(systemName: "2.square.fill")
    Text("Second")
    }
    Text("The Last Tab")
    .tabItem {
    Image(systemName: "3.square.fill")
    Text("Third")
    }
    }
    .font(.headline)
  • Presentation Modifiers: 用來處理 alerts, popovers, sheets, dialogs

Debug

請善用 PreviewProvider.border(Color.red) 就可以清楚地在預覽畫面上看到此 View 或 Container 的範圍。

Reuse View

如果用到相同樣式的元件(ex.按鈕、輸入框),重複設定很麻煩也可能設定缺漏,可以有下面兩種方式

  • 解法: 可以建立客製化 View
  • 宣告方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    struct CakeButton : View {
    let title: String
    let action: () -> Void
    var body: some View {
    Button(action: action) {
    Text(title)
    .font(.system(size: 16))
    .foregroundColor(.white)
    .frame(width: 120, height: 30)
    .background(backgroundColor)
    .cornerRadius(30/2)
    }
    }
    }
  • 使用方式
    1
    2
    3
    CakeButton(title: "123", backgroundColor: .gray){
    print("CakeButton")
    }
  • 解法: 使用 ViewModifier 可以單純封裝設定樣式的部分,最後使用 .modifier 來執行
    1
    2
    3
    public protocol ViewModifier {
    func body(content: Self.Content) -> Self.Body
    }
  • 宣告方式
    1
    2
    3
    4
    5
    6
    7
    8
    struct CakeButtonModifier: ViewModifier { 
    func body(content: Content) -> some View {
    content
    .font(.system(size: 12))
    .foregroundColor(.white)
    .frame(width: 40, height: 40)
    }
    }
  • 使用方式
    1
    2
    3
    4
    Button(action: { print("cake") }) {
    Image(systemName: "cake")
    .modifier(CakeButtonModifier())
    }

Demo