モノトーンの伝説日記

Apex Legends, Splatoon, Programming, and so on...

<mini> watchOS 7 系でのみ発生する不具合

 ウワアアアアアア。

SwiftUI は実質 2.0 の iOS 14+, watchOS 7+, tvOS 14+, macOS Big Sur ではあるが…

 watchOS の SwiftUI は本当に完成度が低いと思う。

mntone.hateblo.jp

toolbar の例

 watchOS 7.1 では修正されている一つの例。

struct BadView: View {
  var body: some View {
    NavigationView {
      Text("Test")
        .toolbar {
          NavigationView(destination: DetailView(content: mockContent)) {
            Label("Settings", systemImage: "gear")
          }
          .padding(.bottom, 8)
        }
    }
  }
}

という、toolbar にごくごく普通のボタンを設置するというやつ。ちなみに、watchOS 7.0 だけ遷移が発生しない。

 ちなみに、回避コードとしてはごくごくシンプルで、

struct OkView: View {
  @State
  private var isPresented = false

  var body: some View {
    NavigationView {
      Text("Test")
        .sheet(isPresented: $isPresented) {
          DetailView(content: mockContent)
        }
        .toolbar {
          Button {
            isPresented = true
          } label: {
            Label("Settings", systemImage: "gear")
          }
          .padding(.bottom, 8)
        }
    }
  }
}

とするだけでいい。まあ一般に、toolbar のボタンをドリルダウン(NavigationView による遷移)とすることはパターンとして一般的ではなく、popup として表示することが普通の流れだと思うし、これでいいのだと思う。ただ、どうしても動かしたい場合、watchOS 7.1+ に絞って設定してもほとんど影響ないため、どうしても使いたい場合は、そうすると良い。

UIImage のロード例

 Combine ベースのコードになって申し訳ないが、

  func downloadAsImage(for url: URL, retry: Int = 3) -> AnyPublisher<UIImage?, Never> {
    download(for: url, ignoreCache: false)
      .retry(retry)
      .map { UIImage(contentsOfFile: $0.path) }
      .replaceError(with: nil)
      .share(replay: 1)
      .eraseToAnyPublisher()
  }

といったコードの例だ。

download では実際は URLSession.shared.downloadTaskPublisher(for: request).tryMap ... といったコードで記述されており、downloadTaskPublishershare(replay:) *1 は標準で定義されていないので、自前で作成する。

 一見普通に動くように見えるコードだが、UIImage の内部遅延ロードの関係で、watchOS 7.x では画像の上少しが読み込まれてそれ以降ロードできなくて破損状態で表示される。

stackoverflow.com

 この最後にあるように、一度 Data にロードすることでこの不具合を回避できる。ただ、UIImage の自己完結型ロードの恩恵を受けられなくはなるが。

  func downloadAsImage(for url: URL, retry: Int = 3) -> AnyPublisher<UIImage?, Never> {
    download(for: url, ignoreCache: false)
      .retry(retry)
      .map { url in
        if #available(watchOS 8.0, *) {
          return UIImage(contentsOfFile: url.path)
        } else {
          guard let data = try? Data(contentsOf: url) else {
            return nil
          }
          return UIImage(data: data)
        }
      }
      .replaceError(with: nil)
      .share(replay: 1)
      .eraseToAnyPublisher()
  }

 downloadTaskPublisher のコードが欲しい方は連絡いただければ MIT license で渡します。

最後に

 Apple Watch Series 7 45mm をポチって、「画面がでかいしせっかくだから大画面を活かしたアプリを作りたいなぁ」って思って作り始めたのが今のアプリなのですが、かなり落とし穴があって、正直 iOS/macOS の SwiftUI を触っていたときより落とし穴が多いです。そして、それを解決した、というドキュメント自体も少ないので、自分は小出しにはなりますが、watchOS 関連の話題は雑ですがこうやってブログにまとめていこうと思います。

 将来 watchOS のアプリがよりリッチになってきて、そしたらもっと軽めのアプリも自然と増えてきますが、軽めのアプリなら watchOS 7 も対応するか!! みたいなノリになったとき、デバッグして動かないってなったら悲しいじゃないですか。そのための歴史として色々と残していきたいですね。

*1:こちらは Entwine を Swift Package Manager で読み込むことですぐに解決する: GitHub - tcldr/Entwine: Testing tools and utilities for Apple's Combine framework.