Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make alert view scrollable #5354

Merged
merged 1 commit into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading