[SwiftUI] FocusState 使い方あれこれ

SwiftUI2021

     
⌛️ 3 min.

SwiftUI で TextField 等のフォーカスを管理する FocusState をいろいろと確認してみます。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS15.1.1 Sequoia
  • Xcode 16.2
  • iOS 18.2RC
  • Swift 5.9

FocusState

FocusState は、フォーカス可能な View の制御を行うために macOS12/ iOS15 で導入されました。

SwiftUI2021 [SwiftUI] @FocusState の使い方


参考
FocusStateApple Developer Documentation

もちろん 制御対象の View は、フォーカス可能であることが必要です。

フォーカス可能にするための View Modifier も用意されています。

参考
focusable(_:)Apple Developer Documentation

1つの View の Focus を On/Off する

シンプルな使い方の1つに、フォーカスの有無を Bool でもつ方法があります。

以下のサンプルは、TextField にフォーカスがあるかどうかを FocusState で判定でき、その変数に設定することでフォーカスを制御することもできます。

import SwiftUI

struct ContentView: View {
    @State private var text = "Hello world"
    @FocusState private var fieldFocus: Bool
    var body: some View {
        VStack {
            TextField("TextField", text: $text)
                .focused($fieldFocus)

            Text("Current fieldFocus value: (fieldFocus.description)")
            Button(action: {
                fieldFocus.toggle()
            }, label: { Text("toggle fieldFocus") })
        }
        .onAppear {
            fieldFocus = true
        }
    }
}

@FocusState 変数に初期値を設定することはできませんが、上記のように.onAppear でその値を設定することができます。

上記では true をセットしているので、表示されたときに TextField がフォーカスを持ちます。

Button を押下するごとに FocusState である fieldFocus 変数を toggle しているので、フォーカスが切り替わっているのがわかります。

複数の View の Focus を制御する

複数のフォーカスできる View があるときに、それらを1つづつ Focus 管理すると手間です。

フォーカスは 高々 1つの View しか持つことができないものです。
ですので、複数の Bool 変数で管理するのは そこに重複した情報が含まれていると見ることもできます。

FocusState の定義をよく見ると Bool だけでなく Hashable である型であればその情報を Value として持つようになっていることがわかります。(実際には、Optionalとして持ちます。)

こちら側(?) の FocusState を使うと以下のように複数の TextField のいずれがフォーカスを持っているのか/いずれにフォーカスをセットするのかを制御できるようになります。

import SwiftUI

struct ContentView: View {
    @State private var text1 = "Hello world"
    @State private var text2 = "こんにちわ 世界"

    enum FocusField: String, RawRepresentable, CustomStringConvertible {
        case field1, field2

        func next() -> FocusField {
            switch self {
            case .field1: return .field2
            case .field2: return .field1
            }
        }

        var description: String {
            return rawValue
        }
    }

    @FocusState private var fieldFocus: FocusField?
    var body: some View {
        VStack {
            TextField("TextField1", text: $text1)
                .focused($fieldFocus, equals: .field1)
            TextField("TextField2", text: $text2)
                .focused($fieldFocus, equals: .field2)

            Text("Current fieldFocus value: (fieldFocus?.description ?? "NoFocus" )")
            Button(action: {
                fieldFocus = fieldFocus?.next()
            }, label: { Text("next fieldFocus") })
        }
        .onAppear {
            fieldFocus = .field2
        }
        .padding()
    }
}

onAppear で .field2 とセットしているので初期のフォーカスは、2つ目のTextField になります。
また、Button で FocusField の値を交互にセットしているので、フォーカスは交互に切り替わっています。

なお、以下のように FocusState は、プログラム的な変更だけではなく、ユーザー操作によるフォーカス変更にも追従して変更されています。

View 階層毎に、Focus を管理する

フォーカスを持ちえるすべての View が1つの View に含まれているとシンプルですが、実際には別階層であることもあります。

そのような場合の FocusState の振る舞いを確認してみます。

以下の例では、View が子 View をもち、親View, 子View のそれぞれで FocusState でフォーカスを管理しています。

当たり前ですが、それぞれの View で きちんと FocusState が制御されます。

import SwiftUI

