summaryrefslogtreecommitdiffhomepage
path: root/ios/MullvadVPN/Notifications/UI/NotificationController.swift
blob: b29e3d2baeecc4b6d28f438eb235e9e07013e286 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//
//  NotificationController.swift
//  MullvadVPN
//
//  Created by pronebird on 01/06/2021.
//  Copyright © 2021 Mullvad VPN AB. All rights reserved.
//

import UIKit

final class NotificationController: UIViewController {
    let bannerView: NotificationBannerView = {
        let bannerView = NotificationBannerView()
        bannerView.translatesAutoresizingMaskIntoConstraints = false
        bannerView.isAccessibilityElement = true
        return bannerView
    }()

    private var showBannerConstraint: NSLayoutConstraint?
    private var hideBannerConstraint: NSLayoutConstraint?

    private(set) var showsBanner = false
    private var lastNotification: InAppNotificationDescriptor?

    override func loadView() {
        view = NotificationContainerView(frame: UIScreen.main.bounds)
        view.clipsToBounds = true
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        showBannerConstraint = bannerView.topAnchor.constraint(equalTo: view.topAnchor)
        hideBannerConstraint = bannerView.bottomAnchor.constraint(equalTo: view.topAnchor)

        view.addSubview(bannerView)

        let verticalConstraint = showsBanner ? showBannerConstraint : hideBannerConstraint
        NSLayoutConstraint.activate([
            verticalConstraint!,
            bannerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            bannerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        ])
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        updateAccessibilityFrame()
    }

    func toggleBanner(show: Bool, animated: Bool, completion: (() -> Void)? = nil) {
        guard showsBanner != show else {
            completion?()
            return
        }

        showsBanner = show

        if show {
            // Make sure to lay out the banner before animating its appearance to
            // avoid undesired horizontal expansion animation.
            view.layoutIfNeeded()

            hideBannerConstraint?.isActive = false
            showBannerConstraint?.isActive = true
        } else {
            showBannerConstraint?.isActive = false
            hideBannerConstraint?.isActive = true
        }

        if animated {
            let timing = UISpringTimingParameters(
                dampingRatio: 0.7,
                initialVelocity: CGVector(dx: 0, dy: 1)
            )
            let animator = UIViewPropertyAnimator(duration: 0.8, timingParameters: timing)
            animator.isInterruptible = false
            animator.addAnimations {
                self.view.layoutIfNeeded()
            }
            animator.addCompletion { _ in
                completion?()
            }
            animator.startAnimation()
        } else {
            view.layoutIfNeeded()
            completion?()
        }
    }

    func setNotification(_ notification: InAppNotificationDescriptor, animated: Bool) {
        guard lastNotification != notification else { return }

        lastNotification = notification

        bannerView.title = notification.title
        bannerView.body = notification.body
        bannerView.style = notification.style
        bannerView.action = notification.action
        bannerView.accessibilityLabel = "\(notification.title)\n\(notification.body.string)"

        // Do not emit the .layoutChanged unless the banner is focused to avoid capturing
        // the voice over focus.
        if bannerView.accessibilityElementIsFocused() {
            UIAccessibility.post(notification: .layoutChanged, argument: bannerView)
        }
    }

    func setNotifications(_ notifications: [InAppNotificationDescriptor], animated: Bool) {
        let nextNotification = notifications.first

        if let notification = nextNotification {
            setNotification(notification, animated: showsBanner)
            toggleBanner(show: true, animated: true)
        } else {
            lastNotification = nil
            toggleBanner(show: false, animated: animated)
        }
    }

    private func updateAccessibilityFrame() {
        let layoutFrame = bannerView.layoutMarginsGuide.layoutFrame
        bannerView.accessibilityFrame = UIAccessibility.convertToScreenCoordinates(
            layoutFrame,
            in: view
        )
    }
}