Skip to content

Commit

Permalink
Fix ConditionalSizeView on iOS 13 (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
luizmb authored Oct 28, 2021
1 parent c2d8bfa commit 3c5264e
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,76 @@ import SwiftUI
/// that you can't have 2 options with exact same height and width.
public struct ConditionalSizeView<Content: View>: View {
public let viewForSize: (CGSize) -> Content?
@State private var availableSize: CGSize?

public init<ContentState: Equatable>(
viewState: ConditionalSizeViewState<ContentState>,
@ViewBuilder content: @escaping (ContentState) -> Content
@ViewBuilder content: @escaping (ContentState, CGSize) -> Content
) {
viewForSize = { size in
viewState
.bestOption(for: size)
.map { bestOption in
content(bestOption.contentState)
content(bestOption.contentState, bestOption.size)
}
}
}

public var body: some View {
GeometryReader { proxy in
viewForSize(proxy.size)
content
.background(
GeometryReader { proxy in
Color.clear.preference(key: SizeKey.self, value: proxy.size)
}
)
.onPreferenceChange(SizeKey.self) { size in
self.availableSize = size
}
}

// This will render twice:
// - 1st time we don't know the available size yet, so:
// - We create a Color.clear, which is greedy and takes all available space.
// - The background will follow the main content size and give this size to the Geometry Reader.
// - The Geometry Reader will publish SizeKey.self with the available space
// - This is reduced by the SizeKey.reducer function, that has initial value of nil, so it accepts
// the new value published by the Geometry Reader.
// - onPreferenceChange is called due to the SizeKey state reduction, and it updates the local
// @State variable availableSize
// - @State is a Combine publisher that causes `content` to be re-rendered, so we enter this again
// - 2nd time we have the available size set, so:
// - We return viewForSize, that is the ViewBuilder user provided
// - All the steps above will happen again up to the SizeKey.reducer function, which, this time will
// realize that value is already set and won't call "nextValue()".
// - Because SizeKey.reducer didn't publish a new value, onPreferenceChange won't be triggered again
// and the loop will be interrupted. Be careful to not create an infinite loop. :-)
// Hopefully user won't "see" the empty space caused by the Color.clear version (spoiler: they won't,
// because everything is supposed to happens in the same UI loop.
@ViewBuilder
private var content: some View {
if let availableSize = self.availableSize {
viewForSize(availableSize)
} else {
// EmptyView or Spacer won't work, we need a greedy View (maxWidth: .infinity, maxHeight: .infinity)
Color.clear
}
}
}

extension View {
public static func conditionSize<ContentState: Equatable>(
viewState: ConditionalSizeViewState<ContentState>,
@ViewBuilder content: @escaping (ContentState) -> Self
@ViewBuilder content: @escaping (ContentState, CGSize) -> Self
) -> some View {
ConditionalSizeView(
viewState: viewState,
content: content
)
}
}

private struct SizeKey: PreferenceKey {
static func reduce(value: inout CGSize?, nextValue: () -> CGSize?) {
value = value ?? nextValue()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public struct ConditionalSizeViewState<ContentState: Equatable>: Equatable {
hasher.combine(size.height)
}

static func ==(lhs: Self, rhs: Self) -> Bool {
static func == (lhs: Self, rhs: Self) -> Bool {
// We should only consider the size when implementing Equatable on the options.
lhs.size == rhs.size
}
Expand Down Expand Up @@ -57,7 +57,7 @@ public struct ConditionalSizeViewState<ContentState: Equatable>: Equatable {
== (availableSize.height >= availableSize.width))
)
}
.sorted(by: {
.max(by: {
let lhsHeight = $0.size.height
let rhsHeight = $1.size.height

Expand All @@ -68,18 +68,17 @@ public struct ConditionalSizeViewState<ContentState: Equatable>: Equatable {
let rhsPixels = rhsHeight * rhsWidth

if lhsPixels != rhsPixels { // When they have different amount of pixels,
return lhsPixels > rhsPixels // sort by pixels on descending order
return lhsPixels < rhsPixels // sort by pixels on ascending order (max will pick the larger)
}
// otherwise check if available space is vertical
let availableSpaceIsVertical = availableSize.height >= availableSize.width

if availableSpaceIsVertical { // if available space is vertical (or square)
return lhsHeight > rhsHeight // sort by height on descending order,
return lhsHeight < rhsHeight // sort by height on ascending order (max will pick the larger),
} else {
return lhsWidth > rhsWidth // but if it's horizontal, then sort by width on descending order
return lhsWidth < rhsWidth // but if it's horizontal, then sort by width on ascending order
}

})
.first
}
}

0 comments on commit 3c5264e

Please sign in to comment.