From 04e9c890aa4ccf82dbd1759cd7f53cb0eabe0c02 Mon Sep 17 00:00:00 2001 From: Josh Converse Date: Mon, 10 Jun 2019 05:32:33 -0700 Subject: [PATCH] Adds support for NSDirectionalEdgeInsets as an inset constant (#594) --- SnapKit.xcodeproj/project.pbxproj | 8 ++ Source/Constraint.swift | 9 ++ Source/ConstraintConstantTarget.swift | 42 ++++++++++ Source/ConstraintDirectionalInsetTarget.swift | 49 +++++++++++ Source/ConstraintDirectionalInsets.swift | 34 ++++++++ Source/ConstraintMakerEditable.swift | 8 ++ Source/ConstraintRelatableTarget.swift | 6 ++ Tests/SnapKitTests/Tests.swift | 84 +++++++++++++++++++ 8 files changed, 240 insertions(+) create mode 100644 Source/ConstraintDirectionalInsetTarget.swift create mode 100644 Source/ConstraintDirectionalInsets.swift diff --git a/SnapKit.xcodeproj/project.pbxproj b/SnapKit.xcodeproj/project.pbxproj index bd593d0..ec9c269 100644 --- a/SnapKit.xcodeproj/project.pbxproj +++ b/SnapKit.xcodeproj/project.pbxproj @@ -8,6 +8,8 @@ /* Begin PBXBuildFile section */ 2DBA080E1F1FAD66001CFED4 /* Typealiases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DBA080D1F1FAD66001CFED4 /* Typealiases.swift */; }; + 7E1CB2AE227BB5520066B6C0 /* ConstraintDirectionalInsetTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E1CB2AD227BB5520066B6C0 /* ConstraintDirectionalInsetTarget.swift */; }; + 7E1CB2B0227BBDF70066B6C0 /* ConstraintDirectionalInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E1CB2AF227BBDF70066B6C0 /* ConstraintDirectionalInsets.swift */; }; EE235F5F1C5785BC00C08960 /* Debugging.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE235F5E1C5785BC00C08960 /* Debugging.swift */; }; EE235F6D1C5785C600C08960 /* Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE235F621C5785C600C08960 /* Constraint.swift */; }; EE235F701C5785C600C08960 /* ConstraintDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE235F631C5785C600C08960 /* ConstraintDescription.swift */; }; @@ -49,6 +51,8 @@ /* Begin PBXFileReference section */ 2DBA080D1F1FAD66001CFED4 /* Typealiases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Typealiases.swift; sourceTree = ""; }; 537DCE9A1C35CD4100B5B899 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS9.1.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; + 7E1CB2AD227BB5520066B6C0 /* ConstraintDirectionalInsetTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstraintDirectionalInsetTarget.swift; sourceTree = ""; }; + 7E1CB2AF227BBDF70066B6C0 /* ConstraintDirectionalInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstraintDirectionalInsets.swift; sourceTree = ""; }; EE235F5E1C5785BC00C08960 /* Debugging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Debugging.swift; sourceTree = ""; }; EE235F621C5785C600C08960 /* Constraint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constraint.swift; sourceTree = ""; }; EE235F631C5785C600C08960 /* ConstraintDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConstraintDescription.swift; sourceTree = ""; }; @@ -172,6 +176,7 @@ EE235F911C5785CE00C08960 /* ConstraintMultiplierTarget.swift */, EE235F921C5785CE00C08960 /* ConstraintOffsetTarget.swift */, EE235F931C5785CE00C08960 /* ConstraintInsetTarget.swift */, + 7E1CB2AD227BB5520066B6C0 /* ConstraintDirectionalInsetTarget.swift */, ); name = Targets; sourceTree = ""; @@ -183,6 +188,7 @@ EE235F621C5785C600C08960 /* Constraint.swift */, EE235F631C5785C600C08960 /* ConstraintDescription.swift */, EE235F641C5785C600C08960 /* ConstraintInsets.swift */, + 7E1CB2AF227BBDF70066B6C0 /* ConstraintDirectionalInsets.swift */, EE235F651C5785C600C08960 /* ConstraintConfig.swift */, EE235F661C5785C600C08960 /* ConstraintView.swift */, EEF68FBB1D78653000980C26 /* ConstraintLayoutGuide.swift */, @@ -372,6 +378,7 @@ EE235FB51C5785D400C08960 /* ConstraintMakerEditable.swift in Sources */, EEF68FBC1D78653000980C26 /* ConstraintLayoutGuide.swift in Sources */, EE235FAC1C5785D400C08960 /* ConstraintMaker.swift in Sources */, + 7E1CB2AE227BB5520066B6C0 /* ConstraintDirectionalInsetTarget.swift in Sources */, EE6087751E4F133E0029CF84 /* ConstraintPriority.swift in Sources */, EE235F941C5785CE00C08960 /* ConstraintRelatableTarget.swift in Sources */, EEF68FA61D784A5300980C26 /* ConstraintDSL.swift in Sources */, @@ -387,6 +394,7 @@ EE6898CB1DA7B3A100D47F33 /* LayoutConstraintItem.swift in Sources */, EE235FB21C5785D400C08960 /* ConstraintMakerPriortizable.swift in Sources */, EE235F8B1C5785C600C08960 /* LayoutConstraint.swift in Sources */, + 7E1CB2B0227BBDF70066B6C0 /* ConstraintDirectionalInsets.swift in Sources */, EE235FA31C5785CE00C08960 /* ConstraintInsetTarget.swift in Sources */, EE235F9D1C5785CE00C08960 /* ConstraintMultiplierTarget.swift in Sources */, EE235FC01C5785DC00C08960 /* ConstraintViewDSL.swift in Sources */, diff --git a/Source/Constraint.swift b/Source/Constraint.swift index dc2c7c8..ea6159e 100644 --- a/Source/Constraint.swift +++ b/Source/Constraint.swift @@ -219,6 +219,15 @@ public final class Constraint { return self } + #if os(iOS) || os(tvOS) + @discardableResult + @available(iOS 11.0, tvOS 11.0, *) + public func update(inset: ConstraintDirectionalInsetTarget) -> Constraint { + self.constant = inset.constraintDirectionalInsetTargetValue + return self + } + #endif + @discardableResult public func update(priority: ConstraintPriorityTarget) -> Constraint { self.priority = priority.constraintPriorityTargetValue diff --git a/Source/ConstraintConstantTarget.swift b/Source/ConstraintConstantTarget.swift index 24052ae..7f54907 100644 --- a/Source/ConstraintConstantTarget.swift +++ b/Source/ConstraintConstantTarget.swift @@ -40,6 +40,12 @@ extension CGSize: ConstraintConstantTarget { extension ConstraintInsets: ConstraintConstantTarget { } +#if os(iOS) || os(tvOS) +@available(iOS 11.0, tvOS 11.0, *) +extension ConstraintDirectionalInsets: ConstraintConstantTarget { +} +#endif + extension ConstraintConstantTarget { internal func constraintConstantTargetValueFor(layoutAttribute: LayoutAttribute) -> CGFloat { @@ -165,6 +171,42 @@ extension ConstraintConstantTarget { #endif } + #if os(iOS) || os(tvOS) + if #available(iOS 11.0, tvOS 11.0, *), let value = self as? ConstraintDirectionalInsets { + switch layoutAttribute { + case .left, .leftMargin: + return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? value.leading : value.trailing + case .top, .topMargin, .firstBaseline: + return value.top + case .right, .rightMargin: + return (ConstraintConfig.interfaceLayoutDirection == .leftToRight) ? -value.trailing : -value.leading + case .bottom, .bottomMargin, .lastBaseline: + return -value.bottom + case .leading, .leadingMargin: + return value.leading + case .trailing, .trailingMargin: + return -value.trailing + case .centerX, .centerXWithinMargins: + return (value.leading - value.trailing) / 2 + case .centerY, .centerYWithinMargins: + return (value.top - value.bottom) / 2 + case .width: + return -(value.leading + value.trailing) + case .height: + return -(value.top + value.bottom) + case .notAnAttribute: + return 0.0 + #if swift(>=5.0) + @unknown default: + return 0.0 + #else + default: + return 0.0 + #endif + } + } + #endif + return 0.0 } diff --git a/Source/ConstraintDirectionalInsetTarget.swift b/Source/ConstraintDirectionalInsetTarget.swift new file mode 100644 index 0000000..955aec3 --- /dev/null +++ b/Source/ConstraintDirectionalInsetTarget.swift @@ -0,0 +1,49 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) +import UIKit +#else +import AppKit +#endif + +#if os(iOS) || os(tvOS) +public protocol ConstraintDirectionalInsetTarget: ConstraintConstantTarget { +} + +@available(iOS 11.0, tvOS 11.0, *) +extension ConstraintDirectionalInsets: ConstraintDirectionalInsetTarget { +} + +extension ConstraintDirectionalInsetTarget { + + @available(iOS 11.0, tvOS 11.0, *) + internal var constraintDirectionalInsetTargetValue: ConstraintDirectionalInsets { + if let amount = self as? ConstraintDirectionalInsets { + return amount + } else { + return ConstraintDirectionalInsets(top: 0, leading: 0, bottom: 0, trailing: 0) + } + } +} +#endif diff --git a/Source/ConstraintDirectionalInsets.swift b/Source/ConstraintDirectionalInsets.swift new file mode 100644 index 0000000..ada8ed5 --- /dev/null +++ b/Source/ConstraintDirectionalInsets.swift @@ -0,0 +1,34 @@ +// +// SnapKit +// +// Copyright (c) 2011-Present SnapKit Team - https://github.com/SnapKit +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) + import UIKit +#else + import AppKit +#endif + + +#if os(iOS) || os(tvOS) + @available(iOS 11.0, tvOS 11.0, *) + public typealias ConstraintDirectionalInsets = NSDirectionalEdgeInsets +#endif diff --git a/Source/ConstraintMakerEditable.swift b/Source/ConstraintMakerEditable.swift index fb88c41..f768a17 100644 --- a/Source/ConstraintMakerEditable.swift +++ b/Source/ConstraintMakerEditable.swift @@ -53,4 +53,12 @@ public class ConstraintMakerEditable: ConstraintMakerPriortizable { return self } + #if os(iOS) || os(tvOS) + @discardableResult + @available(iOS 11.0, tvOS 11.0, *) + public func inset(_ amount: ConstraintDirectionalInsetTarget) -> ConstraintMakerEditable { + self.description.constant = amount.constraintDirectionalInsetTargetValue + return self + } + #endif } diff --git a/Source/ConstraintRelatableTarget.swift b/Source/ConstraintRelatableTarget.swift index 6976367..d517a61 100644 --- a/Source/ConstraintRelatableTarget.swift +++ b/Source/ConstraintRelatableTarget.swift @@ -55,6 +55,12 @@ extension CGPoint: ConstraintRelatableTarget { extension ConstraintInsets: ConstraintRelatableTarget { } +#if os(iOS) || os(tvOS) +@available(iOS 11.0, tvOS 11.0, *) +extension ConstraintDirectionalInsets: ConstraintRelatableTarget { +} +#endif + extension ConstraintItem: ConstraintRelatableTarget { } diff --git a/Tests/SnapKitTests/Tests.swift b/Tests/SnapKitTests/Tests.swift index a4b587f..750ea1d 100644 --- a/Tests/SnapKitTests/Tests.swift +++ b/Tests/SnapKitTests/Tests.swift @@ -413,6 +413,90 @@ class SnapKitTests: XCTestCase { XCTAssertEqual(constraints[3].constant, -25, "Should be -25") } + #if os(iOS) || os(tvOS) + @available(iOS 11.0, tvOS 11.0, *) + func testConstraintDirectionalInsetsAsImpliedEqualToConstraints() { + let view = View() + self.container.addSubview(view) + + view.snp.makeConstraints { (make) -> Void in + make.top.leading.bottom.trailing.equalTo(self.container).inset(ConstraintDirectionalInsets(top: 25, leading: 25, bottom: 25, trailing: 25)) + } + + XCTAssertEqual(self.container.snp_constraints.count, 4, "Should have 4 constraints") + + + let constraints = (self.container.snp_constraints as! [NSLayoutConstraint]).sorted { $0.firstAttribute.rawValue < $1.firstAttribute.rawValue } + + let verify: (NSLayoutConstraint, NSLayoutConstraint.Attribute, CGFloat) -> Void = { constraint, attribute, constant in + XCTAssertEqual(constraint.firstAttribute, attribute, "First attribute \(constraint.firstAttribute.rawValue) is not \(attribute.rawValue)") + XCTAssertEqual(constraint.secondAttribute, attribute, "Second attribute \(constraint.secondAttribute.rawValue) is not \(attribute.rawValue)") + XCTAssertEqual(constraint.constant, constant, "Attribute \(attribute.rawValue) should have constant \(constant)") + } + + verify(constraints[0], .top, 25) + verify(constraints[1], .bottom, -25) + verify(constraints[2], .leading, 25) + verify(constraints[3], .trailing, -25) + } + #endif + + #if os(iOS) || os(tvOS) + @available(iOS 11.0, tvOS 11.0, *) + func testConstraintDirectionalInsetsAsConstraintsConstant() { + let view = View() + self.container.addSubview(view) + + view.snp.makeConstraints { (make) -> Void in + make.top.leading.bottom.trailing.equalTo(self.container).inset(ConstraintDirectionalInsets(top: 25, leading: 25, bottom: 25, trailing: 25)) + } + + XCTAssertEqual(self.container.snp_constraints.count, 4, "Should have 4 constraints") + + + let constraints = (self.container.snp_constraints as! [NSLayoutConstraint]).sorted { $0.firstAttribute.rawValue < $1.firstAttribute.rawValue } + + let verify: (NSLayoutConstraint, NSLayoutConstraint.Attribute, CGFloat) -> Void = { constraint, attribute, constant in + XCTAssertEqual(constraint.firstAttribute, attribute, "First attribute \(constraint.firstAttribute.rawValue) is not \(attribute.rawValue)") + XCTAssertEqual(constraint.secondAttribute, attribute, "Second attribute \(constraint.secondAttribute.rawValue) is not \(attribute.rawValue)") + XCTAssertEqual(constraint.constant, constant, "Attribute \(attribute.rawValue) should have constant \(constant)") + } + + verify(constraints[0], .top, 25) + verify(constraints[1], .bottom, -25) + verify(constraints[2], .leading, 25) + verify(constraints[3], .trailing, -25) + } + #endif + + #if os(iOS) || os(tvOS) + @available(iOS 11.0, tvOS 11.0, *) + func testConstraintDirectionalInsetsFallBackForNonDirectionalConstraints() { + let view = View() + self.container.addSubview(view) + + view.snp.makeConstraints { (make) -> Void in + make.edges.equalTo(self.container).inset(ConstraintDirectionalInsets(top: 25, leading: 25, bottom: 25, trailing: 25)) + } + + XCTAssertEqual(self.container.snp_constraints.count, 4, "Should have 4 constraints") + + + let constraints = (self.container.snp_constraints as! [NSLayoutConstraint]).sorted { $0.firstAttribute.rawValue < $1.firstAttribute.rawValue } + + let verify: (NSLayoutConstraint, NSLayoutConstraint.Attribute, CGFloat) -> Void = { constraint, attribute, constant in + XCTAssertEqual(constraint.firstAttribute, attribute, "First attribute \(constraint.firstAttribute.rawValue) is not \(attribute.rawValue)") + XCTAssertEqual(constraint.secondAttribute, attribute, "Second attribute \(constraint.secondAttribute.rawValue) is not \(attribute.rawValue)") + XCTAssertEqual(constraint.constant, constant, "Attribute \(attribute.rawValue) should have constant \(constant)") + } + + verify(constraints[0], .left, 25) + verify(constraints[1], .right, -25) + verify(constraints[2], .top, 25) + verify(constraints[3], .bottom, -25) + } + #endif + func testSizeConstraints() { let view = View() self.container.addSubview(view)