abceedをvisionOSネイティブビルドできるようにした

# これは何

[Globeeアドベントカレンダー](https://qiita.com/advent-calendar/2023/globee)1日目の記事です。
本記事では、visionOSでのabceed iOSアプリのネイティブビルドのプロセスと、その際に遭遇した具体的な課題と解決策について説明します。

先日の「visionOS Engineer LT会 vol.4: オフライン限定Meetup」では先行して口頭で共有させてもらった内容になりますが、改めて確認するとビルドが通らなくなっていたので、最新の情報で内容を更新しました。
こちらの資料ではなぜvisionOSをやろうと思ったのかという部分にも簡単ですが触れていますので、よろしければご笑覧ください。
https://speakerdeck.com/toshi0383/visionosnituiteglobeegaqu-rizu-ndeirukoto

内容は2023年12月時点のものになります。
visionOS開発は新しい技術領域ですぐに情報が古くなると思いますので、その点あくまで参考情報ということでご留意ください。

# アプリ挙動

まずは結果から共有しますと、こんな感じになりました。

![abceed-visionos-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35008/9da36a22-8d00-ec6b-587b-9a31ea9d1c1f.gif)

普通にサクサク動いています。
iOSアプリはこんな感じです。

![abceed-ios.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35008/7bc94e26-5611-1cd8-3bed-129126d3817d.gif)

## 挙動で気がついたこと

- `UITabBarController`がいい感じで変換された。やはり標準コンポーネントを使っていると利点があるなと思った。
  iPadでもステージマネージャでこのタブスタイルになってても良さそう。（ならない）
- モーダルはスワイプで閉じるには対応していないので、閉じるボタンが必要そう。
- `UICollectionViewCell` など、カーソルが上に乗った時のHover効果が自動的に追加されていた。

## モーダルが勝手にダークモードになってしまう

こちらは未解決の問題です。betaのバグでしょうか。
iOSでは問題ないのですが、`traitCollection.userInterfaceStyle`が勝手に`.dark`になってしまうことがあるようでした。
![dark-modal.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/35008/2f3b70ee-51be-98f8-2156-6941021b52e0.gif)

## 意外と細かい表示崩れが多い

- `UIScreen` がないので依存している画面が正しいレイアウトになってなさそう
- タブバーはそんなに邪魔にならないので基本隠さない方が良さそう(`hidesBottomBarWhenPushed`)
  - 一度隠すとその後表示されなくなるようだった
- PanModalがランタイムクラッシュしてそうだった
- ボタンに不要な背景がついてしまっている箇所がある

# visionOS対応時にやったこと

実装側で必要だった対応について記載していきます。

## Package.swift

swift-tools-versionを5.9にすると、visionOSの指定ができるようになりました。
```diff
-// swift-tools-version: 5.7
+// swift-tools-version: 5.9
```

```diff
-    platforms: [.iOS(.v15)],
+    platforms: [.iOS(.v15), .visionOS(.v1)],
```

## 3rd Partyライブラリの対応

### 対応済みのブランチが存在していた

```diff
+        .package(url: "https://github.com/SDWebImage/SDWebImageSwiftUI.git", exact: "3.0.0-beta.2"),
```

### forkして対応を入れた

```diff
+        .package(url: "https://github.com/toshi0383/DifferenceKit", branch: "visionos"),
+        .package(url: "https://github.com/toshi0383/HorizontalStickyHeaderLayout", branch: "visionos"),
+        .package(url: "https://github.com/toshi0383/APIKit", branch: "visionos"),
+        .package(url: "https://github.com/toshi0383/RxSwift.git", branch: "visionOS-compilability"),
+        .package(url: "https://github.com/toshi0383/MenuItemKit", branch: "visionos"),
+        .package(url: "https://github.com/toshi0383/MBProgressHUD", branch: "visionos"),
+        .package(url: "https://github.com/toshi0383/PanModal", branch: "visionos"),
+        .package(url: "https://github.com/toshi0383/ReverseExtension", branch: "visionos"),
```

対応内容は大体同じで、以下のような内容です。

- targetの対象デバイスにvisionOSを追加（これはやらなくても良かったかも）
- コンパイルエラーを修正

コンパイルエラーの原因は、単純にvisionOSがwatchOS扱いになっていたというレベルのものが多かったです。
こういったものの修正は誰にでもできる簡単なお仕事ですので、今の時期はOSSへのコントリビューションチャンスですね！

上記のforkについて、場当たり的な対応を入れただけのものが多いので本家へのPRにはしていませんが、しばらく置いておくと思うのでご自由にどうぞ！

#### RxSwiftの対応

一度マージされていましたが、どうもmainブランチに反映されていない様子。
よくよく経緯を追うとRevertしたよとのコメント。
Revertのためにmainブランチがforce-pushされたようでした。
古いPRのコミット履歴からcherry-pickする形でforkに反映しました。

### 対応せず削除した

この辺りは今は確認できなくてもいいかと思って、単純に関連コードごと削除しました。

```diff
-        .package(url: "https://github.com/line/line-sdk-ios-swift", exact: "5.10.1"),
-        .package(url: "https://github.com/globeejp/GoogleSignIn-iOS", exact: "6.3.0"),
-        .package(url: "https://github.com/toshi0383/KeyboardLayoutGuide", exact: "1.10.1"),
-        .package(url: "https://github.com/bizz84/SwiftyStoreKit", exact: "0.16.4"),
-        .package(url: "https://github.com/nathantannar4/InputBarAccessoryView", from: "6.2.0"),
-        .package(url: "https://github.com/ephread/Instructions", branch: "2a93135"),
-        .package(url: "https://github.com/HeroTransitions/Hero", exact: "1.6.2"),
-        .package(url: "https://github.com/aws-amplify/aws-sdk-ios-spm", from: "2.33.4"),
-        .package(url: "https://github.com/evgenyneu/Cosmos", exact: "23.0.0"),
```

理想的には依存関係を削除するのではなく、visionOSとiOSで別々にインポートする形で`Package.swift`を定義できるとよさそうです。
その辺りは自分にはまだ知見がなかったので、一旦後回しにした形でした。
参考リンクなどあればぜひコメントしてください🙇🏻‍♂️

## その他

### `rx.tap` がないと言われる

途中で `rx.tap` がどうしても解決できず、結局諦めました。
今のうちからタップイベントにRxを使わないようにするのが良いと思っています。

ただこれはエラーメッセージが嘘をついていただけかもしれないし、気のせいだったかもしれません。
IDEが進化しているとはいえ、開発時のわかりづらいエラーメッセージというのはどうしても発生してしまうので、根気が大切だと思います。

### UISelectionFeedbackGenerator

いわゆるHaptic Feedback（ブルっとするやつ）はvisionOSにはないので、 `#if os(iOS)` など対応を入れました。
すごく余談ですが、もしヘッドセットが震えたらどんな体験なんでしょうね。


### `UIView.inputAccessoryView`がなくなった

10月時点ではコンパイル通っていましたが、Xcode 15 beta 3で再度確認したところコンパイルエラーになっていました。
確認したところ、 `inputAccessoryView` を使っている箇所はかなり古いコードばかりでしたので、そのうち全部別のコンポーネントに移行してしまおうと決意しました。
こういったプラットフォーム依存APIの使用をできる限り避けておくことで、visionOSやmacOSへの移行がしやすい状況を意図的に作っていけそうです。
やっていきましょう。

# まとめ

今回は大体6時間くらい格闘し、無事に動かすことができました。
この内容をLTした時、「6時間速い！」と言っていただけたのですが、確かに、自分でも6時間しか経っていなかったんだなという印象でした。ビルドエラーが出続ける6時間は精神的に大変で、体感では2日間くらいかかっていました。
ただ6時間で無理やりコンパイルが通って動かせました！という段階なので、適切なユーザー体験を目指して改善するにはこの何十倍も時間がかかりそうだなと感じています。

まあでも、とりあえず動くからリリースしたらいいんじゃないでしょうか？（なぜか他人事）
現実的な今後の展望としては、例えばいくつかの機能のみを抜き出すような形でアプリ化することが考えられると思います。

以上、何かの参考になれば幸いです。
不明点やアドバイスなど、コメントやSNSでお気軽にお声かけください。

# 宣伝

Globeeではエンジニアの主体的な意思決定が尊重される文化がありますので、積極的に手を挙げていくことでこのような実験的な活動も経験することができます。
ぜひ一緒に「教育」の事業を盛り上げていきませんか？

なお勉強会でもお伝えしたのですが、iOSエンジニアが足りないです。（現在私1人！）
visionOSを含めやりたいことが山盛りなので、正社員orフリーランス問わず募集中です。
よろしくお願いいたします。
https://globee.notion.site/Globee-d184eefc3561480292481299c64b04bc