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

Fix cross-axis natural measurements #510

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
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
86 changes: 32 additions & 54 deletions BlueprintUILists/Sources/List.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -185,72 +185,50 @@ extension List {
verticalFill : Measurement.FillRule
) -> CGSize
{
precondition(
layoutMode == .caffeinated,
"Listable only supports the `.caffeinated` layout mode in Blueprint."
)
Comment on lines +188 to +191
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this still feature flagged? Does this need to occur in this PR?

I think I'd prefer that the switch to only supporting caffeinated layout be in a separate PR that's combined with the work to do so in Market/Register if we can...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flag still exists, though in theory its 100% rolled out. I had been hoping maybe we'd be ripping out the old layout mode before this merged, but it looks like that's not happening! I'll add this back in.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK feel free to ping me on Slack for another review when it's ready!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also decided to take a swing at removing legacy layout: square/Blueprint#475


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
}
}()

Expand Down
35 changes: 27 additions & 8 deletions BlueprintUILists/Tests/ListTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
),
Expand All @@ -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
),
Expand All @@ -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
),
Expand All @@ -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(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the test for this new case. Note the 900 in the returned size. Before, it would be 1000.

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() {
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Fixed

- Fixed cross-axis measurements when using a `.natural` measurement size.

### Added

### Removed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 [
Expand Down
12 changes: 9 additions & 3 deletions ListableUI/Sources/ListView/ListView+ContentSize.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
Expand All @@ -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
}
}
12 changes: 8 additions & 4 deletions ListableUI/Tests/ListView/ListView+ContentSizeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
)
}
Expand All @@ -65,7 +66,8 @@ class ListView_ContentSizeTests : XCTestCase

MeasuredListSize(
contentSize: CGSize(width: 100.0, height: 300.0),
naturalWidth: nil
naturalWidth: nil,
layoutDirection: .vertical
)
)
}
Expand All @@ -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
)
)
}
Expand All @@ -106,7 +109,8 @@ class ListView_ContentSizeTests : XCTestCase

MeasuredListSize(
contentSize: CGSize(width: 300.0, height: 100.0),
naturalWidth: nil
naturalWidth: nil,
layoutDirection: .horizontal
)
)
}
Expand Down
Loading