WWDC 22 的重点更新:SwiftUI 4.0 新功能一览

WWDC 22 剛剛完結,其中的一大重點還是 SwiftUI 框架。如大家所料,隨著 iOS 16 和 Xcode 14,Apple 也推出了新版本的 SwiftUI。

這次更新帶來了非常多的功能,讓開發者可以構建更好的 App,並減少需要編寫的程式碼。在這篇教學文章中,我會為大家簡單介紹 SwiftUI 4.0 的新功能。

SwiftUI 圖表

以後要建立圖表,我們再也不需要構建自己圖表庫,或是依靠第三方程式庫了!現在,SwiftUI 框架有 Charts API。有了這個宣告式框架,只要編寫幾行程式碼,就可以構建出一個圖表動畫。

簡單來說,我們只需要定義 Mark,就可以構建出 SwiftUI 圖表。讓我們看看這個簡單的例子:

import SwiftUI
import Charts

struct ContentView: View {
    var body: some View {
        Chart {
            BarMark(
                x: .value("Day", "Monday"),
                y: .value("Steps", 6019)
            )

            BarMark(
                x: .value("Day", "Tuesday"),
                y: .value("Steps", 7200)
            )
        }
    }
}

無論我們想要構建長條圖還是折線圖,我們都會從 Chart 視圖開始。在圖表裡面,我們可以定義 bar mark,來提供圖表資料。BarMark 視圖是用來構建長條圖的,每一個 BarMark 視圖都會有 x 和 y 值,x 值就是代表 x 軸的圖表資料,如此類推。在以上的程式碼中,我把 x 軸的標籤設置為 Day,而 y 軸就是總步數。

讓我們在 Xcode 14 輸入以上程式碼,預覽就會自動顯示有兩個垂直長方體的長條圖。

以上就是創建長條圖最簡單的方法。不過,我們通常都不會對圖表數據進行硬編碼 (hardcode),而是在 Charts API 編寫一組數據。讓我們看看以下例子:

在預設情況下,Charts API 會以相同顏色呈現所有長方體。如果我們想把每個長方體設置為不同的顏色,可以將 foregroundStyle 修飾符附加到 BarMark 視圖:

.foregroundStyle(by: .value("Day", weekdays[index]))

如果我們想為所有長方體添加註釋,可以使用 annotation 修飾符:

.annotation {
    Text("\(steps[index])")
}

作出這些改動後,長條圖就更加漂亮了。

如果想要建立橫向的長條圖,我們只需要把 BarMark 視圖內的 x 和 y 參數 (parameter) 交換就可以了。

如果把 BarMark 視圖轉換成 LineMark,圖表就會變成折線圖了。

Chart {
    ForEach(weekdays.indices, id: \.self) { index in
        LineMark(
            x: .value("Day", weekdays[index]),
            y: .value("Steps", steps[index])
        )
        .foregroundStyle(.purple)
        .lineStyle(StrokeStyle(lineWidth: 4.0))
    }
}

我們也可以使用 foregroundStyle 更改折線圖的顏色。如果要更改線的寬度,就可以附加 lineStyle 修飾符。

Charts API 非常靈活,我們可以在同一個視圖中疊加多個圖表:

我除了 BarMark 和 LineMark 之外,SwiftUI Charts 框架還有 PointMarkAreaMarkRectangularMark、和 RuleMark,讓我們構建不同類型的圖表。

可擴展的 Bottom Sheet

Apple 在 iOS 15 推出了 UISheetPresentationController,用來呈現可擴展的 Bottom Sheet;可惜這個類別只在 UIKit 中可用。如果我們想在 SwiftUI 中使用它,就需要編寫額外的程式碼,來把組件集合到 SwiftUI 專案中。今年,Swift 提供了一個新的修飾符 PresentationDetents,用來呈現可擴展的 Bottom Sheet。

我們只需要把這個修佈符放在一個 sheet 視圖中,就可以使用它:

struct BottomSheetDemo: View {
    @State private var showSheet = false

    var body: some View {
        VStack {
            Button("Show Bottom Sheet") {
                showSheet.toggle()
            }
            .buttonStyle(.borderedProminent)
            .sheet(isPresented: $showSheet) {
                Text("This is the resizable bottom sheet.")
                    .presentationDetents([.medium])
            }

            Spacer()
        }
    }
}

presentationDetents 修飾符會接受一組用於 Sheet 的 detent。在上面的程式碼中,我們將 detent 設置為 .medium,這表示一個 Bottom Sheet 會佔據螢幕一半。

要讓 Bottom Sheet 變成可擴展,我們要為 presentationDetents 修飾符提供多於一個 detent。

.presentationDetents([.medium, .large])

