From 4e44a87c247e39c432c63ead79b17bf03751de65 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 11 Jun 2026 14:15:51 +1200 Subject: [PATCH 1/3] Format the notices files with swift-format ahead of the code changes --- .../System/Notices/NoticePresenter.swift | 185 +++++++++++------- .../System/Notices/UntouchableWindow.swift | 15 +- 2 files changed, 128 insertions(+), 72 deletions(-) diff --git a/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift b/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift index 8599d382e2cd..21b86917ff40 100644 --- a/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift +++ b/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift @@ -62,8 +62,14 @@ class NoticePresenter { private var notificationObservers = Set() - init(store: NoticeStore = StoreContainer.shared.notice, - animator: NoticeAnimator = NoticeAnimator(duration: Animations.appearanceDuration, springDampening: Animations.appearanceSpringDamping, springVelocity: NoticePresenter.Animations.appearanceSpringVelocity)) { + init( + store: NoticeStore = StoreContainer.shared.notice, + animator: NoticeAnimator = NoticeAnimator( + duration: Animations.appearanceDuration, + springDampening: Animations.appearanceSpringDamping, + springVelocity: NoticePresenter.Animations.appearanceSpringVelocity + ) + ) { self.store = store self.animator = animator @@ -99,8 +105,9 @@ class NoticePresenter { guard let self, let userInfo = notification.userInfo, let keyboardFrameValue = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, - let durationValue = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else { - return + let durationValue = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber + else { + return } self.currentKeyboardPresentation = .present(height: keyboardFrameValue.cgRectValue.size.height) @@ -109,10 +116,14 @@ class NoticePresenter { return } - UIView.animate(withDuration: durationValue.doubleValue, animations: { - currentContainer.bottomConstraint?.constant = self.onScreenNoticeContainerBottomConstraintConstant - self.view.layoutIfNeeded() - }) + UIView.animate( + withDuration: durationValue.doubleValue, + animations: { + currentContainer.bottomConstraint?.constant = + self.onScreenNoticeContainerBottomConstraintConstant + self.view.layoutIfNeeded() + } + ) } .store(in: ¬ificationObservers) @@ -124,14 +135,19 @@ class NoticePresenter { guard let self, let currentContainer = self.currentNoticePresentation?.containerView, let userInfo = notification.userInfo, - let durationValue = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber else { - return + let durationValue = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber + else { + return } - UIView.animate(withDuration: durationValue.doubleValue, animations: { - currentContainer.bottomConstraint?.constant = self.onScreenNoticeContainerBottomConstraintConstant - self.view.layoutIfNeeded() - }) + UIView.animate( + withDuration: durationValue.doubleValue, + animations: { + currentContainer.bottomConstraint?.constant = + self.onScreenNoticeContainerBottomConstraintConstant + self.view.layoutIfNeeded() + } + ) } .store(in: ¬ificationObservers) } @@ -142,8 +158,9 @@ class NoticePresenter { NotificationCenter.default.publisher(for: .WPTabBarHeightChanged) .sink { [weak self] _ in guard let self, - let containerView = self.currentNoticePresentation?.containerView else { - return + let containerView = self.currentNoticePresentation?.containerView + else { + return } containerView.bottomConstraint?.constant = -self.window.untouchableViewController.offsetOnscreen @@ -197,15 +214,21 @@ class NoticePresenter { } let content = UNMutableNotificationContent(notice: notice) - let request = UNNotificationRequest(identifier: notificationInfo.identifier, - content: content, - trigger: nil) - - UNUserNotificationCenter.current().add(request, withCompletionHandler: { _ in - DispatchQueue.main.async { - ActionDispatcher.dispatch(NoticeAction.clear(notice)) - } - }) + let request = UNNotificationRequest( + identifier: notificationInfo.identifier, + content: content, + trigger: nil + ) + + UNUserNotificationCenter.current() + .add( + request, + withCompletionHandler: { _ in + DispatchQueue.main.async { + ActionDispatcher.dispatch(NoticeAction.clear(notice)) + } + } + ) return NoticePresentation(notice: notice, containerView: nil) } @@ -222,13 +245,18 @@ class NoticePresenter { addTopConstraintToNoticeContainer(noticeContainerView) // At regular width, the notice shouldn't be any wider than 1/2 the app's width - noticeContainerView.noticeWidthConstraint = noticeView.widthAnchor.constraint(equalTo: noticeContainerView.widthAnchor, multiplier: 0.5) - let isRegularWidth = noticeContainerView.traitCollection.containsTraits(in: UITraitCollection(horizontalSizeClass: .regular)) + noticeContainerView.noticeWidthConstraint = noticeView.widthAnchor.constraint( + equalTo: noticeContainerView.widthAnchor, + multiplier: 0.5 + ) + let isRegularWidth = noticeContainerView.traitCollection.containsTraits( + in: UITraitCollection(horizontalSizeClass: .regular) + ) noticeContainerView.noticeWidthConstraint?.isActive = isRegularWidth NSLayoutConstraint.activate([ noticeContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - noticeContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + noticeContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) let dismiss = { @@ -246,17 +274,29 @@ class NoticePresenter { view.mask = MaskView(parent: view, untouchableViewController: self.window.untouchableViewController) let offScreenBottomOffset = offScreenNoticeContainerBottomOffset(for: noticeContainerView) - let fromState = animator.state(for: noticeContainerView, in: view, withTransition: .offscreen, - bottomOffset: offScreenBottomOffset) - let toState = animator.state(for: noticeContainerView, in: view, withTransition: .onscreen, - bottomOffset: onScreenNoticeContainerBottomConstraintConstant) - animator.animatePresentation(fromState: fromState, toState: toState, completion: { - guard notice.style.isDismissable else { - return - } + let fromState = animator.state( + for: noticeContainerView, + in: view, + withTransition: .offscreen, + bottomOffset: offScreenBottomOffset + ) + let toState = animator.state( + for: noticeContainerView, + in: view, + withTransition: .onscreen, + bottomOffset: onScreenNoticeContainerBottomConstraintConstant + ) + animator.animatePresentation( + fromState: fromState, + toState: toState, + completion: { + guard notice.style.isDismissable else { + return + } - DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Animations.dismissDelay, execute: dismiss) - }) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Animations.dismissDelay, execute: dismiss) + } + ) UIAccessibility.post(notification: .layoutChanged, argument: noticeContainerView) @@ -284,38 +324,45 @@ class NoticePresenter { public class func dismiss(container: NoticeContainerView) { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .nanoseconds(1)) { - UIView.animate(withDuration: Animations.appearanceDuration, - delay: 0, - usingSpringWithDamping: Animations.appearanceSpringDamping, - initialSpringVelocity: Animations.appearanceSpringVelocity, - options: [], - animations: { + UIView.animate( + withDuration: Animations.appearanceDuration, + delay: 0, + usingSpringWithDamping: Animations.appearanceSpringDamping, + initialSpringVelocity: Animations.appearanceSpringVelocity, + options: [], + animations: { container.noticeView.alpha = 0 - }, - completion: { _ in + }, + completion: { _ in container.removeFromSuperview() - }) + } + ) } } private func dismissForegroundNotice() { guard let container = currentNoticePresentation?.containerView, - container.superview != nil else { - return + container.superview != nil + else { + return } let bottomOffset = offScreenNoticeContainerBottomOffset(for: container) let toState = animator.state(for: container, in: view, withTransition: .offscreen, bottomOffset: bottomOffset) - animator.animatePresentation(fromState: {}, toState: toState, completion: { [weak self] in - container.removeFromSuperview() - - // It is possible that when the dismiss animation finished, another Notice was already - // being shown. Hiding the window would cause that new Notice to be invisible. - if self?.currentNoticePresentation == nil { - UIAccessibility.post(notification: .layoutChanged, argument: nil) - - self?.window.isHidden = true + animator.animatePresentation( + fromState: {}, + toState: toState, + completion: { [weak self] in + container.removeFromSuperview() + + // It is possible that when the dismiss animation finished, another Notice was already + // being shown. Hiding the window would cause that new Notice to be invisible. + if self?.currentNoticePresentation == nil { + UIAccessibility.post(notification: .layoutChanged, argument: nil) + + self?.window.isHidden = true + } } - }) + ) } // MARK: - Animations @@ -353,7 +400,7 @@ private extension UIWindow { /// - Returns: CGRect based on this window's frame /// - Note: Turns out that a small alteration to the frame is enough to accomplish this. func offsetToAvoidStatusBar() -> CGRect { - return self.frame.insetBy(dx: Offsets.minimalEdgeOffset, dy: Offsets.minimalEdgeOffset) + self.frame.insetBy(dx: Offsets.minimalEdgeOffset, dy: Offsets.minimalEdgeOffset) } private enum Offsets { @@ -422,7 +469,7 @@ class NoticeContainerView: UIView { } override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - return noticeView.point(inside: convert(point, to: noticeView), with: event) + noticeView.point(inside: convert(point, to: noticeView), with: event) } } @@ -450,8 +497,12 @@ private extension NoticePresenter { backgroundColor = .blue let nc = NotificationCenter.default - nc.addObserver(self, selector: #selector(updateFrame(notification:)), - name: UIDevice.orientationDidChangeNotification, object: nil) + nc.addObserver( + self, + selector: #selector(updateFrame(notification:)), + name: UIDevice.orientationDidChangeNotification, + object: nil + ) } required init?(coder aDecoder: NSCoder) { @@ -462,9 +513,11 @@ private extension NoticePresenter { setNeedsLayout() } - private static func calculateFrame(parent: UIView, - untouchableVC: UntouchableViewController) -> CGRect { - return CGRect( + private static func calculateFrame( + parent: UIView, + untouchableVC: UntouchableViewController + ) -> CGRect { + CGRect( x: 0, y: 0, width: parent.bounds.width, diff --git a/WordPress/Classes/ViewRelated/System/Notices/UntouchableWindow.swift b/WordPress/Classes/ViewRelated/System/Notices/UntouchableWindow.swift index 1d8b8208b3bb..882539aa260b 100644 --- a/WordPress/Classes/ViewRelated/System/Notices/UntouchableWindow.swift +++ b/WordPress/Classes/ViewRelated/System/Notices/UntouchableWindow.swift @@ -37,7 +37,8 @@ class UntouchableViewController: UIViewController { // this causes the check for the presentedViewController to return nil // thus placing the notice incorrectly, without the proper space for the tabBar guard let mainWindow = UIApplication.shared.delegate?.window, - let tabBarController = mainWindow?.rootViewController as? WPTabBarController else { + let tabBarController = mainWindow?.rootViewController as? WPTabBarController + else { return 0 } @@ -48,8 +49,9 @@ class UntouchableViewController: UIViewController { // we want 0 unless the tab bar has presented a VC guard let mainWindow = UIApplication.shared.delegate?.window, let tabBarController = mainWindow?.rootViewController as? WPTabBarController, - tabBarController.presentedViewController != nil else { - return 0 + tabBarController.presentedViewController != nil + else { + return 0 } return Constants.offsetWithoutTabbar @@ -62,8 +64,9 @@ class UntouchableViewController: UIViewController { override var supportedInterfaceOrientations: UIInterfaceOrientationMask { get { guard let mainWindow = UIApplication.shared.delegate?.window, - let rootController = mainWindow?.rootViewController else { - return .all + let rootController = mainWindow?.rootViewController + else { + return .all } return rootController.topmostPresentedViewController.supportedInterfaceOrientations @@ -79,7 +82,7 @@ private class UntouchableView: UIView { override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { let visibleViews = subviews.filter { view -> Bool in - return view.alpha >= 0.01 && !view.isHidden && view.isUserInteractionEnabled + view.alpha >= 0.01 && !view.isHidden && view.isUserInteractionEnabled } for view in visibleViews { let relativePoint = convert(point, to: view) From a49a4573f932e326a69c3ded1714fc2e6dea2ae9 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 12 Jun 2026 11:13:14 +1200 Subject: [PATCH 2/3] Create the notice presenter's window lazily, attached to the scene UntouchableWindow was created eagerly at setup via init(frame:), which leaves it without a windowScene; such a window does not display (or attaches to the wrong scene) under the UIScene life cycle. The window is now created on the first foreground presentation, attached to the active scene, reattaching it if the scene has changed since the window was created. When no scene is connected yet (e.g. a notice posted during launch, before UIKit connects the scene), the notice stays at the head of the queue and presentation is retried once the app becomes active. The presenter also processes any notice posted before it was created, since the store does not replay state to new subscribers. The frame-based initializer is replaced with a windowScene one. --- .../Classes/System/WordPressAppDelegate.swift | 3 + .../System/Notices/NoticePresenter.swift | 103 ++++++++++++++---- .../System/Notices/UntouchableWindow.swift | 4 +- 3 files changed, 86 insertions(+), 24 deletions(-) diff --git a/WordPress/Classes/System/WordPressAppDelegate.swift b/WordPress/Classes/System/WordPressAppDelegate.swift index 7de881d30e7b..3dbab1e02b20 100644 --- a/WordPress/Classes/System/WordPressAppDelegate.swift +++ b/WordPress/Classes/System/WordPressAppDelegate.swift @@ -141,6 +141,9 @@ public class WordPressAppDelegate: UIResponder, UIApplicationDelegate { InteractiveNotificationsManager.shared.registerForUserNotifications() setupPingHub() setupBackgroundRefresh(application) + // The notice presenter must exist from process launch: background launches + // (e.g. the share extension's background URL session) post notices that it + // delivers as local notifications, with no UI visible. setupNoticePresenter() DebugMenuViewController.configure(in: window) AppTips.initialize() diff --git a/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift b/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift index 21b86917ff40..c25bbe30c049 100644 --- a/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift +++ b/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift @@ -46,10 +46,14 @@ class NoticePresenter { private let store: NoticeStore private let animator: NoticeAnimator - private let window: UntouchableWindow + /// The overlay window hosting foreground notices. Created lazily on the first + /// foreground presentation: the presenter itself is created at process launch, + /// when no scene (and hence no window to attach to) may exist yet, e.g. on a + /// background launch. Background notices don't need a window at all. + private var window: UntouchableWindow? private var view: UIView { - guard let view = window.rootViewController?.view else { - fatalError("Root view controller shouldn't be nil") + guard let view = window?.rootViewController?.view else { + fatalError("The notice window doesn't exist or has no root view controller") } return view } @@ -62,6 +66,11 @@ class NoticePresenter { private var notificationObservers = Set() + /// Set when a foreground notice could not be presented because no scene was + /// connected (e.g. during launch). The notice stays at the head of the queue + /// and presentation is retried when the app becomes active. + private var retriesPresentationOnActivation = false + init( store: NoticeStore = StoreContainer.shared.notice, animator: NoticeAnimator = NoticeAnimator( @@ -73,20 +82,6 @@ class NoticePresenter { self.store = store self.animator = animator - let windowFrame: CGRect - if let mainWindow = UIApplication.shared.mainWindow { - windowFrame = mainWindow.offsetToAvoidStatusBar() - } else { - windowFrame = .zero - } - window = UntouchableWindow(frame: windowFrame) - - // this window level may affect some UI elements like share sheets. - // however, since the alerts aren't permanently on screen, this isn't - // often a problem. - window.windowLevel = .alert - window.isHidden = true - // Keep the storeReceipt to prevent the `onChange` subscription from being deactivated. storeReceipt = store.onChange { [weak self] in self?.onStoreChange() @@ -94,6 +89,14 @@ class NoticePresenter { listenToKeyboardEvents() listenToOrientationChangeEvents() + listenToApplicationActivation() + + // `onChange` doesn't replay the current state to a new subscriber. Process any + // notice that was posted before this presenter existed so it doesn't block the + // queue forever. + if store.currentNotice != nil { + onStoreChange() + } } // MARK: - Events @@ -163,7 +166,23 @@ class NoticePresenter { return } - containerView.bottomConstraint?.constant = -self.window.untouchableViewController.offsetOnscreen + guard let window = self.window else { + return + } + containerView.bottomConstraint?.constant = -window.untouchableViewController.offsetOnscreen + } + .store(in: ¬ificationObservers) + } + + private func listenToApplicationActivation() { + NotificationCenter.default + .publisher(for: UIApplication.didBecomeActiveNotification) + .sink { [weak self] _ in + guard let self, self.retriesPresentationOnActivation else { + return + } + self.retriesPresentationOnActivation = false + self.onStoreChange() } .store(in: ¬ificationObservers) } @@ -187,6 +206,9 @@ class NoticePresenter { if let presentation = present(notice) { currentNoticePresentation = presentation + } else if retriesPresentationOnActivation { + // The notice stays at the head of the queue; the activation observer + // re-runs this method once a scene is available. } else { // We were not able to show the `notice` so we will dispatch a .clear action. This // should prevent us from getting in a stuck state where `NoticeStore` thinks its @@ -234,6 +256,14 @@ class NoticePresenter { } private func presentNoticeInForeground(_ notice: Notice) -> NoticePresentation? { + // No scene is connected yet (e.g. the launch sequence posted this notice + // before UIKit connected the scene). Keep the notice queued and retry when + // the app becomes active, when a scene is guaranteed to exist. + guard let window = ensureWindow() else { + retriesPresentationOnActivation = true + return nil + } + generator.prepare() let noticeView = NoticeView(notice: notice) @@ -271,7 +301,7 @@ class NoticePresenter { window.isHidden = false // Mask must be initialized after the window is shown or the view.frame will be zero - view.mask = MaskView(parent: view, untouchableViewController: self.window.untouchableViewController) + view.mask = MaskView(parent: view, untouchableViewController: window.untouchableViewController) let offScreenBottomOffset = offScreenNoticeContainerBottomOffset(for: noticeContainerView) let fromState = animator.state( @@ -303,6 +333,35 @@ class NoticePresenter { return NoticePresentation(notice: notice, containerView: noticeContainerView) } + /// Returns the overlay window, creating it on first use. Returns nil when no + /// scene is connected: a window not attached to a scene would never display. + private func ensureWindow() -> UntouchableWindow? { + if let window { + // The scene can disconnect and reconnect while the process lives. + // Reattach the cached window so it isn't bound to a dead scene. + if let scene = UIApplication.shared.mainWindow?.windowScene, window.windowScene !== scene { + window.windowScene = scene + } + return window + } + guard let mainWindow = UIApplication.shared.mainWindow, + let windowScene = mainWindow.windowScene + else { + return nil + } + let window = UntouchableWindow(windowScene: windowScene) + window.frame = mainWindow.offsetToAvoidStatusBar() + + // this window level may affect some UI elements like share sheets. + // however, since the alerts aren't permanently on screen, this isn't + // often a problem. + window.windowLevel = .alert + window.isHidden = true + + self.window = window + return window + } + private func addNoticeContainerToPresentingViewController(_ noticeContainer: UIView) { view.addSubview(noticeContainer) } @@ -359,7 +418,7 @@ class NoticePresenter { if self?.currentNoticePresentation == nil { UIAccessibility.post(notification: .layoutChanged, argument: nil) - self?.window.isHidden = true + self?.window?.isHidden = true } } ) @@ -372,7 +431,7 @@ class NoticePresenter { case .present(let keyboardHeight): return -keyboardHeight case .notPresent: - return -window.untouchableViewController.offsetOnscreen + return -(window?.untouchableViewController.offsetOnscreen ?? 0) } } @@ -381,7 +440,7 @@ class NoticePresenter { case .present(let keyboardHeight): return -keyboardHeight + container.bounds.height case .notPresent: - return window.untouchableViewController.offsetOffscreen + return window?.untouchableViewController.offsetOffscreen ?? 0 } } diff --git a/WordPress/Classes/ViewRelated/System/Notices/UntouchableWindow.swift b/WordPress/Classes/ViewRelated/System/Notices/UntouchableWindow.swift index 882539aa260b..c787349e21a1 100644 --- a/WordPress/Classes/ViewRelated/System/Notices/UntouchableWindow.swift +++ b/WordPress/Classes/ViewRelated/System/Notices/UntouchableWindow.swift @@ -3,8 +3,8 @@ import UIKit @objc class UntouchableWindow: UIWindow { let untouchableViewController = UntouchableViewController() - override init(frame: CGRect) { - super.init(frame: frame) + override init(windowScene: UIWindowScene) { + super.init(windowScene: windowScene) rootViewController = untouchableViewController } From a6a1f300007637dd43d0e6c91d3c7de5719e1e88 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 12 Jun 2026 13:09:11 +1200 Subject: [PATCH 3/3] Resolve notice views from the window or container at hand The presenter's view accessor required the lazily created window to exist, backed by a fatalError; its safety depended on guards scattered across the call sites. Presentation now reads the view from the window it just ensured, and the keyboard and dismissal paths reach it through the container's superview, so the accessor and its fatalError are gone. --- .../System/Notices/NoticePresenter.swift | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift b/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift index c25bbe30c049..22e63752388a 100644 --- a/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift +++ b/WordPress/Classes/ViewRelated/System/Notices/NoticePresenter.swift @@ -51,12 +51,6 @@ class NoticePresenter { /// when no scene (and hence no window to attach to) may exist yet, e.g. on a /// background launch. Background notices don't need a window at all. private var window: UntouchableWindow? - private var view: UIView { - guard let view = window?.rootViewController?.view else { - fatalError("The notice window doesn't exist or has no root view controller") - } - return view - } private let generator = UINotificationFeedbackGenerator() private var storeReceipt: Receipt? @@ -124,7 +118,7 @@ class NoticePresenter { animations: { currentContainer.bottomConstraint?.constant = self.onScreenNoticeContainerBottomConstraintConstant - self.view.layoutIfNeeded() + currentContainer.superview?.layoutIfNeeded() } ) } @@ -148,7 +142,7 @@ class NoticePresenter { animations: { currentContainer.bottomConstraint?.constant = self.onScreenNoticeContainerBottomConstraintConstant - self.view.layoutIfNeeded() + currentContainer.superview?.layoutIfNeeded() } ) } @@ -264,15 +258,17 @@ class NoticePresenter { return nil } + let view: UIView = window.untouchableViewController.view + generator.prepare() let noticeView = NoticeView(notice: notice) noticeView.translatesAutoresizingMaskIntoConstraints = false let noticeContainerView = NoticeContainerView(noticeView: noticeView) - addNoticeContainerToPresentingViewController(noticeContainerView) - addBottomConstraintToNoticeContainer(noticeContainerView) - addTopConstraintToNoticeContainer(noticeContainerView) + view.addSubview(noticeContainerView) + addBottomConstraintToNoticeContainer(noticeContainerView, in: view) + addTopConstraintToNoticeContainer(noticeContainerView, in: view) // At regular width, the notice shouldn't be any wider than 1/2 the app's width noticeContainerView.noticeWidthConstraint = noticeView.widthAnchor.constraint( @@ -362,17 +358,13 @@ class NoticePresenter { return window } - private func addNoticeContainerToPresentingViewController(_ noticeContainer: UIView) { - view.addSubview(noticeContainer) - } - - private func addBottomConstraintToNoticeContainer(_ container: NoticeContainerView) { + private func addBottomConstraintToNoticeContainer(_ container: NoticeContainerView, in view: UIView) { let constraint = container.bottomAnchor.constraint(equalTo: view.bottomAnchor) container.bottomConstraint = constraint constraint.isActive = false } - private func addTopConstraintToNoticeContainer(_ container: NoticeContainerView) { + private func addTopConstraintToNoticeContainer(_ container: NoticeContainerView, in view: UIView) { let constraint = container.topAnchor.constraint(equalTo: view.bottomAnchor) container.topConstraint = constraint constraint.priority = UILayoutPriority.defaultHigh @@ -401,12 +393,17 @@ class NoticePresenter { private func dismissForegroundNotice() { guard let container = currentNoticePresentation?.containerView, - container.superview != nil + let containerSuperview = container.superview else { return } let bottomOffset = offScreenNoticeContainerBottomOffset(for: container) - let toState = animator.state(for: container, in: view, withTransition: .offscreen, bottomOffset: bottomOffset) + let toState = animator.state( + for: container, + in: containerSuperview, + withTransition: .offscreen, + bottomOffset: bottomOffset + ) animator.animatePresentation( fromState: {}, toState: toState,