SwiftUI 框架

如何在 SwiftUI 使用@FocusState, @FocusedValue and @FocusedObject

在這篇教程中,我們將探討 SwiftUI 的「焦點」管理API的細節,讓你有能力創造出吸引人且互動的使用者體驗。具體來說,我們將深入探討關鍵屬性包裝器的使用,像是@FocusState、@FocusedValue 和@FocusObject。
如何在 SwiftUI 使用@FocusState, @FocusedValue and @FocusedObject
如何在 SwiftUI 使用@FocusState, @FocusedValue and @FocusedObject
In: SwiftUI 框架

在任何使用者介面中,「焦點(Focus)」在確定哪個元素接收下一個輸入的過程中起到了關鍵性的作用。SwiftUI 提供了一套強大的工具和視圖修飾器,使你能在你的應用程式中控制並管理「焦點」。通過使用這些修飾器,你可以指出哪些視圖有資格接收「焦點」,偵測哪個視圖目前擁有「焦點」,甚至可以程式化地控制「焦點」狀態。

在這篇教程中,我們將探討 SwiftUI 的「焦點」管理API的細節,讓你有能力創造出吸引人且互動的使用者體驗。具體來說,我們將深入探討關鍵屬性包裝器的使用,像是@FocusState@FocusedValue@FocusObject

處理@FocusState

首先,讓我們開始講解@FocusState。有了這個包裹器(Property Wrapper),開發者可以輕鬆地管理特定視圖的「焦點」並追蹤視圖目前是否擁有「焦點」。為了觀察和更新視圖的「焦點」狀態,我們通常使用focused修改器與@FocusState屬性包裹器一起使用。利用這些API,你將獲得對SwiftUI視圖的「焦點」行為的精確控制。

為了讓你更清楚地理解focused@FocusState如何共同作用,讓我們來看一個例子。

struct FocusStateDemoView: View {
    
    @State private var comment: String = ""

    @FocusState private var isCommentFocused: Bool
    
    var body: some View {
        VStack {
            Text("👋Help us improve")
                .font(.system(.largeTitle, design: .rounded, weight: .black))
            
            TextField("Any comment?", text: $comment)
                .padding()
                .border(.gray, width: 1)
                .focused($isCommentFocused)
            
            Button("Submit") {
                isCommentFocused = false
            }
            .controlSize(.extraLarge)
            .buttonStyle(.borderedProminent)
            
        }
        .padding()
        .onChange(of: isCommentFocused) { oldValue, newValue in
            print(newValue ? "Focused" : "Not focused")
        }
    }
}

在上述的程式碼中,我們創建了一個簡單的表單,其中有一個“評論”文字欄位。我們有一個名為isCommentFocused的屬性,這個屬性用@FocusState註釋來追蹤文字欄位的「焦點」狀態。對於“評論”欄位,我們加上了focused修飾器並綁定到isCommentFocused屬性。

透過這樣做,SwiftUI自動監控“評論”欄位的「焦點」狀態。當欄位進入「焦點」時,isCommentFocused的值將設為真實。相反地,當欄位失去「焦點」時,該值將更新為假。你也可以通過更新其值來程式化地控制文字欄位的「焦點」。例如,當提交按鈕被輕觸時,我們通過設置isCommentFocusedfalse來重設「焦點」。

onChange修飾器被用來揭示「焦點」狀態的變化。它監控isCommentFocused變數並打印其值。

當你在預覽窗格中測試應用程式示範時,控制台應該在“評論”欄位的狀態為「焦點」時顯示“Focused”訊息。此外,輕觸提交按鈕應該會觸發“Not focused”的訊息。

swiftui-focusstate-demo

使用Enum來管理「焦點」狀態

當你只需要追蹤單個文字欄位的「焦點」狀態時,使用布爾變數效果出色。然而,當你需要同時處理多個文字欄位的「焦點」狀態時,這種方式可能變得繁瑣。

你可以定義一種符合Hashable的enum類型,而非布爾變數,來管理多個文字欄位(或SwiftUI視圖)的「焦點」狀態。

讓我們繼續以同樣的應用程式示範來說明這種技術。我們將在表單視圖中新增兩個文字欄位,包括名稱和電子郵件。這是修改後的程式:

