SwiftUI
SwiftUI 是 2019-06-03 (iOS 13) 取代 UIKit 的 UI 框架,擁有以下特性
- Declarative Syntax (宣告式框架)
- 開發者描述畫面,而系統負責 UI 更新
- Preview
- 開發者透過 Coding (程式碼) 編輯 UI,會有 Preview (預覽圖) 同步顯示
- DSL (領域專用語言)
- Domain-Specific Language
- 專門描述 UI 的結構,與 Swift 語法感受不同
- 跨平台
- iOS、MacOS、WatchOS 等都能使用 SwiftUI
- 但細節依然要自行調整
- 相容 UIKit
- UIKit 元件只要遵循特定協議,就能在 SwiftUI 中使用
語法核心
- SwiftUICore
- 負責以多種 View 協議組合 UI Layout (佈局)
- Property Wrappers (屬性包裝器)
- 一種將程式碼封裝,並且重複套用的封裝機制
- 負責Layout 以外的邏輯 (狀態管理、環境變數、手勢監聽等)
- 如 @State 以
@開頭的前綴屬性
import SwiftUI
struct ContentView: View {
@Environment(\.colorScheme) var colorScheme // 取得顏色模式的屬性包裝器
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundStyle(.tint)
Text("Hello, world!")
.foregroundColor(.primary)
Text("當前模式: \(colorScheme == .dark ? "深色" : "淺色")")
}
.padding()
}
}
#Preview {
ContentView()
}
SwiftUICore
- 負責以多種 View 協議組合 UI Layout (佈局)
- 組合元件可以區分為
- Layout Containers (佈局容器)
- 用來組織 UI,本身沒有視覺效果的容器
- 如 VStack、HStack 等佈局用容器
- View Components (UI 元件)
- 顯示在畫面上的視覺元件
- 如文字、圖片、按鈕等
- Modifiers (修飾詞)
- 用來改變 View 的外觀或行為
- 如邊距、尺寸、背景顏色等
- Layout Containers (佈局容器)
UIKit 轉換協定
- SwiftUI 可以透過特定協議來使用 UIKit 元件
| Object | Protocol |
|---|---|
| UIView | UIViewRepresentable |
| UIViewController | UIViewControllerRepresentable |
| NSView | NSViewRepresentable |
| NSViewController | NSViewControllerRepresentable |
| WKInterfaceObject | WKInterfaceObjectRepresentable |
經典範例 : SwiftUI 使用 WKWebView
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let url: URL
func makeUIView(context: Context) -> some UIView {
let webView = WKWebView()
webView.load(URLRequest(url: url))
return webView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
}
}
Property Wrappers
- 一種將程式碼封裝,並且重複套用的封裝機制
- SwiftUI 的屬性包裝器主要用來
- 狀態管理
- 在數值變動時做 UI 的更新
- 手勢監聽
- 追蹤拖曳、縮放等短暫的數值狀態
- 環境變數
- 用來取得系統層級的 function、參數設定
- 狀態管理
狀態管理
- 狀態指的是 UI 上的各種數值
- 當數值變更時 SwiftUI 會自動響應 UI 更新
- Reactive (響應式)
- 當值改變時會通知訂閱對象
- Diffing (差異機制)
- 當狀態變化時,系統會檢查 View 的樹狀結構,並且更新有差異的部分
- 對於
List、ForEach等動態列表會透過id判斷是否改變
- Reactive (響應式)
@State
- 用來管理 單個 View 狀態
- 當 @State 值改變時,畫面會自動更新
import SwiftUI
struct CounterView: View {
@State private var count = 0 // 屬性包裝:狀態變數
var body: some View {
VStack {
Text("計數:\(count)")
Button("增加") {
count += 1 // 修改 @State 變數,UI 會同時更新
}
}
}
}
@Binding
- SwiftUI 用來宣告 View 的參數
- 代表狀態由父層管理,父層需建立 @State 與其綁定
- @State 變數傳入時需添加
$前綴,轉換成Binding<value>屬性
struct ParentView: View {
@State private var isOn = false // 父層的狀態變數
var body: some View {
VStack {
ChildView(isOn: $isOn) // 轉換成 Binding <Bool> 傳遞給子 View
}
}
}
struct ChildView: View {
@Binding var isOn: Bool // 透過 Binding 綁定父層的狀態
var body: some View {
Button(isOn ? "關閉" : "開啟") {
isOn.toggle() // 修改 isOn ,UI 會同步更新
}
}
}
@Published
- ObservableObject
- 透過 @Published 成為 Publisher (發送者)
- 有變更時發送
objectWillChange.send
- SwiftUI
- 監聽
objectWillChange.send成為 Subscriber (訂閱者) - 透過 @StateObject、@ObservedObject、@EnvironmentObject 添加訂閱
- 收到通知時處理 UI 更新
- 監聽
class UserSettings: ObservableObject {
@Published var username: String = "Guest" // 添加發送者
}
@StateObject
- SwiftUI 用來管理遵循
ObservableObject協議的物件 - 此物件會在 View 初始化時建立
- 由 View 本身擁有與管理
class Counter: ObservableObject {
@Published var value = 0
}
struct CounterView: View {
@StateObject private var counter = Counter()
var body: some View {
VStack {
Text("計數: \(counter.value)")
Button("增加") { counter.value += 1 }
}
}
}
@ObservedObject
- SwiftUI 用來宣告 View 參數屬於 ObservableObject 類型
class DataModel: ObservableObject {
@Published var name = "Some Name"
@Published var isEnabled = false
}
struct MyView: View {
@StateObject private var model = DataModel()
var body: some View {
Text(model.name)
MySubView(model: model)
}
}
struct MySubView: View {
@ObservedObject var model: DataModel
var body: some View {
Toggle("Enabled", isOn: $model.isEnabled)
}
}
環境變數
- SwiftUI 透過 @Environment 取得系統級的 function、參數設定
- 開發者也可以自行新增 @Environment 變數
- 對於
ObservableObject類型則使用 @EnvironmentObject
@Environment
- 系統預設的功能常見的如下
- UI 設定
- colorScheme
- colorSchemeContrast
- displayScale
- sizeCateGory
- legibilityWeight
- 無障礙設定
- accessibilityReduceMotion
- accessibilityReduceTransparency
- accessibilityDifferentiateWithoutColor
- 系統功能
- openURL
- undoManager
- UI 設定
可以透過專門語法取得
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
@Environment(\.openURL) var openURL
var body: some View {
Text("當前模式: \(colorScheme == .dark ? "深色" : "淺色")")
Button("關閉") {
dismiss()
}
Button("打開 Apple 網站") {
openURL(URL(string: "https://apple.com")!)
}
}
添加 @Environment
- 實作
EnvironmentKey協議 - 擴展
EnvironmentValues如系統環境變數一樣使用
struct MyCustomKey: EnvironmentKey {
static let defaultValue: String = "Default Value"
}
extension EnvironmentValues {
var myCustomValue: String {
get { self[MyCustomKey.self] }
set { self[MyCustomKey.self] = newValue }
}
}
設定 @Environment
- @Environment 可隨時取得,不需要額外傳入
- 也可透過
.environment額外設定 (可選)
struct ContentView: View {
var body: some View {
MyView()
.environment(\.myCustomValue, "Hello, SwiftUI!") // 設定環境變數值
}
}
struct MyView: View {
@Environment(\.myCustomValue) var customValue
var body: some View {
Text("環境變數: \(customValue)")
}
}
@EnvironmentObject
- SwiftUI 讓開發者自訂義的全域變數
- Root View 透過
.environmentObject將ObservableObject添加為全域變數 - Root 的所有子 View 可透過 @EnvironmentObject 取得
class UserSettings: ObservableObject {
@Published var username = "Swift"
}
@main
struct MyApp: App {
@StateObject private var settings = UserSettings()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(settings) // 傳入全局物件
}
}
}
struct ContentView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
Text("使用者名稱: \(settings.username)")
}
}
焦點追蹤
@FocusState
- iOS 15 添加的屬性包裝器
- 搭配 TextField 管理輸入焦點
enum Field {
case username, password
}
struct MultiFocusView: View {
@State private var username = ""
@State private var password = ""
@FocusState private var focusedField: Field?
var body: some View {
VStack {
TextField("帳號", text: $username)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .username)
SecureField("密碼", text: $password)
.textFieldStyle(.roundedBorder)
.focused($focusedField, equals: .password)
Button("下一步") {
if focusedField == .username {
focusedField = .password
} else {
focusedField = nil // 失焦
}
}
}
.padding()
}
}
手勢追蹤
@GestureState
- SwiftUI 用來處理手勢的屬性包裝器
- 手勢結束後狀態會自動重置
- 用來追蹤拖曳、縮放等短暫狀態
struct DragGestureExample: View {
@GestureState private var dragOffset: CGSize = .zero
var body: some View {
Circle()
.fill(Color.blue)
.frame(width: 100, height: 100)
.offset(dragOffset) // 使用手勢狀態
.gesture(
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation // 更新拖曳距離
}
)
}
}
資料儲存
@AppStorage
- 資料會儲存到 UserDefaults 永久存在
struct SettingsView: View {
@AppStorage("username") private var username = "Swift"
var body: some View {
VStack {
Text("使用者名稱: \(username)")
TextField("輸入名稱", text: $username)
}
}
}
@SceneStorage
- 從開啟 App 到結束代表一個 Scene
- @SceneStorage 在 App 重啟時自動清除
struct NotesView: View {
@SceneStorage("draft") private var draft = ""
var body: some View {
TextEditor(text: $draft)
}
}