iOS13で変わったこと
iOS13以降ではiOSアプリでもマルチウィンドウで使えるようになり
アプリの起動経路が変わりました。
※ マルチウィンドウ自体はオプトインの仕組みで
ONにするにはinfo.plistのUIApplicationSceneManifest
の設定が必要になります。
https://developer.apple.com/documentation/uikit/app_and_environment/managing_your_app_s_life_cycle
https://developer.apple.com/documentation/bundleresources/information_property_list/uiapplicationscenemanifest
具体的には下記のSceneDelegate
で画面の最初の表示を行います。
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
// Use a UIHostingController as window root view controller
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()
}
}
}
UIWindow
はAppDelegate
からSceneDelegate
に移動し
下記のにUIWindowインスタンスを取得することができなくなりました。
let delegate = UIApplication.shared.delegate as! AppDelegate
let window = delegate.window
これまで見てきた限りですと
SceneDelegate
への適切なアクセス方法が見つからず
さらにUIWindow
が一つのインスタンスとは限らないため
シングルトンでインスタンスを保持していても
マルチウィンドウを使用している場合
別のUIWindow
を参照してしまうリスクもあります。
どういう時にUIWindowを使うか?
例えば
Sign in With Appleなどのような認証を使用する場合
認証を行う画面を表示するためにUIWindowを提供する必要があります。
extension LoginViewController: ASAuthorizationControllerPresentationContextProviding {
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
return self.view.window!
}
}
SwiftUIのViewからアクセスするには?
上記のような認証をSwiftUIから使用する場合
View
からUIWindow
を提供することがあるかもしれません。
その際にView
からUIWindow
のインスタンスへアクセスするにはどうしたら良いでしょうか?
@Environment
を利用する
一つの方法として
@Environment
を使ってUIWindow
インスタンスをDIする方法があります。
これを実現するために
EnvironmentKey
とEnvironmentValues
を活用します。
https://developer.apple.com/documentation/swiftui/environmentkey
https://developer.apple.com/documentation/swiftui/environmentvalues
まずEnvironmentKey
プロトコルに適合したstructを作成します。
必須なのはdefaultValue
プロパティのみです。
struct WindowKey: EnvironmentKey {
static let defaultValue: UIWindow? = nil
}
次に上記のEnvironmentValues
を拡張して
上記のキーのsubscript
を定義します。
extension EnvironmentValues {
var window: UIWindow? {
get {
self[WindowKey.self]
}
set {
self[WindowKey.self] = newValue
}
}
}
こうすることでenvironment(_:_:)
を使ってwindowをDIすることができます。
https://developer.apple.com/documentation/swiftui/view/3289000-environment
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
// ↓↓↓↓UIWindowを設定している↓↓↓↓↓↓
let contentView = ContentView().environment(\.window, window)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
}
これでView
からは
struct ContentView: View {
@Environment(\.window) var window
}
とアクセスすることができます。
参考
https://github.com/objcio/swift-talk-app-swiftui/blob/episode-163/Account.swift
なぜ@EnvironmentObject
ではなく@Environment
のか?
実は同じようにUIWindow
にアクセスできます。
しかしその場合は
UIWindow
をObservableObject
に
適合させないといけなくなります。
UIWindow
を拡張するのはあまり良くないのではないかということで
@Environment
を使用しています。
まとめ
SwiftUIに触っていると
「あれ?これどうやるんだ?」と悩むことって意外と多く感じています。
まだまだ勉強中なので
これからも知見、経験をためていって使いこなせるようになりたいです😃
何か間違いなどございましたら教えていただけますと嬉しいです🙇🏻♂️