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 18 App 程式設計實戰心法》、《iOS 18 App程式設計進階攻略》以及《精通SwiftUI》。曾任職於HSBC, FedEx等跨國企業,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda業務,致力於iOS程式教學、產品設計及開發。你可以到推特與我聯絡。
評論
更多來自 AppCoda 中文版
如何在 SwiftUI App 中開發 Live Activities
SwiftUI 框架

如何在 SwiftUI App 中開發 Live Activities

Live Activities 首次於 iOS 16 推出,是 Apple 最令人興奮的更新之一,能讓 App 與使用者在即時互動上更有連結。它不再需要使用者不斷打開 App,Live Activities 可以讓資訊直接顯示在鎖定畫面和 Dynamic Island 上。
使用 Tool Calling 強化 Foundation Models 功能
AI

使用 Tool Calling 強化 Foundation Models 功能

在前幾篇教學中,我們介紹了 Foundation Models 在 iOS 26 中的運作方式,以及如何使用這個全新框架打造具備 AI 功能的應用。我們也介紹了 @Generable 巨集,它能輕鬆地將模型回應轉換為結構化的 Swift 類型。 現在,在這個 Foundation
活用 Foundation Models 的 @Generable 與 @Guide 製作測驗 App
AI

活用 Foundation Models 的 @Generable 與 @Guide 製作測驗 App

在前一篇教學中,我們介紹了 Foundation Models 框架,並示範了如何用它來進行基本的內容生成。那個過程相當簡單——你提供一個提示詞(prompt),等幾秒鐘,就能獲得自然語言的回應。在我們的範例中,我們建立了一個簡單的問答 App,讓使用者可以提問,App 則直接顯示生成的文字。 但如果回應變得更複雜——你需要把非結構化文字轉換為結構化的物件呢? 舉例來說,
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。