Skip to content

Commit

Permalink
Update cell layout and behavior for obfuscation methods
Browse files Browse the repository at this point in the history
  • Loading branch information
rablador committed Oct 24, 2024
1 parent 9fe50f7 commit 22b078b
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 58 deletions.
8 changes: 8 additions & 0 deletions ios/MullvadVPN.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,8 @@
7A27E3C92CAE85710088BCFF /* SettingsInfoButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3C82CAE85660088BCFF /* SettingsInfoButtonItem.swift */; };
7A27E3CB2CAE861D0088BCFF /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3CA2CAE86170088BCFF /* SettingsViewModel.swift */; };
7A27E3CD2CB814EF0088BCFF /* DAITAInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3CC2CB814EA0088BCFF /* DAITAInfoView.swift */; };
7A27E3CF2CBD4A8C0088BCFF /* SelectableSettingsDetailsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3CE2CBD4A830088BCFF /* SelectableSettingsDetailsCell.swift */; };
7A27E3D12CC299F90088BCFF /* VPNSettingsDetailsButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27E3D02CC299E60088BCFF /* VPNSettingsDetailsButtonItem.swift */; };
7A28826A2BA8336600FD9F20 /* VPNSettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2882692BA8336600FD9F20 /* VPNSettingsCoordinator.swift */; };
7A2960F62A963F7500389B82 /* AlertCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960F52A963F7500389B82 /* AlertCoordinator.swift */; };
7A2960FD2A964BB700389B82 /* AlertPresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2960FC2A964BB700389B82 /* AlertPresentation.swift */; };
Expand Down Expand Up @@ -1803,6 +1805,8 @@
7A27E3C82CAE85660088BCFF /* SettingsInfoButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsInfoButtonItem.swift; sourceTree = "<group>"; };
7A27E3CA2CAE86170088BCFF /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
7A27E3CC2CB814EA0088BCFF /* DAITAInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAITAInfoView.swift; sourceTree = "<group>"; };
7A27E3CE2CBD4A830088BCFF /* SelectableSettingsDetailsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableSettingsDetailsCell.swift; sourceTree = "<group>"; };
7A27E3D02CC299E60088BCFF /* VPNSettingsDetailsButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsDetailsButtonItem.swift; sourceTree = "<group>"; };
7A2882692BA8336600FD9F20 /* VPNSettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNSettingsCoordinator.swift; sourceTree = "<group>"; };
7A2960F52A963F7500389B82 /* AlertCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCoordinator.swift; sourceTree = "<group>"; };
7A2960FC2A964BB700389B82 /* AlertPresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertPresentation.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2803,6 +2807,7 @@
children = (
7A9FA1432A2E3FE5000B728D /* CheckableSettingsCell.swift */,
7A1A264A2A29D65E00B978AA /* SelectableSettingsCell.swift */,
7A27E3CE2CBD4A830088BCFF /* SelectableSettingsDetailsCell.swift */,
5819C2162729595500D6EC38 /* SettingsAddDNSEntryCell.swift */,
582BB1AE229566420055B6EF /* SettingsCell.swift */,
5864AF0029C7879B005B0CD9 /* SettingsCellFactory.swift */,
Expand Down Expand Up @@ -2845,6 +2850,7 @@
5864AF0229C7879B005B0CD9 /* VPNSettingsCellFactory.swift */,
584D26C3270C855A004EA533 /* VPNSettingsDataSource.swift */,
587EB6732714520600123C75 /* VPNSettingsDataSourceDelegate.swift */,
7A27E3D02CC299E60088BCFF /* VPNSettingsDetailsButtonItem.swift */,
7A6F2FAE2AFE36E7006D0856 /* VPNSettingsInfoButtonItem.swift */,
5871167E2910035700D41AAC /* VPNSettingsInteractor.swift */,
58ACF6482655365700ACE4B7 /* VPNSettingsViewController.swift */,
Expand Down Expand Up @@ -5658,6 +5664,7 @@
A91614D62B10B26B00F416EB /* TunnelControlViewModel.swift in Sources */,
7A5869972B32EA4500640D27 /* AppButton.swift in Sources */,
586C0D8F2B03D88100E7CDD7 /* ProxyProtocolConfigurationItemIdentifier.swift in Sources */,
7A27E3CF2CBD4A8C0088BCFF /* SelectableSettingsDetailsCell.swift in Sources */,
7A27E3CB2CAE861D0088BCFF /* SettingsViewModel.swift in Sources */,
588527B2276B3F0700BAA373 /* LoadTunnelConfigurationOperation.swift in Sources */,
7A9F29392CABFAFC005F2089 /* InfoHeaderView.swift in Sources */,
Expand Down Expand Up @@ -5854,6 +5861,7 @@
F0DA87492A9CBA9F006044F1 /* AccountDeviceRow.swift in Sources */,
58FF9FE42B075BDD00E4C97D /* EditAccessMethodItemIdentifier.swift in Sources */,
5878A27329091D6D0096FC88 /* TunnelBlockObserver.swift in Sources */,
7A27E3D12CC299F90088BCFF /* VPNSettingsDetailsButtonItem.swift in Sources */,
A9E034642ABB302000E59A5A /* UIEdgeInsets+Extensions.swift in Sources */,
58CEB2E92AFBBA4A00E6E088 /* AddAccessMethodCoordinator.swift in Sources */,
58DFF7D02B02560400F864E0 /* NSAttributedString+Extensions.swift in Sources */,
Expand Down
3 changes: 2 additions & 1 deletion ios/MullvadVPN/Classes/AccessbilityIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ public enum AccessibilityIdentifier: String {
case wireGuardObfuscationAutomatic
case wireGuardObfuscationPort
case wireGuardObfuscationOff
case wireGuardObfuscationOn
case wireGuardObfuscationUDPTCP
case wireGuardObfuscationShadowsocks
case wireGuardPort

// Custom DNS
Expand Down
2 changes: 2 additions & 0 deletions ios/MullvadVPN/UI appearance/UIMetrics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ enum UIMetrics {
static let customListsCellHeight: CGFloat = 44
static let apiAccessSwitchCellTrailingMargin: CGFloat = apiAccessInsetLayoutMargins.trailing - 4
static let apiAccessPickerListContentInsetTop: CGFloat = 16
static let buttonSeparatorHeight: CGFloat = 22
static let detailsButtonSize: CGFloat = 60
}

enum InAppBannerNotification {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class CheckableSettingsCell: SettingsCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)

setLeftView(checkboxView, spacing: UIMetrics.SettingsCell.checkableSettingsCellLeftViewSpacing)
setCheckboxView()
selectedBackgroundView?.backgroundColor = .clear
}