struct FocusStateDemoView: View {
    
    enum Field: Hashable {
        case name
        case email
        case comment
    }
    
    @State private var name: String = ""
    @State private var email: String = ""
    @State private var comment: String = ""

    @FocusState private var selectedField: Field?
    
    var body: some View {
        VStack {
            Text("👋Help us improve")
                .font(.system(.largeTitle, design: .rounded, weight: .black))
            
            TextField("Name", text: $name)
                .padding()
                .border(.gray, width: 1)
                .focused($selectedField, equals: .name)
            
            TextField("Email", text: $email)
                .padding()
                .border(.gray, width: 1)
                .focused($selectedField, equals: .email)
            
            TextField("Any comment?", text: $comment)
                .padding()
                .border(.gray, width: 1)
                .focused($selectedField, equals: .comment)
            
            Button("Submit") {
                selectedField = nil
            }
            .controlSize(.extraLarge)
            .buttonStyle(.borderedProminent)
            
        }
        .padding()
        .onChange(of: selectedField) { oldValue, newValue in
            print(newValue ?? "No field is selected")
        }
    }
}

為了有效地管理多個文字欄位的「焦點」,我們避免定義額外的 Boolean 變數,而是引入了一種名為Field的enum類型。這個enum符合Hashable協定,並定義了三種情況,每一種情況代表表單中的一個文字欄位。

利用這個enum,我們使用@FocusState屬性包裝器來宣告selectedField屬性。這個屬性讓我們能夠方便地追蹤目前擁有「焦點」的文字欄位。

為了建立連接,每個文字欄位都與focused修飾器相關,該修飾器使用對應的值來綁定到「焦點」狀態屬性。例如,當「焦點」移到“評論”欄位時,綁定會將綁定值設置為.comment

你現在可以測試程式碼的更改。當你輕觸任何欄位時,控制台將顯示相關文字欄位的名稱。然而,如果你輕觸提交按鈕,控制台將顯示 “No field is selected” 的訊息。

swiftui-focusstate-focused

你可以程式化地更改文字欄位的「焦點」。讓我們修改Submit按鈕的"行動"區塊,如下所示:

Button("Submit") {
    selectedField = .email
}

透過為提交按鈕將selectedField的值設為.email,當輕觸提交按鈕時,應用程式將自動將「焦點」移至電子郵件欄位。

使用 FocusedValue

現在你應該理解@FocusState的工作方式,我們接著來看看下一個屬性包裝器@FocusedValue。該屬性包裝器允許開發者監控當前擁有焦點的文字欄位(或其他可焦點的視圖)的值。

為了更好地了解使用方法,讓我們繼續在範例中進行操作。假設,我們想在表單下方添加一個預覽部分,以顯示用戶的評論,但我們只希望在評論欄位有焦點時,評論才可見。以下是預覽部分的程式碼範例:

struct CommentPreview: View {
    
    var body: some View {
        VStack {
            Text("")
        }
        .frame(minWidth: 0, maxWidth: .infinity)
        .frame(height: 100)
        .padding()
        .background(.yellow)
    }
}

我們將預覽部分放在Submit按鈕的正下方,如下所示:

struct FocusStateDemoView: View {
    
    ...
    
    var body: some View {
        VStack {
            
            .
            .
            .
            
            Button("Submit") {
                selectedField = nil
            }
            .controlSize(.extraLarge)
            .buttonStyle(.borderedProminent)
            
            Spacer()
            
            CommentPreview()
        }
        .padding()
        .onChange(of: selectedField) { oldValue, newValue in
            print(newValue ?? "No field is selected")
        }
    }
}

為了監控評論欄位的變化,我們首先創建一個符合FocusedValueKey協議的結構。在這個結構中,我們定義了要監視的值的類型。在這個例子中,評論的類型是String

struct CommentFocusedKey: FocusedValueKey {
    typealias Value = String
}

接下來,我們為FocusedValues提供了一個擴展,該擴展有一個計算屬性,該屬性使用新的鍵來獲取和設置值。

extension FocusedValues {
    var commentFocusedValue: CommentFocusedKey.Value? {
        get { self[CommentFocusedKey.self] }
        set { self[CommentFocusedKey.self] = newValue }
    }
}

