From f5b4847ba33ac92a3664147b9b825041cc58fc17 Mon Sep 17 00:00:00 2001 From: Jaclyn Chen Date: Thu, 15 Aug 2019 09:44:25 +0800 Subject: [PATCH] Auto Layout helpers: add insets to edge pinning and safe area with insets (#39) * Add insets to `pinSubviewToAllEdges` with zero insets by default + basic tests. * Add Auto Layout helpers to pin a subview to safe area, with basic tests. * Bump version to beta. --- WordPressUI.podspec | 2 +- WordPressUI.xcodeproj/project.pbxproj | 4 + WordPressUI/Extensions/UIView+Helpers.swift | 44 ++++++- .../UIView+AutoLayoutHelperTests.swift | 122 ++++++++++++++++++ 4 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 WordPressUITests/Extensions/UIView+AutoLayoutHelperTests.swift diff --git a/WordPressUI.podspec b/WordPressUI.podspec index 45485d9..bb88e8a 100644 --- a/WordPressUI.podspec +++ b/WordPressUI.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "WordPressUI" - s.version = "1.3.4" + s.version = "1.3.5-beta.1" s.summary = "Home of reusable WordPress UI components." s.description = <<-DESC diff --git a/WordPressUI.xcodeproj/project.pbxproj b/WordPressUI.xcodeproj/project.pbxproj index 512b6ad..2fc1560 100644 --- a/WordPressUI.xcodeproj/project.pbxproj +++ b/WordPressUI.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 02CCC587230121440051D40B /* UIView+AutoLayoutHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02CCC586230121440051D40B /* UIView+AutoLayoutHelperTests.swift */; }; 17576E6320AC7A28008612EF /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17576E6220AC7A28008612EF /* GradientView.swift */; }; 1A40951C2271B3C4009AA86D /* NSBundle+ResourceBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A40951B2271B3C4009AA86D /* NSBundle+ResourceBundle.swift */; }; 43067E2B203C8CC4001DD610 /* UIControl+BlockEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43067E2A203C8CC4001DD610 /* UIControl+BlockEvents.swift */; }; @@ -86,6 +87,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 02CCC586230121440051D40B /* UIView+AutoLayoutHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+AutoLayoutHelperTests.swift"; sourceTree = ""; }; 17576E6220AC7A28008612EF /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; 1A40951B2271B3C4009AA86D /* NSBundle+ResourceBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSBundle+ResourceBundle.swift"; sourceTree = ""; }; 43067E2A203C8CC4001DD610 /* UIControl+BlockEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+BlockEvents.swift"; sourceTree = ""; }; @@ -211,6 +213,7 @@ B5226C67207CCDB2003C606E /* GravatarTest.swift */, B529F288202C855B00895D88 /* UIColorHelpersTests.swift */, 577FC78622985DD0005BA78F /* UIView+ChangeLayoutMarginsTests.swift */, + 02CCC586230121440051D40B /* UIView+AutoLayoutHelperTests.swift */, ); path = Extensions; sourceTree = ""; @@ -566,6 +569,7 @@ files = ( 577FC78722985DD0005BA78F /* UIView+ChangeLayoutMarginsTests.swift in Sources */, 9A6EC88D21DA4832007815FF /* UIViewControllerHelperTest.swift in Sources */, + 02CCC587230121440051D40B /* UIView+AutoLayoutHelperTests.swift in Sources */, B529F289202C855B00895D88 /* UIColorHelpersTests.swift in Sources */, 57BC0C6D228DF1E000C1F070 /* UIView+GhostTests.swift in Sources */, 57BC0C6F228DF35400C1F070 /* UITableView+GhostTests.swift in Sources */, diff --git a/WordPressUI/Extensions/UIView+Helpers.swift b/WordPressUI/Extensions/UIView+Helpers.swift index 8bdd3d4..a37b911 100644 --- a/WordPressUI/Extensions/UIView+Helpers.swift +++ b/WordPressUI/Extensions/UIView+Helpers.swift @@ -14,12 +14,24 @@ extension UIView { addConstraints(newConstraints) } + /// Adds constraints that pin a subview to self with zero insets. + /// + /// - Parameter subview: a subview to be pinned to self. @objc public func pinSubviewToAllEdges(_ subview: UIView) { + pinSubviewToAllEdges(subview, insets: .zero) + } + + /// Adds constraints that pin a subview to self with padding insets. + /// + /// - Parameters: + /// - subview: a subview to be pinned to self. + /// - insets: spacing between each subview edge to self. A positive value for an edge indicates that the subview is inside self on that edge. + @objc public func pinSubviewToAllEdges(_ subview: UIView, insets: UIEdgeInsets) { NSLayoutConstraint.activate([ - leadingAnchor.constraint(equalTo: subview.leadingAnchor), - trailingAnchor.constraint(equalTo: subview.trailingAnchor), - topAnchor.constraint(equalTo: subview.topAnchor), - bottomAnchor.constraint(equalTo: subview.bottomAnchor), + leadingAnchor.constraint(equalTo: subview.leadingAnchor, constant: -insets.left), + trailingAnchor.constraint(equalTo: subview.trailingAnchor, constant: insets.right), + topAnchor.constraint(equalTo: subview.topAnchor, constant: -insets.top), + bottomAnchor.constraint(equalTo: subview.bottomAnchor, constant: insets.bottom), ]) } @@ -32,6 +44,30 @@ extension UIView { ]) } + /// Adds constraints that pin a subview to self's safe area with padding insets. + /// + /// - Parameters: + /// - subview: a subview to be pinned to self's safe area. + @objc public func pinSubviewToSafeArea(_ subview: UIView) { + pinSubviewToSafeArea(subview, insets: .zero) + } + + /// Adds constraints that pin a subview to self's safe area with padding insets. + /// + /// - Parameters: + /// - subview: a subview to be pinned to self's safe area. + /// - insets: spacing between each subview edge to self's safe area. A positive value for an edge indicates that the subview is inside safe area on that edge. + @objc public func pinSubviewToSafeArea(_ subview: UIView, insets: UIEdgeInsets) { + if #available(iOS 11.0, *) { + NSLayoutConstraint.activate([ + safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: subview.leadingAnchor, constant: -insets.left), + safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: subview.trailingAnchor, constant: insets.right), + safeAreaLayoutGuide.topAnchor.constraint(equalTo: subview.topAnchor, constant: -insets.top), + safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: subview.bottomAnchor, constant: insets.bottom), + ]) + } + } + @objc public func findFirstResponder() -> UIView? { if isFirstResponder { return self diff --git a/WordPressUITests/Extensions/UIView+AutoLayoutHelperTests.swift b/WordPressUITests/Extensions/UIView+AutoLayoutHelperTests.swift new file mode 100644 index 0000000..656f0f0 --- /dev/null +++ b/WordPressUITests/Extensions/UIView+AutoLayoutHelperTests.swift @@ -0,0 +1,122 @@ +import XCTest + +@testable import WordPressUI + +class UIViewAutoLayoutHelperTests: XCTestCase { + private var view: UIView! + private var subview: UIView! + + override func setUp() { + super.setUp() + view = UIView(frame: .zero) + subview = UIView(frame: .zero) + } + + // MARK: tests for `pinSubviewToAllEdges` + + func testPinSubviewToAllEdgesWithZeroInsets() { + view.addSubview(subview) + view.pinSubviewToAllEdges(subview) + + let topConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.topAnchor && $0.secondAnchor == subview.topAnchor }) + XCTAssertEqual(topConstraint.constant, 0) + + let leadingConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.leadingAnchor && $0.secondAnchor == subview.leadingAnchor }) + XCTAssertEqual(leadingConstraint.constant, 0) + + let trailingConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.trailingAnchor && $0.secondAnchor == subview.trailingAnchor }) + XCTAssertEqual(trailingConstraint.constant, 0) + + let bottomConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.bottomAnchor && $0.secondAnchor == subview.bottomAnchor }) + XCTAssertEqual(bottomConstraint.constant, 0) + XCTAssertEqual(bottomConstraint.secondAnchor, subview.bottomAnchor) + } + + func testPinSubviewToAllEdgesWithNonZeroInsets() { + view.addSubview(subview) + let insets = UIEdgeInsets(top: 10, left: 12, bottom: 17, right: 25) + view.pinSubviewToAllEdges(subview, insets: insets) + + // Self.top = subview.top - insets.top + let topConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.topAnchor && $0.secondAnchor == subview.topAnchor }) + XCTAssertEqual(topConstraint.constant, -insets.top) + + // Self.leading = subview.leading - insets.left + let leadingConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.leadingAnchor && $0.secondAnchor == subview.leadingAnchor }) + XCTAssertEqual(leadingConstraint.constant, -insets.left) + + // Self.trailing = subview.trailing + insets.right + let trailingConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.trailingAnchor && $0.secondAnchor == subview.trailingAnchor }) + XCTAssertEqual(trailingConstraint.constant, insets.right) + + // Self.bottom = subview.bottom + insets.bottom + let bottomConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.bottomAnchor && $0.secondAnchor == subview.bottomAnchor }) + XCTAssertEqual(bottomConstraint.constant, insets.bottom) + } + + // MARK: tests for `pinSubviewToSafeArea` + + func testPinSubviewToSafeAreaWithZeroInsets() { + view.addSubview(subview) + view.pinSubviewToSafeArea(subview) + + let topConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.safeAreaLayoutGuide.topAnchor && $0.secondAnchor == subview.topAnchor }) + XCTAssertEqual(topConstraint.constant, 0) + + let leadingConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.safeAreaLayoutGuide.leadingAnchor && $0.secondAnchor == subview.leadingAnchor }) + XCTAssertEqual(leadingConstraint.constant, 0) + + let trailingConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.safeAreaLayoutGuide.trailingAnchor && $0.secondAnchor == subview.trailingAnchor }) + XCTAssertEqual(trailingConstraint.constant, 0) + + let bottomConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.safeAreaLayoutGuide.bottomAnchor && $0.secondAnchor == subview.bottomAnchor }) + XCTAssertEqual(bottomConstraint.constant, 0) + } + + func testPinSubviewToSafeAreaWithNonZeroInsets() { + view.addSubview(subview) + let insets = UIEdgeInsets(top: 10, left: 12, bottom: 17, right: 25) + view.pinSubviewToSafeArea(subview, insets: insets) + + // Self safe area.top = subview.top - insets.top + let topConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.safeAreaLayoutGuide.topAnchor && $0.secondAnchor == subview.topAnchor }) + XCTAssertEqual(topConstraint.constant, -insets.top) + + // Self safe area.leading = subview.leading - insets.left + let leadingConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.safeAreaLayoutGuide.leadingAnchor && $0.secondAnchor == subview.leadingAnchor }) + XCTAssertEqual(leadingConstraint.constant, -insets.left) + + // Self safe area.trailing = subview.trailing + insets.right + let trailingConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.safeAreaLayoutGuide.trailingAnchor && $0.secondAnchor == subview.trailingAnchor }) + XCTAssertEqual(trailingConstraint.constant, insets.right) + + // Self safe area.bottom = subview.bottom + insets.bottom + let bottomConstraint = getConstraint(from: view, + filter: { $0.firstAnchor == view.safeAreaLayoutGuide.bottomAnchor && $0.secondAnchor == subview.bottomAnchor }) + XCTAssertEqual(bottomConstraint.constant, insets.bottom) + } + + private func getConstraint(from view: UIView, filter: (NSLayoutConstraint) -> Bool) -> NSLayoutConstraint { + let constraints = view.constraints.filter(filter) + guard let constraint = constraints.first, constraints.count == 1 else { + XCTFail("Exactly one constraint corresponding to the given filter should have been created") + fatalError() + } + return constraint + } +}