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 中文版
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。