From 96ddfa42ddd63a165774647e6b82a0945110294b Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Tue, 26 Sep 2023 11:13:00 -0700 Subject: [PATCH 1/8] Initial commit --- BlueprintUILists/Sources/List.swift | 9 ++++++++- BlueprintUILists/Tests/List.MeasurementTests.swift | 4 ++++ ListableUI/Sources/ListView/ListView+ContentSize.swift | 10 ++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/BlueprintUILists/Sources/List.swift b/BlueprintUILists/Sources/List.swift index 1d755de93..a75cc36ef 100644 --- a/BlueprintUILists/Sources/List.swift +++ b/BlueprintUILists/Sources/List.swift @@ -219,6 +219,7 @@ extension List { }() let height : CGFloat = { + switch verticalFill { case .fillParent: if let max = constraint.height.constrainedValue { @@ -250,7 +251,13 @@ extension List { if case .caffeinated = layoutMode, let maxHeight = constraint.height.constrainedValue { return min(size.contentSize.height, maxHeight) } - return size.contentSize.height + + /// Temporary + if size.layoutDirection == .horizontal { + return size.naturalWidth ?? size.contentSize.height + } else { + return size.contentSize.height + } } }() diff --git a/BlueprintUILists/Tests/List.MeasurementTests.swift b/BlueprintUILists/Tests/List.MeasurementTests.swift index 0804e1d05..5bf93fb0e 100644 --- a/BlueprintUILists/Tests/List.MeasurementTests.swift +++ b/BlueprintUILists/Tests/List.MeasurementTests.swift @@ -23,4 +23,8 @@ class List_MeasurementTests : XCTestCase { XCTFail() } } + + func test_vertical_measurement() { + + } } diff --git a/ListableUI/Sources/ListView/ListView+ContentSize.swift b/ListableUI/Sources/ListView/ListView+ContentSize.swift index f48d69b05..fff942ce2 100644 --- a/ListableUI/Sources/ListView/ListView+ContentSize.swift +++ b/ListableUI/Sources/ListView/ListView+ContentSize.swift @@ -69,7 +69,8 @@ extension ListView width: fittingSize.width > 0 ? min(fittingSize.width, totalSize.width) : totalSize.width, height: fittingSize.height > 0 ? min(fittingSize.height, totalSize.height) : totalSize.height ), - naturalWidth: layout.content.naturalContentWidth + naturalWidth: layout.content.naturalContentWidth, + layoutDirection: layout.direction ) } } @@ -90,11 +91,16 @@ public struct MeasuredListSize : Equatable { /// cannot provide a natural width because it takes up as much space as it as given. public var naturalWidth : CGFloat? + /// The layout direction of the list. + public var layoutDirection : LayoutDirection + public init( contentSize: CGSize, - naturalWidth: CGFloat? + naturalWidth: CGFloat?, + layoutDirection : LayoutDirection ) { self.contentSize = contentSize self.naturalWidth = naturalWidth + self.layoutDirection = layoutDirection } } From 5d70a33bb21e3a4302600f58016032f11a32d59c Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Tue, 26 Sep 2023 11:34:09 -0700 Subject: [PATCH 2/8] temp --- BlueprintUILists/Sources/List.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/BlueprintUILists/Sources/List.swift b/BlueprintUILists/Sources/List.swift index a75cc36ef..792c86d94 100644 --- a/BlueprintUILists/Sources/List.swift +++ b/BlueprintUILists/Sources/List.swift @@ -248,9 +248,9 @@ extension List { ) } case .natural: - if case .caffeinated = layoutMode, let maxHeight = constraint.height.constrainedValue { - return min(size.contentSize.height, maxHeight) - } +// if case .caffeinated = layoutMode, let maxHeight = constraint.height.constrainedValue { +// return min(size.contentSize.height, maxHeight) +// } /// Temporary if size.layoutDirection == .horizontal { From 6dd984e5d8df6242a5cfdc1a5825b592e01fc6ba Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Tue, 26 Sep 2023 11:49:48 -0700 Subject: [PATCH 3/8] more --- BlueprintUILists/Sources/List.swift | 7 ++ .../Tests/List.MeasurementTests.swift | 64 ++++++++++++++++++- BlueprintUILists/Tests/ListTests.swift | 12 ++-- .../Tests/XCTestCaseAdditions.swift | 5 ++ .../ListView/ListView+ContentSizeTests.swift | 12 ++-- 5 files changed, 91 insertions(+), 9 deletions(-) diff --git a/BlueprintUILists/Sources/List.swift b/BlueprintUILists/Sources/List.swift index 792c86d94..e2cb8a0d5 100644 --- a/BlueprintUILists/Sources/List.swift +++ b/BlueprintUILists/Sources/List.swift @@ -250,6 +250,13 @@ extension List { case .natural: // if case .caffeinated = layoutMode, let maxHeight = constraint.height.constrainedValue { // return min(size.contentSize.height, maxHeight) +// } + +// switch size.layoutDirection { +// case .vertical: +// +// case .horizontal: +// // } /// Temporary diff --git a/BlueprintUILists/Tests/List.MeasurementTests.swift b/BlueprintUILists/Tests/List.MeasurementTests.swift index 5bf93fb0e..22596d4eb 100644 --- a/BlueprintUILists/Tests/List.MeasurementTests.swift +++ b/BlueprintUILists/Tests/List.MeasurementTests.swift @@ -6,6 +6,7 @@ // @testable import BlueprintUILists +import BlueprintUI import XCTest class List_MeasurementTests : XCTestCase { @@ -24,7 +25,68 @@ class List_MeasurementTests : XCTestCase { } } - func test_vertical_measurement() { + func test_natural() { + func testList( + with measurement: List.Measurement, + direction: LayoutDirection + ) -> some Element { + List( + measurement: measurement, + configure: { + $0.layout = .table { + $0.direction = direction + } + }, + sections: { + Section("1") { + TestingItem(identifierValue: "1") + TestingItem(identifierValue: "2") + TestingItem(identifierValue: "3") + TestingItem(identifierValue: "4") + TestingItem(identifierValue: "5") + } + } + ) + } + + testcase("Horizontal Layouts") { + + let constraint = SizeConstraint(.init(width: 200, height: 200)) + + XCTAssertEqual( + CGSize(width: 0, height: 0), + testList( + with: .measureContent(horizontalFill: .natural, verticalFill: .natural), + direction: .vertical + ) + .measure(in: constraint) + ) + + } + + testcase("Vertical Layouts") { + + } + + } + + private struct TestingItem : BlueprintItemContent, Equatable { + + var identifierValue: AnyHashable + + func element(with info: ApplyItemContentInfo) -> Element { + Empty() + .constrainedTo(size: .init(width: 100, height: 50)) + } + } +} + + +fileprivate extension Element { + func measure(in constraint : SizeConstraint, environment : Environment = .empty) -> CGSize { + self + .content + .measure(in: constraint, environment: environment) } } diff --git a/BlueprintUILists/Tests/ListTests.swift b/BlueprintUILists/Tests/ListTests.swift index 5ce538380..ce562be5e 100644 --- a/BlueprintUILists/Tests/ListTests.swift +++ b/BlueprintUILists/Tests/ListTests.swift @@ -63,7 +63,8 @@ class ListTests : XCTestCase { List.ListContent.size( with: .init( contentSize: CGSize(width: 1200, height: 1000), - naturalWidth: 900 + naturalWidth: 900, + layoutDirection: .vertical ), in: constraint, layoutMode: .default, @@ -77,7 +78,8 @@ class ListTests : XCTestCase { List.ListContent.size( with: .init( contentSize: CGSize(width: 1200, height: 1000), - naturalWidth: 900 + naturalWidth: 900, + layoutDirection: .vertical ), in: constraint, layoutMode: .default, @@ -91,7 +93,8 @@ class ListTests : XCTestCase { List.ListContent.size( with: .init( contentSize: CGSize(width: 1200, height: 1000), - naturalWidth: nil + naturalWidth: nil, + layoutDirection: .vertical ), in: constraint, layoutMode: .default, @@ -105,7 +108,8 @@ class ListTests : XCTestCase { List.ListContent.size( with: .init( contentSize: CGSize(width: 1200, height: 1000), - naturalWidth: 900 + naturalWidth: 900, + layoutDirection: .vertical ), in: constraint, layoutMode: .default, diff --git a/BlueprintUILists/Tests/XCTestCaseAdditions.swift b/BlueprintUILists/Tests/XCTestCaseAdditions.swift index 26a0a6674..a6ff5fd2c 100644 --- a/BlueprintUILists/Tests/XCTestCaseAdditions.swift +++ b/BlueprintUILists/Tests/XCTestCaseAdditions.swift @@ -34,4 +34,9 @@ extension XCTestCase print("Iterations: \(iterations), Average Time: \(average)") } + + func testcase(_ name : String = "", _ block : () -> ()) + { + block() + } } diff --git a/ListableUI/Tests/ListView/ListView+ContentSizeTests.swift b/ListableUI/Tests/ListView/ListView+ContentSizeTests.swift index 743b46dd4..4def753f1 100644 --- a/ListableUI/Tests/ListView/ListView+ContentSizeTests.swift +++ b/ListableUI/Tests/ListView/ListView+ContentSizeTests.swift @@ -44,7 +44,8 @@ class ListView_ContentSizeTests : XCTestCase MeasuredListSize( contentSize: CGSize(width: 300.0, height: 190.0), - naturalWidth: 200.0 + naturalWidth: 200.0, + layoutDirection: .vertical ) ) } @@ -65,7 +66,8 @@ class ListView_ContentSizeTests : XCTestCase MeasuredListSize( contentSize: CGSize(width: 100.0, height: 300.0), - naturalWidth: nil + naturalWidth: nil, + layoutDirection: .vertical ) ) } @@ -85,7 +87,8 @@ class ListView_ContentSizeTests : XCTestCase MeasuredListSize( contentSize: CGSize(width: 510.0, height: 100.0), - naturalWidth: 50.0 + naturalWidth: 50.0, + layoutDirection: .horizontal ) ) } @@ -106,7 +109,8 @@ class ListView_ContentSizeTests : XCTestCase MeasuredListSize( contentSize: CGSize(width: 300.0, height: 100.0), - naturalWidth: nil + naturalWidth: nil, + layoutDirection: .horizontal ) ) } From 523beccef7cf7f9c7fb6657f6adc3420b9b2f7f6 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 15 Oct 2023 17:44:24 -0700 Subject: [PATCH 4/8] Ok this looks right? --- BlueprintUILists/Sources/List.swift | 100 ++++++------------ .../ListView/ListView+ContentSize.swift | 2 +- 2 files changed, 33 insertions(+), 69 deletions(-) diff --git a/BlueprintUILists/Sources/List.swift b/BlueprintUILists/Sources/List.swift index e2cb8a0d5..07a967dd5 100644 --- a/BlueprintUILists/Sources/List.swift +++ b/BlueprintUILists/Sources/List.swift @@ -141,7 +141,7 @@ extension List { } case .measureContent(let horizontalFill, let verticalFill, let safeArea, let limit): - return ElementContent() { constraint, environment -> CGSize in + return ElementContent { constraint, environment -> CGSize in let measurements = ListView.contentSize( in: constraint.maximum, for: self.properties, @@ -185,85 +185,49 @@ extension List { verticalFill : Measurement.FillRule ) -> CGSize { + precondition( + layoutMode == .caffeinated, + "Listable only supports the `.caffeinated` layout mode in Blueprint." + ) + let width : CGFloat = { switch horizontalFill { case .fillParent: - if let max = constraint.width.constrainedValue { - return max - } else if case .caffeinated = layoutMode { - return .infinity - } else { - fatalError( - """ - `List` is being used with the `.fillParent` measurement option, which takes \ - up the full width it is afforded by its parent element. However, \ - the parent element provided the `List` an unconstrained width, which is meaningless. - - How do you fix this? - -------------------- - 1) This usually means that your `List` itself has been \ - placed in a `ScrollView` or other element which intentionally provides an \ - unconstrained measurement to its content. If your `List` is in a `ScrollView`, \ - remove the outer scroll view – `List` manages its own scrolling. Two `ScrollViews` \ - that are nested within each other is generally meaningless unless they scroll \ - in different directions (eg, horizontal vs vertical). - - 2) If your `List` is not in a `ScrollView`, ensure that the element - measuring it is providing a constrained `SizeConstraint`. - """ + return constraint.width.constrainedValue ?? .infinity + + case .natural: + switch size.layoutDirection { + case .vertical: + return min( + size.naturalWidth ?? size.contentSize.width, + constraint.width.maximum + ) + case .horizontal: + return min( + size.contentSize.width, + constraint.width.maximum ) } - case .natural: - return size.naturalWidth ?? size.contentSize.width } }() let height : CGFloat = { - switch verticalFill { case .fillParent: - if let max = constraint.height.constrainedValue { - return max - } else if case .caffeinated = layoutMode { - return .infinity - } else { - fatalError( - """ - `List` is being used with the `.fillParent` measurement option, which takes \ - up the full height it is afforded by its parent element. However, \ - the parent element provided the `List` an unconstrained height, which is meaningless. - - How do you fix this? - -------------------- - 1) This usually means that your `List` itself has been \ - placed in a `ScrollView` or other element which intentionally provides an \ - unconstrained measurement to its content. If your `List` is in a `ScrollView`, \ - remove the outer scroll view – `List` manages its own scrolling. Two `ScrollViews` \ - that are nested within each other is generally meaningless unless they scroll \ - in different directions (eg, horizontal vs vertical). - - 2) If your `List` is not in a `ScrollView`, ensure that the element - measuring it is providing a constrained `SizeConstraint`. - """ - ) - } - case .natural: -// if case .caffeinated = layoutMode, let maxHeight = constraint.height.constrainedValue { -// return min(size.contentSize.height, maxHeight) -// } - -// switch size.layoutDirection { -// case .vertical: -// -// case .horizontal: -// -// } + return constraint.height.constrainedValue ?? .infinity - /// Temporary - if size.layoutDirection == .horizontal { - return size.naturalWidth ?? size.contentSize.height - } else { - return size.contentSize.height + case .natural: + switch size.layoutDirection { + case .vertical: + return min( + size.contentSize.height, + constraint.height.maximum + ) + case .horizontal: + return min( + size.naturalWidth ?? size.contentSize.height, + constraint.height.maximum + ) } } }() diff --git a/ListableUI/Sources/ListView/ListView+ContentSize.swift b/ListableUI/Sources/ListView/ListView+ContentSize.swift index fff942ce2..59b0980e1 100644 --- a/ListableUI/Sources/ListView/ListView+ContentSize.swift +++ b/ListableUI/Sources/ListView/ListView+ContentSize.swift @@ -88,7 +88,7 @@ public struct MeasuredListSize : Equatable { /// /// ### Note /// Not all layouts support or provide a natural width. For example, a `.flow` layout - /// cannot provide a natural width because it takes up as much space as it as given. + /// cannot provide a natural width because it takes up as much space as it as given in both dimensions. public var naturalWidth : CGFloat? /// The layout direction of the list. From db6a5b7b6335443aaf99e0ce54bf34c46247ddb8 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Sun, 15 Oct 2023 18:32:18 -0700 Subject: [PATCH 5/8] more --- .../Tests/List.MeasurementTests.swift | 46 ------------------- BlueprintUILists/Tests/ListTests.swift | 23 ++++++++-- 2 files changed, 19 insertions(+), 50 deletions(-) diff --git a/BlueprintUILists/Tests/List.MeasurementTests.swift b/BlueprintUILists/Tests/List.MeasurementTests.swift index 22596d4eb..3706072ab 100644 --- a/BlueprintUILists/Tests/List.MeasurementTests.swift +++ b/BlueprintUILists/Tests/List.MeasurementTests.swift @@ -25,52 +25,6 @@ class List_MeasurementTests : XCTestCase { } } - func test_natural() { - - func testList( - with measurement: List.Measurement, - direction: LayoutDirection - ) -> some Element { - List( - measurement: measurement, - configure: { - $0.layout = .table { - $0.direction = direction - } - }, - sections: { - Section("1") { - TestingItem(identifierValue: "1") - TestingItem(identifierValue: "2") - TestingItem(identifierValue: "3") - TestingItem(identifierValue: "4") - TestingItem(identifierValue: "5") - } - } - ) - } - - testcase("Horizontal Layouts") { - - let constraint = SizeConstraint(.init(width: 200, height: 200)) - - XCTAssertEqual( - CGSize(width: 0, height: 0), - testList( - with: .measureContent(horizontalFill: .natural, verticalFill: .natural), - direction: .vertical - ) - .measure(in: constraint) - ) - - } - - testcase("Vertical Layouts") { - - } - - } - private struct TestingItem : BlueprintItemContent, Equatable { var identifierValue: AnyHashable diff --git a/BlueprintUILists/Tests/ListTests.swift b/BlueprintUILists/Tests/ListTests.swift index ce562be5e..429bad7b1 100644 --- a/BlueprintUILists/Tests/ListTests.swift +++ b/BlueprintUILists/Tests/ListTests.swift @@ -67,7 +67,7 @@ class ListTests : XCTestCase { layoutDirection: .vertical ), in: constraint, - layoutMode: .default, + layoutMode: .caffeinated, horizontalFill: .fillParent, verticalFill: .fillParent ), @@ -82,7 +82,7 @@ class ListTests : XCTestCase { layoutDirection: .vertical ), in: constraint, - layoutMode: .default, + layoutMode: .caffeinated, horizontalFill: .natural, verticalFill: .fillParent ), @@ -97,7 +97,7 @@ class ListTests : XCTestCase { layoutDirection: .vertical ), in: constraint, - layoutMode: .default, + layoutMode: .caffeinated, horizontalFill: .natural, verticalFill: .fillParent ), @@ -112,12 +112,27 @@ class ListTests : XCTestCase { layoutDirection: .vertical ), in: constraint, - layoutMode: .default, + layoutMode: .caffeinated, horizontalFill: .natural, verticalFill: .natural ), CGSize(width: 900, height: 1000) ) + + XCTAssertEqual( + List.ListContent.size( + with: .init( + contentSize: CGSize(width: 1200, height: 1000), + naturalWidth: 900, + layoutDirection: .horizontal + ), + in: constraint, + layoutMode: .caffeinated, + horizontalFill: .natural, + verticalFill: .natural + ), + CGSize(width: 1200, height: 900) + ) } func test_listContentContext() { From 19f3cdf69f9ef54ca9941ab7e3a42f0cf26f2b94 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Fri, 8 Dec 2023 09:04:00 -0800 Subject: [PATCH 6/8] revert --- .../Tests/List.MeasurementTests.swift | 20 ------------------- .../Tests/XCTestCaseAdditions.swift | 5 ----- 2 files changed, 25 deletions(-) diff --git a/BlueprintUILists/Tests/List.MeasurementTests.swift b/BlueprintUILists/Tests/List.MeasurementTests.swift index 3706072ab..0804e1d05 100644 --- a/BlueprintUILists/Tests/List.MeasurementTests.swift +++ b/BlueprintUILists/Tests/List.MeasurementTests.swift @@ -6,7 +6,6 @@ // @testable import BlueprintUILists -import BlueprintUI import XCTest class List_MeasurementTests : XCTestCase { @@ -24,23 +23,4 @@ class List_MeasurementTests : XCTestCase { XCTFail() } } - - private struct TestingItem : BlueprintItemContent, Equatable { - - var identifierValue: AnyHashable - - func element(with info: ApplyItemContentInfo) -> Element { - Empty() - .constrainedTo(size: .init(width: 100, height: 50)) - } - } -} - - -fileprivate extension Element { - func measure(in constraint : SizeConstraint, environment : Environment = .empty) -> CGSize { - self - .content - .measure(in: constraint, environment: environment) - } } diff --git a/BlueprintUILists/Tests/XCTestCaseAdditions.swift b/BlueprintUILists/Tests/XCTestCaseAdditions.swift index a6ff5fd2c..26a0a6674 100644 --- a/BlueprintUILists/Tests/XCTestCaseAdditions.swift +++ b/BlueprintUILists/Tests/XCTestCaseAdditions.swift @@ -34,9 +34,4 @@ extension XCTestCase print("Iterations: \(iterations), Average Time: \(average)") } - - func testcase(_ name : String = "", _ block : () -> ()) - { - block() - } } From 5dafa9bd6e56e7beb39c4247228dccedb6bd4068 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Fri, 8 Dec 2023 09:04:44 -0800 Subject: [PATCH 7/8] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70bc891a2..fdd7007b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Fixed +- Fixed cross-axis measurements when using a `.natural` measurement size. + ### Added ### Removed From 9300cc1a112b079a996391e77ba4f95048d55566 Mon Sep 17 00:00:00 2001 From: Kyle Van Essen Date: Fri, 8 Dec 2023 10:42:16 -0800 Subject: [PATCH 8/8] Update demo to show natural measurements --- .../BlueprintListDemoViewController.swift | 71 +++++++++++++++---- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/Demo/Sources/Demos/Demo Screens/BlueprintListDemoViewController.swift b/Demo/Sources/Demos/Demo Screens/BlueprintListDemoViewController.swift index f9a6007a2..bf463ab7a 100644 --- a/Demo/Sources/Demos/Demo Screens/BlueprintListDemoViewController.swift +++ b/Demo/Sources/Demos/Demo Screens/BlueprintListDemoViewController.swift @@ -48,27 +48,68 @@ final class BlueprintListDemoViewController : UIViewController func reloadData() { - self.blueprintView.element = List { - Section("podcasts") { - let podcasts = Podcast.podcasts.sorted { $0.episode < $1.episode } - - for podcast in podcasts { - ElementItem(podcast, id: \.name) { _, _ in - PodcastElement(podcast: podcast) - } background: { _, _ in - Box(backgroundColor: .white, cornerStyle: .square) - } selectedBackground: { _, _ in - Box(backgroundColor: .lightGray, cornerStyle: .square) - } configure: { - $0.insertAndRemoveAnimations = .scaleUp + self.blueprintView.element = Overlay { + + List { + Section("podcasts") { + let podcasts = Podcast.podcasts.sorted { $0.episode < $1.episode } + + for podcast in podcasts { + ElementItem(podcast, id: \.name) { _, _ in + PodcastElement(podcast: podcast) + } background: { _, _ in + Box(backgroundColor: .white, cornerStyle: .square) + } selectedBackground: { _, _ in + Box(backgroundColor: .lightGray, cornerStyle: .square) + } configure: { + $0.insertAndRemoveAnimations = .scaleUp + } } } } + + EnvironmentReader { env in + List(measurement: .measureContent(verticalFill: .natural)) { list in + list.layout = .table { + $0.direction = .horizontal + + $0.layout.itemSpacing = 10 + + $0.bounds = .init(padding: .init(top: 0, left: 10, bottom: 0, right: 10)) + } + + list.appearance.backgroundColor = .clear + list.appearance.showsScrollIndicators = false + + } sections: { + Section("shows") { + for show in Podcast.shows { + ElementItem(show, id: \.self) { _, _ in + ShowElement(name:show) + } + } + } + } + .inset(bottom: env.safeAreaInsets.bottom) + .aligned(vertically: .bottom, horizontally: .fill) + } } } } +fileprivate struct ShowElement : ProxyElement { + + var name : String + + var elementRepresentation: Element { + Label(text: name) + .inset(uniform: 10) + .box(background: .systemGray6, corners: .capsule, borders: .solid(color: .systemGray3, width: 1)) + } +} + + fileprivate struct PodcastElement : ProxyElement { var podcast : Podcast @@ -134,6 +175,10 @@ struct Podcast : Equatable case downloaded case error } + + static var shows : [String] { + podcasts.map(\.name).sorted(by: <) + } static var podcasts : [Podcast] { return [