Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update sizeThatFits to check each axis' constraint separately #92

Merged
merged 2 commits into from
May 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions BlueprintUI/Sources/Blueprint View/BlueprintView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,43 @@ public final class BlueprintView: UIView {
fatalError("init(coder:) has not been implemented")
}

/// Forwarded to the `measure(in:)` implementation of the root element.
///
/// Measures the size needed to display the view within then given constraining size,
/// by measuring the current `element` of the `BlueprintView`.
///
/// If you would like to not constrain the measurement in a given axis,
/// pass `0.0` or `.greatestFiniteMagnitude` for that axis, eg:
///
/// ```
/// // Measures with a width of 100, and no height constraint.
/// blueprintView.sizeThatFits(CGSize(width: 100.0, height: 0.0))
///
/// // Measures with a height of 100, and no width constraint.
/// blueprintView.sizeThatFits(CGSize(width: 0.0, height: 100.0))
///
/// // Measures unconstrained in both axes.
/// blueprintView.sizeThatFits(.zero)
/// ```
///
override public func sizeThatFits(_ size: CGSize) -> CGSize {
guard let element = element else { return .zero }
let constraint: SizeConstraint
if size == .zero {
constraint = SizeConstraint(width: .unconstrained, height: .unconstrained)
} else {
constraint = SizeConstraint(size)
guard let element = element else {
return .zero
}
return element.content.measure(in: constraint)

func measurementConstraint(with size : CGSize) -> SizeConstraint {

let unconstrainedValues : [CGFloat] = [0.0, .greatestFiniteMagnitude, .infinity]

let widthUnconstrained = unconstrainedValues.contains(size.width)
let heightUnconstrained = unconstrainedValues.contains(size.height)

return SizeConstraint(
width: widthUnconstrained ? .unconstrained : .atMost(size.width),
height: heightUnconstrained ? .unconstrained : .atMost(size.height)
)
}

return element.content.measure(in: measurementConstraint(with: size))
}

/// Returns the size of the element bound to the current width (mimicking
Expand Down
94 changes: 94 additions & 0 deletions BlueprintUI/Tests/BlueprintViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,86 @@ import XCTest
@testable import BlueprintUI

class BlueprintViewTests: XCTestCase {

func test_sizeThatFits() {

/// NOTE: References to `.greatestFiniteMagnitude` always refer to the fully qualified type,
/// `CGFloat.greatestFiniteMagnitude` to ensure we are not implicitly comparing against doubles,
/// which have a different underlying type on 32 bit devices.

let blueprintView = BlueprintView()

// Normal sizes.

do {
let element = MeasurableElement { constraint in
XCTAssertEqual(constraint.maximum, CGSize(width: 200, height: 100))
return CGSize(width: 100, height: 50)
}

blueprintView.element = element

XCTAssertEqual(blueprintView.sizeThatFits(CGSize(width: 200, height: 100)), CGSize(width: 100, height: 50))
}

// Unconstrained in both axes.

do {
let element = MeasurableElement { constraint in
XCTAssertEqual(constraint.width, .unconstrained)
XCTAssertEqual(constraint.height, .unconstrained)
XCTAssertEqual(constraint.maximum, CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude))
kyleve marked this conversation as resolved.
Show resolved Hide resolved

return CGSize(width: 100, height: 50)
}

blueprintView.element = element

XCTAssertEqual(blueprintView.sizeThatFits(.zero), CGSize(width: 100, height: 50))
XCTAssertEqual(blueprintView.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)), CGSize(width: 100, height: 50))
XCTAssertEqual(blueprintView.sizeThatFits(CGSize(width: CGFloat.infinity, height: CGFloat.infinity)), CGSize(width: 100, height: 50))
}

// Unconstrained in one axis only.

do {
// X

do {
let element = MeasurableElement { constraint in
XCTAssertEqual(constraint.width, .unconstrained)
XCTAssertEqual(constraint.height.maximum, 100.0)
XCTAssertEqual(constraint.maximum, CGSize(width: CGFloat.greatestFiniteMagnitude, height: 100.0))

return CGSize(width: 100, height: 50)
}

blueprintView.element = element

XCTAssertEqual(blueprintView.sizeThatFits(CGSize(width: 0.0, height: 100.0)), CGSize(width: 100, height: 50))
XCTAssertEqual(blueprintView.sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: 100.0)), CGSize(width: 100, height: 50))
XCTAssertEqual(blueprintView.sizeThatFits(CGSize(width: CGFloat.infinity, height: 100.0)), CGSize(width: 100, height: 50))
}

// Y

do {
let element = MeasurableElement { constraint in
XCTAssertEqual(constraint.width.maximum, 100.0)
XCTAssertEqual(constraint.height, .unconstrained)
XCTAssertEqual(constraint.maximum, CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude))

return CGSize(width: 100, height: 50)
}

blueprintView.element = element

