summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2023-03-24 13:11:50 +0100
committerAndrej Mihajlov <and@mullvad.net>2023-03-27 16:28:40 +0200
commite86a962ce095e145154f6431477e589e4a3013ac (patch)
treed728ba6c1e710fe96ace4d451a18513baf871636
parent7f7f4f82038f54808bd3632196bd341ab0350748 (diff)
downloadmullvadvpn-e86a962ce095e145154f6431477e589e4a3013ac.tar.xz
mullvadvpn-e86a962ce095e145154f6431477e589e4a3013ac.zip
Add autolayout builder
-rw-r--r--ios/MullvadVPN.xcodeproj/project.pbxproj4
-rw-r--r--ios/MullvadVPN/Extensions/UIView+AutoLayoutBuilder.swift231
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())
+ }
+}