現在,我們會看到一個 drag bar,表示 Sheet 可以擴展。如果想隱藏 drag indicator,我們可以附加 presentationDragIndicator 修飾符,並設置為 .hidden

.presentationDragIndicator(.hidden)

除了 .medium 等預設的 detent 之外,我們還可以使用 .height 和 .fraction 來創建客製化的 detent。讓我們看看這個例子:

.presentationDetents([.fraction(0.1), .medium, .large])

這樣的這,Bottom Sheet 第一次出現時,就只會佔據螢幕的 10% 左右。

MultiDatePicker

最新版本的 SwiftUI 帶來了新的日期選擇器 (date picker),讓使用者可以選擇多個日期。以下是範例程式碼:

struct MultiDatePickerDemo: View {

    @State private var selectedDates: Set<DateComponents> = []

    var body: some View {
        MultiDatePicker("Choose your preferred dates", selection: $selectedDates)
            .frame(height: 300)
    }
}

NavigationView 在 iOS 16 已經被棄用,取而代之的是新的 NavigationStack 和 NavigationSplitView。在 iOS 16 之前,我們會使用 NavigationView 來建立導航界面:

NavigationView {
    List {
        ForEach(1...10, id: \.self) { index in
            NavigationLink(destination: Text("Item #\(index) detail")) {
                Text("Item #\(index)")
            }
        }
    }
    .listStyle(.plain)

    .navigationTitle("Navigation Demo")
}

我們可以搭配 NavigationLink 使用,來建立 push 和 pop 導航。

由於 NavigationView 在 iOS 16 已經被棄用,它提供了一個新的視圖 NavigationStack,讓開發者建立同類型的導航界面。讓我們看看以下例子:

NavigationStack {
    List {
        ForEach(1...10, id: \.self) { index in
            NavigationLink {
                Text("Item #\(index) Detail")
            } label: {
                Text("Item #\(index)")
            }
        }
    }
    .listStyle(.plain)

    .navigationTitle("Navigation Demo")
}

以上程式碼與舊方法十分類似,唯一的不同之處就是我們用的是 NavigationStack 而不是 NavigationView 。那 NavigationStack 有甚麼改善呢?

讓我們看看另一個例子:

NavigationStack {
    List {
        NavigationLink(value: "Text Item") {
            Text("Text Item")
        }

        NavigationLink(value: Color.purple) {
            Text("Purple color")
        }
    }
    .listStyle(.plain)

    .navigationTitle("Navigation Demo")
    .navigationDestination(for: Color.self) { item in
        item.clipShape(Circle())
    }
    .navigationDestination(for: String.self) { item in
        Text("This is the detail view for \(item)")
    }
}

以上的列表很簡單,只有兩行:Text item 和 Purple color。但是,這兩行的 underlying type 並不相同,一個是文本物件,而另一個是 Color 物件。

NavigationLink 視圖在 iOS 16 中進步了。我們不再需要指定目標視圖,它可以採用一個數值來代表示目標。與新的 navigationDestination 修飾符搭配使用時,我們就可以輕鬆控制目標視圖。在上面的程式碼中,我們有兩個 navigationDestination 修飾符,一個用於文本物件,另一個用於 Color 物件。

當使用者選擇了 NavigationStack 內的某個物件,SwiftUI 就會檢查 NavigationLink 內 value 的物件型別,並調用與該物件型別相關的目標視圖。

這就是新的 NavigationStack 的操作方式。以上只是 NavigationStack 的簡單介紹。我們還可以使用新的 navigationDestination 修飾符,來以編程方式控制導航。比如說,我們可以創建一個按鈕,讓使用者從 navigation stack 中任何一個細節視圖直接跳轉到主視圖。我們會另外再寫一篇教學文章,來詳細說說這個題目。

iOS 16 在 SwiftUI 推出了 ShareLink 控件 (control),讓開發者顯示分享選單 (Share Sheet)。使用 ShareLink 非常簡單,讓我們看看以下例子:

struct ShareLinkDemo: View {
    private let url = URL(string: "https://www.appcoda.com")!

    var body: some View {
        ShareLink(item: url)
    }
}

我們要向 ShareLink 控件提供要分享的物件,這會顯示一個預設的分享按鈕。點擊按鈕後,App 會顯示一個分享選單。

我們可以提供自己的文本和圖像,來客製化分享按鈕:

ShareLink(item: url) {
    Label("Share", systemImage: "link.icloud")
}

我們也可以附加 presentationDetents 修飾符,來控制分享選單的大小:

ShareLink(item: url) {
    Label("Share", systemImage: "link.icloud")
}
.presentationDetents([.medium, .large])

iPadOS 的 Table

Apple 為 iPadOS 引入了新的 Table container,讓我們可以更容易地以表格形式呈現數據。以下的範例程式碼是一個包含 3 列的表格:

