🗂

【watchOS】navigationTitle(_:)をBindingすると表示されない

2023/05/11に公開

WristCounterというApple Watch向けのアプリをリリースしました。

https://apps.apple.com/app/wristcounter-watch-app/id6448880587

しばらくの間、この開発の中で得たwatchOS開発の知見を流していきます。

前提

  • watchOS 9.0
  • Xcode 14.3

SwiftUIで全プラットフォームで動くコード←本当か?

SwiftUI使って開発しました。
SwiftUIで書いたUIは、iOS/iPadOS/MacOS/watchOS/tvOSそれぞれのプラットフォームで最適なスタイルとなって動作します。
基本的には、動作します。

今回僕の開発したカウンターアプリは、watchOSのみで動くアプリなので、別にOSで分岐させることは考えずにすみました。
もし本気で対応するとなると、SwiftUIのデフォルト動作に任せつつ、問題が出たところは、

#if os(watchOS)
    // …
#else
    // …
#endif

のようにOSごとに分岐させることになるかと思います。

しかしどうにもならなかった点があって、それがnavigationTitleでした。

navigationTitle

watchOSの↓の部分です。

Apple Watchは画面がめちゃくちゃ狭いので、文字のラベルを一行入れるだけでめちゃくちゃ画面を圧迫します。
端末にもよりますが、標準の文字サイズで、画面の縦1/4ぐらいが必要となります。

なので、なるべくnavigationTitleは活用したくなります。

要件

タイトルが固定文字列のときは問題ありません。
しかし状況に応じて変えたい場合、どうすればいいでしょう?

今回の要件としては、カウンター画面を左スワイプすると設定画面に行ける、というものでした。
複数カウンターが設定できるので、設定画面には今選択しているカウンターの名前が出ます。
左右スワイプは、TabViewを使うと実現できます。

// 一部コードを簡略化しています
@State private var counters = [Counters()]
@State private var selection: Tab = .counter
@State private var selectionCounter: Int = 0
enum Tab {
    case setting, counter
}

var body: some View {
    TabView(selection: $selection) {
        SettingView()
            .tag(Tab.setting)
        TabView(selection: $selectionCounter) {
            ForEach(counters) { counter in
                CounterView(counter: counter)
                    .tag(counter.tag)
            }
        }
        .tabViewStyle(.carousel)
        .tag(Tab.counter)
    }
}

このSettingViewに、Counter名をnavigationTitleとして渡したい、というのが今回の要件です。

navigationTitleのBinding

iOS/iPadOSなら、↓このようにBidingでNavigationTitleを設定することが可能です。

import SwiftUI

struct ContentView: View {
    @State var title = "Title"
    var body: some View {
        NavigationStack {
            VStack {
                TextField("change title", text: $title)
            }
            .padding()
            .navigationTitle($title)
        }
    }
}

これをwatchOSで動かすと、ビルドは通るんですが、文字列が表示されません。

ダメだったサンプル

なので、watchOSアプリでこんな感じで実装すると、タイトルが表示されません。

struct SettingView: View {
    @Binding var counter: Counter {
    var body: some View {
        NavigationStack {
            ScrollView {}
	    .navigationTitle($counter.name)
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

Bindingにしないで渡すと、更新タイミングが意図通りにならなかったと思います。

今試して大丈夫そうだったサンプル

「これNavigationStack、親Viewで指定したらイケそうじゃない?」とふと思って、こんな風に書いてみました。

TabView(selection: $selection) {
    NavigationStack {
        SettingView()
            .tag(Tab.setting)
            .navigationTitle(counters[selectionCounter].name)
            .navigationBarTitleDisplayMode(.inline)
    }
        // …
}

このタイトルの渡し方ならBindingっぽい更新がされました……

final class Counter: ObservableObject, Identifiable, Equatable {
    var id = UUID()
    var tag: Int
    @Published var name = ""

Counterの定義はこんな感じです。

「watchOSだとnavigationTitle(_:)のBindigが効かない」だと思って記事書き始めたんですが、なんか別の問題なのかもしれません。
正解ご存知の方いたら教えてください。

(了)

Discussion