Expand All @@ -24,8 +24,7 @@ class CheckableSettingsCell: SettingsCell {

override func prepareForReuse() {
super.prepareForReuse()

setLeftView(checkboxView, spacing: UIMetrics.SettingsCell.checkableSettingsCellLeftViewSpacing)
setCheckboxView()
}

override func setSelected(_ selected: Bool, animated: Bool) {
Expand All @@ -39,4 +38,17 @@ class CheckableSettingsCell: SettingsCell {

contentView.layoutMargins.left = 0
}

private func setCheckboxView() {
setLeadingView { superview in
superview.addConstrainedSubviews([checkboxView]) {
checkboxView.pinEdgesToSuperview(PinnableEdges([
.leading(0),
.trailing(UIMetrics.SettingsCell.checkableSettingsCellLeftViewSpacing),
.top(0),
.bottom(0),
]))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import UIKit
class SelectableSettingsCell: SettingsCell {
let tickImageView: UIImageView = {
let imageView = UIImageView(image: UIImage(named: "IconTick"))
imageView.contentMode = .center
imageView.tintColor = .white
imageView.alpha = 0
return imageView
Expand All @@ -19,7 +20,7 @@ class SelectableSettingsCell: SettingsCell {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)

setLeftView(tickImageView, spacing: UIMetrics.SettingsCell.selectableSettingsCellLeftViewSpacing)
setTickView()
selectedBackgroundView?.backgroundColor = UIColor.Cell.Background.selected
}

Expand All @@ -29,13 +30,25 @@ class SelectableSettingsCell: SettingsCell {

override func prepareForReuse() {
super.prepareForReuse()

setLeftView(tickImageView, spacing: UIMetrics.SettingsCell.selectableSettingsCellLeftViewSpacing)
setTickView()
}

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)

tickImageView.alpha = selected ? 1 : 0
}

private func setTickView() {
setLeadingView { superview in
superview.addConstrainedSubviews([tickImageView]) {
tickImageView.pinEdgesToSuperview(PinnableEdges([
.leading(0),
.trailing(UIMetrics.SettingsCell.selectableSettingsCellLeftViewSpacing),
.top(0),
.bottom(0),
]))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// SelectableSettingsDetailsCell.swift
// MullvadVPN
//
// Created by Jon Petersson on 2024-10-14.
// Copyright © 2024 Mullvad VPN AB. All rights reserved.
//

import UIKit

class SelectableSettingsDetailsCell: SelectableSettingsCell {
let viewContainer = UIView()

var action: (() -> Void)?

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)

let actionButton = IncreasedHitButton(type: .system)
actionButton.setImage(UIImage(systemName: "ellipsis"), for: .normal)
actionButton.tintColor = .white
actionButton.addTarget(
self,
action: #selector(didPressActionButton),
for: .valueChanged
)

let separatorView = UIView()
separatorView.backgroundColor = .primaryColor

viewContainer.addConstrainedSubviews([separatorView, actionButton]) {
separatorView.leadingAnchor.constraint(equalTo: viewContainer.leadingAnchor, constant: 16)
separatorView.centerYAnchor.constraint(equalTo: viewContainer.centerYAnchor)
separatorView.heightAnchor.constraint(equalToConstant: UIMetrics.SettingsCell.buttonSeparatorHeight)
separatorView.widthAnchor.constraint(equalToConstant: 1)

actionButton.pinEdgesToSuperview(.all().excluding(.leading))
actionButton.leadingAnchor.constraint(equalTo: separatorView.trailingAnchor)
actionButton.widthAnchor.constraint(equalToConstant: UIMetrics.SettingsCell.detailsButtonSize)
}

setViewContainer()
}

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

override func prepareForReuse() {
super.prepareForReuse()
setViewContainer()
}

private func setViewContainer() {
#if DEBUG
setTrailingView { superview in
superview.addConstrainedSubviews([viewContainer]) {
viewContainer.pinEdgesToSuperview()
}
}
#endif
}

// MARK: - Actions

@objc private func didPressActionButton() {
action?()
}
}
92 changes: 53 additions & 39 deletions ios/MullvadVPN/View controllers/Settings/SettingsCell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@ enum SettingsDisclosureType {
class SettingsCell: UITableViewCell, CustomCellDisclosureHandling {
typealias InfoButtonHandler = () -> Void

let contentContainerSubviewMaxCount = 2
let titleLabel = UILabel()
let detailTitleLabel = UILabel()
let disclosureImageView = UIImageView(image: nil)
let contentContainer = UIStackView()
let mainContentContainer = UIView()
let leftContentContainer = UIView()
let rightContentContainer = UIView()
var infoButtonHandler: InfoButtonHandler? { didSet {
infoButton.isHidden = infoButtonHandler == nil
}}
Expand All @@ -59,8 +58,27 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling {
}
}

let titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 17)
label.textColor = UIColor.Cell.titleTextColor
label.setContentHuggingPriority(.defaultHigh, for: .horizontal)
label.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
return label
}()

let detailTitleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 13)
label.textColor = UIColor.Cell.detailTextColor
label.setContentHuggingPriority(.defaultLow, for: .horizontal)
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}()

private var subCellLeadingIndentation: CGFloat = 0
private let buttonWidth: CGFloat = 24

private let infoButton: UIButton = {
let button = UIButton(type: .custom)
button.accessibilityIdentifier = .infoButton
Expand All @@ -83,47 +101,36 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling {
backgroundColor = .clear
contentView.backgroundColor = .clear

infoButton.isHidden = true
infoButton.addTarget(self, action: #selector(handleInfoButton(_:)), for: .touchUpInside)

subCellLeadingIndentation = contentView.layoutMargins.left + UIMetrics.TableView.cellIndentationWidth

titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.font = UIFont.systemFont(ofSize: 17)
titleLabel.textColor = UIColor.Cell.titleTextColor

detailTitleLabel.translatesAutoresizingMaskIntoConstraints = false
detailTitleLabel.font = UIFont.systemFont(ofSize: 13)
detailTitleLabel.textColor = UIColor.Cell.detailTextColor

titleLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
detailTitleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)

titleLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
detailTitleLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
rightContentContainer.setContentHuggingPriority(.required, for: .horizontal)

setLayoutMargins()

let buttonAreaWidth = UIMetrics.contentLayoutMargins.leading + UIMetrics
.contentLayoutMargins.trailing + buttonWidth

let content = UIView()
content.addConstrainedSubviews([titleLabel, infoButton, detailTitleLabel]) {
let infoButtonConstraint = infoButton.trailingAnchor.constraint(
greaterThanOrEqualTo: mainContentContainer.trailingAnchor
)
infoButtonConstraint.priority = .defaultLow

mainContentContainer.addConstrainedSubviews([titleLabel, infoButton, detailTitleLabel]) {
switch style {
case .subtitle:
titleLabel.pinEdgesToSuperview(.init([.top(0), .leading(0)]))
detailTitleLabel.pinEdgesToSuperview(.all().excluding(.top))
detailTitleLabel.topAnchor.constraint(equalToSystemSpacingBelow: titleLabel.bottomAnchor, multiplier: 1)
infoButton.trailingAnchor.constraint(greaterThanOrEqualTo: content.trailingAnchor)
detailTitleLabel.pinEdgesToSuperview(.all().excluding([.top, .trailing]))
detailTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor)
infoButtonConstraint

default:
titleLabel.pinEdgesToSuperview(.all().excluding(.trailing))
detailTitleLabel.pinEdgesToSuperview(.all().excluding(.leading))
detailTitleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: infoButton.trailingAnchor)
}

infoButton.pinEdgesToSuperview(.init([.top(0)]))
infoButton.bottomAnchor.constraint(lessThanOrEqualTo: content.bottomAnchor)
infoButton.leadingAnchor.constraint(
equalTo: titleLabel.trailingAnchor,
constant: -UIMetrics.interButtonSpacing
Expand All @@ -132,10 +139,14 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling {
infoButton.widthAnchor.constraint(equalToConstant: buttonAreaWidth)
}

contentContainer.addArrangedSubview(content)
contentView.addConstrainedSubviews([leftContentContainer, mainContentContainer, rightContentContainer]) {
mainContentContainer.pinEdgesToSuperviewMargins(.all().excluding([.leading, .trailing]))

contentView.addConstrainedSubviews([contentContainer]) {
contentContainer.pinEdgesToSuperviewMargins()
leftContentContainer.pinEdgesToSuperviewMargins(.all().excluding(.trailing))
leftContentContainer.trailingAnchor.constraint(equalTo: mainContentContainer.leadingAnchor)

rightContentContainer.pinEdgesToSuperview(.all().excluding(.leading))
rightContentContainer.leadingAnchor.constraint(equalTo: mainContentContainer.trailingAnchor)
}
}

Expand All @@ -147,7 +158,8 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling {
super.prepareForReuse()

infoButton.isHidden = true
removeLeftView()
removeLeadingView()
removeTrailingView()
setLayoutMargins()
}

Expand All @@ -156,20 +168,22 @@ class SettingsCell: UITableViewCell, CustomCellDisclosureHandling {
backgroundView?.backgroundColor = UIColor.Cell.Background.indentationLevelOne
}

func setLeftView(_ view: UIView, spacing: CGFloat) {
removeLeftView()
func setLeadingView(superviewProvider: (UIView) -> Void) {
removeLeadingView()
superviewProvider(leftContentContainer)
}

if contentContainer.arrangedSubviews.count <= 1 {
contentContainer.insertArrangedSubview(view, at: 0)
}
func removeLeadingView() {
leftContentContainer.subviews.forEach { $0.removeFromSuperview() }
}

contentContainer.spacing = spacing
func setTrailingView(superviewProvider: (UIView) -> Void) {
removeTrailingView()
superviewProvider(rightContentContainer)
}

func removeLeftView() {
if contentContainer.arrangedSubviews.count >= contentContainerSubviewMaxCount {
contentContainer.arrangedSubviews.first?.removeFromSuperview()
}
func removeTrailingView() {
rightContentContainer.subviews.forEach { $0.removeFromSuperview() }
}

@objc private func handleInfoButton(_ sender: UIControl) {
Expand Down
Loading

0 comments on commit 22b078b

Please sign in to comment.