struct TableViewDemo: View {

    private let members: [Staff] = [
        .init(name: "Vanessa Ramos", position: "Software Engineer", phone: "2349-233-323"),
        .init(name: "Margarita Vicente", position: "Senior Software Engineer", phone: "2332-333-423"),
        .init(name: "Yara Hale", position: "Development Manager", phone: "2532-293-623"),
        .init(name: "Carlo Tyson", position: "Business Analyst", phone: "2399-633-899"),
        .init(name: "Ashwin Denton", position: "Software Engineer", phone: "2741-333-623")
    ]

    var body: some View {
        Table(members) {
            TableColumn("Name", value: \.name)
            TableColumn("Position", value: \.position)
            TableColumn("Phone", value: \.phone)
        }
    }
}

我們可以從一組數據(例如:一個 Staff 的陣列)建立一個 Table。我們可以利用 TableColumn,指定每一列的名稱和數值。

Table 在 iPadOS 和 macOS 都適用。同一個列表可以在 iOS 上自動呈現,但它只會顯示第一列。

可擴展的 Text Field

TextField 在 iOS 16 可以說是大大改善了。我們現在可以使用 axis 參數,去告訴 iOS 應否擴展 Text Field。來看看以下例子:

Form {
    Section("Comment") {
        TextField("Please type your feedback here", text: $inputText, axis: .vertical)
            .lineLimit(5)
    } 
}

lineLimit 修飾符指定了最大行數。上面的程式碼會在一開始呈現一個單行的 Text Field,當我們輸入時,它就會自動擴展,但將其大小會被限制為 5 行。

我們可以這樣在 lineLimit 修飾符中指定一個範圍,來更改 Text Field 一開始的大小:

Form {
    Section("Comment") {
        TextField("Please type your feedback here", text: $inputText, axis: .vertical)
            .lineLimit(3...5)
    } 
}

在這個情況下,iOS 就會預設顯示一個 3 行的 Text Field。

Gauge

SwiftUI 推出了一個新的視圖 Gauge,用來顯示進度條,最簡單的使用方法是這樣的:

struct GaugeViewDemo: View {
    @State private var progress = 0.5

    var body: some View {
        Gauge(value: progress) {
            Text("Upload Status")
        }
    }
}

在這個最基本的形式中,Gauge 的預設範圍是 0 到 1。如果我們將 value 參數設置為 0.5,SwiftUI 就會呈現一個進度條,指示任務已完成了 50%。

或者,我們可以為 current value、minimum value 和 maximum 設置標籤:

Gauge(value: progress) {
    Text("Upload Status")
} currentValueLabel: {
    Text(progress.formatted(.percent))
} minimumValueLabel: {
    Text(0.formatted(.percent))
} maximumValueLabel: {
    Text(100.formatted(.percent))
}

如果不想使用預設範圍,我們也可以如此指定客製化的範圍:

Gauge(value: progress, in: 0...100) {
  .
  .
  .
}

Gauge 視圖提供了不同的樣式,讓我們可以客製化自己的進度條。除了上圖直線樣式的進度條外,我們讓可以附加 gaugeStyle 修飾符來客製化樣式:

ViewThatFits

SwiftUI 另外一個新功能 ViewThatFits 十分有用,可以讓開發者建立更有彈性的 UI layout。這是一個特殊型別的視圖,用來評估可用空間,並在顯示最適合的視圖。

讓我們看看以下的例子。我們用了 ViewThatFits 來定義 Button Group 兩種可用的 layout:

struct ButtonGroupView: View {
    var body: some View {
        ViewThatFits {
            VStack {
                Button(action: {}) {
                    Text("Buy")
                        .frame(maxWidth: .infinity)
                        .padding()
                }
                .buttonStyle(.borderedProminent)
                .padding(.horizontal)

                Button(action: {}) {
                    Text("Cancel")
                        .frame(maxWidth: .infinity)
                        .padding()
                }
                .tint(.gray)
                .buttonStyle(.borderedProminent)
                .padding(.horizontal)
            }
            .frame(maxHeight: 200)


            HStack {
                Button(action: {}) {
                    Text("Buy")
                        .frame(maxWidth: .infinity)
                        .padding()
                }
                .buttonStyle(.borderedProminent)
                .padding(.leading)

                Button(action: {}) {
                    Text("Cancel")
                        .frame(maxWidth: .infinity)
                        .padding()
                }
                .tint(.gray)
                .buttonStyle(.borderedProminent)
                .padding(.trailing)
            }
            .frame(maxHeight: 100)

        }
    }
}

一個 Button Group 是使用 VStack 視圖垂直對齊的,而另一個 Button Group 則是水平對齊的。垂直的 Group maxHeight 為 200,而水平的 Group 的 maxHeight 則是 100