struct ContentView: View {
    @State private var text1 = "Hello world"
    @State private var text2 = "こんにちわ 世界"

    enum FocusField: String, RawRepresentable, CustomStringConvertible {
        case field1, field2, subview

        func next() -> FocusField {
            switch self {
            case .field1: return .field2
            case .field2: return .subview
            case .subview: return .field1

            }
        }

        var description: String {
            return rawValue
        }
    }

    @FocusState private var fieldFocus: FocusField?
    var body: some View {
        VStack {
            TextField("TextField1", text: $text1)
                .focused($fieldFocus, equals: .field1)
            TextField("TextField2", text: $text2)
                .focused($fieldFocus, equals: .field2)

            Text("Current fieldFocus value: (fieldFocus?.description ?? "NoFocus" )")
            GroupBox("SubView", content: {
                SubView()
                    .focused($fieldFocus, equals: .subview)
            })
            Button(action: {
                fieldFocus = fieldFocus?.next()
            }, label: { Text("next fieldFocus") })
        }
        .onAppear {
            fieldFocus = .field2
        }
        .padding()
    }
}

struct SubView: View {
    @FocusState private var subviewFocus: Bool
    @State private var text = "SubView"
    var body: some View {
        VStack {
            TextField("SubView TextField", text: $text)
                .focused($subviewFocus)
            Text("subviewFocus: (subviewFocus.description)")
        }
    }
}

以下の動画を見るとわかりますが、ContentView の FocusState と SubView の FocusState はそれぞれの値を矛盾なく持っています。

具体的にはそれぞれ 以下のような値を持っていることがわかります。

  • Field1 or Field1 がフォーカスをもっているとき、fieldFocus は.field1 or field2 で、subviewFocus は false
  • SubViewTextField がフォーカスを持っているとき、subviewFocus は true で、fieldFocus は、.subview

View 階層に跨って、FocusState を使用する

基本的には、その View の中で フォーカスを管理するのがわかりやすいのですが、複雑性等で View を分割したい時があります。

そのようなときに、FocusState を 子View に渡すこともできます。

import SwiftUI

struct ContentView: View {
    @State private var text1 = "Hello world"
    @State private var text2 = "こんにちわ 世界"

    enum FocusField: String, RawRepresentable, CustomStringConvertible {
        case field1, field2, subview

        func next() -> FocusField {
            switch self {
            case .field1: return .field2
            case .field2: return .subview
            case .subview: return .field1

            }
        }

        var description: String {
            return rawValue
        }
    }

    @FocusState private var fieldFocus: FocusField?
    var body: some View {
        VStack {
            TextField("TextField1", text: $text1)
                .focused($fieldFocus, equals: .field1)
            TextField("TextField2", text: $text2)
                .focused($fieldFocus, equals: .field2)

            Text("Current fieldFocus value: (fieldFocus?.description ?? "NoFocus" )")
            GroupBox("SubView", content: {
                SubView(fieldFocus: $fieldFocus)
            })
            Button(action: {
                fieldFocus = fieldFocus?.next()
            }, label: { Text("next fieldFocus") })
        }
        .onAppear {
            fieldFocus = .field2
        }
        .padding()
    }
}

struct SubView: View {
    @FocusState.Binding var fieldFocus: ContentView.FocusField?
    @State private var text = "SubView"
    var body: some View {
        VStack {
            TextField("SubView TextField", text: $text)
                .focused($fieldFocus, equals: .subview)
        }
    }
}

FocusState の Binding を受け取る側は、@FocusState.Binding とするのがポイントです。

以下のような振る舞いになります。(外部から見ても違いは分かりにくいかもしれません・・・)

まとめ

FocusState を使用した フォーカス管理方法を確認しました。

FocusState を使用した フォーカス管理方法
  • Bool を使うと、該当 View がフォーカスされているか判定できる
  • Bool を使うと、該当 View をフォーカスするかを制御できる
  • enum (など Hashable) を使うと、複数の View のいずれかがフォーカスされているか判定できる
  • enum (など Hashable) を使うと、複数の View のどの View をフォーカスするか制御できる
  • FocusState.Binding を使用すると FocusState を渡すことができる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です