본문 바로가기
iOS

Multiple Window를 이용하여 Cover Window 만들기.

by SeoB-P 2023. 2. 5.

Cover Window?

앱 개발을 하다가 보면  Toast나 Popup과 같이 항상 Top Layer에 위치해야하는 View들이 있다.

하지만, SwiftUI로 예를 들어서 View들의 Layer를 관리하기위해 제공하는 ZStack이나 Sheet와 같은 View들은 그 뷰에 종속되기 때문에 전역적으로 항상 Top Layer에 위치하기 힘들다.

 

어떤 뷰가 현재 보이든 간에 항상 최상단에 보여져야 할 View가 있다면 어떻게 보여줄 수 있을까?

에 대한 고민을 하던중 좋은 아티클을 발견하여 번역해보았다.

UIWindow

UIWindow를 이용하여 View가 아닌 Window로 Layer를 쌓아보자! 

 

?? 그게 뭔말?

우리가 평소에 View들을 쌓을 때에(Layer를 만들때)는 하나의 UIWindow위에 UIView를 쌓고 또 쌓는 구조였다.

그렇기 때문에, 특정 window위에 있는 View들은 모두 영향을 받을 수 밖에 없는데.. 

 

그 대신에!

아예 window자체를 여러개를 만들어서 Layer를 만든다면 서로 다른 window위에 있는 View들은 서로 영향을 받지 않기 때문에

만약 A Window가 B Window보다 윗 Layer에 있다면 A Window위에 쌓인 View는 항상 B보다는 위쪽에 위치하여 보이게 될 것이다.

 

Multi Window 설정

이제 우리만의 window layer를 구성해보자.

 

Window를 추가히기 위해서는 두가지 필수 요구사항이 있다.

  • window에 대한 강한 참조
  • window에 연관된 Scene(UIWindowScene intance)를 제공

Scene들은 UIWindow들을 관리하지만 우리는 우리만의 윈도우 라이프 사이클이 필요하기 때문이다.

만약 Scene만이 Window에 참조를 가지고 있다면, window는 de-allocated될 것이고 UI는 보이지 않을 것이다.

 

아직 무슨말인지 잘 모르겠으니 예제 코드를 보면서 살펴본다.

 

SceneDelegate에 Window를 추가한다.

UISceneDelegate, UIWindowSceneDelegate를 통해 app의 UIWindowScene의 Instance를 가져오는 것이

가장 쉬운 방법중 하나이기 때문.

 

UIKit의 LifeCycle으로 Scene Delegate를 본다면 이런 모습일 것이다.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?

  func scene(
    _ scene: UIScene, 
    willConnectTo session: UISceneSession, 
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    let contentView = ContentView()

    if let windowScene = scene as? UIWindowScene {
      let window = UIWindow(windowScene: windowScene)
      // 예제 코드에서는 UIkit lifeCycle app에서 View는 SwiftUI View로 표현했기때문에 UIHostingController를 사용했다.
      window.rootViewController = UIHostingController(rootView: contentView)
      self.window = window
      window.makeKeyAndVisible()
    }
  }

  ...
}

Scene Delegate는 app의 main window를 설정할 책임이 있는 곳이다.

그리고 앞서 말했던 window를 추가하기 위해 필요했던 조건과 부합한다.

 

  • window에 대한 강한 참조: var window: UIWindow
  • window에 연관된 scene을 제공: let window = UIWindow(windowScene: windowScene)

 

Scene은 Multi Window를 지원한다고 했으므로 이제 우리만의 Window를 추가해본다.

예제에서는 hudWindow를 추가로 setup한 모습을 볼 수 있다.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var keyWindow: UIWindow?
  var hudWindow: UIWindow?

  func scene(
    _ scene: UIScene, 
    willConnectTo session: UISceneSession, 
    options connectionOptions: UIScene.ConnectionOptions
  ) {
    if let windowScene = scene as? UIWindowScene {
      setupKeyWindow(in: windowScene)
      setupHUDWindow(in: windowScene)
    }
  }

  func setupKeyWindow(in scene: UIWindowScene) {
    let window = UIWindow(windowScene: scene)
    window.rootViewController = UIHostingController(rootView: MainSceneView())
    self.keyWindow = window
    window.makeKeyAndVisible()
  }

  func setupHUDWindow(in scene: UIWindowScene) {
    let hudWindow = UIWindow(windowScene: scene)
    let hudViewController = UIHostingController(rootView: HUDSceneView())
    hudViewController.view.backgroundColor = .clear
    hudWindow.rootViewController = hudViewController
    hudWindow.isHidden = false
    self.hudWindow = hudWindow
  }
}

