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

Refine UIKit to SwiftUI Measurement Strategies #162

Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
approach to resolve an issue that could cause collection view cells to layout with
unexpected dimensions
- Made new layout-based SwiftUI cell rendering option the default.
- Fixed an issue where a UIKit view bridged to SwiftUI that wraps would always take up the proposed
Copy link
Contributor

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?

Copy link
Contributor Author

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.

size instead of its intrinsic width.

## [0.10.0](https://github.com/airbnb/epoxy-ios/compare/0.9.0...0.10.0) - 2023-06-29

Expand Down
12 changes: 8 additions & 4 deletions Example/EpoxyExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -41,6 +41,7 @@
25F71A9E273D990E004D30CE /* DynamicRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25F71A9D273D990E004D30CE /* DynamicRow.swift */; };
25FEB79225AE431100F8EFBD /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25FEB79125AE431100F8EFBD /* MainViewController.swift */; };
2E8B007623F47E7E00D82A31 /* CustomSizingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E8B007523F47E7E00D82A31 /* CustomSizingView.swift */; };
601A2B0F2B716C5800FDB8FE /* EpoxyInSwiftUISizingStrategiesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 601A2B0E2B716C5800FDB8FE /* EpoxyInSwiftUISizingStrategiesViewController.swift */; };
A5AD02A72637CBF9007261BC /* TextFieldRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AD02A62637CBF9007261BC /* TextFieldRow.swift */; };
A61AFF592602B86E005356A8 /* Example.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61AFF582602B86E005356A8 /* Example.swift */; };
A61AFF5C2602B8D7005356A8 /* ReadmeExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61AFF5B2602B8D7005356A8 /* ReadmeExample.swift */; };
Expand Down Expand Up @@ -106,6 +107,7 @@
25F71A9D273D990E004D30CE /* DynamicRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRow.swift; sourceTree = "<group>"; };
25FEB79125AE431100F8EFBD /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = "<group>"; };
2E8B007523F47E7E00D82A31 /* CustomSizingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSizingView.swift; sourceTree = "<group>"; };
601A2B0E2B716C5800FDB8FE /* EpoxyInSwiftUISizingStrategiesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpoxyInSwiftUISizingStrategiesViewController.swift; sourceTree = "<group>"; };
A5AD02A62637CBF9007261BC /* TextFieldRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldRow.swift; sourceTree = "<group>"; };
A61AFF582602B86E005356A8 /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = "<group>"; };
A61AFF5B2602B8D7005356A8 /* ReadmeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadmeExample.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -298,9 +300,10 @@
A6725564271787E50085346B /* SwiftUI */ = {
isa = PBXGroup;
children = (
601A2B0E2B716C5800FDB8FE /* EpoxyInSwiftUISizingStrategiesViewController.swift */,
A67255602717874A0085346B /* EpoxyInSwiftUIViewController.swift */,
A67255612717874A0085346B /* SwiftUIInEpoxyViewController.swift */,
A6BABA742874B6E6004C49E3 /* SwiftUIInEpoxyResizingViewController.swift */,
A67255612717874A0085346B /* SwiftUIInEpoxyViewController.swift */,
);
path = SwiftUI;
sourceTree = "<group>";
Expand Down Expand Up @@ -443,6 +446,7 @@
25D39B5C262789E000B3DBF9 /* AlignableTextRow.swift in Sources */,
A6BABA752874B6E6004C49E3 /* SwiftUIInEpoxyResizingViewController.swift in Sources */,
25D39B3626277F0D00B3DBF9 /* ColorsViewController.swift in Sources */,
601A2B0F2B716C5800FDB8FE /* EpoxyInSwiftUISizingStrategiesViewController.swift in Sources */,
25B6765F25AE883700C00B20 /* ProductViewController.swift in Sources */,
A61AFF632602BA2B005356A8 /* NavigationWrapperViewController.swift in Sources */,
25D39B3726277F0D00B3DBF9 /* LayoutGroupsReadmeExamplesViewController.swift in Sources */,
Expand Down Expand Up @@ -610,7 +614,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = 5LL7P8E8RA;
INFOPLIST_FILE = EpoxyExample/Assets/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
LD_RUNPATH_SEARCH_PATHS = (
Expand All @@ -629,7 +633,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_TEAM = "";
DEVELOPMENT_TEAM = 5LL7P8E8RA;
INFOPLIST_FILE = EpoxyExample/Assets/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
LD_RUNPATH_SEARCH_PATHS = (
Expand Down
5 changes: 5 additions & 0 deletions Example/EpoxyExample/Data/Example.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ enum Example: CaseIterable {
case layoutGroups
case swiftUIToEpoxy
case epoxyToSwiftUI
case epoxyToSwiftUISizingStrategies
case swiftUIToEpoxyResizing

// MARK: Internal
Expand Down Expand Up @@ -42,6 +43,8 @@ enum Example: CaseIterable {
return "SwiftUI in Epoxy"
case .epoxyToSwiftUI:
return "Epoxy in SwiftUI"
case .epoxyToSwiftUISizingStrategies:
return "Epoxy in SwiftUI, Sizing Strategies"
case .swiftUIToEpoxyResizing:
return "SwiftUI in Epoxy, Resizing Cells"
}
Expand Down Expand Up @@ -71,6 +74,8 @@ enum Example: CaseIterable {
return "An example of SwiftUI views being embedded in Epoxy"
case .epoxyToSwiftUI:
return "An example of Epoxy views being embedded in SwiftUI"
case .epoxyToSwiftUISizingStrategies:
return "An example of the different strategies to size Epoxy views being embedded in SwiftUI"
case .swiftUIToEpoxyResizing:
return "An example of SwiftUI views being embedded in Epoxy that can invalidate their size"
}
Expand Down
2 changes: 2 additions & 0 deletions Example/EpoxyExample/ViewControllers/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ final class MainViewController: NavigationController {
return SwiftUIInEpoxyViewController()
case .epoxyToSwiftUI:
return EpoxyInSwiftUIViewController()
case .epoxyToSwiftUISizingStrategies:
return EpoxyInSwiftUISizingStrategiesViewController()
case .swiftUIToEpoxyResizing:
return SwiftUIInEpoxyResizingViewController()
}
Expand Down
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
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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
}

Expand All @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really understand the "viewType" part of this name

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
}

}
Loading
Loading