ViewThatFits 就會評估特定空間的高度,並在瑩幕上呈現最適合的視圖。假設我們把幀高度 (frame height) 設置為 100

ButtonGroupView()
    .frame(height: 100)

ViewThatFits 就會決定這個情況比較適合呈現水平對齊的 Button Group。假設我們把框架的高度更改為 150ViewThatFits 視圖就會顯示垂直的 Button Group。

Gradient 和 Shadow

新版本的 SwiftUI 讓我們可以簡單地添加線性漸變 (linear gradient)。我們只需要把 gradient 修佈符添加到 Color,SwiftUI 就會自動產生漸變。看看以下的例子:

Image(systemName: "trash")
    .frame(width: 100, height: 100)
    .background(in: Rectangle())
    .backgroundStyle(.purple.gradient)
    .foregroundStyle(.white.shadow(.drop(radius: 1, y: 3.0)))
    .font(.system(size: 50))

我們也可以使用 shadow 修佈符來添加陰影效果。以下的範例程式碼就可以添加 drop shadow 效果:

.foregroundStyle(.white.shadow(.drop(radius: 1, y: 3.0)))

Grid API

SwiftUI 4.0 推出了一個新的 Grid API,讓我們建立Grid layout。當然,我們也可以使用 VStack 和 HStact 來製作 Grid layout,不過 Grid 視圖就可以簡化製作過程。

我們可以這樣編寫程式碼,來構建一個 2x2 的 Grid:

Grid {
    GridRow {
        IconView(systemName: "trash")
        IconView(systemName: "trash")
    }

    GridRow {
        IconView(systemName: "trash")
        IconView(systemName: "trash")
    }
}

在 Grid 視圖中,我們會有一系列嵌套著 Grid Cell 的 GridRow

比如說,我們想把第二行的兩列合併,並顯示一個圖標視圖。我們可以附加 gridCellColumns 修飾符,並把數值設置為 2

Grid {
    GridRow {
        IconView(systemName: "trash")
        IconView(systemName: "trash")
    }

    GridRow {
        IconView(systemName: "trash")
            .gridCellColumns(2)
    }
}

我們也可以嵌套 Grid 視圖,來組成更複雜的 layout:

AnyLayout 和 Layout 協定

新版本的 SwiftUI 推出了 AnyLayout 和 Layout 協定,讓開發者可以建立客製化和更複雜的 layout。AnyLayout 是 layout 協定的類型擦除實例 (type-erased instance)。我們可以使用 AnyLayout 創建一個 dynamic layout,來回應使用者交互 (users' interactions) 或環境變化。

例如,我們的 App 一開始時使用 VStack 垂直排列兩個圖像,在使用者點擊堆疊視圖時,就會變成水平堆疊。我們可以如此使用 AnyLayout 來實作:

struct AnyLayoutDemo: View {

    @State private var changeLayout = false

    var body: some View {
        let layout = changeLayout ? AnyLayout(HStack(spacing: 0)) : AnyLayout(VStack(spacing: 0))


        layout {
            Image("macbook-1")
                .resizable()
                .scaledToFill()
                .frame(maxWidth: 300, maxHeight: 200)
                .clipped()

            Image("macbook-2")
                .resizable()
                .scaledToFill()
                .frame(maxWidth: 300, maxHeight: 200)
                .clipped()

        }
        .animation(.default, value: changeLayout)
        .onTapGesture {
            changeLayout.toggle()
        }

    }
}

我們可以定義一個 layout 變數,來保存 AnyLayout 的實例。如此一來,layout 就會根據 changeLayout 的數值改變為水平或垂直 layout。

另外,我們也可以附加 animation 到 layout,來動畫化 layout 的轉換。

這個範例讓使用者點擊堆疊視圖來改變 layout。在其他 App 中,我們可能想 layout 根據設備的方向和螢幕尺寸而更改。在這種情況下,我們可以使用 .horizo​​ntalSizeClass 變數來偵測方向改變:

@Environment(\.horizontalSizeClass) var horizontalSizeClass

然後,我們就可以這樣更新 layout 變數:

let layout = horizontalSizeClass == .regular ? AnyLayout(HStack(spacing: 0)) : AnyLayout(VStack(spacing: 0))

舉個例子,如果我們將 iPhone 13 Pro Max 轉為橫向,layout 就會變成水平堆疊視圖。

在大多數情況下,我們可以使用 SwiftUI 內建的 layout container(例如 HStack 和 VStack)來組合 layout。那如果這些 layout container 不足以讓我們製作需要的 layout 類型怎麼辦?iOS 16 引入的 Layout 協定就可以讓我們定義自己的客製化 layout。這個課題就更加複雜了,因此我們會再寫一篇教學文章,深入了解這個新的協定。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值