SwiftUIの@FocusStateを試す

UITextViewの設定がTextEditorに反映されるソースを求めて*1インターネットの海をさまよっていたときに出会ったproperty wrapper。
使ったことが無かったので試す。

開発環境

> xcodebuild -version
Xcode 13.1
Build version 13A1030d

記事中のスクリーンショット: iPhone 13 Pro Max / iOS15.0

FocusStateとは

A property wrapper type that can read and write a value that SwiftUI updates as the placement of focus within the scene changes.
https://developer.apple.com/documentation/swiftui/focusstate

シーン内のフォーカスの配置が変更された時にSwiftUIが更新する値を読み書きできるproperty wrapper。

focused(_:)

フォーカス状況の取得

import SwiftUI

struct ContentView: View {
    
    @FocusState private var addAttendeeIsFocused: Bool
    @State private var newAttendee: String = ""
    
    var body: some View {
        TextField("New Person", text: $newAttendee)
            .focused($addAttendeeIsFocused)

        Button("FOCUSED?") {
            // TextFieldがフォーカスされていればtrue
            print(addAttendeeIsFocused)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

WWDC2021:What's new in SwiftUI でも動画上で触れられている。

強制フォーカス

@FocusState が付与されているBool型変数をTextEditorにバインドさせることで、その変数をtrueにすれば強制的にTextEditorにフォーカスを当てることが可能。

f:id:tokizuoh:20220301223338g:plain
import SwiftUI

struct ContentView: View {
    
    @FocusState private var isFocused: Bool
    @State private var text: String = "これはサンプルテキストです"
    
    var body: some View {
        Button("GO!") {
            isFocused = true
        }
        
        TextEditor(text: $text)
            .frame(width: 250,
                   height: 250)
            .focused($isFocused)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

focused(_:equals:)

複数TextEditorのフォーカスを強制的に移動

ボタン押下でTextEditorのフォーカスを別のTextEditorに移す。

f:id:tokizuoh:20220301225422g:plain
import SwiftUI

struct ContentView: View {
    
    private enum Field: Int, Hashable {
        case first, second, third
    }
    
    @FocusState private var focusedField: Field?
    @State private var firstText: String = ""
    @State private var secondText: String = ""
    @State private var thirdText: String = ""
    
    var body: some View {
        ZStack(alignment: .center) {
            Color.yellow
                .ignoresSafeArea()
            VStack {
                TextEditor(text: $firstText)
                    .frame(width: 250,
                           height: 250)
                    .focused($focusedField,
                             equals: .first)
                
                TextEditor(text: $secondText)
                    .frame(width: 250,
                           height: 250)
                    .focused($focusedField,
                             equals: .second)
                
                TextEditor(text: $thirdText)
                    .frame(width: 250,
                           height: 250)
                    .focused($focusedField,
                             equals: .third)
                
                Button("CHANGE FOCUSED") {
                    guard let focusedField = focusedField else {
                        focusedField = .first
                        return
                    }
                    
                    switch focusedField {
                    case .first:
                        self.focusedField = .second
                    case .second:
                        self.focusedField = .third
                    case .third:
                        self.focusedField = .first
                    }
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

参考