From 363042e47c74cc5192967e3b4410acf4238aa457 Mon Sep 17 00:00:00 2001 From: Bryn Bodayle Date: Thu, 15 Feb 2024 14:27:03 -0800 Subject: [PATCH 1/6] Update UIKit from swiftUI measurement strategies --- .../MeasuringViewRepresentable.swift | 58 ++++-- .../SwiftUIMeasurementContainer.swift | 192 ++++++++++-------- 2 files changed, 142 insertions(+), 108 deletions(-) diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift index ed4fdc7c..fcc0643a 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift @@ -56,9 +56,10 @@ 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 } @@ -71,12 +72,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.constraintSafeValue return uiView.measuredFittingSize } #endif @@ -91,14 +87,8 @@ 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) - + nsView.proposedSize = proposal.constraintSafeValue size = nsView.measuredFittingSize } @@ -112,14 +102,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.constraintSafeValue return nsView.measuredFittingSize } #endif } #endif + +#if swift(>=5.7) // Proxy check for being built with the iOS 15 SDK +@available(iOS 16.0, tvOS 16.0, macOS 13.0, *) + +extension ProposedViewSize { + // Creates a size by replacing `nil`s with `UIView.noIntrinsicMetric` + var constraintSafeValue: CGSize { + .init( + width: width?.constraintSafeValue ?? ViewType.noIntrinsicMetric, + height: height?.constraintSafeValue ?? ViewType.noIntrinsicMetric) + } +} + +extension CGFloat { + var constraintSafeValue: CGFloat { + isInfinite ? .maxConstraintValue : self + } +} + +#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 + } +} diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift index 8730410f..3d564c3c 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift @@ -73,8 +73,11 @@ public final class SwiftUIMeasurementContainer: ViewType { public var proposedSize = CGSize.noIntrinsicMetric { didSet { guard oldValue != proposedSize else { return } + _resolvedStrategy = nil + // The proposed size is only used by the measurement, so just re-measure. _measuredFittingSize = nil + setNeedsUpdateConstraints() } } @@ -128,6 +131,11 @@ public final class SwiftUIMeasurementContainer: ViewType { _measuredFittingSize = nil } + public override func updateConstraints() { + updateSizeConstraints() + super.updateConstraints() + } + // MARK: Private /// The most recently measured intrinsic content size of the `uiView`, else `noIntrinsicMetric` if @@ -140,14 +148,21 @@ public final class SwiftUIMeasurementContainer: ViewType { /// The bounds size at the time of the latest measurement. private var latestMeasurementBoundsSize: CGSize? - /// The most recently updated set of constraints constraining `uiView` to `self`. - private var uiViewConstraints = [NSLayoutConstraint.Attribute: NSLayoutConstraint]() + private var topConstraint: NSLayoutConstraint? + private var leadingConstraint: NSLayoutConstraint? + private var maxWidthConstraint: NSLayoutConstraint? + private var fixedWidthConstraint: NSLayoutConstraint? + private var fixedHeightConstraint: NSLayoutConstraint? /// The cached `resolvedStrategy` to prevent unnecessary re-measurements. private var _resolvedStrategy: ResolvedSwiftUIMeasurementContainerStrategy? /// The cached `measuredFittingSize` to prevent unnecessary re-measurements. - private var _measuredFittingSize: CGSize? + private var _measuredFittingSize: CGSize? { + didSet { + setNeedsUpdateConstraints() + } + } /// The resolved measurement strategy. private var resolvedStrategy: ResolvedSwiftUIMeasurementContainerStrategy { @@ -158,15 +173,10 @@ public final class SwiftUIMeasurementContainer: ViewType { let resolved: ResolvedSwiftUIMeasurementContainerStrategy switch strategy { case .automatic: - // Perform an intrinsic size measurement pass, which gives us valid values for - // `UILabel.preferredMaxLayoutWidth`. - let intrinsicSize = content.systemLayoutFittingIntrinsicSize() - - // If the view has a intrinsic width and contains a double layout pass subview, give it the - // proposed width to allow the label content to gracefully wrap to multiple lines. - if intrinsicSize.width > 0, content.containsDoubleLayoutPassSubviews() { - resolved = .intrinsicHeightProposedWidth + if content.containsDoubleLayoutPassSubviews() { + resolved = .intrinsicHeightProposedOrIntrinsicWidth } else { + let intrinsicSize = content.systemLayoutFittingIntrinsicSize() let zero = CGFloat(0) switch (width: intrinsicSize.width, height: intrinsicSize.height) { case (width: ...zero, height: ...zero): @@ -185,6 +195,8 @@ public final class SwiftUIMeasurementContainer: ViewType { resolved = .intrinsicHeightProposedWidth case .intrinsicWidthProposedHeight: resolved = .intrinsicWidthProposedHeight + case .intrinsicHeightProposedOrIntrinsicWidth: + resolved = .intrinsicHeightProposedOrIntrinsicWidth case .intrinsic: resolved = .intrinsic(content.systemLayoutFittingIntrinsicSize()) } @@ -195,57 +207,75 @@ public final class SwiftUIMeasurementContainer: ViewType { private func setUpConstraints() { content.translatesAutoresizingMaskIntoConstraints = false - let leading = content.leadingAnchor.constraint(equalTo: leadingAnchor) - let top = content.topAnchor.constraint(equalTo: topAnchor) - let trailing = content.trailingAnchor.constraint(equalTo: trailingAnchor) - let bottom = content.bottomAnchor.constraint(equalTo: bottomAnchor) - let newConstraints: [NSLayoutConstraint.Attribute: NSLayoutConstraint] = [ - .leading: leading, .top: top, .trailing: trailing, .bottom: bottom, - ] - // Start with the lowest priority constraints so we aren't measuring the view too early, the - // priorities will be updated later on. - prioritizeConstraints(newConstraints, strategy: .intrinsic(.zero)) - - NSLayoutConstraint.deactivate(Array(uiViewConstraints.values)) - uiViewConstraints = newConstraints - NSLayoutConstraint.activate(Array(uiViewConstraints.values)) + let oldConstraints = [ + leadingConstraint, + topConstraint, + maxWidthConstraint, + fixedWidthConstraint, + fixedHeightConstraint] + .compactMap { $0 } + NSLayoutConstraint.deactivate(oldConstraints) + + leadingConstraint = content.leadingAnchor.constraint(equalTo: leadingAnchor) + topConstraint = content.topAnchor.constraint(equalTo: topAnchor) + maxWidthConstraint = content.widthAnchor.constraint( + lessThanOrEqualToConstant: .maxConstraintValue) + fixedWidthConstraint = content.widthAnchor.constraint(equalToConstant: 0) + fixedHeightConstraint = content.heightAnchor.constraint(equalToConstant: 0) + + NSLayoutConstraint.activate([leadingConstraint, topConstraint].compactMap { $0 }) } - /// Prioritizes the given constraints based on the provided resolved strategy. - private func prioritizeConstraints( - _ constraints: [NSLayoutConstraint.Attribute: NSLayoutConstraint], - strategy: ResolvedSwiftUIMeasurementContainerStrategy) - { - // Give a required constraint in the dimensions that are fixed to the bounds, otherwise almost - // required. - switch strategy { - case .proposed: - constraints[.trailing]?.priority = .required - constraints[.bottom]?.priority = .required - case .intrinsicHeightProposedWidth: - constraints[.trailing]?.priority = .required - constraints[.bottom]?.priority = .almostRequired - case .intrinsicWidthProposedHeight: - constraints[.trailing]?.priority = .almostRequired - constraints[.bottom]?.priority = .required - case .intrinsic: - constraints[.trailing]?.priority = .almostRequired - constraints[.bottom]?.priority = .almostRequired + private func updateSizeConstraints() { + // deactivate all size constraints to avoid side effects when doing a sizing pass to resolve the + // measurement strategy + let constraints = [ + maxWidthConstraint, + fixedWidthConstraint, + fixedHeightConstraint].compactMap { $0 } + NSLayoutConstraint.deactivate(constraints) + + // avoid creating negative value constraints + let nonNegativeProposedSize = CGSize( + width: max(proposedSize.width, 0), + height: max(proposedSize.height, 0)) + + if let measuredSize = _measuredFittingSize { + fixedWidthConstraint?.constant = measuredSize.width + fixedHeightConstraint?.constant = measuredSize.height + fixedWidthConstraint?.isActive = true + fixedHeightConstraint?.isActive = true + } else { + switch resolvedStrategy { + case .proposed: + fixedWidthConstraint?.constant = nonNegativeProposedSize.width + fixedHeightConstraint?.constant = nonNegativeProposedSize.height + fixedWidthConstraint?.isActive = true + fixedHeightConstraint?.isActive = true + + case .intrinsicHeightProposedWidth: + fixedWidthConstraint?.constant = nonNegativeProposedSize.width + fixedWidthConstraint?.isActive = true + + case .intrinsicWidthProposedHeight: + fixedHeightConstraint?.constant = nonNegativeProposedSize.height + fixedHeightConstraint?.isActive = true + + case .intrinsicHeightProposedOrIntrinsicWidth: + maxWidthConstraint?.constant = nonNegativeProposedSize.width + maxWidthConstraint?.isActive = nonNegativeProposedSize.width > 0 + + case .intrinsic: + break // no op, all size constraints already deactivated + + } } - - #if os(macOS) - // On macOS, views default to having required constraints setting their height / width - // equal to their intrinsic content size. These have to be disabled in favor of the constraints - // we create here. - content.isVerticalContentSizeConstraintActive = false - content.isHorizontalContentSizeConstraintActive = false - #endif } /// Measures the `uiView`, storing the resulting size in `measuredIntrinsicContentSize`. private func measureView() -> CGSize { + updateConstraintsIfNeeded() latestMeasurementBoundsSize = bounds.size - prioritizeConstraints(uiViewConstraints, strategy: resolvedStrategy) var measuredSize: CGSize let proposedSizeElseBounds = proposedSize.replacingNoIntrinsicMetric(with: bounds.size) @@ -262,30 +292,14 @@ public final class SwiftUIMeasurementContainer: ViewType { measuredSize = content.systemLayoutFittingIntrinsicWidthFixedHeight(proposedSizeElseBounds.height) measuredSize.height = ViewType.noIntrinsicMetric + case .intrinsicHeightProposedOrIntrinsicWidth: + let fittingSize = content.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + measuredSize = CGSize( + width: min(fittingSize.width, (proposedSize.width > 0 ? proposedSize.width : fittingSize.width)), + height: fittingSize.height) + case .intrinsic(let size): measuredSize = size - - // If the measured size exceeds an available width or height, set the measured size to - // `noIntrinsicMetric` to ensure that the component can be compressed, otherwise it will - // overflow beyond the proposed size. - // - If the previous intrinsic content size is the same as the new proposed size, we don't - // do this as SwiftUI sometimes "proposes" the same intrinsic size back to the component and - // we don't want that scenario to prevent size changes when there is actually more space - // available. - if - proposedSize.width != ViewType.noIntrinsicMetric, - measuredSize.width > proposedSizeElseBounds.width, - _intrinsicContentSize.width != proposedSize.width - { - measuredSize.width = ViewType.noIntrinsicMetric - } - if - proposedSize.height != ViewType.noIntrinsicMetric, - measuredSize.height > proposedSizeElseBounds.height, - _intrinsicContentSize.height != proposedSize.height - { - measuredSize.height = ViewType.noIntrinsicMetric - } } _intrinsicContentSize = measuredSize @@ -306,9 +320,8 @@ public enum SwiftUIMeasurementContainerStrategy { /// - The `uiView` will be given its intrinsic width and/or height when measurement in that /// dimension produces a positive value, while zero/negative values will result in that /// dimension receiving the available space proposed by the parent. - /// - If the view contains `UILabel` subviews that require a double layout pass as determined by - /// a `preferredMaxLayoutWidth` that's greater than zero after a layout, then the view will - /// default to `intrinsicHeightProposedWidth` to allow the labels to wrap. + /// - If the view contains `UILabel` subviews that require a double layout pass as determined by support multiple lines of text + /// the view will default to `intrinsicHeightProposedOrIntrinsicWidth` to allow the labels to wrap. /// /// If you would like to opt out of automatic sizing for performance or to override the default /// behavior, choose another strategy. @@ -319,11 +332,17 @@ public enum SwiftUIMeasurementContainerStrategy { /// Typically used for views that should expand greedily in both axes, e.g. a background view. case proposed - /// The `uiView` is sized with its intrinsic height and expands horizontally to fill the width - /// proposed by its parent. + /// The `uiView`'s receives either its intrinsic width or the proposed width, whichever is smaller. The view receives its height based + /// on the chosen width. /// /// Typically used for views that have a height that's a function of their width, e.g. a row with /// text that can wrap to multiple lines. + case intrinsicHeightProposedOrIntrinsicWidth + + /// The `uiView` is sized with its intrinsic height and expands horizontally to fill the width + /// proposed by its parent. + /// + /// Typically used for views that have a height that's a function of their parent's width. case intrinsicHeightProposedWidth /// The `uiView` is sized with its intrinsic width and expands vertically to fill the height @@ -345,7 +364,8 @@ public enum SwiftUIMeasurementContainerStrategy { /// The resolved measurement strategy of a `SwiftUIMeasurementContainer`, matching the cases of the /// `SwiftUIMeasurementContainerStrategy` without the automatic case. private enum ResolvedSwiftUIMeasurementContainerStrategy { - case proposed, intrinsicHeightProposedWidth, intrinsicWidthProposedHeight, intrinsic(CGSize) + case proposed, intrinsicHeightProposedWidth, intrinsicWidthProposedHeight, + intrinsicHeightProposedOrIntrinsicWidth, intrinsic(CGSize) } // MARK: - UILayoutPriority @@ -413,16 +433,16 @@ extension ViewType { #endif } - /// Whether this view or any of its subviews has a subview that has a double layout pass `UILabel` - /// as determined by a non-zero `preferredMaxLayoutWidth`, which implies that it should get a - /// `intrinsicHeightProposedWidth` sizing strategy to allow the label to wrap and grow. + /// Whether this view or any of its subviews has a subview that has a double layout pass `UILabel` as determined by being + /// configured to show multiple lines of text. This view should get a `intrinsicHeightProposedOrIntrinsicWidth` sizing + /// strategy so that it wraps correctly. @nonobjc fileprivate func containsDoubleLayoutPassSubviews() -> Bool { #if os(macOS) return false #else var contains = false - if let label = self as? UILabel, label.preferredMaxLayoutWidth > 0 { + if let label = self as? UILabel, label.numberOfLines != 1 { contains = true } for subview in subviews { From 54a3a237e4b96aa13772475f756139005268743f Mon Sep 17 00:00:00 2001 From: Bryn Bodayle Date: Thu, 15 Feb 2024 14:27:10 -0800 Subject: [PATCH 2/6] Add example code --- CHANGELOG.md | 2 + .../EpoxyExample.xcodeproj/project.pbxproj | 12 +- Example/EpoxyExample/Data/Example.swift | 5 + .../ViewControllers/MainViewController.swift | 2 + ...wiftUISizingStrategiesViewController.swift | 122 ++++++++++++++++++ .../MeasuringViewRepresentable.swift | 17 ++- 6 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4767e3..bf4a64c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 + size instead of it's intrinsic width. ## [0.10.0](https://github.com/airbnb/epoxy-ios/compare/0.9.0...0.10.0) - 2023-06-29 diff --git a/Example/EpoxyExample.xcodeproj/project.pbxproj b/Example/EpoxyExample.xcodeproj/project.pbxproj index e2819189..29a8d2d7 100644 --- a/Example/EpoxyExample.xcodeproj/project.pbxproj +++ b/Example/EpoxyExample.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -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 */; }; @@ -106,6 +107,7 @@ 25F71A9D273D990E004D30CE /* DynamicRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicRow.swift; sourceTree = ""; }; 25FEB79125AE431100F8EFBD /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; 2E8B007523F47E7E00D82A31 /* CustomSizingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSizingView.swift; sourceTree = ""; }; + 601A2B0E2B716C5800FDB8FE /* EpoxyInSwiftUISizingStrategiesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EpoxyInSwiftUISizingStrategiesViewController.swift; sourceTree = ""; }; A5AD02A62637CBF9007261BC /* TextFieldRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldRow.swift; sourceTree = ""; }; A61AFF582602B86E005356A8 /* Example.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Example.swift; sourceTree = ""; }; A61AFF5B2602B8D7005356A8 /* ReadmeExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadmeExample.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, @@ -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 = ( @@ -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 = ( diff --git a/Example/EpoxyExample/Data/Example.swift b/Example/EpoxyExample/Data/Example.swift index 2aa38455..b44a6e4c 100644 --- a/Example/EpoxyExample/Data/Example.swift +++ b/Example/EpoxyExample/Data/Example.swift @@ -14,6 +14,7 @@ enum Example: CaseIterable { case layoutGroups case swiftUIToEpoxy case epoxyToSwiftUI + case epoxyToSwiftUISizingStrategies case swiftUIToEpoxyResizing // MARK: Internal @@ -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" } @@ -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" } diff --git a/Example/EpoxyExample/ViewControllers/MainViewController.swift b/Example/EpoxyExample/ViewControllers/MainViewController.swift index 18b2ee9f..28bf7010 100644 --- a/Example/EpoxyExample/ViewControllers/MainViewController.swift +++ b/Example/EpoxyExample/ViewControllers/MainViewController.swift @@ -120,6 +120,8 @@ final class MainViewController: NavigationController { return SwiftUIInEpoxyViewController() case .epoxyToSwiftUI: return EpoxyInSwiftUIViewController() + case .epoxyToSwiftUISizingStrategies: + return EpoxyInSwiftUISizingStrategiesViewController() case .swiftUIToEpoxyResizing: return SwiftUIInEpoxyResizingViewController() } diff --git a/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift b/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift new file mode 100644 index 00000000..7e3471b6 --- /dev/null +++ b/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift @@ -0,0 +1,122 @@ +// Created by Bryn Bodayle on 2/5/24. +// Copyright © 2024 Airbnb Inc. All rights reserved. + +import Epoxy +import SwiftUI +import UIKit + +// MARK: - EpoxyInSwiftUISizingStrategiesViewViewController + +/// Demo of the various sizing strategies for UIKit views bridged to SwiftUI +final class EpoxyInSwiftUISizingStrategiesViewController: UIHostingController { + init() { + super.init(rootView: EpoxyInSwiftUISizingStrategiesView()) + } + + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: - EpoxyInSwiftUIView + +struct EpoxyInSwiftUISizingStrategiesView: View { + 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) + } + } + } + } + + @State private var wordCount = 12.0 +} + +extension SwiftUIMeasurementContainerStrategy: Identifiable, CaseIterable { + public static var allCases: [SwiftUIMeasurementContainerStrategy] = [ + .automatic, + .proposed, + .intrinsicHeightProposedOrIntrinsicWidth, + .intrinsicHeightProposedWidth, + .intrinsicWidthProposedHeight, + .intrinsic + ] + + public var id: Self { + self + } + + var displayString: String { + switch self { + case .automatic: + "Automatic" + case .proposed: + "Proposed" + case .intrinsicHeightProposedOrIntrinsicWidth: + "Intrinsic Height, Proposed Width or Intrinsic Width" + case .intrinsicHeightProposedWidth: + "Intrinsic Height, Proposed Width" + case .intrinsicWidthProposedHeight: + "Intrinsic Width, Proposed Height" + case .intrinsic: + "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 + } + } +} + +struct LabelView: UIViewConfiguringSwiftUIView { + + let text: String? + let measurementStrategy: SwiftUIMeasurementContainerStrategy + + var configurations = [SwiftUIView.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) + } +} diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift index fcc0643a..ce6b8677 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift @@ -72,7 +72,7 @@ extension MeasuringViewRepresentable { -> CGSize? { uiView.strategy = sizing - uiView.proposedSize = proposal.constraintSafeValue + uiView.proposedSize = proposal.viewTypeValue return uiView.measuredFittingSize } #endif @@ -88,7 +88,7 @@ extension MeasuringViewRepresentable { { nsView.strategy = sizing let children = Mirror(reflecting: proposedSize).children - nsView.proposedSize = proposal.constraintSafeValue + nsView.proposedSize = proposal.viewTypeValue size = nsView.measuredFittingSize } @@ -102,7 +102,7 @@ extension MeasuringViewRepresentable { -> CGSize? { nsView.strategy = sizing - nsView.proposedSize = proposal.constraintSafeValue + nsView.proposedSize = proposal.viewTypeValue return nsView.measuredFittingSize } #endif @@ -113,23 +113,22 @@ extension MeasuringViewRepresentable { @available(iOS 16.0, tvOS 16.0, macOS 13.0, *) extension ProposedViewSize { - // Creates a size by replacing `nil`s with `UIView.noIntrinsicMetric` - var constraintSafeValue: CGSize { + /// Creates a size by replacing `nil`s with `UIView.noIntrinsicMetric` + var viewTypeValue: CGSize { .init( width: width?.constraintSafeValue ?? ViewType.noIntrinsicMetric, height: height?.constraintSafeValue ?? ViewType.noIntrinsicMetric) } } +#endif + extension CGFloat { + /// Returns a value suitable for configuring auto layout constraints var constraintSafeValue: CGFloat { isInfinite ? .maxConstraintValue : self } -} -#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 From c0c12cd13ded3186e0bf1bbb82389c6deb07b17b Mon Sep 17 00:00:00 2001 From: Bryn Bodayle Date: Fri, 16 Feb 2024 14:44:23 -0800 Subject: [PATCH 3/6] Swiftlint and fixes --- ...wiftUISizingStrategiesViewController.swift | 23 +++++++++++++++---- .../MeasuringViewRepresentable.swift | 22 ++++++++++-------- .../SwiftUIMeasurementContainer.swift | 16 +++++++++---- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift b/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift index 7e3471b6..3bf37270 100644 --- a/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift +++ b/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift @@ -5,7 +5,7 @@ import Epoxy import SwiftUI import UIKit -// MARK: - EpoxyInSwiftUISizingStrategiesViewViewController +// MARK: - EpoxyInSwiftUISizingStrategiesViewController /// Demo of the various sizing strategies for UIKit views bridged to SwiftUI final class EpoxyInSwiftUISizingStrategiesViewController: UIHostingController { @@ -18,10 +18,14 @@ final class EpoxyInSwiftUISizingStrategiesViewController: UIHostingController=5.7) // Proxy check for being built with the iOS 15 SDK @available(iOS 16.0, tvOS 16.0, macOS 13.0, *) - extension ProposedViewSize { /// Creates a size by replacing `nil`s with `UIView.noIntrinsicMetric` var viewTypeValue: CGSize { @@ -124,15 +125,16 @@ extension ProposedViewSize { #endif extension CGFloat { - /// Returns a value suitable for configuring auto layout constraints - var constraintSafeValue: CGFloat { - isInfinite ? .maxConstraintValue : self - } - 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 + } + } diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift index 3d564c3c..2658a5be 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift @@ -1,6 +1,11 @@ // Created by Bryn Bodayle on 1/24/22. // Copyright © 2022 Airbnb Inc. All rights reserved. +#if os(iOS) || os(tvOS) +import UIKit +#elseif os(macOS) +import AppKit +#endif import SwiftUI // MARK: - SwiftUIMeasurementContainer @@ -212,8 +217,9 @@ public final class SwiftUIMeasurementContainer: ViewType { topConstraint, maxWidthConstraint, fixedWidthConstraint, - fixedHeightConstraint] - .compactMap { $0 } + fixedHeightConstraint, + ] + .compactMap { $0 } NSLayoutConstraint.deactivate(oldConstraints) leadingConstraint = content.leadingAnchor.constraint(equalTo: leadingAnchor) @@ -232,7 +238,8 @@ public final class SwiftUIMeasurementContainer: ViewType { let constraints = [ maxWidthConstraint, fixedWidthConstraint, - fixedHeightConstraint].compactMap { $0 } + fixedHeightConstraint, + ].compactMap { $0 } NSLayoutConstraint.deactivate(constraints) // avoid creating negative value constraints @@ -267,7 +274,6 @@ public final class SwiftUIMeasurementContainer: ViewType { case .intrinsic: break // no op, all size constraints already deactivated - } } } @@ -295,7 +301,7 @@ public final class SwiftUIMeasurementContainer: ViewType { case .intrinsicHeightProposedOrIntrinsicWidth: let fittingSize = content.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) measuredSize = CGSize( - width: min(fittingSize.width, (proposedSize.width > 0 ? proposedSize.width : fittingSize.width)), + width: min(fittingSize.width, proposedSize.width > 0 ? proposedSize.width : fittingSize.width), height: fittingSize.height) case .intrinsic(let size): From 2ca6dc4af74dd75b30e8c413f91a9478d91f6336 Mon Sep 17 00:00:00 2001 From: Bryn Bodayle Date: Tue, 20 Feb 2024 13:32:32 -0800 Subject: [PATCH 4/6] CI fixes --- ...wiftUISizingStrategiesViewController.swift | 12 +++++----- .../MeasuringViewRepresentable.swift | 12 +++++++--- .../SwiftUIMeasurementContainer.swift | 24 +++++++++++++++---- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift b/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift index 3bf37270..26e2b3fa 100644 --- a/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift +++ b/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUISizingStrategiesViewController.swift @@ -79,17 +79,17 @@ extension SwiftUIMeasurementContainerStrategy: Identifiable, CaseIterable { var displayString: String { switch self { case .automatic: - "Automatic" + return "Automatic" case .proposed: - "Proposed" + return "Proposed" case .intrinsicHeightProposedOrIntrinsicWidth: - "Intrinsic Height, Proposed Width or Intrinsic Width" + return "Intrinsic Height, Proposed Width or Intrinsic Width" case .intrinsicHeightProposedWidth: - "Intrinsic Height, Proposed Width" + return "Intrinsic Height, Proposed Width" case .intrinsicWidthProposedHeight: - "Intrinsic Width, Proposed Height" + return "Intrinsic Width, Proposed Height" case .intrinsic: - "Intrinsic" + return "Intrinsic" } } diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift index 04c54a10..25b16fcc 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift @@ -65,7 +65,7 @@ extension MeasuringViewRepresentable { 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, @@ -90,7 +90,13 @@ extension MeasuringViewRepresentable { { nsView.strategy = sizing let children = Mirror(reflecting: proposedSize).children - nsView.proposedSize = proposal.viewTypeValue + nsView.proposedSize = .init( + 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 } @@ -111,7 +117,7 @@ extension MeasuringViewRepresentable { } #endif -#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, *) extension ProposedViewSize { /// Creates a size by replacing `nil`s with `UIView.noIntrinsicMetric` diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift index 2658a5be..8374d6be 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift @@ -82,7 +82,7 @@ public final class SwiftUIMeasurementContainer: ViewType { // The proposed size is only used by the measurement, so just re-measure. _measuredFittingSize = nil - setNeedsUpdateConstraints() + setNeedsUpdateConstraintsForPlatform() } } @@ -165,7 +165,7 @@ public final class SwiftUIMeasurementContainer: ViewType { /// The cached `measuredFittingSize` to prevent unnecessary re-measurements. private var _measuredFittingSize: CGSize? { didSet { - setNeedsUpdateConstraints() + setNeedsUpdateConstraintsForPlatform() } } @@ -278,9 +278,25 @@ public final class SwiftUIMeasurementContainer: ViewType { } } + private func setNeedsUpdateConstraintsForPlatform() { + #if os(iOS) || os(tvOS) + setNeedsUpdateConstraints() + #elseif os(macOS) + needsUpdateConstraints = true + #endif + } + + private func updateConstraintsForPlatformIfNeeded() { + #if os(iOS) || os(tvOS) + updateConstraintsIfNeeded() + #elseif os(macOS) + updateConstraintsForSubtreeIfNeeded() + #endif + } + /// Measures the `uiView`, storing the resulting size in `measuredIntrinsicContentSize`. private func measureView() -> CGSize { - updateConstraintsIfNeeded() + updateConstraintsForPlatformIfNeeded() latestMeasurementBoundsSize = bounds.size var measuredSize: CGSize @@ -299,7 +315,7 @@ public final class SwiftUIMeasurementContainer: ViewType { measuredSize.height = ViewType.noIntrinsicMetric case .intrinsicHeightProposedOrIntrinsicWidth: - let fittingSize = content.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + let fittingSize = content.systemLayoutFittingIntrinsicSize() measuredSize = CGSize( width: min(fittingSize.width, proposedSize.width > 0 ? proposedSize.width : fittingSize.width), height: fittingSize.height) From 35215ce8bb968d6da66be2d855f1446fa38420c9 Mon Sep 17 00:00:00 2001 From: Bryn Bodayle Date: Wed, 21 Feb 2024 10:47:02 -0800 Subject: [PATCH 5/6] Updates for self review --- CHANGELOG.md | 2 +- .../LayoutUtilities/MeasuringViewRepresentable.swift | 3 ++- .../LayoutUtilities/SwiftUIMeasurementContainer.swift | 7 ++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf4a64c7..1ddcd0f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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 - size instead of it's intrinsic width. + 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 diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift index 25b16fcc..f8b794ab 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift @@ -120,7 +120,8 @@ extension MeasuringViewRepresentable { #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, *) extension ProposedViewSize { - /// Creates a size by replacing `nil`s with `UIView.noIntrinsicMetric` + /// Creates a size by capping infinite values to a significantly large value and replacing `nil`s + /// with `UIView.noIntrinsicMetric` var viewTypeValue: CGSize { .init( width: width?.constraintSafeValue ?? ViewType.noIntrinsicMetric, diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift index 8374d6be..74d75fae 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift @@ -78,7 +78,6 @@ public final class SwiftUIMeasurementContainer: ViewType { public var proposedSize = CGSize.noIntrinsicMetric { didSet { guard oldValue != proposedSize else { return } - _resolvedStrategy = nil // The proposed size is only used by the measurement, so just re-measure. _measuredFittingSize = nil @@ -296,6 +295,8 @@ public final class SwiftUIMeasurementContainer: ViewType { /// Measures the `uiView`, storing the resulting size in `measuredIntrinsicContentSize`. private func measureView() -> CGSize { + // immediately update constraints to the latest values so that the measurements below take them + // into account updateConstraintsForPlatformIfNeeded() latestMeasurementBoundsSize = bounds.size @@ -354,8 +355,8 @@ public enum SwiftUIMeasurementContainerStrategy { /// Typically used for views that should expand greedily in both axes, e.g. a background view. case proposed - /// The `uiView`'s receives either its intrinsic width or the proposed width, whichever is smaller. The view receives its height based - /// on the chosen width. + /// The `uiView`'s receives either its intrinsic width or the proposed width, whichever is smaller. The view receives its intrinsic height + /// based on the chosen width. /// /// Typically used for views that have a height that's a function of their width, e.g. a row with /// text that can wrap to multiple lines. From 0b7f36e3b497f3c8f5b7f595de21d928691faca8 Mon Sep 17 00:00:00 2001 From: Bryn Bodayle Date: Fri, 1 Mar 2024 14:48:26 -0800 Subject: [PATCH 6/6] Updates for PR feedback --- .../SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift | 4 ++-- .../SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift index f8b794ab..bda29027 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringViewRepresentable.swift @@ -120,8 +120,8 @@ extension MeasuringViewRepresentable { #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, *) extension ProposedViewSize { - /// Creates a size by capping infinite values to a significantly large value and replacing `nil`s - /// with `UIView.noIntrinsicMetric` + /// 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 { .init( width: width?.constraintSafeValue ?? ViewType.noIntrinsicMetric, diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift index 74d75fae..eb6b329d 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift @@ -343,7 +343,7 @@ public enum SwiftUIMeasurementContainerStrategy { /// - The `uiView` will be given its intrinsic width and/or height when measurement in that /// dimension produces a positive value, while zero/negative values will result in that /// dimension receiving the available space proposed by the parent. - /// - If the view contains `UILabel` subviews that require a double layout pass as determined by support multiple lines of text + /// - If the view contains `UILabel` subviews that require a double layout pass as determined by supporting multiple lines of text /// the view will default to `intrinsicHeightProposedOrIntrinsicWidth` to allow the labels to wrap. /// /// If you would like to opt out of automatic sizing for performance or to override the default