Skip to content

Commit

Permalink
♻️ Update modal context traits to present in their own UIWindow
Browse files Browse the repository at this point in the history
  • Loading branch information
mmaatttt committed Oct 3, 2024
1 parent 742ec34 commit caf6122
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// ModalContextManager.swift
// AppcuesKit
//
// Created by Matt on 2024-09-25.
// Copyright © 2024 Appcues. All rights reserved.
//

import UIKit

@available(iOS 13.0, *)
internal class ModalContextManager {
var presentingWindow: AppcuesUIWindow?

func present(viewController: UIViewController, useSameWindow: Bool = false, completion: (() -> Void)?) throws {
guard !useSameWindow else {
try presentInSameWindow(viewController: viewController, completion: completion)
return
}

guard let windowScene = UIApplication.shared.activeWindowScenes.first else {
throw AppcuesTraitError(description: "No active window scene")
}

let window = AppcuesUIWindow(windowScene: windowScene)

guard let rootViewController = window.rootViewController else {
throw AppcuesTraitError(description: "No root view controller")
}

rootViewController.present(viewController, animated: true, completion: completion)

presentingWindow = window
}

private func presentInSameWindow(viewController: UIViewController, completion: (() -> Void)?) throws {
guard let topViewController = UIApplication.shared.topViewController() else {
throw AppcuesTraitError(description: "No top VC found")
}

topViewController.present(viewController, animated: true, completion: completion)
}

func remove(viewController: UIViewController, completion: (() -> Void)?) {
viewController.dismiss(animated: true) {
self.presentingWindow = nil
completion?()
}
}
}

@available(iOS 13.0, *)
extension ModalContextManager {
class AppcuesUIWindow: UIWindow {
override init(windowScene: UIWindowScene) {
super.init(windowScene: windowScene)

rootViewController = AppcuesWindowRootViewController()
isHidden = false
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard let hitView = super.hitTest(point, with: event), hitView != self else {
return nil
}

// Ignore UITransitionViews to allow interaction with the underlying app while the window is overlayed.
if String(describing: type(of: hitView)) == "UITransitionView" {
return nil
}

return hitView
}
}

class AppcuesWindowRootViewController: UIViewController {
init() {
super.init(nibName: nil, bundle: nil)

// Ensure this root view isn't eligible for a hitTest
// (any ViewController it presents will be though)
view.alpha = 0
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ internal class AppcuesModalTrait: AppcuesStepDecoratingTrait, AppcuesWrapperCrea

weak var metadataDelegate: AppcuesTraitMetadataDelegate?

let modalContextManager = ModalContextManager()

private let presentationStyle: PresentationStyle
private let modalStyle: ExperienceComponent.Style?
private let transition: Transition
Expand Down Expand Up @@ -79,15 +81,15 @@ internal class AppcuesModalTrait: AppcuesStepDecoratingTrait, AppcuesWrapperCrea
}

func present(viewController: UIViewController, completion: (() -> Void)?) throws {
guard let topViewController = UIApplication.shared.topViewController() else {
throw AppcuesTraitError(description: "No top VC found")
}

topViewController.present(viewController, animated: true, completion: completion)
try modalContextManager.present(
viewController: viewController,
useSameWindow: presentationStyle.useSameWindow,
completion: completion
)
}

func remove(viewController: UIViewController, completion: (() -> Void)?) {
viewController.dismiss(animated: true, completion: completion)
modalContextManager.remove(viewController: viewController, completion: completion)
}
}

Expand All @@ -111,6 +113,15 @@ extension AppcuesModalTrait {
return .formSheet
}
}

var useSameWindow: Bool {
switch self {
case .full, .dialog:
return false
case .sheet, .halfSheet:
return true
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ internal class AppcuesTooltipTrait: AppcuesStepDecoratingTrait, AppcuesWrapperCr

weak var metadataDelegate: AppcuesTraitMetadataDelegate?

let modalContextManager = ModalContextManager()

let tooltipStyle: ExperienceComponent.Style?
let hidePointer: Bool
let pointerSize: CGSize
Expand Down Expand Up @@ -80,14 +82,10 @@ internal class AppcuesTooltipTrait: AppcuesStepDecoratingTrait, AppcuesWrapperCr
}

func present(viewController: UIViewController, completion: (() -> Void)?) throws {
guard let topViewController = UIApplication.shared.topViewController() else {
throw AppcuesTraitError(description: "No top VC found")
}

topViewController.present(viewController, animated: true, completion: completion)
try modalContextManager.present(viewController: viewController, completion: completion)
}

func remove(viewController: UIViewController, completion: (() -> Void)?) {
viewController.dismiss(animated: true, completion: completion)
modalContextManager.remove(viewController: viewController, completion: completion)
}
}

0 comments on commit caf6122

Please sign in to comment.