Skip to content

Commit

Permalink
Make alert view scrollable
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon Petersson authored and buggmagnet committed Oct 27, 2023
1 parent 44676ed commit 3001603
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 60 deletions.
5 changes: 4 additions & 1 deletion ios/MullvadVPN/UI appearance/UIMetrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ enum UIMetrics {
trailing: 16
)

/// Spacing between views in container (main view) in `CustomAlertViewController`
/// Spacing between view containers in `CustomAlertViewController`
static let containerSpacing: CGFloat = 16

/// Spacing between view containers in `CustomAlertViewController`
static let interContainerSpacing: CGFloat = 4
}

enum DimmingView {
Expand Down
175 changes: 116 additions & 59 deletions ios/MullvadVPN/View controllers/Alert/AlertViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,34 @@ class AlertViewController: UIViewController {
typealias Handler = () -> Void
var onDismiss: Handler?

private let containerView: UIStackView = {
let view = UIStackView()
private let scrollView = UIScrollView()
private var scrollViewHeightConstraint: NSLayoutConstraint!
private let presentation: AlertPresentation

private let viewContainer: UIView = {
let view = UIView()

view.axis = .vertical
view.backgroundColor = .secondaryColor
view.layer.cornerRadius = 11

return view
}()

private let buttonView: UIStackView = {
let view = UIStackView()

view.axis = .vertical
view.spacing = UIMetrics.CustomAlert.containerSpacing
view.isLayoutMarginsRelativeArrangement = true
view.directionalLayoutMargins = UIMetrics.CustomAlert.containerMargins

return view
}()

private let contentView: UIStackView = {
let view = UIStackView()

view.axis = .vertical
view.spacing = UIMetrics.CustomAlert.containerSpacing
view.isLayoutMarginsRelativeArrangement = true
view.directionalLayoutMargins = UIMetrics.CustomAlert.containerMargins
Expand All @@ -59,18 +81,45 @@ class AlertViewController: UIViewController {
private var handlers = [UIButton: Handler]()

init(presentation: AlertPresentation) {
self.presentation = presentation

super.init(nibName: nil, bundle: nil)

setUp(
header: presentation.header,
title: presentation.title,
icon: presentation.icon
) {
if let message = presentation.attributedMessage {
addMessage(message)
} else if let message = presentation.message {
addMessage(message)
}
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

view.layoutIfNeeded()
scrollViewHeightConstraint.constant = scrollView.contentSize.height

adjustButtonMargins()
}

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = .black.withAlphaComponent(0.5)

setContent()
setConstraints()
}

private func setContent() {
presentation.icon.flatMap { addIcon($0) }
presentation.header.flatMap { addHeader($0) }
presentation.title.flatMap { addTitle($0) }

if let message = presentation.attributedMessage {
addMessage(message)
} else if let message = presentation.message {
addMessage(message)
}

presentation.buttons.forEach { action in
Expand All @@ -80,65 +129,63 @@ class AlertViewController: UIViewController {
handler: action.handler
)
}
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
// Icon only alerts should have equal top and bottom margin.
if presentation.icon != nil, contentView.arrangedSubviews.count == 1 {
contentView.directionalLayoutMargins.bottom = UIMetrics.CustomAlert.containerMargins.top
}
}

// This code runs before viewDidLoad(). As such, no implicit calls to self.view should be made before this point.
private func setUp(header: String?, title: String?, icon: AlertIcon?, addMessageCallback: () -> Void) {
modalPresentationStyle = .overFullScreen
modalTransitionStyle = .crossDissolve

icon.flatMap { addIcon($0) }
header.flatMap { addHeader($0) }
title.flatMap { addTitle($0) }
addMessageCallback()

containerView.arrangedSubviews.last.flatMap {
containerView.setCustomSpacing(UIMetrics.CustomAlert.containerMargins.top, after: $0)
private func setConstraints() {
viewContainer.addConstrainedSubviews([scrollView, buttonView]) {
scrollView.pinEdgesToSuperview(.all().excluding(.bottom))
buttonView.pinEdgesToSuperview(.all().excluding(.top))
buttonView.topAnchor.constraint(equalTo: scrollView.bottomAnchor)
}

// Icon only alerts should have equal top and bottom margin.
if icon != nil, containerView.arrangedSubviews.count == 1 {
containerView.directionalLayoutMargins.bottom = UIMetrics.CustomAlert.containerMargins.top
scrollView.addConstrainedSubviews([contentView]) {
contentView.pinEdgesToSuperview(.all())
contentView.widthAnchor.constraint(equalTo: scrollView.widthAnchor)
}
}

override func viewDidLoad() {
super.viewDidLoad()

view.backgroundColor = .black.withAlphaComponent(0.5)
scrollViewHeightConstraint = scrollView.heightAnchor.constraint(equalToConstant: 0).withPriority(.defaultLow)
scrollViewHeightConstraint.isActive = true

view.addConstrainedSubviews([containerView]) {
containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
view.addConstrainedSubviews([viewContainer]) {
viewContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor)
viewContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor)

containerView.widthAnchor
viewContainer.widthAnchor
.constraint(lessThanOrEqualToConstant: UIMetrics.preferredFormSheetContentSize.width)

containerView.leadingAnchor
viewContainer.topAnchor
.constraint(greaterThanOrEqualTo: view.layoutMarginsGuide.topAnchor)
.withPriority(.defaultHigh)

view.layoutMarginsGuide.bottomAnchor
.constraint(greaterThanOrEqualTo: viewContainer.bottomAnchor)
.withPriority(.defaultHigh)

viewContainer.leadingAnchor
.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor)
.withPriority(.defaultHigh)

view.layoutMarginsGuide.trailingAnchor
.constraint(equalTo: containerView.trailingAnchor)
.constraint(equalTo: viewContainer.trailingAnchor)
.withPriority(.defaultHigh)
}
}

func addAction(title: String, style: AlertActionStyle, handler: (() -> Void)? = nil) {
// The presence of a button should reset any custom button margin to default.
containerView.directionalLayoutMargins.bottom = UIMetrics.CustomAlert.containerMargins.bottom

let button = AppButton(style: style.buttonStyle)

button.setTitle(title, for: .normal)
button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
private func adjustButtonMargins() {
if !buttonView.arrangedSubviews.isEmpty {
// The presence of a button should yield a custom top margin.
buttonView.directionalLayoutMargins.top = UIMetrics.CustomAlert.interContainerSpacing

containerView.addArrangedSubview(button)
handler.flatMap { handlers[button] = $0 }
// Buttons below scrollable content should have more margin.
if scrollView.contentSize.height > scrollView.bounds.size.height {
buttonView.directionalLayoutMargins.top = UIMetrics.CustomAlert.containerSpacing
}
}
}

private func addHeader(_ title: String) {
Expand All @@ -151,8 +198,8 @@ class AlertViewController: UIViewController {
header.textAlignment = .center
header.numberOfLines = 0

containerView.addArrangedSubview(header)
containerView.setCustomSpacing(16, after: header)
contentView.addArrangedSubview(header)
contentView.setCustomSpacing(16, after: header)
}

private func addTitle(_ title: String) {
Expand All @@ -164,8 +211,8 @@ class AlertViewController: UIViewController {
label.adjustsFontForContentSizeCategory = true
label.numberOfLines = 0

containerView.addArrangedSubview(label)
containerView.setCustomSpacing(8, after: label)
contentView.addArrangedSubview(label)
contentView.setCustomSpacing(8, after: label)
}

private func addMessage(_ message: String) {
Expand All @@ -185,7 +232,7 @@ class AlertViewController: UIViewController {
label.adjustsFontForContentSizeCategory = true
label.numberOfLines = 0

containerView.addArrangedSubview(label)
contentView.addArrangedSubview(label)
}

private func addMessage(_ message: NSAttributedString) {
Expand All @@ -196,12 +243,22 @@ class AlertViewController: UIViewController {
label.adjustsFontForContentSizeCategory = true
label.numberOfLines = 0

containerView.addArrangedSubview(label)
contentView.addArrangedSubview(label)
}

private func addIcon(_ icon: AlertIcon) {
let iconView = icon == .spinner ? getSpinnerView() : getImageView(for: icon)
containerView.addArrangedSubview(iconView)
contentView.addArrangedSubview(iconView)
}

private func addAction(title: String, style: AlertActionStyle, handler: (() -> Void)? = nil) {
let button = AppButton(style: style.buttonStyle)

button.setTitle(title, for: .normal)
button.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)

buttonView.addArrangedSubview(button)
handler.flatMap { handlers[button] = $0 }
}

private func getImageView(for icon: AlertIcon) -> UIView {
Expand Down

0 comments on commit 3001603

Please sign in to comment.