예제에서는 App실행시에 추가하도록 설정했지만, 앞서 말했던 두 조건만 부합한다면 App의 어디서든 추가가 가능하다고 한다!

코드 분석

1. makeKeyAndVisible() 

두번째 Window에서는 절대 부르면 안된다.

키보드 및 기타 이벤트를 수신하는 창을 설정하는 것인데 하나의 Scene에 KeyWindow는 하나밖에 안되기 때문이다.

대신, isHidden = false를 통해 Second Window가 보이도록 설정한다.

 

2. background = .clear

배경을 투명하게 만들어 주지  않으면 우리의 keyWindow는 SecondView에 가려져서 보여지지 않는다.

 

Touch 설정

이제 Window를 추가했으니 바로 쓰면 될까?

 

놉.!

 

왜냐하면, Touch Event를 받을 수 있는 Window는 최상단에 있는 Window하나이기 때문이다.

SceneDelegate는 Window를 stack처럼 쌓아두기 때문에 원하는 View의 Touch가 안 될 수 있다.

왜냐하면... 모든  window들은 app의 responsder chain에 영향을 받기 때문인데..

 

이를 해결하기위해 UIWindow의 SubClass를 하나 만들고, hitTest메서드를 overrride한다. hitTest?

class PassThroughWindow: UIWindow {
  override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    // Get view from superclass.
    guard let hitView = super.hitTest(point, with: event) else { return nil }
    // If the returned view is the `UIHostingController`'s view, ignore.
    return rootViewController?.view == hitView ? nil : hitView
  }
}

짧지만 매우 필요한 이 코드는, 여러 Window로 하여금 터치를 허용 한다.

 

만약.. 해당 Window는 Touch를 모두 무시하고 싶다. 라고 한다면

밑과 같은 코드를 추가하면 될것이다.

func setupNonInteractiveWindow(in scene: UIWindowScene) {
  let nonInteractiveWindow = UIWindow(windowScene: scene)

  let controller = UIHostingController(rootView: NonInteractiveView())
  controller.view.backgroundColor = .clear
  nonInteractiveWindow.rootViewController = controller
  nonInteractiveWindow.isHidden = false
  nonInteractiveWindow.isUserInteractionEnabled = false // 👈🏻
  self.nonInteractiveWindow = secondWindow
}

 

 

 

 

참고

https://www.fivestars.blog/articles/swiftui-windows/

 

How to layer multiple windows in SwiftUI | FIVE STARS

How to layer multiple windows in SwiftUI Sometimes our apps require to display UI on top of what's on screen: a global alert, a HUD/toast, etc. ZStack is the closest official SwiftUI answer for such needs, and we've covered an example here. However, ZStack

www.fivestars.blog

https://eunjin3786.tistory.com/164

 

[UIScene] UIScene, UIWindowScene, UISceneSession 이란 무엇인가

iOS13부터 멀티 윈도우가 가능해지면서 UI Structure에 UIWindowScene이라는 개념이 등장했습니다. UISceneSession이라는 개념도 함께요..!! 그럼 UIScene, UIWindowScene, UISceneSession에 대해 간단히 살펴보겠습니다

eunjin3786.tistory.com

https://jouureee.tistory.com/72

 

[iOS] UIScene, UIWindowScene, UISceneSession

SceneDelegate.swift class SceneDelegate: UIResponder, UIWindowSceneDelegate { ... } SceneDelegate가 상속받는 UIWindowSceneDelegate에 대해 자세히 살펴보고자 한다. 우선 기본적인 UI 구조는 이렇게 되어 있다고 한다. 그리

jouureee.tistory.com

https://zeddios.tistory.com/536

 

iOS ) hitTest

안녕하세요 :) Zedd입니다. 라이브러리를 사용하면서 소스 보면 가아끔 hitTest가 있었는데, 뭐지?하고 그냥 지나쳤던 기억이...오늘 제대로 공부해볼려고 해용이를 위해서..UIResponder를 썼었죠.. hitTe

zeddios.tistory.com

 

'iOS' 카테고리의 다른 글

iOS Bluetooth  (1) 2022.10.07
Method Swizzling in iOS  (0) 2022.07.10
iOS의 뷰가 그려지는 과정  (0) 2022.06.19
서버없이 Networking Test하기 with URLProtocol  (2) 2022.05.15
iOS Cache  (2) 2022.04.24

댓글