XCTAssertEqual(blueprintView.sizeThatFits(CGSize(width: 100.0, height: 0.0)), CGSize(width: 100, height: 50))
XCTAssertEqual(blueprintView.sizeThatFits(CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude)), CGSize(width: 100, height: 50))
XCTAssertEqual(blueprintView.sizeThatFits(CGSize(width: 100.0, height: CGFloat.infinity)), CGSize(width: 100, height: 50))
}
}
}

func test_displaysSimpleView() {

Expand Down Expand Up @@ -63,6 +143,20 @@ class BlueprintViewTests: XCTestCase {

}

fileprivate struct MeasurableElement : Element {

var validate : (SizeConstraint) -> CGSize

var content: ElementContent {
ElementContent {
self.validate($0)
}
}

func backingViewDescription(bounds: CGRect, subtreeExtent: CGRect?) -> ViewDescription? {
return nil
}
}

fileprivate struct SimpleViewElement: Element {

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

- [Fixed `ConstrainedSize`](https://github.com/square/Blueprint/pull/87) to ensure that the measurement of its inner element respects the `ConstrainedSize`'s maximum size, which matters for measuring elements which re-flow based on width, such as elements containing text.

- Changed [BlueprintView.sizeThatFits(:)](https://github.com/square/Blueprint/pull/92/files) to treat width and height separately when determining if measurement should be unconstrained in a given axis.

### Added

- [Added support](https://github.com/square/Blueprint/pull/88) for `SwiftUI`-style element building within `BlueprintUI` and `BlueprintUICommonControls`.
Expand Down
12 changes: 12 additions & 0 deletions SampleApp/SampleApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
/* Begin PBXBuildFile section */
0AEA09B32428360500F9ED0C /* ScrollViewKeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEA09B22428360500F9ED0C /* ScrollViewKeyboardViewController.swift */; };
0AEF0C4E24463B880092248C /* XcodePreviewDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEF0C4D24463B880092248C /* XcodePreviewDemo.swift */; };
0AF36345246266AF0027E172 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 0AF36342246266AF0027E172 /* README.md */; };
0AF36346246266AF0027E172 /* RELEASING.md in Resources */ = {isa = PBXBuildFile; fileRef = 0AF36343246266AF0027E172 /* RELEASING.md */; };
0AF36347246266AF0027E172 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = 0AF36344246266AF0027E172 /* CHANGELOG.md */; };
2009E82B0B9D98A685E8637B /* libPods-Tutorial 1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 237956659E4F5F4AEE53B303 /* libPods-Tutorial 1.a */; };
4EE23713D657C21C6806E771 /* libPods-SampleApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = ADDB22E4D9B4379A15C5AF69 /* libPods-SampleApp.a */; };
6B58C7884AE69FA55EA64E57 /* libPods-Tutorial 2 (Completed).a in Frameworks */ = {isa = PBXBuildFile; fileRef = 187E50F5B5395EA0C0BCA8B2 /* libPods-Tutorial 2 (Completed).a */; };
Expand Down Expand Up @@ -42,6 +45,9 @@
/* Begin PBXFileReference section */
0AEA09B22428360500F9ED0C /* ScrollViewKeyboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewKeyboardViewController.swift; sourceTree = "<group>"; };
0AEF0C4D24463B880092248C /* XcodePreviewDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodePreviewDemo.swift; sourceTree = "<group>"; };
0AF36342246266AF0027E172 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = "<group>"; };
0AF36343246266AF0027E172 /* RELEASING.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = RELEASING.md; path = ../RELEASING.md; sourceTree = "<group>"; };
0AF36344246266AF0027E172 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = "<group>"; };
0DA29F056002872418F7D2C9 /* Pods-SampleApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SampleApp.debug.xcconfig"; path = "Target Support Files/Pods-SampleApp/Pods-SampleApp.debug.xcconfig"; sourceTree = "<group>"; };
187E50F5B5395EA0C0BCA8B2 /* libPods-Tutorial 2 (Completed).a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Tutorial 2 (Completed).a"; sourceTree = BUILT_PRODUCTS_DIR; };
1A147478D522E763BFC19F37 /* Pods-Tutorial 2 (Completed).debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tutorial 2 (Completed).debug.xcconfig"; path = "Target Support Files/Pods-Tutorial 2 (Completed)/Pods-Tutorial 2 (Completed).debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -141,6 +147,9 @@
975454F1223C1289003E353F = {
isa = PBXGroup;
children = (
0AF36344246266AF0027E172 /* CHANGELOG.md */,
0AF36342246266AF0027E172 /* README.md */,
0AF36343246266AF0027E172 /* RELEASING.md */,
97545512223C12E9003E353F /* Resources */,
9754550F223C12E9003E353F /* Sources */,
979F49E1224D1B4500A3C5D4 /* Tutorials */,
Expand Down Expand Up @@ -401,6 +410,9 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0AF36345246266AF0027E172 /* README.md in Resources */,
0AF36347246266AF0027E172 /* CHANGELOG.md in Resources */,
0AF36346246266AF0027E172 /* RELEASING.md in Resources */,
9754551B223C12E9003E353F /* Assets.xcassets in Resources */,
9754551C223C12E9003E353F /* LaunchScreen.storyboard in Resources */,
);
Expand Down