一旦你設定好所有這些,你就可以將focusedValue修改器附加到“評論”文本欄位,並指定觀察評論的值。

TextField("Any comment?", text: $comment)
    .padding()
    .border(.gray, width: 1)
    .focused($selectedField, equals: .comment)
    .focusedValue(\.commentFocusedValue, comment)

現在回到CommentPreview結構,並使用@FocusedValue屬性包裝器聲明一個comment屬性:

struct CommentPreview: View {
    
    @FocusedValue(\.commentFocusedValue) var comment
    
    var body: some View {
        VStack {
            Text(comment ?? "Not focused")
        }
        .frame(minWidth: 0, maxWidth: .infinity)
        .frame(height: 100)
        .padding()
        .background(.yellow)
    }
}

我們使用 @FocusedValue 屬性包裝器來監控並在評論欄位處於焦點時檢索其最新的值。

現在,當你在評論欄位中輸入任何文字時,預覽區域應該會顯示相同的值。然而,當你離開評論欄位時,預覽區域將會顯示"Not focused"的訊息。

swiftui-focusstate-focusedvalue

使用 @FocusedObject

@FocusedValue用於監視值類型的變化。對於參考類型,你可以使用另一個名為@FocusedObject的屬性包裝器。假設,你希望在評論欄位上方的預覽區域顯示名稱和電子郵件欄位的內容。

為了做到這一點,你可以定義一個符合ObservableObject協議的類,像這樣:

class FormViewModel: ObservableObject {
    @Published var name: String = ""
    @Published var email: String = ""
    @Published var comment: String = ""
}

在表單視圖中,我們可以為視圖模型宣告一個狀態物件:

@StateObject private var viewModel: FormViewModel = FormViewModel()

要將可觀察對象與焦點關聯,我們將focusedObject修飾器附加到下面的文本字段:

TextField("Name", text: $viewModel.name)
    .padding()
    .border(.gray, width: 1)
    .focused($selectedField, equals: .name)
    .focusedObject(viewModel)

TextField("Email", text: $viewModel.email)
    .padding()
    .border(.gray, width: 1)
    .focused($selectedField, equals: .email)
    .focusedObject(viewModel)

TextField("Any comment?", text: $viewModel.comment)
    .padding()
    .border(.gray, width: 1)
    .focused($selectedField, equals: .comment)
    .focusedObject(viewModel)

對於 CommentPreview 結構,我們使用 @FocusedObject 屬性包裝器來獲取值的變化:

struct CommentPreview: View {
    
    @FocusedObject var viewModel: FormViewModel?
    
    var body: some View {
        VStack {
            Text(viewModel?.name ?? "Not focused")
            Text(viewModel?.email ?? "Not focused")
            Text(viewModel?.comment ?? "Not focused")
        }
        .frame(minWidth: 0, maxWidth: .infinity)
        .frame(height: 100)
        .padding()
        .background(.yellow)
    }
}

總結

這個教程解釋了如何使用 SwiftUI 的焦點管理 API,特別是 @FocusState@FocusedValue@FocusedObject。通過利用這些包裝器,你可以有效地監視焦點狀態的變化並訪問可獲取焦點的視圖的值。這些強大的工具使開發人員能夠在各種平台上提供增強的用戶體驗,包括 iOS,macOS 和 tvOS 應用程序。

我希望你喜歡這篇教程。如果你有任何問題,請在下面留言。

作者
Simon Ng
軟體工程師,AppCoda 創辦人。著有《iOS 17 App 程式設計實戰心法》、《iOS 17 App程式設計進階攻略》以及《精通SwiftUI》。曾任職於HSBC, FedEx等跨國企業,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda業務,致力於iOS程式教學、產品設計及開發。你可以到推特與我聯絡。
評論
更多來自 AppCoda 中文版
如何使用 Swift 整合 Google Gemini AI
SwiftUI 框架

如何使用 Swift 整合 Google Gemini AI

在即將到來的 WWDC,Apple 預計將會發佈一個本地端的大型語言模型 (LLM)。 接下來的 iOS SDK 版本將讓開發者更輕易地整合 AI 功能至他們的應用程式中。然而,當我們正在等待 Apple 推出自家的生成 AI 模型時,其他公司(如 OpenAI
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。