UITraitDefinitionをUIHostingControllerに適用する
iOS17からUITraitDefinitionが追加され、traitCollectionにカスタムなtraitを追加できるようになりました。
また、UITraitBridgedEnvironmentKeyを使用することで、SwiftUIのEnvironmentとUIKitのtraitCollectionの値を共有させることができるようになりました。
WWDC2023のセッションでは、実際の実装を確認することができます。
このセッションではSwiftUIからUIHostingConfigurationやUIViewRepresentableへのtraitCollectionを渡す方法が紹介されていました。
しかし、UIHostingControllerの場合は、次のような実装をしてもRootViewにUITraitDefinitionが伝播しないことがわかりました。
import UIKit
extension UIViewController {
func onTapButton() {
// ここでは、traitValueに"custom"が設定されている
print(traitCollection.traitValue)
let vc = UIHostingController(rootView: RootView())
present(vc, animated: true)
}
}
struct RootView: View {
@Environment(\.traitValue)
var traitValue
var body: some View {
// traitValueは"custom"にならず初期値が表示される
Text(traitValue)
}
}
挙動の深掘り
上記のWWDCのセッションでは、traitCollectionの値はlayoutSubViewsで更新されると説明されていました。
なので、以下のようにviewDidLoadとviewDidLayoutSubviewsでtraitCollectionの値を確認してみました。
import UIKit
final class RootHostingController: UIHostingController<RootView> {
override func viewDidLoad() {
super.viewDidLoad()
// traitCollectionの値は"custom"になっている
print("viewDidLoad traitValue: \(traitCollection.traitValue)")
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// traitCollectionの値は"initial"になっている
print("viewDidLayoutSubviews traitValue: \(traitCollection.traitValue)")
}
}
上記の通り、viewDidLoadではtraitCollectionの値は"custom"になっているのに対して、viewDidLayoutSubviewsでは"initial"になっていました。
このことから、traitCollectionの値はviewDidLoadでは親コントローラから正しく伝播されているものの、viewDidLayoutSubviewsのタイミングで初期値に戻ってしまうことがわかります。
この現象の根本的な原因は明確ではありませんが、viewDidLoadでtraitCollectionをオーバーライドすることで問題を解決できます。
ワークアラウンド
traitCollectionの値を明示的にオーバーライドすることで、SwiftUIのViewにtraitCollectionの値を正しく伝播させることができます。
具体的には以下のような実装方法があります。
特定のtraitをオーバーライドする方法
public final class WorkaroundViewController<Content: View>: UIHostingController<Content> {
public override func viewDidLoad() {
super.viewDidLoad()
// 特定のtraitだけをオーバーライドする
traitOverrides.traitValue = traitCollection.traitValue
}
}
すべてのtraitをオーバーライドする方法
より汎用的な解決策として、以下のようなアプローチも可能です。
ただし、setOverrideTraitCollectionメソッドはiOS17でdeprecatedになっているため、警告が表示される点に注意が必要です。
public final class WorkaroundViewController<Content: View>: UIHostingController<Content> {
public override func viewDidLoad() {
super.viewDidLoad()
// 現在のtraitCollection全体を自身に適用する
setOverrideTraitCollection(traitCollection, forChild: self)
}
}
このワークアラウンドにより、UIHostingControllerを使用する際でもカスタムtraitを正しくSwiftUIのViewに伝達できるようになります。
Discussion