SwiftUI

SwiftUI 是 2019-06-03 (iOS 13) 取代 UIKit 的 UI 框架,擁有以下特性

語法核心

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

UIKit 轉換協定

ObjectProtocol
UIViewUIViewRepresentable
UIViewControllerUIViewControllerRepresentable
NSViewNSViewRepresentable
NSViewControllerNSViewControllerRepresentable
WKInterfaceObjectWKInterfaceObjectRepresentable

經典範例 : 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

狀態管理

@State

import SwiftUI

struct CounterView: View {
    @State private var count = 0  // 屬性包裝:狀態變數

    var body: some View {
        VStack {
            Text("計數:\(count)")
            Button("增加") {
                count += 1  // 修改 @State 變數,UI 會同時更新
            }
        }
    }
}

@Binding

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

class UserSettings: ObservableObject {
    @Published var username: String = "Guest" // 添加發送者
}

@StateObject

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

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)
    }
}

環境變數

@Environment

可以透過專門語法取得

@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
struct MyCustomKey: EnvironmentKey {
    static let defaultValue: String = "Default Value"
}

extension EnvironmentValues {
    var myCustomValue: String {
        get { self[MyCustomKey.self] }
        set { self[MyCustomKey.self] = newValue }
    }
}
設定 @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

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

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

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

struct SettingsView: View {
    @AppStorage("username") private var username = "Swift"

    var body: some View {
        VStack {
            Text("使用者名稱: \(username)")
            TextField("輸入名稱", text: $username)
        }
    }
}

@SceneStorage

struct NotesView: View {
    @SceneStorage("draft") private var draft = ""

    var body: some View {
        TextEditor(text: $draft)
    }
}

索引