diff --git a/BlueprintUILists/Sources/List.swift b/BlueprintUILists/Sources/List.swift index 1d755de9..07a967dd 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,72 +185,50 @@ 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`. - """ - ) - } + return constraint.height.constrainedValue ?? .infinity + case .natural: - if case .caffeinated = layoutMode, let maxHeight = constraint.height.constrainedValue { - return min(size.contentSize.height, maxHeight) + 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 + ) } - return size.contentSize.height } }() diff --git a/BlueprintUILists/Tests/ListTests.swift b/BlueprintUILists/Tests/ListTests.swift index 5ce53838..429bad7b 100644 --- a/BlueprintUILists/Tests/ListTests.swift +++ b/BlueprintUILists/Tests/ListTests.swift @@ -63,10 +63,11 @@ class ListTests : XCTestCase { List.ListContent.size( with: .init( contentSize: CGSize(width: 1200, height: 1000), - naturalWidth: 900 + naturalWidth: 900, + layoutDirection: .vertical ), in: constraint, - layoutMode: .default, + layoutMode: .caffeinated, horizontalFill: .fillParent, verticalFill: .fillParent ), @@ -77,10 +78,11 @@ class ListTests : XCTestCase { List.ListContent.size( with: .init( contentSize: CGSize(width: 1200, height: 1000), - naturalWidth: 900 + naturalWidth: 900, + layoutDirection: .vertical ), in: constraint, - layoutMode: .default, + layoutMode: .caffeinated, horizontalFill: .natural, verticalFill: .fillParent ), @@ -91,10 +93,11 @@ class ListTests : XCTestCase { List.ListContent.size( with: .init( contentSize: CGSize(width: 1200, height: 1000), - naturalWidth: nil + naturalWidth: nil, + layoutDirection: .vertical ), in: constraint, - layoutMode: .default, + layoutMode: .caffeinated, horizontalFill: .natural, verticalFill: .fillParent ), @@ -105,15 +108,31 @@ class ListTests : XCTestCase { List.ListContent.size( with: .init( contentSize: CGSize(width: 1200, height: 1000), - naturalWidth: 900 + naturalWidth: 900, + 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() { diff --git a/CHANGELOG.md b/CHANGELOG.md index 70bc891a..fdd7007b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Fixed +- Fixed cross-axis measurements when using a `.natural` measurement size. + ### Added ### Removed diff --git a/Demo/Sources/Demos/Demo Screens/BlueprintListDemoViewController.swift b/Demo/Sources/Demos/Demo Screens/BlueprintListDemoViewController.swift index f9a6007a..bf463ab7 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 [ diff --git a/ListableUI/Sources/ListView/ListView+ContentSize.swift b/ListableUI/Sources/ListView/ListView+ContentSize.swift index f48d69b0..59b0980e 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 ) } } @@ -87,14 +88,19 @@ 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. + public var layoutDirection : LayoutDirection + public init( contentSize: CGSize, - naturalWidth: CGFloat? + naturalWidth: CGFloat?, + layoutDirection : LayoutDirection ) { self.contentSize = contentSize self.naturalWidth = naturalWidth + self.layoutDirection = layoutDirection } } diff --git a/ListableUI/Tests/ListView/ListView+ContentSizeTests.swift b/ListableUI/Tests/ListView/ListView+ContentSizeTests.swift index 743b46dd..4def753f 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 ) ) }