-
Notifications
You must be signed in to change notification settings - Fork 76
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
Refine UIKit to SwiftUI Measurement Strategies #162
Changes from all commits
363042e
54a3a23
c0c12cd
2ca6dc4
35215ce
0b7f36e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
// Created by Bryn Bodayle on 2/5/24. | ||
// Copyright © 2024 Airbnb Inc. All rights reserved. | ||
|
||
import Epoxy | ||
import SwiftUI | ||
import UIKit | ||
|
||
// MARK: - EpoxyInSwiftUISizingStrategiesViewController | ||
|
||
/// Demo of the various sizing strategies for UIKit views bridged to SwiftUI | ||
final class EpoxyInSwiftUISizingStrategiesViewController: UIHostingController<EpoxyInSwiftUISizingStrategiesView> { | ||
init() { | ||
super.init(rootView: EpoxyInSwiftUISizingStrategiesView()) | ||
} | ||
|
||
required init?(coder _: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
} | ||
|
||
// MARK: - EpoxyInSwiftUISizingStrategiesView | ||
|
||
struct EpoxyInSwiftUISizingStrategiesView: View { | ||
|
||
// MARK: Internal | ||
|
||
let text = "The text" | ||
|
||
var body: some View { | ||
ScrollView { | ||
LazyVStack(alignment: .leading, spacing: 12) { | ||
Text("Word Count: \(wordCount)") | ||
.padding() | ||
Slider(value: $wordCount, in: 0...100) | ||
.padding() | ||
Text("Proposed Width/Height set to 150pt") | ||
.padding() | ||
|
||
ForEach(SwiftUIMeasurementContainerStrategy.allCases) { value in | ||
Text(value.displayString) | ||
.bold() | ||
.padding() | ||
LabelView( | ||
text: BeloIpsum.sentence(count: 1, wordCount: Int(wordCount)), | ||
measurementStrategy: value) | ||
.frame(width: value.proposedWidth, height: value.proposedHeight) | ||
.border(.red) | ||
} | ||
} | ||
} | ||
} | ||
|
||
// MARK: Private | ||
|
||
@State private var wordCount = 12.0 | ||
} | ||
|
||
// MARK: - SwiftUIMeasurementContainerStrategy + Identifiable, CaseIterable | ||
|
||
extension SwiftUIMeasurementContainerStrategy: Identifiable, CaseIterable { | ||
|
||
// MARK: Public | ||
|
||
public static var allCases: [SwiftUIMeasurementContainerStrategy] = [ | ||
.automatic, | ||
.proposed, | ||
.intrinsicHeightProposedOrIntrinsicWidth, | ||
.intrinsicHeightProposedWidth, | ||
.intrinsicWidthProposedHeight, | ||
.intrinsic, | ||
] | ||
|
||
public var id: Self { | ||
self | ||
} | ||
|
||
// MARK: Internal | ||
|
||
var displayString: String { | ||
switch self { | ||
case .automatic: | ||
return "Automatic" | ||
case .proposed: | ||
return "Proposed" | ||
case .intrinsicHeightProposedOrIntrinsicWidth: | ||
return "Intrinsic Height, Proposed Width or Intrinsic Width" | ||
case .intrinsicHeightProposedWidth: | ||
return "Intrinsic Height, Proposed Width" | ||
case .intrinsicWidthProposedHeight: | ||
return "Intrinsic Width, Proposed Height" | ||
case .intrinsic: | ||
return "Intrinsic" | ||
} | ||
} | ||
|
||
var proposedWidth: CGFloat? { | ||
switch self { | ||
case .proposed, .intrinsicHeightProposedWidth: | ||
return 150 | ||
default: | ||
return nil | ||
} | ||
} | ||
|
||
var proposedHeight: CGFloat? { | ||
switch self { | ||
case .proposed, .intrinsicWidthProposedHeight: | ||
return 150 | ||
default: | ||
return nil | ||
} | ||
} | ||
} | ||
|
||
// MARK: - LabelView | ||
|
||
struct LabelView: UIViewConfiguringSwiftUIView { | ||
|
||
let text: String? | ||
let measurementStrategy: SwiftUIMeasurementContainerStrategy | ||
|
||
var configurations = [SwiftUIView<UILabel, Void>.Configuration]() | ||
|
||
var body: some View { | ||
UILabel.swiftUIView { | ||
let label = UILabel(frame: .zero) | ||
label.numberOfLines = 0 | ||
return label | ||
} | ||
.configure { context in | ||
context.view.text = text | ||
context.container.invalidateIntrinsicContentSize() | ||
} | ||
.configurations(configurations) | ||
.sizing(measurementStrategy) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,13 +56,16 @@ extension MeasuringViewRepresentable { | |
|
||
// Creates a `CGSize` by replacing `nil`s with `UIView.noIntrinsicMetric` | ||
uiView.proposedSize = .init( | ||
width: children.first { $0.label == "width" }?.value as? CGFloat ?? ViewType.noIntrinsicMetric, | ||
height: children.first { $0.label == "height" }?.value as? CGFloat ?? ViewType.noIntrinsicMetric) | ||
|
||
width: ( | ||
children.first { $0.label == "width" }? | ||
.value as? CGFloat ?? ViewType.noIntrinsicMetric).constraintSafeValue, | ||
height: ( | ||
children.first { $0.label == "height" }? | ||
.value as? CGFloat ?? ViewType.noIntrinsicMetric).constraintSafeValue) | ||
size = uiView.measuredFittingSize | ||
} | ||
|
||
#if swift(>=5.7) // Proxy check for being built with the iOS 15 SDK | ||
#if swift(>=5.7.1) // Proxy check for being built with the iOS 15 SDK | ||
@available(iOS 16.0, tvOS 16.0, macOS 13.0, *) | ||
public func sizeThatFits( | ||
_ proposal: ProposedViewSize, | ||
|
@@ -71,12 +74,7 @@ extension MeasuringViewRepresentable { | |
-> CGSize? | ||
{ | ||
uiView.strategy = sizing | ||
|
||
// Creates a size by replacing `nil`s with `UIView.noIntrinsicMetric` | ||
uiView.proposedSize = .init( | ||
width: proposal.width ?? ViewType.noIntrinsicMetric, | ||
height: proposal.height ?? ViewType.noIntrinsicMetric) | ||
|
||
uiView.proposedSize = proposal.viewTypeValue | ||
return uiView.measuredFittingSize | ||
} | ||
#endif | ||
|
@@ -91,14 +89,14 @@ extension MeasuringViewRepresentable { | |
nsView: NSViewType) | ||
{ | ||
nsView.strategy = sizing | ||
|
||
let children = Mirror(reflecting: proposedSize).children | ||
|
||
// Creates a `CGSize` by replacing `nil`s with `UIView.noIntrinsicMetric` | ||
nsView.proposedSize = .init( | ||
width: children.first { $0.label == "width" }?.value as? CGFloat ?? ViewType.noIntrinsicMetric, | ||
height: children.first { $0.label == "height" }?.value as? CGFloat ?? ViewType.noIntrinsicMetric) | ||
|
||
width: ( | ||
children.first { $0.label == "width" }? | ||
.value as? CGFloat ?? ViewType.noIntrinsicMetric).constraintSafeValue, | ||
height: ( | ||
children.first { $0.label == "height" }? | ||
.value as? CGFloat ?? ViewType.noIntrinsicMetric).constraintSafeValue) | ||
size = nsView.measuredFittingSize | ||
} | ||
|
||
|
@@ -112,14 +110,38 @@ extension MeasuringViewRepresentable { | |
-> CGSize? | ||
{ | ||
nsView.strategy = sizing | ||
|
||
// Creates a size by replacing `nil`s with `UIView.noIntrinsicMetric` | ||
nsView.proposedSize = .init( | ||
width: proposal.width ?? ViewType.noIntrinsicMetric, | ||
height: proposal.height ?? ViewType.noIntrinsicMetric) | ||
|
||
nsView.proposedSize = proposal.viewTypeValue | ||
return nsView.measuredFittingSize | ||
} | ||
#endif | ||
} | ||
#endif | ||
|
||
#if swift(>=5.7.1) // Proxy check for being built with the iOS 15 SDK | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was getting a build error until I bumped this swift version up |
||
@available(iOS 16.0, tvOS 16.0, macOS 13.0, *) | ||
extension ProposedViewSize { | ||
/// Creates a size suitable for the current platform's view building framework by capping infinite values to a significantly large value and | ||
/// replacing `nil`s with `UIView.noIntrinsicMetric` | ||
var viewTypeValue: CGSize { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't really understand the "viewType" part of this name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment to clarify |
||
.init( | ||
width: width?.constraintSafeValue ?? ViewType.noIntrinsicMetric, | ||
height: height?.constraintSafeValue ?? ViewType.noIntrinsicMetric) | ||
} | ||
} | ||
|
||
#endif | ||
|
||
extension CGFloat { | ||
static var maxConstraintValue: CGFloat { | ||
// On iOS 15 and below, configuring an auto layout constraint with the constant | ||
// `.greatestFiniteMagnitude` exceeds an internal limit and logs an exception to console. To | ||
// avoid, we use a significantly large value. | ||
1_000_000 | ||
} | ||
|
||
/// Returns a value suitable for configuring auto layout constraints | ||
var constraintSafeValue: CGFloat { | ||
isInfinite ? .maxConstraintValue : self | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess we fixed it for AppKit too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBD, waiting for some updates from @calda since Epoxy iOS doesn't officially support native macOS, but Lottie iOS pulls in the code and uses it directly for macOS.