🐬
iOSアプリで常に画面上にイルカを表示する
表題通りです。よくあるTab+Navigationの構造を持つアプリで、何時如何なる時も右下にイルカを表示させるようにしてみます。
ZStackを使う
真っ先に考えられそうなのはZStackですね。
import SwiftUI
struct DolphinOverlay: View {
var body: some View {
VStack(alignment: .trailing) {
Spacer()
.frame(maxWidth: .infinity)
Image(.dolphin)
.resizable()
.frame(width: 100, height: 100)
.padding()
}
.safeAreaPadding(.bottom, 50)
}
}
struct ContentView: View {
var body: some View {
ZStack {
MyTabView()
DolphinOverlay()
}
}
}
割といい感じです。ナビゲーションの遷移(navigationDestination
)でも上に表示できています。…が、全画面のモーダルの遷移(fullScreenCover
)の下になってしまっています。
原因を探る
View Hierarchyで見ると、UITransitionView
が2枚UIWindowのsubviewsにあることがわかります。1枚目が遷移元、2枚目がモーダルで表示するViewですね。
UIKitの世界ではsubviewsの順番が表示の順番になりますので、イルカが1枚目のUITransitionViewに属している限りはモーダルの上に表示することはできません。以下の2点は最低でも満たす必要がありそうです。
- イルカのViewが遷移元画面のUITransitionViewに属さないこと
- イルカのViewがモーダル表示されるUITransitionViewよりも常に前面に配置されること
ここからはUIWindowに対してごにょごにょしないと解決は難しそうです。
別のUIWindowを作成して表示
ということでUIKitの力を借りてゴリ押します。イルカ専用のUIWindowを重ねて表示させることで解決します。
イルカのViewを表示するUIWindowを表示する拡張を作成して、
@MainActor
extension UIApplication {
static var dolphinWindow: UIWindow?
static func showDolphinWindow() {
guard dolphinWindow == nil,
let windowScene = shared.connectedScenes.first(where: { $0 is UIWindowScene }) as? UIWindowScene else {
return
}
let newWindow = UIWindow(windowScene: windowScene)
let vc = UIHostingController(rootView: DolphinOverlay())
vc.view.backgroundColor = .clear
newWindow.rootViewController = vc
// このwindowがタップイベントを奪わないようにする
newWindow.isUserInteractionEnabled = false
dolphinWindow = newWindow
newWindow.makeKeyAndVisible()
}
}
ContentView表示時にイルカのWindowを表示する。
struct ContentView: View {
var body: some View {
MyTabView()
.task {
UIApplication.showDolphinWindow()
}
}
}
目的達成?です。お疲れ様でした。
余談: 既存のUIWindowにaddSubviewする
はじめは既存のUIWindowを抜き出して、addSubviewすることを考えていました。
@MainActor
extension UIApplication {
var connectedSceneKeyWindow: UIWindow? {
guard let windowScene = connectedScenes
.first(where: { $0 is UIWindowScene }) as? UIWindowScene else {
return nil
}
return windowScene.keyWindow
}
}
extension View {
func windowOverlay<V>(@ViewBuilder content: () -> V) -> some View where V : View {
let tag = 12345
if let window = UIApplication.shared.connectedSceneKeyWindow,
!window.subviews.contains(where: { $0.tag == tag }) {
let vc = UIHostingController(rootView: content())
vc.view.tag = tag
vc.view.frame = window.bounds
vc.view.backgroundColor = .clear
vc.view.isUserInteractionEnabled = false
window.addSubview(vc.view)
}
return self
}
}
遷移元のUITransitionViewには属しなくなりますが、結局モーダル表示よって隠れてしまうため断念しました🥺 (swizzlingで常に最前面にするなんて黒魔術は不採用です)
Discussion