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