inSmartBank

B/43を運営する株式会社スマートバンクのメンバーによるブログです

SwiftUIでTouch Targetのサイズを意識する

こんにちは、スマートバンクでアプリエンジニアをしているロクネムです。

みなさんはiOSアプリの開発を行う上で、ボタン等のタッチ可能なコンポーネントの”Touch Targetのサイズ”を意識していますか?

小さすぎるTouch Targetは、指で操作するiOSアプリにおいてユーザーのアクセシビリティを低下させてしまいます。

Human Interface Guidelinesにおいても、タッチ可能な領域を最低でも44 x 44pt以上は確保するように記されています。

この記事では、SwiftUIでiOSアプリを開発する上で、どのように十分なTouch Targetを確保するかについて、弊社の開発しているB/43という家計簿プリカアプリを例にご紹介します。

Button

まずは1番わかりやすいButtonにおけるTouch Targetの定義について見ていきます。

44 × 44pt以上のTouch TargetとなるようにButtonのサイズを確保してあげればよいだけなので、 frame(minWidth:minHeight:) を指定します。

struct AccentButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .foregroundStyle(Color(.white))
            .font(.body)
            .padding(.horizontal, 16)
            // ✅ .frame(minHeight:) で44ptの高さを確保
            .frame(minHeight: 44) 
            .background(Color.accentColor)
            .opacity(configuration.isPressed ? 0.2 : 1.0)
            .clipShape(Capsule())
    }
}

しかし、ユーザーへ与えるアクションの優先度を表現する上で、どうしても44 × 44pt未満の大きさでButtonを表示したい場合があります。

この場合、見た目上の大きさは44 × 44pt未満で定義しつつ、”見えないTouch Target” を44 × 44pt以上で確保すると良いでしょう。

struct AccentButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .foregroundStyle(Color(.white))
            .font(.body)
            .padding(.horizontal, 16)
            // ✅ Buttonの見た目上の高さを28pt以上確保
            .frame(minHeight: 28) 
            .background(Color.accentColor)
            .opacity(configuration.isPressed ? 0.2 : 1.0)
            .clipShape(Capsule())
            // ✅ "見えないTouch Target"として44pt以上の高さを確保
            .frame(minHeight: 44) 
    }
}

レイアウトを組む上では当然この“見えないTouch Target”を考慮する必要があり、他コンポーネントと重ならないように注意する必要があります。 Figmaでデザインする段階で"見えないTouch Target"を考慮してコンポーネントを設計できていると良いでしょう。

B/43のFigmaでは、Buttonコンポーネントにて"見えないTouch Target"を考慮した余白が表現されています。

テキスト内のリンク

続いては、テキスト内のリンクにおけるTouch Targetです。

テキストの一部分のみがタッチ可能であるため、Buttonのように"見えないTouch Target"を確保することは難しいです。

そこでB/43では、リンク部分に限らずすべてのテキストをタッチ可能な領域として確保するようにしています。

その際に、もしテキストが一行しかない場合でも44 x 44pt以上のTouch Targetが確保されるように、 frame(minHeight:) を指定しています。

struct Terms: View {
    ...
    var body: some View {
        Button(action: {...}) {
            // ✅ リンク部分に限らずすべてのテキストをButtonのlabelとする
            Text(makeTermsAttributedString())
                .foregroundStyle(Color(.label))
                .font(.caption)
                // ✅ 一行しかない場合でも44pt以上の高さを確保
                .frame(minHeight: 44) 
        }
    }

    func makeTermsAttributedString() -> AttributedString {
        var attributedString = AttributedString("申し込む場合は利用条件をご確認ください")
        let range = attributedString.range(of: "利用条件")!
        attributedString[range].foregroundColor = .accentColor
        attributedString[range].underlineStyle = .single
        return attributedString
    }
}

ちなみにこちらの実装は、Apple公式のPodcastアプリの挙動を参考にしました。

リンク以外のテキストもタップ可能で、全体がハイライトされる

💡 リンクが2箇所以上存在する場合は?

そもそもスマートバンクでは、リンクはTouch Targetの確保が難しいため、ボタンなどによる代替を考えながらできるだけ使わないようにデザインすることを心がけております。それでもリンクを使わざるを得ないケースにおいては上述のようにテキスト全体にTouch Targetを適用することでアクセシビリティを担保しています。2箇所以上リンクが存在する場合においても、情報設計を見直して他コンポーネントで表現できないかを検討するのが良いでしょう。

TextField

最後に、TextFieldにおけるTouch Targetです。

これまでと同様に frame(minHeight:) を指定して、44pt以上の高さを確保すると良さそうに思えます。

TextField(
    "Placeholder",
    text: ...
)
.textFieldStyle(.roundedBorder)
.frame(minHeight: 44) // 👀 minHeightで44ptの高さを確保すれば良さそうだが...?

ですが、SwiftUIのTextFieldの高さは font Modifierで指定されたフォントサイズに応じて動的に決定されるようになっており、frameで指定した44ptの高さは確保されません。

background で適当な色を指定してみるとframeでの高さ指定はTextFieldの外側のViewに対して指定されていることがわかりやすいです。

TextField(
    "Placeholder",
    text: ...
)
.textFieldStyle(.roundedBorder)
.frame(minHeight: 44) 
// 👀 backgroundでColorを指定してViewの描画領域を確認
.background(Color.green) 

当然この緑の領域にはタッチ判定は設定されていないため、TextFieldのTouch Targetは44ptの高さを満たせていません。

B/43では、frame(minHeight:)をTextFieldに指定しつつ、 onTapGesture でタッチイベントを拾い、 focused へ渡す値を更新するようにしています。

struct B43TextField: View {
    @FocusState private var isFocused
    ...
    var body: some View {
        TextField(
            ...,
            text: ...
        )
        .padding(.horizontal, 12)
        .frame(minHeight: 44)
        .background(Color(.systemGray4))
        .cornerRadius(8)
        // ✅ `isFocused` がtrueのときにフォーカスを当てる
        .focused($isFocused) 
        // ✅ `.frame(minHeight:)` で拡張したViewのタッチイベントを取る
        .onTapGesture { 
            isFocused = true
        }
    }
}

このようにSwiftUIのTextFieldにおいては、 focused および onTapGesture を活用してTouch Targetを確保することが可能です。

まとめ

以上をまとめると、SwiftUIにおいてTouch Targetのサイズを意識する際には以下の点が重要です。

  • タッチ可能なコンポーネントのサイズが44pt x 44pt未満の場合は “見えないTouch Target” を設定する
  • “見えないTouch Target” はデザイン時点で考慮できるように設計する
  • テキスト内のリンクは該当テキスト以外を含めてTouch Targetを設定すると十分な領域を確保することができる
  • TextFieldの場合は拡張されたViewに対してonTapGestureを仕込んでTextFieldへフォーカスさせる必要がある

本記事が、みなさんがSwiftUIでTouch Targetのサイズを意識するきっかけになれば幸いです。


スマートバンクでは一緒に B/43 を作り上げていくメンバーを募集しています!カジュアル面談も受け付けていますので、お気軽にご応募ください 🙌 smartbank.co.jp

We create the new normal of easy budgeting, easy banking, and easy living.
In this blog, engineers, product managers, designers, business development, legal, CS, and other members will share their insights.