From 06b287b471296ed0be955e376659d4d5bdd91c86 Mon Sep 17 00:00:00 2001 From: Filipe Lemos Date: Mon, 25 Jul 2022 18:16:46 +0100 Subject: [PATCH 1/2] Add `AspectRatioConstrainableProxy` Some layouts are more easily achieved by defining a specific aspect ratio for a view which, when combined with constraints to establish the width or height of the view, allows Auto Layout to infer the size in the other dimension. --- Alicerce.xcodeproj/project.pbxproj | 8 ++- Sources/AutoLayout/ConstrainableProxy.swift | 26 +++++++- ...spectRatioConstrainableProxyTestCase.swift | 66 +++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 Tests/AlicerceTests/AutoLayout/AspectRatioConstrainableProxyTestCase.swift diff --git a/Alicerce.xcodeproj/project.pbxproj b/Alicerce.xcodeproj/project.pbxproj index f4fa5c7..c86347d 100644 --- a/Alicerce.xcodeproj/project.pbxproj +++ b/Alicerce.xcodeproj/project.pbxproj @@ -333,6 +333,7 @@ 65D46CB5203C90E5008E847B /* PersistencePerformanceMetricsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65D46CB4203C90E5008E847B /* PersistencePerformanceMetricsTracker.swift */; }; 73C6051B20F3BBA300D0B643 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73C6051A20F3BBA300D0B643 /* Token.swift */; }; 73C6051D20F3BD0E00D0B643 /* TokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73C6051C20F3BD0E00D0B643 /* TokenTests.swift */; }; + 9D0CAD83288F012100E4EEF0 /* AspectRatioConstrainableProxyTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0CAD82288F012100E4EEF0 /* AspectRatioConstrainableProxyTestCase.swift */; }; 9D4E3AA1239A6557007F3050 /* CollectionReusableViewSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D4E3AA0239A6557007F3050 /* CollectionReusableViewSizer.swift */; }; 9D4E3AA3239A6841007F3050 /* CollectionReusableViewSizerTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D4E3AA2239A6841007F3050 /* CollectionReusableViewSizerTestCase.swift */; }; 9DEC00AB209A043A00F94353 /* BuilderCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC00AA209A043A00F94353 /* BuilderCache.swift */; }; @@ -674,6 +675,7 @@ 65D46CB4203C90E5008E847B /* PersistencePerformanceMetricsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistencePerformanceMetricsTracker.swift; sourceTree = ""; }; 73C6051A20F3BBA300D0B643 /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; 73C6051C20F3BD0E00D0B643 /* TokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenTests.swift; sourceTree = ""; }; + 9D0CAD82288F012100E4EEF0 /* AspectRatioConstrainableProxyTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AspectRatioConstrainableProxyTestCase.swift; sourceTree = ""; }; 9D4E3AA0239A6557007F3050 /* CollectionReusableViewSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionReusableViewSizer.swift; sourceTree = ""; }; 9D4E3AA2239A6841007F3050 /* CollectionReusableViewSizerTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionReusableViewSizerTestCase.swift; sourceTree = ""; }; 9DEC00AA209A043A00F94353 /* BuilderCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderCache.swift; sourceTree = ""; }; @@ -1465,11 +1467,13 @@ 4838FE3823A9508E007311F0 /* AutoLayout */ = { isa = PBXGroup; children = ( + 9D0CAD82288F012100E4EEF0 /* AspectRatioConstrainableProxyTestCase.swift */, 4838FE4123A950CB007311F0 /* BaseConstrainableProxyTestCase.swift */, 4838FE3923A950CA007311F0 /* BottomConstrainableProxyTestCase.swift */, 48BF456F23D1FD0C00DFBB28 /* CenterConstrainableProxyTestCase.swift */, 48BF456623D1D26F00DFBB28 /* CenterXConstrainableProxyTestCase.swift */, 48BF456923D1D4A300DFBB28 /* CenterYConstrainableProxyTestCase.swift */, + 3E8D61932546F8AF00C08EA2 /* ConstraintGroupToggleTestCase.swift */, 4838FE4323A950CB007311F0 /* EdgesConstrainableProxyTestCase.swift */, 48A5ECC623D5AC3A0014B2B7 /* FirstBaselineConstrainableProxyTestCase.swift */, 48BF456323D1CFB300DFBB28 /* HeightConstrainableProxyTestCase.swift */, @@ -1481,12 +1485,11 @@ 4838FE3E23A950CA007311F0 /* NSLayoutConstraintTestCase.swift */, 4838FE3A23A950CA007311F0 /* RightConstrainableProxyTestCase.swift */, 48BF457223D204DC00DFBB28 /* SizeConstrainableProxyTestCase.swift */, + 48A5ECCF23D5B9F70014B2B7 /* TopBottomConstrainableProxyTestCase.swift */, 4838FE4023A950CA007311F0 /* TopConstrainableProxyTestCase.swift */, 4838FE3B23A950CA007311F0 /* TrailingConstrainableProxyTestCase.swift */, 4833D5B223D0D28C00EBD925 /* WidthConstrainableProxyTestCase.swift */, 4838FE3C23A950CA007311F0 /* XCTAssertConstraint.swift */, - 48A5ECCF23D5B9F70014B2B7 /* TopBottomConstrainableProxyTestCase.swift */, - 3E8D61932546F8AF00C08EA2 /* ConstraintGroupToggleTestCase.swift */, ); path = AutoLayout; sourceTree = ""; @@ -1924,6 +1927,7 @@ 0A266FB41ED59FB6009CD0D7 /* TableViewCellTestCase.swift in Sources */, 0ADC9F6F24B89EF00038FFBD /* MockURLSessionResourceInterceptor.swift in Sources */, 0A266FB51ED59FB6009CD0D7 /* TableViewHeaderFooterViewTestCase.swift in Sources */, + 9D0CAD83288F012100E4EEF0 /* AspectRatioConstrainableProxyTestCase.swift in Sources */, 0AEEE11820CF208A00B47687 /* MockLogItemFormatter.swift in Sources */, 0A708F5F20E94DF8001784DA /* ModuleLoggerTestCase.swift in Sources */, 0A266FB61ED59FB6009CD0D7 /* UIColorTestCase.swift in Sources */, diff --git a/Sources/AutoLayout/ConstrainableProxy.swift b/Sources/AutoLayout/ConstrainableProxy.swift index 2a85418..d779bea 100644 --- a/Sources/AutoLayout/ConstrainableProxy.swift +++ b/Sources/AutoLayout/ConstrainableProxy.swift @@ -733,6 +733,29 @@ public extension SizeConstrainableProxy { } } +public protocol AspectRatioConstrainableProxy: WidthConstrainableProxy, HeightConstrainableProxy {} + +public extension AspectRatioConstrainableProxy { + + @discardableResult + func aspectRatio( + _ multiplier: CGFloat = 1, + offset: CGFloat = 0, + relation: ConstraintRelation = .equal, + priority: UILayoutPriority = .required + ) -> NSLayoutConstraint { + + constrain( + from: width, + to: height, + multiplier: multiplier, + constant: offset, + relation: relation, + priority: priority + ) + } +} + public protocol BaselineConstrainableProxy: ConstrainableProxy { var firstBaseline: NSLayoutYAxisAnchor { get } @@ -844,7 +867,8 @@ public extension BaselineConstrainableProxy { } } -public protocol PositionConstrainableProxy: EdgesConstrainableProxy, SizeConstrainableProxy, CenterConstrainableProxy {} +public protocol PositionConstrainableProxy: + EdgesConstrainableProxy, SizeConstrainableProxy, AspectRatioConstrainableProxy, CenterConstrainableProxy {} // MARK: - Helpers diff --git a/Tests/AlicerceTests/AutoLayout/AspectRatioConstrainableProxyTestCase.swift b/Tests/AlicerceTests/AutoLayout/AspectRatioConstrainableProxyTestCase.swift new file mode 100644 index 0000000..f843b98 --- /dev/null +++ b/Tests/AlicerceTests/AutoLayout/AspectRatioConstrainableProxyTestCase.swift @@ -0,0 +1,66 @@ +import XCTest +@testable import Alicerce + +class AspectRatioConstrainableProxyTestCase: BaseConstrainableProxyTestCase { + + private(set) var constraints0: NSLayoutConstraint! + + func testConstrain_WithAbsoluteWidthAndAspectRatioConstraint_ShouldSupportRelativeSize() { + + constrain(view0) { view0 in + view0.width(1920) + constraints0 = view0.aspectRatio(16/9) + } + + let expected = NSLayoutConstraint( + item: view0!, + attribute: .width, + relatedBy: .equal, + toItem: view0!, + attribute: .height, + multiplier: 16/9, + constant: 0, + priority: .required, + active: true + ) + + XCTAssertConstraint(constraints0, expected) + + host.layoutIfNeeded() + + XCTAssertEqual(view0.frame.size, Constants.size0) + } + + func testConstrain_WithRelativeHeightAndAspectRatioConstraint_ShouldSupportRelativeSize() { + + constrain(view0, host) { view0, host in + view0.top(to: host, offset: 100) + view0.bottom(to: host) + constraints0 = view0.aspectRatio(0.5) + } + + let expected0 = NSLayoutConstraint( + item: view0!, + attribute: .width, + relatedBy: .equal, + toItem: view0!, + attribute: .height, + multiplier: 0.5, + constant: 0, + priority: .required, + active: true + ) + + XCTAssertConstraint(constraints0, expected0) + + host.layoutIfNeeded() + + XCTAssertEqual(view0.frame.size, Constants.size1) + } +} + +private enum Constants { + + static let size0 = CGSize(width: 1920, height: 1080) + static let size1 = CGSize(width: 200, height: 400) +} From b0df56b95149eb2e483584085b9e12784762e484 Mon Sep 17 00:00:00 2001 From: Filipe Lemos Date: Tue, 26 Jul 2022 09:56:25 +0100 Subject: [PATCH 2/2] Code review improvements - Move `aspectRatio` to `SizeConstrainableProxy` --- Alicerce.xcodeproj/project.pbxproj | 4 -- Sources/AutoLayout/ConstrainableProxy.swift | 8 +-- ...spectRatioConstrainableProxyTestCase.swift | 66 ------------------- .../SizeConstrainableProxyTestCase.swift | 57 ++++++++++++++++ 4 files changed, 58 insertions(+), 77 deletions(-) delete mode 100644 Tests/AlicerceTests/AutoLayout/AspectRatioConstrainableProxyTestCase.swift diff --git a/Alicerce.xcodeproj/project.pbxproj b/Alicerce.xcodeproj/project.pbxproj index c86347d..5b5fcf9 100644 --- a/Alicerce.xcodeproj/project.pbxproj +++ b/Alicerce.xcodeproj/project.pbxproj @@ -333,7 +333,6 @@ 65D46CB5203C90E5008E847B /* PersistencePerformanceMetricsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65D46CB4203C90E5008E847B /* PersistencePerformanceMetricsTracker.swift */; }; 73C6051B20F3BBA300D0B643 /* Token.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73C6051A20F3BBA300D0B643 /* Token.swift */; }; 73C6051D20F3BD0E00D0B643 /* TokenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73C6051C20F3BD0E00D0B643 /* TokenTests.swift */; }; - 9D0CAD83288F012100E4EEF0 /* AspectRatioConstrainableProxyTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D0CAD82288F012100E4EEF0 /* AspectRatioConstrainableProxyTestCase.swift */; }; 9D4E3AA1239A6557007F3050 /* CollectionReusableViewSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D4E3AA0239A6557007F3050 /* CollectionReusableViewSizer.swift */; }; 9D4E3AA3239A6841007F3050 /* CollectionReusableViewSizerTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D4E3AA2239A6841007F3050 /* CollectionReusableViewSizerTestCase.swift */; }; 9DEC00AB209A043A00F94353 /* BuilderCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC00AA209A043A00F94353 /* BuilderCache.swift */; }; @@ -675,7 +674,6 @@ 65D46CB4203C90E5008E847B /* PersistencePerformanceMetricsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistencePerformanceMetricsTracker.swift; sourceTree = ""; }; 73C6051A20F3BBA300D0B643 /* Token.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Token.swift; sourceTree = ""; }; 73C6051C20F3BD0E00D0B643 /* TokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenTests.swift; sourceTree = ""; }; - 9D0CAD82288F012100E4EEF0 /* AspectRatioConstrainableProxyTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AspectRatioConstrainableProxyTestCase.swift; sourceTree = ""; }; 9D4E3AA0239A6557007F3050 /* CollectionReusableViewSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionReusableViewSizer.swift; sourceTree = ""; }; 9D4E3AA2239A6841007F3050 /* CollectionReusableViewSizerTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionReusableViewSizerTestCase.swift; sourceTree = ""; }; 9DEC00AA209A043A00F94353 /* BuilderCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderCache.swift; sourceTree = ""; }; @@ -1467,7 +1465,6 @@ 4838FE3823A9508E007311F0 /* AutoLayout */ = { isa = PBXGroup; children = ( - 9D0CAD82288F012100E4EEF0 /* AspectRatioConstrainableProxyTestCase.swift */, 4838FE4123A950CB007311F0 /* BaseConstrainableProxyTestCase.swift */, 4838FE3923A950CA007311F0 /* BottomConstrainableProxyTestCase.swift */, 48BF456F23D1FD0C00DFBB28 /* CenterConstrainableProxyTestCase.swift */, @@ -1927,7 +1924,6 @@ 0A266FB41ED59FB6009CD0D7 /* TableViewCellTestCase.swift in Sources */, 0ADC9F6F24B89EF00038FFBD /* MockURLSessionResourceInterceptor.swift in Sources */, 0A266FB51ED59FB6009CD0D7 /* TableViewHeaderFooterViewTestCase.swift in Sources */, - 9D0CAD83288F012100E4EEF0 /* AspectRatioConstrainableProxyTestCase.swift in Sources */, 0AEEE11820CF208A00B47687 /* MockLogItemFormatter.swift in Sources */, 0A708F5F20E94DF8001784DA /* ModuleLoggerTestCase.swift in Sources */, 0A266FB61ED59FB6009CD0D7 /* UIColorTestCase.swift in Sources */, diff --git a/Sources/AutoLayout/ConstrainableProxy.swift b/Sources/AutoLayout/ConstrainableProxy.swift index d779bea..55989d1 100644 --- a/Sources/AutoLayout/ConstrainableProxy.swift +++ b/Sources/AutoLayout/ConstrainableProxy.swift @@ -731,11 +731,6 @@ public extension SizeConstrainableProxy { return constraints } -} - -public protocol AspectRatioConstrainableProxy: WidthConstrainableProxy, HeightConstrainableProxy {} - -public extension AspectRatioConstrainableProxy { @discardableResult func aspectRatio( @@ -867,8 +862,7 @@ public extension BaselineConstrainableProxy { } } -public protocol PositionConstrainableProxy: - EdgesConstrainableProxy, SizeConstrainableProxy, AspectRatioConstrainableProxy, CenterConstrainableProxy {} +public protocol PositionConstrainableProxy: EdgesConstrainableProxy, SizeConstrainableProxy, CenterConstrainableProxy {} // MARK: - Helpers diff --git a/Tests/AlicerceTests/AutoLayout/AspectRatioConstrainableProxyTestCase.swift b/Tests/AlicerceTests/AutoLayout/AspectRatioConstrainableProxyTestCase.swift deleted file mode 100644 index f843b98..0000000 --- a/Tests/AlicerceTests/AutoLayout/AspectRatioConstrainableProxyTestCase.swift +++ /dev/null @@ -1,66 +0,0 @@ -import XCTest -@testable import Alicerce - -class AspectRatioConstrainableProxyTestCase: BaseConstrainableProxyTestCase { - - private(set) var constraints0: NSLayoutConstraint! - - func testConstrain_WithAbsoluteWidthAndAspectRatioConstraint_ShouldSupportRelativeSize() { - - constrain(view0) { view0 in - view0.width(1920) - constraints0 = view0.aspectRatio(16/9) - } - - let expected = NSLayoutConstraint( - item: view0!, - attribute: .width, - relatedBy: .equal, - toItem: view0!, - attribute: .height, - multiplier: 16/9, - constant: 0, - priority: .required, - active: true - ) - - XCTAssertConstraint(constraints0, expected) - - host.layoutIfNeeded() - - XCTAssertEqual(view0.frame.size, Constants.size0) - } - - func testConstrain_WithRelativeHeightAndAspectRatioConstraint_ShouldSupportRelativeSize() { - - constrain(view0, host) { view0, host in - view0.top(to: host, offset: 100) - view0.bottom(to: host) - constraints0 = view0.aspectRatio(0.5) - } - - let expected0 = NSLayoutConstraint( - item: view0!, - attribute: .width, - relatedBy: .equal, - toItem: view0!, - attribute: .height, - multiplier: 0.5, - constant: 0, - priority: .required, - active: true - ) - - XCTAssertConstraint(constraints0, expected0) - - host.layoutIfNeeded() - - XCTAssertEqual(view0.frame.size, Constants.size1) - } -} - -private enum Constants { - - static let size0 = CGSize(width: 1920, height: 1080) - static let size1 = CGSize(width: 200, height: 400) -} diff --git a/Tests/AlicerceTests/AutoLayout/SizeConstrainableProxyTestCase.swift b/Tests/AlicerceTests/AutoLayout/SizeConstrainableProxyTestCase.swift index 122f016..9d30332 100644 --- a/Tests/AlicerceTests/AutoLayout/SizeConstrainableProxyTestCase.swift +++ b/Tests/AlicerceTests/AutoLayout/SizeConstrainableProxyTestCase.swift @@ -189,6 +189,63 @@ class SizeConstrainableProxyTestCase: BaseConstrainableProxyTestCase { XCTAssert(constraintGroup1.isActive) XCTAssertEqual(view0.frame.size, Constants.size1) } + + func testConstrain_WithAbsoluteWidthAndAspectRatioConstraint_ShouldSupportRelativeSize() { + + var constraint: NSLayoutConstraint! + + constrain(view0) { view0 in + view0.width(1920) + constraint = view0.aspectRatio(16/9) + } + + let expected = NSLayoutConstraint( + item: view0!, + attribute: .width, + relatedBy: .equal, + toItem: view0!, + attribute: .height, + multiplier: 16/9, + constant: 0, + priority: .required, + active: true + ) + + XCTAssertConstraint(expected, constraint) + + host.layoutIfNeeded() + + XCTAssertEqual(view0.frame.size, CGSize(width: 1920, height: 1080)) + } + + func testConstrain_WithRelativeHeightAndAspectRatioConstraint_ShouldSupportRelativeSize() { + + var constraint: NSLayoutConstraint! + + constrain(view0, host) { view0, host in + view0.top(to: host, offset: 100) + view0.bottom(to: host) + constraint = view0.aspectRatio(0.5) + } + + let expected = NSLayoutConstraint( + item: view0!, + attribute: .width, + relatedBy: .equal, + toItem: view0!, + attribute: .height, + multiplier: 0.5, + constant: 0, + priority: .required, + active: true + ) + + XCTAssertConstraint(constraint, expected) + + host.layoutIfNeeded() + + XCTAssertEqual(view0.frame.size, CGSize(width: 200, height: 400)) + } } private extension SizeConstrainableProxyTestCase {