はじめに
この記事は iOS Advent Calendar 2023 21日目の投稿になります。
今回は、普段開発している中で、ふと感じた 「違和感」 に関して自分なりにどのように向き合ったかを書かせていただきました。お時間ある時にでも読んでいただければ幸いです。
本題
例えば以下の要件がある画面を実装するとします。
- カレンダーを表示する
- 日付を選択し保存をできる画面
- ナビゲーションタイトルは「日付を選択」
- ナビゲーションタイトルはインライン表示
- ナビゲーション左には「キャンセル」ボタン
- ナビゲーション右には「保存」ボタン
- モーダル遷移で表示され、画面遷移しない、1つの画面でタスクを完結できること
上記の要件を満たすための画面を作る際、私は当初以下のような実装をしました。
import SwiftUI
struct SampleDatePickerView: View {
@Environment (\.dismiss) private var dismiss
@Binding var selectionDate: Date
var body: some View {
NavigationStack {
VStack {
DatePicker(
"",
selection: $selectionDate,
displayedComponents: .date
)
.datePickerStyle(.graphical)
.labelsHidden()
Spacer()
}
.navigationTitle("日付を選択")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("キャンセル") {
dismiss()
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("保存") {
// save action
}
}
}
}
}
}
実際の画面はこちらです。
上記の実装をした時、私は 「この画面は画面遷移が発生しないのにNavigationStackを使って実装するのは違うんじゃないか」 と、ふと違和感を感じてしまいました。
そもそもこの実装をした背景としては以下があります。
- SwiftUIではNavigationTitleを表示させるには
NavigationStack
かNavigationView
が画面に実装されている必要があること - または画面遷移元(ナビゲーションツリーのRootになる画面)に
NavigationStack
やNavigationView
が実装されており、かつ遷移先が.navigationTitle
を実装している場合NavigationTitleを表示できる状態であること
またApple公式ドキュメントの Configure your apps navigation titles の See Alsoの部分記載されているメソッドの説明を読むと
~ for purposes of navigation
と記載されています。翻訳すると「ナビゲーションを目的とした」となっています。
このような記載があるように、ナビゲーションすることを前提にNavigationTitleを実装しなければいけないのではないか、ということにたどり着きました。
話は戻り、上記実装時の要件には
モーダル遷移で表示され、画面遷移しない、1つの画面でタスクを完結できること
というのがあります。
ということは、つまりナビゲーションを目的としていません。ここで私が抱いていた違和感は間違っていないのでは? ということに気づきました。
「この画面実装においてNavigationStackを使って実装するのは間違っているのではないか」
ということで私は他のやり方を考え実装してみました。
代替案を考えてみた
NavigationStackを使わなくてもNavigationTitleの位置に画面のタイトルをおけるようなものは標準APIにはありませんでした。UIKit時代の実装を思い出し、「確かStoryboardのViewControllerにtitleを入力するところががあったよな」ということを思い出したので、久しぶりに見てみましたが、この機能もNavigationControllerを使っていないと表示されないようになっていました。ということは以前からこのナビゲーションにタイトルをセットするにはナビゲーションを目的としていないとタイトルを設置できないということがわかりました。
そんなこともあり、結果自前で実装することにし、色々と考え結果以下の実装になりました。
import SwiftUI
struct DatePickerView: View {
@Environment (\.dismiss) private var dismiss
@Binding var selectionDate: Date
var body: some View {
VStack(spacing: 0) {
ZStack {
Text("日付を選択")
.fontWeight(.bold)
HStack {
Button("キャンセル") {
dismiss()
}
Spacer()
Button("保存") {
// save action
}
}
.padding(.horizontal, 16)
}
DatePicker(
"",
selection: $selectionDate,
displayedComponents: .date
)
.datePickerStyle(.graphical)
.labelsHidden()
Spacer()
}
}
}
実際の画面はこちら
結構シンプルに実装できました。要件としては一旦満たされる実装にはできました。
上記実装を具体的に説明します。
まずVStackで全体を囲いY軸を基準とします。VStackの中にZStackを実装し、その中にNavigationTitleになるTextを設置します。
ToolbarItemに値するのボタンはHStackでボタンを配置し、Spacer()を入れて、両端一杯に配置しています。
マージンに関しては、ZStack対して、Top:20pt、Horizontal:16pt 指定しています。(20pt, 16ptという数値はFigmaで使えるApple Design Resources を確認して指定していますが、正確な値であるかは不明です。)
最後にTextとHStackをZStackで囲い、Z軸を基準としViewを重ねて表示しています。
どうでしょうか、見た感じ、ほとんど変わらないように思います。
なぜこのような実装になったのか
気になった方もいるかもしれませんが、「なぜ、ZStackにTextとHStackを重ねてるの?HStackでやれば良くね?」と思った方もいるのではないでしょうか? そう思った前提で話を進めます。(違うことを思った人はすみません)
私も実装当初は「HStackで実装すれば良いかな」くらいに軽く思ってました。ですが見た目が上手くいきませんでした。それはなぜかと言いますと、
HStackは、X軸を基準として横並びにViewを並べるレイアウト であり、要素の間にスペーサーを入れても、X軸が基準になるため、AutoLayoutを考慮した実装をしようとすると、2番目の要素であるナビゲーションタイトルの部分を、ViewのCenterに配置することがどうしても上手く実現できません。
Hstackを使うと以下のような見え方になってしまいます。
HStack {
Button("キャンセル") {
dismiss()
}
Spacer()
Text("日付を選択")
.fontWeight(.bold)
Spacer()
Button("保存") {
// save action
}
}
上記は、日付がCenterに表示されていません。
この見え方に関しては「まあそうだよね、Spacerを入れてるんだもん、こうになる決まってんじゃん」てなると思いますが、
そうなんです。こうになるに決まっているんです。
ではこのSpacerを具体的な数字にしてしまうと、画面サイズが変わった時に考慮されないレイアウトになるため現実的ではありません。
またSwiftUIのViewの優先度などは.layoutPriorityなどでしか設定できないためUIKitのように複雑な優先度組むことができません。
以上の理由から結果として、VStack、ZStack、HStackを使用し実現することに至りました。
注意しなければいけないこと
この方法でに関して一つ注意することがあります。
それは、「表示の優先順位を無視した作りになっている」ということです。
具体的にみてもらうのが早いので下の画像を見てください。
この画像では、左のボタンのタイトルに「キャンセルしてくださーーーーーい」と長い単語を設定しています。
見ての通り、ボタンのタイトルが長いとナビゲーションタイトルに被ってしまいます。これも実装上「そりゃこうなるよね」という実装になっているのでこのようになるのは当たり前です。
なので、この実装には採用できる場合と採用できない場合があります。
採用できる場合は
- 各要素を表示する際の単語の文字数が少ない、またはボタン表示がiconのみ
- UIKitを使わないでカスタムしたい場合
採用できない場合は
- 各要素を表示する際の単語の文字数が多い
- 多言語対応などを考えているアプリで文字数が多い言語(例えばドイツ語とか、フィンランド語とか)を扱う場合
となります。
、、、
とゆう感じで、当初自分が抱いた違和感から始まり、公式ドキュメントを読み、他に代替案がないか調べ、代替案がないので実際にそれに近い実装をし、一応要件に満たせるような実装にたどり着く(標準APIのような品質には程遠いですが...)というところまで行いました。
結果としてプロダクトのコードに落とし込めるかどうかは個人の判断にお任せしますし、「いやお前何言ってんの?」みたいな意見大歓迎です。もっといい実装があったら教えてください。
ということで自分としては結果として大きな学びを得ることができました。
「違和感から行動し結果として別の何かを生み出すまでの取り組み」 はとてつもなく面白いし学びしかないなということです。
結論何が言いたいかというと、日々実装する上で、気づきや違和感、を大事にし、納得を持って実装していくことは非常に重要だなと改めて感じたということです。
まとめ
自分も日々意識しているつもりですが、時間に追われ忙しかったりすると惰性でコードを書いてしまったり、「これはあれだから」や、「この実装は以前こうだったからこれでいいや」や「Appleさんはこう使えって言っていし...」みたいな雑な実装になりがちになってしまい結果として、小さな違和感に気づくことができず、学びの機会を逃してしまうので、そうならないように気付きのアンテナを貼りながら実装していきましょうと今年を振り返り思った次第です。
技術的に何か紹介するような記事より最終的には思考や行動的な記事になってしまいましたが、一応iOSのことを書いてる気がしたので iOS Advent Calendar で書かせていただきました。
読んでいただきありがとうございました。