241 lines
8.7 KiB
Swift
241 lines
8.7 KiB
Swift
//
|
|
// HeaderBarView.swift
|
|
// MullvadVPN
|
|
//
|
|
// Created by pronebird on 19/06/2020.
|
|
// Copyright © 2025 Mullvad VPN AB. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
class HeaderBarView: UIView {
|
|
private let brandNameImage = UIImage(named: "LogoText")?
|
|
.withTintColor(UIColor.HeaderBar.brandNameColor, renderingMode: .alwaysOriginal)
|
|
|
|
private let logoImageView = UIImageView(image: UIImage(named: "LogoIcon"))
|
|
|
|
private lazy var brandNameImageView: UIImageView = {
|
|
let imageView = UIImageView(image: brandNameImage)
|
|
imageView.contentMode = .scaleAspectFill
|
|
return imageView
|
|
}()
|
|
|
|
private let deviceInfoHolder: UIStackView = {
|
|
let stackView = UIStackView()
|
|
stackView.axis = .horizontal
|
|
stackView.distribution = .fill
|
|
stackView.spacing = 16.0
|
|
return stackView
|
|
}()
|
|
|
|
private lazy var deviceNameLabel: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .mullvadMiniSemiBold
|
|
label.textColor = UIColor(white: 1.0, alpha: 0.8)
|
|
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
|
label.setAccessibilityIdentifier(.headerDeviceNameLabel)
|
|
return label
|
|
}()
|
|
|
|
private lazy var timeLeftLabel: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .mullvadMiniSemiBold
|
|
label.textColor = UIColor(white: 1.0, alpha: 0.8)
|
|
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
|
return label
|
|
}()
|
|
|
|
private lazy var buttonContainer: UIStackView = {
|
|
let stackView = UIStackView(arrangedSubviews: [accountButton, settingsButton])
|
|
stackView.spacing = 12
|
|
return stackView
|
|
}()
|
|
|
|
private let borderLayer: CALayer = {
|
|
let layer = CALayer()
|
|
layer.backgroundColor = UIColor.HeaderBar.dividerColor.cgColor
|
|
return layer
|
|
}()
|
|
|
|
let accountButton: UIButton = {
|
|
let button = makeHeaderBarButton(with: UIImage.Buttons.account)
|
|
button.setAccessibilityIdentifier(.accountButton)
|
|
button.accessibilityLabel = NSLocalizedString(
|
|
"HEADER_BAR_ACCOUNT_BUTTON_ACCESSIBILITY_LABEL",
|
|
tableName: "HeaderBar",
|
|
value: "Account",
|
|
comment: ""
|
|
)
|
|
button.heightAnchor.constraint(equalToConstant: UIMetrics.Button.barButtonSize).isActive = true
|
|
button.widthAnchor.constraint(equalTo: button.heightAnchor, multiplier: 1).isActive = true
|
|
return button
|
|
}()
|
|
|
|
let settingsButton: UIButton = {
|
|
let button = makeHeaderBarButton(with: UIImage.Buttons.settings)
|
|
button.setAccessibilityIdentifier(.settingsButton)
|
|
button.accessibilityLabel = NSLocalizedString(
|
|
"HEADER_BAR_SETTINGS_BUTTON_ACCESSIBILITY_LABEL",
|
|
tableName: "HeaderBar",
|
|
value: "Settings",
|
|
comment: ""
|
|
)
|
|
button.heightAnchor.constraint(equalToConstant: UIMetrics.Button.barButtonSize).isActive = true
|
|
button.widthAnchor.constraint(equalTo: button.heightAnchor, multiplier: 1).isActive = true
|
|
return button
|
|
}()
|
|
|
|
class func makeHeaderBarButton(with image: UIImage?) -> IncreasedHitButton {
|
|
let buttonImage = image?.withTintColor(UIColor.HeaderBar.buttonColor, renderingMode: .alwaysOriginal)
|
|
let barButton = IncreasedHitButton(type: .system)
|
|
barButton.setBackgroundImage(buttonImage, for: .normal)
|
|
barButton.configureForAutoLayout()
|
|
|
|
return barButton
|
|
}
|
|
|
|
var showsDivider = false {
|
|
didSet {
|
|
if showsDivider {
|
|
layer.addSublayer(borderLayer)
|
|
} else {
|
|
borderLayer.removeFromSuperlayer()
|
|
}
|
|
}
|
|
}
|
|
|
|
var isDeviceInfoHidden = false {
|
|
didSet {
|
|
deviceInfoHolder.arrangedSubviews.forEach { $0.isHidden = isDeviceInfoHidden }
|
|
}
|
|
}
|
|
|
|
private var isAccountButtonHidden = false {
|
|
didSet {
|
|
accountButton.isHidden = isAccountButtonHidden
|
|
}
|
|
}
|
|
|
|
private var timeLeft: Date? {
|
|
didSet {
|
|
if let timeLeft {
|
|
let formattedTimeLeft = NSLocalizedString(
|
|
"TIME_LEFT_HEADER_VIEW",
|
|
tableName: "Account",
|
|
value: "Time left: %@",
|
|
comment: ""
|
|
)
|
|
timeLeftLabel.text = String(
|
|
format: formattedTimeLeft,
|
|
CustomDateComponentsFormatting.localizedString(
|
|
from: Date(),
|
|
to: timeLeft,
|
|
unitsStyle: .full
|
|
) ?? ""
|
|
)
|
|
} else {
|
|
timeLeftLabel.text = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
private var deviceName: String? {
|
|
didSet {
|
|
if let deviceName {
|
|
let formattedDeviceName = NSLocalizedString(
|
|
"DEVICE_NAME_HEADER_VIEW",
|
|
tableName: "Account",
|
|
value: "Device name: %@",
|
|
comment: ""
|
|
)
|
|
deviceNameLabel.text = String(format: formattedDeviceName, deviceName)
|
|
} else {
|
|
deviceNameLabel.text = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
directionalLayoutMargins = NSDirectionalEdgeInsets(
|
|
top: 0,
|
|
leading: 16,
|
|
bottom: 0,
|
|
trailing: 16
|
|
)
|
|
|
|
accessibilityContainerType = .semanticGroup
|
|
setAccessibilityIdentifier(.headerBarView)
|
|
|
|
let brandImageSize = brandNameImage?.size ?? .zero
|
|
let brandNameAspectRatio = brandImageSize.width / max(brandImageSize.height, 1)
|
|
|
|
var buttonContainerTrailingAdjustment: CGFloat = 0
|
|
if let buttonImageWidth = settingsButton.currentImage?.size.width {
|
|
buttonContainerTrailingAdjustment = max((UIMetrics.Button.barButtonSize - buttonImageWidth) / 2, 0)
|
|
}
|
|
|
|
[deviceNameLabel, timeLeftLabel].forEach { deviceInfoHolder.addArrangedSubview($0) }
|
|
|
|
addConstrainedSubviews([logoImageView, brandNameImageView, buttonContainer, deviceInfoHolder]) {
|
|
logoImageView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor)
|
|
logoImageView.centerYAnchor.constraint(equalTo: brandNameImageView.centerYAnchor)
|
|
logoImageView.widthAnchor.constraint(equalToConstant: UIMetrics.headerBarLogoSize)
|
|
logoImageView.heightAnchor.constraint(equalTo: logoImageView.widthAnchor, multiplier: 1)
|
|
|
|
brandNameImageView.leadingAnchor.constraint(
|
|
equalToSystemSpacingAfter: logoImageView.trailingAnchor,
|
|
multiplier: 1
|
|
)
|
|
brandNameImageView.topAnchor.constraint(
|
|
equalTo: layoutMarginsGuide.topAnchor,
|
|
constant: UIMetrics.headerBarLogoSize * 0.5
|
|
)
|
|
brandNameImageView.widthAnchor.constraint(
|
|
equalTo: brandNameImageView.heightAnchor,
|
|
multiplier: brandNameAspectRatio
|
|
)
|
|
brandNameImageView.heightAnchor.constraint(equalToConstant: UIMetrics.headerBarBrandNameHeight)
|
|
|
|
buttonContainer.centerYAnchor.constraint(equalTo: brandNameImageView.centerYAnchor)
|
|
buttonContainer.trailingAnchor.constraint(
|
|
equalTo: layoutMarginsGuide.trailingAnchor,
|
|
constant: buttonContainerTrailingAdjustment
|
|
)
|
|
|
|
deviceInfoHolder.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor)
|
|
deviceInfoHolder.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor)
|
|
deviceInfoHolder.topAnchor.constraint(equalToSystemSpacingBelow: logoImageView.bottomAnchor, multiplier: 1)
|
|
layoutMarginsGuide.bottomAnchor.constraint(
|
|
equalToSystemSpacingBelow: deviceInfoHolder.bottomAnchor,
|
|
multiplier: 1
|
|
)
|
|
}
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
borderLayer.frame = CGRect(x: 0, y: frame.maxY - 1, width: frame.width, height: 1)
|
|
brandNameImageView.isHidden = shouldHideBrandName()
|
|
}
|
|
|
|
/// Returns `true` if container holding buttons intersects brand name.
|
|
private func shouldHideBrandName() -> Bool {
|
|
let buttonContainerRect = buttonContainer.convert(buttonContainer.bounds, to: nil)
|
|
let brandNameRect = brandNameImageView.convert(brandNameImageView.bounds, to: nil)
|
|
|
|
return brandNameRect.intersects(buttonContainerRect)
|
|
}
|
|
|
|
func update(configuration: RootConfiguration) {
|
|
deviceName = configuration.deviceName
|
|
timeLeft = configuration.expiry
|
|
isAccountButtonHidden = !configuration.showsAccountButton
|
|
}
|
|
}
|