[SwiftUI]Listのタップ範囲が狭い?余白もタップ可能にする方法
はじめに
添付のgif画像のようにListのテキストやアイコンしかタップできない場合、Userはタップ範囲が狭く、操作感を悪く感じます。

そのため、今回はListのタップ範囲に空白箇所も含めてタップできるように設定していきます。
改善のポイント
SwiftUIでは、HStackやSpacerなどの空白部分は見た目はあってもタップ判定がないことがあります。
そのため、次のような工夫を加える必要があります:
-
HStackとSpacer()でレイアウトを拡張 -
.contentShape(Rectangle())でタップ範囲を明示
改善前のコード
まず、Listの文字やアイコンのみしかタップできない状態のコードは下記のとおりです。
この状態だと、テキストやアイコン以外の余白はタップできません。
改善前のコード全体
import SwiftUI
struct ListView: View {
@State private var isAccountInfoActive = false
var body: some View {
NavigationView {
ZStack {
Color(UIColor.systemGroupedBackground)
.ignoresSafeArea()
VStack(spacing: 20) {
List {
Section {
// 遷移先リンク(非表示)+ onTapGesture
ZStack {
SettingsRow(icon: "person.circle.fill", iconColor: .blue, text: "アカウント情報")
.onTapGesture {
isAccountInfoActive = true
}
NavigationLink(destination: AccountInfoView(), isActive: $isAccountInfoActive) {
EmptyView()
}
.hidden()
}
SettingsRow(icon: "envelope.circle.fill", iconColor: .pink, text: "お問い合わせ")
SettingsRow(icon: "doc.text.fill", iconColor: .orange, text: "利用規約")
SettingsRow(icon: "doc.plaintext.fill", iconColor: .gray, text: "ライセンス情報")
}
}
.listStyle(InsetGroupedListStyle())
.frame(height: 300)
// ログアウトボタン
Button(action: {
print("ログアウト tapped")
}) {
Text("ログアウト")
.foregroundColor(.blue)
.frame(maxWidth: .infinity)
.padding()
.background(Color(UIColor.secondarySystemGroupedBackground))
.cornerRadius(10)
.shadow(radius: 1)
}
.padding(.horizontal)
Spacer()
}
.navigationTitle("設定")
}
}
}
}
struct SettingsRow: View {
let icon: String
let iconColor: Color
let text: String
var onTextTap: (() -> Void)? = nil
var body: some View {
HStack {
Image(systemName: icon)
.foregroundColor(.white)
.frame(width: 30, height: 30)
.background(iconColor)
.cornerRadius(5)
if let onTextTap = onTextTap {
Text(text)
.onTapGesture {
onTextTap()
}
} else {
Text(text)
}
Spacer()
}
.padding(.vertical, 5)
}
}
struct AccountInfoView: View {
var body: some View {
Text("アカウント情報画面")
.font(.title)
.navigationTitle("アカウント情報")
.navigationBarTitleDisplayMode(.inline)
}
}
#Preview {
ListView()
}
改善後のコード
以下のように修正を加えることで、空白部分も含めてタップ可能なList行になります。
修正後のコード全体
import SwiftUI
struct ListView: View {
@State private var isAccountInfoActive = false
var body: some View {
NavigationView {
ZStack {
Color(UIColor.systemGroupedBackground)
.ignoresSafeArea()
VStack(spacing: 20) {
List {
Section {
// 遷移先リンク(非表示)+ onTapGesture
ZStack {
// SettingsRowの部分をHStackでラップして、Spacerで幅を広げる
HStack {
SettingsRow(icon: "person.circle.fill", iconColor: .blue, text: "アカウント情報")
Spacer() // これで右端までスペースを確保
}
.contentShape(Rectangle()) // タップ判定を行全体に拡げる
.onTapGesture {
isAccountInfoActive = true
}
NavigationLink(destination: AccountInfoView(), isActive: $isAccountInfoActive) {
EmptyView()
}
.hidden()
}
SettingsRow(icon: "envelope.circle.fill", iconColor: .pink, text: "お問い合わせ")
SettingsRow(icon: "doc.text.fill", iconColor: .orange, text: "利用規約")
SettingsRow(icon: "doc.plaintext.fill", iconColor: .gray, text: "ライセンス情報")
}
}
.listStyle(InsetGroupedListStyle())
.frame(height: 300)
// ログアウトボタン
Button(action: {
print("ログアウト tapped")
}) {
Text("ログアウト")
.foregroundColor(.blue)
.frame(maxWidth: .infinity)
.padding()
.background(Color(UIColor.secondarySystemGroupedBackground))
.cornerRadius(10)
.shadow(radius: 1)
}
.padding(.horizontal)
Spacer()
}
.navigationTitle("設定")
}
}
}
}
struct SettingsRow: View {
let icon: String
let iconColor: Color
let text: String
var onTextTap: (() -> Void)? = nil
var body: some View {
HStack {
Image(systemName: icon)
.foregroundColor(.white)
.frame(width: 30, height: 30)
.background(iconColor)
.cornerRadius(5)
if let onTextTap = onTextTap {
Text(text)
.onTapGesture {
onTextTap()
}
} else {
Text(text)
}
Spacer()
}
.padding(.vertical, 5)
}
}
struct AccountInfoView: View {
var body: some View {
Text("アカウント情報画面")
.font(.title)
.navigationTitle("アカウント情報")
.navigationBarTitleDisplayMode(.inline)
}
}
#Preview {
ListView()
}
差分で見てみる
どこが変更されたのか細かく見ていきます。
↓before
List {
Section {
ZStack {
SettingsRow(icon: "person.circle.fill", iconColor: .blue, text: "アカウント情報")
.onTapGesture {
isAccountInfoActive = true
}
NavigationLink(destination: AccountInfoView(), isActive: $isAccountInfoActive) {
EmptyView()
}
.hidden()
}
SettingsRow(icon: "envelope.circle.fill", iconColor: .pink, text: "お問い合わせ")
SettingsRow(icon: "doc.text.fill", iconColor: .orange, text: "利用規約")
SettingsRow(icon: "doc.plaintext.fill", iconColor: .gray, text: "ライセンス情報")
}
}
↓after
List {
Section {
ZStack {
// SettingsRowの部分をHStackでラップして、Spacerで幅を広げる
+ HStack {
SettingsRow(icon: "person.circle.fill", iconColor: .blue, text: "アカウント情報")
+ Spacer() // これで右端までスペースを確保
+ }
+ .contentShape(Rectangle()) // タップ判定を行全体に拡げる
.onTapGesture {
isAccountInfoActive = true
}
NavigationLink(destination: AccountInfoView(), isActive: $isAccountInfoActive) {
EmptyView()
}
.hidden()
}
SettingsRow(icon: "envelope.circle.fill", iconColor: .pink, text: "お問い合わせ")
SettingsRow(icon: "doc.text.fill", iconColor: .orange, text: "利用規約")
SettingsRow(icon: "doc.plaintext.fill", iconColor: .gray, text: "ライセンス情報")
}
下記のコード箇所が追加されていることが確認できました。
List {
HStack {
Spacer()
}
.contentShape(Rectangle())
}
解説:なぜこれでタップ範囲が広がるのか?
①HStack + Spacer() の意味
HStackの解説については、FuyanTechさんの記事が分かりやすいです。
画面に複数の要素を並べるときに「スタック」という仕組みを使います。
「スタック」がなければ、要素を1つずつバラバラに配置しないといけなくなり、非常に大変になります。
HStackは、要素を横に並べる配列方法になります。
イメージとしては以下の通りです。
HStack {}の中に、icon、text、space()の三種類を入れ、ひとまとめにしていることが確認できます。
HStack {
SettingsRow(icon: "person.circle.fill", iconColor: .blue, text: "アカウント情報")
Spacer()
}
今回、space()も入れることにより、余白も含めて横幅が形成されたことが分かります。
| HStack導入前 | HStack導入後 |
|---|---|
![]() |
![]() |
② .contentShape(Rectangle())の役割
.contentShape()は、「タップできる当たり判定エリア」を指定するための修飾子です。
SwiftUIのHStackやSpacerは、見た目上のサイズはあるけど、タップ判定がないことが多いです。
この.contentShape(Rectangle())を加えることで、全体を1つの大きなタップ領域として認識させることができます。
タップ範囲が広がった結果、以下のように余白部分でもスムーズに反応するUIが実現できました。

まとめ
-
HStack + Spacer + .contentShape(Rectangle())の組み合わせで、行全体をタップ可能にできる - 小さな改善でもUXが大きく向上する



Discussion
NavigationView(すでに非推奨です)ではなくNavigationStackを利用し、NavigationLinkのみを書くようにすればこの不具合は既に解消されています!