diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2023-03-24 13:11:50 +0100 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2023-03-27 16:28:40 +0200 |
| commit | e86a962ce095e145154f6431477e589e4a3013ac (patch) | |
| tree | d728ba6c1e710fe96ace4d451a18513baf871636 | |
| parent | 7f7f4f82038f54808bd3632196bd341ab0350748 (diff) | |
| download | mullvadvpn-e86a962ce095e145154f6431477e589e4a3013ac.tar.xz mullvadvpn-e86a962ce095e145154f6431477e589e4a3013ac.zip | |
Add autolayout builder
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 4 | ||||
| -rw-r--r-- | ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift | 231 |
2 files changed, 235 insertions, 0 deletions
diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 33dc34a1b4..b202225aae 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -151,6 +151,7 @@ 5878A279290954790096FC88 /* TunnelViewControllerInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A278290954790096FC88 /* TunnelViewControllerInteractor.swift */; }; 5878A27B2909649A0096FC88 /* CustomOverlayRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A27A2909649A0096FC88 /* CustomOverlayRenderer.swift */; }; 5878A27D2909657C0096FC88 /* RevokedDeviceInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878A27C2909657C0096FC88 /* RevokedDeviceInteractor.swift */; }; + 5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5878F4FF29CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift */; }; 587988C728A2A01F00E3DF54 /* AccountDataThrottling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */; }; 587A01FC23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */; }; 587AD7C623421D7000E93A53 /* TunnelSettingsV1.swift in Sources */ = {isa = PBXBuildFile; fileRef = 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */; }; @@ -770,6 +771,7 @@ 5878A278290954790096FC88 /* TunnelViewControllerInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TunnelViewControllerInteractor.swift; sourceTree = "<group>"; }; 5878A27A2909649A0096FC88 /* CustomOverlayRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomOverlayRenderer.swift; sourceTree = "<group>"; }; 5878A27C2909657C0096FC88 /* RevokedDeviceInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokedDeviceInteractor.swift; sourceTree = "<group>"; }; + 5878F4FF29CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+AutoLayoutBuilder.swift"; sourceTree = "<group>"; }; 587988C628A2A01F00E3DF54 /* AccountDataThrottling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDataThrottling.swift; sourceTree = "<group>"; }; 587A01FB23F1F0BE00B68763 /* SimulatorTunnelProviderHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimulatorTunnelProviderHost.swift; sourceTree = "<group>"; }; 587AD7C523421D7000E93A53 /* TunnelSettingsV1.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelSettingsV1.swift; sourceTree = "<group>"; }; @@ -1419,6 +1421,7 @@ E158B35F285381C60002F069 /* String+AccountFormatting.swift */, 5891BF5025E66B1E006D6FB0 /* UIBarButtonItem+KeyboardNavigation.swift */, 587CBFE222807F530028DED3 /* UIColor+Helpers.swift */, + 5878F4FF29CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift */, ); path = Extensions; sourceTree = "<group>"; @@ -2680,6 +2683,7 @@ 583FE01229C0F99A006E85F9 /* PresentationControllerDismissalInterceptor.swift in Sources */, 58677712290976FB006F721F /* SettingsInteractor.swift in Sources */, 58CE5E66224146200008646E /* LoginViewController.swift in Sources */, + 5878F50029CDA742003D4BE2 /* UIView+AutoLayoutBuilder.swift in Sources */, 583FE01029C0F532006E85F9 /* CustomSplitViewController.swift in Sources */, 58EF580B25D69D7A00AEBA94 /* ProblemReportSubmissionOverlayView.swift in Sources */, 5892A45E265FABFF00890742 /* EmptyTableViewHeaderFooterView.swift in Sources */, diff --git a/ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift b/ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift new file mode 100644 index 0000000000..7e207b0c68 --- /dev/null +++ b/ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift @@ -0,0 +1,231 @@ +// +// UIView+AutoLayoutBuilder.swift +// MullvadVPN +// +// Created by pronebird on 24/03/2023. +// Copyright © 2023 Mullvad VPN AB. All rights reserved. +// + +import UIKit + +extension UIView { + /** + Pin edges to edges of other view edges. + */ + func pinEdgesTo( + _ other: UIView, + insets: NSDirectionalEdgeInsets = .zero, + excludingEdges: NSDirectionalRectEdge = [] + ) -> [NSLayoutConstraint] { + var constraints = [NSLayoutConstraint]() + + if !excludingEdges.contains(.top) { + constraints.append( + topAnchor.constraint(equalTo: other.topAnchor, constant: insets.top) + ) + } + + if !excludingEdges.contains(.bottom) { + constraints.append( + bottomAnchor.constraint(equalTo: other.bottomAnchor, constant: insets.bottom) + ) + } + + if !excludingEdges.contains(.leading) { + constraints.append( + leadingAnchor.constraint(equalTo: other.leadingAnchor, constant: insets.leading) + ) + } + + if !excludingEdges.contains(.trailing) { + constraints.append( + trailingAnchor.constraint(equalTo: other.trailingAnchor, constant: insets.trailing) + ) + } + + return constraints + } + + /** + Pin edges to superview edges. + */ + func pinEdgesToSuperview( + insets: NSDirectionalEdgeInsets = .zero, + excludingEdges: NSDirectionalRectEdge = [] + ) -> [NSLayoutConstraint] { + guard let superview = superview else { return [] } + + return pinEdgesTo(superview, insets: insets, excludingEdges: excludingEdges) + } + + /** + Pin edges to superview margins. + */ + func pinEdgesToSuperviewMargins( + insets: NSDirectionalEdgeInsets = .zero, + excludingEdges: NSDirectionalRectEdge = [] + ) -> [NSLayoutConstraint] { + guard let superview = superview else { return [] } + + return pinEdgesToMargins(superview, insets: insets, excludingEdges: excludingEdges) + } + + /** + Pin edges to other view layout margins. + */ + func pinEdgesToMargins( + _ other: UIView, + insets: NSDirectionalEdgeInsets = .zero, + excludingEdges: NSDirectionalRectEdge = [] + ) -> [NSLayoutConstraint] { + return pinEdgesTo(other.layoutMarginsGuide, insets: insets, excludingEdges: excludingEdges) + } + + /** + Pin edges to layout guide. + */ + func pinEdgesTo( + _ layoutGuide: UILayoutGuide, + insets: NSDirectionalEdgeInsets = .zero, + excludingEdges: NSDirectionalRectEdge = [] + ) -> [NSLayoutConstraint] { + var constraints = [NSLayoutConstraint]() + + if !excludingEdges.contains(.top) { + constraints.append( + topAnchor.constraint(equalTo: layoutGuide.topAnchor, constant: insets.top) + ) + } + + if !excludingEdges.contains(.bottom) { + constraints.append( + bottomAnchor.constraint( + equalTo: layoutGuide.bottomAnchor, + constant: insets.bottom + ) + ) + } + + if !excludingEdges.contains(.leading) { + constraints.append( + leadingAnchor.constraint( + equalTo: layoutGuide.leadingAnchor, + constant: insets.leading + ) + ) + } + + if !excludingEdges.contains(.trailing) { + constraints.append( + trailingAnchor.constraint( + equalTo: layoutGuide.trailingAnchor, + constant: insets.trailing + ) + ) + } + + return constraints + } + + /** + Pin horizontal edges to other view edges. + */ + func pinHorizontalEdgesTo( + _ other: UIView, + leadingInset: CGFloat = .zero, + trailingInset: CGFloat = .zero + ) -> [NSLayoutConstraint] { + return pinEdgesTo( + other, + insets: NSDirectionalEdgeInsets( + top: 0, + leading: leadingInset, + bottom: 0, + trailing: trailingInset + ), + excludingEdges: [.bottom, .top] + ) + } + + /** + Pin horizontal edges to other view layout margins. + */ + func pinHorizontalEdgesToMargins( + _ other: UIView, + leadingInset: CGFloat = .zero, + trailingInset: CGFloat = .zero + ) -> [NSLayoutConstraint] { + return pinEdgesToMargins( + other, + insets: NSDirectionalEdgeInsets( + top: 0, + leading: leadingInset, + bottom: 0, + trailing: trailingInset + ), + excludingEdges: [.bottom, .top] + ) + } +} + +/** + AutoLayout builder. + + Use it in conjunction with `NSLayoutConstraint.activate()`, for example: + + ``` + let view = UIView() + let subview = UIView() + + subview.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(subview) + + NSLayoutConstraint.activate { + subview.pinEdgesToSuperview( + insets: NSDirectionalEdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 24), + excludingEdges: .bottom + ) + subview.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + } + + ``` + */ +@resultBuilder enum AutoLayoutBuilder { + static func buildBlock(_ components: [NSLayoutConstraint]...) -> [NSLayoutConstraint] { + return components.flatMap { $0 } + } + + static func buildExpression(_ expression: NSLayoutConstraint) -> [NSLayoutConstraint] { + return [expression] + } + + static func buildExpression(_ expression: [NSLayoutConstraint]) -> [NSLayoutConstraint] { + return expression + } + + static func buildOptional(_ components: [NSLayoutConstraint]?) -> [NSLayoutConstraint] { + return components ?? [] + } + + static func buildEither(first components: [NSLayoutConstraint]) -> [NSLayoutConstraint] { + return components + } + + static func buildEither(second components: [NSLayoutConstraint]) -> [NSLayoutConstraint] { + return components + } + + static func buildArray(_ components: [[NSLayoutConstraint]]) -> [NSLayoutConstraint] { + return components.flatMap { $0 } + } +} + +extension NSLayoutConstraint { + /** + Activate constraints produced by a builder. + */ + static func activate(@AutoLayoutBuilder builder: () -> [NSLayoutConstraint]) { + activate(builder()) + } +} |
