diff --git a/CHANGELOG.md b/CHANGELOG.md index 4941c77c..b7c5a31a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://github.com/airbnb/epoxy-ios/compare/0.8.0...HEAD) +### Changed +- Remove all of the `EpoxyableView` flavors of `MeasuringUIViewRepresentable` in favor of a + single shared `SwiftUIUIView` that supports a generic `Storage`, which has the added benefit of + fixing some Xcode preview crashes. + ### Fixed - Improved double layout pass heuristics for views that have intrinsic size dimensions below 1 or for views that have double layout pass subviews that aren't horizontally constrained to the edges. diff --git a/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUIViewController.swift b/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUIViewController.swift index 0befdee4..70e5851e 100644 --- a/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUIViewController.swift +++ b/Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUIViewController.swift @@ -27,9 +27,9 @@ struct EpoxyInSwiftUIView: View { TextRow.swiftUIView( content: .init(title: "Row \(index)", body: BeloIpsum.sentence(count: 1, wordCount: index)), style: .small) - .configure { row in + .configure { context in // swiftlint:disable:next no_direct_standard_out_logs - print("Configuring \(row)") + print("Configuring \(context.view)") } .onTapGesture { // swiftlint:disable:next no_direct_standard_out_logs diff --git a/Sources/EpoxyCore/SwiftUI/EpoxyableView+SwiftUIView.swift b/Sources/EpoxyCore/SwiftUI/EpoxyableView+SwiftUIView.swift index 5b7ef301..45ccec3e 100644 --- a/Sources/EpoxyCore/SwiftUI/EpoxyableView+SwiftUIView.swift +++ b/Sources/EpoxyCore/SwiftUI/EpoxyableView+SwiftUIView.swift @@ -12,8 +12,8 @@ extension StyledView where Self: ContentConfigurableView & BehaviorsConfigurable /// returned SwiftUI `View`: /// ``` /// MyView.swiftUIView(…) - /// .configure { (view: MyView) in - /// … + /// .configure { context in + /// context.view.doSomething() /// } /// ``` /// @@ -26,9 +26,27 @@ extension StyledView where Self: ContentConfigurableView & BehaviorsConfigurable content: Content, style: Style, behaviors: Behaviors? = nil) - -> SwiftUIEpoxyableView + -> SwiftUIUIView { - .init(content: content, style: style, behaviors: behaviors) + SwiftUIUIView(storage: (content: content, style: style)) { + let view = Self(style: style) + view.setContent(content, animated: false) + return view + } + .configure { context in + // We need to create a new view instance when the style changes. + if context.oldStorage.style != style { + context.view = Self(style: style) + context.view.setContent(content, animated: context.animated) + } + // Otherwise, if the just the content changes, we need to update it. + else if context.oldStorage.content != content { + context.view.setContent(content, animated: context.animated) + context.container.invalidateIntrinsicContentSize() + } + + context.view.setBehaviors(behaviors) + } } } @@ -43,8 +61,8 @@ extension StyledView /// returned SwiftUI `View`: /// ``` /// MyView.swiftUIView(…) - /// .configure { (view: MyView) in - /// … + /// .configure { context in + /// context.view.doSomething() /// } /// ``` /// @@ -56,9 +74,22 @@ extension StyledView public static func swiftUIView( content: Content, behaviors: Behaviors? = nil) - -> SwiftUIStylelessEpoxyableView + -> SwiftUIUIView { - .init(content: content, behaviors: behaviors) + SwiftUIUIView(storage: content) { + let view = Self() + view.setContent(content, animated: false) + return view + } + .configure { context in + // We need to update the content of the existing view when the content is updated. + if context.oldStorage != content { + context.view.setContent(content, animated: context.animated) + context.container.invalidateIntrinsicContentSize() + } + + context.view.setBehaviors(behaviors) + } } } @@ -73,8 +104,8 @@ extension StyledView /// returned SwiftUI `View`: /// ``` /// MyView.swiftUIView(…) - /// .configure { (view: MyView) in - /// … + /// .configure { context in + /// context.view.doSomething() /// } /// ``` /// @@ -87,9 +118,19 @@ extension StyledView public static func swiftUIView( style: Style, behaviors: Behaviors? = nil) - -> SwiftUIContentlessEpoxyableView + -> SwiftUIUIView { - .init(style: style, behaviors: behaviors) + SwiftUIUIView(storage: style) { + Self(style: style) + } + .configure { context in + // We need to create a new view instance when the style changes. + if context.oldStorage != style { + context.view = Self(style: style) + } + + context.view.setBehaviors(behaviors) + } } } @@ -105,8 +146,8 @@ extension StyledView /// returned SwiftUI `View`: /// ``` /// MyView.swiftUIView(…) - /// .configure { (view: MyView) in - /// … + /// .configure { context in + /// context.view.doSomething() /// } /// ``` /// @@ -116,182 +157,12 @@ extension StyledView /// MyView.swiftUIView(…).sizing(.intrinsicSize) /// ``` /// The sizing defaults to `.automatic`. - public static func swiftUIView( - behaviors: Behaviors? = nil, - sizing: SwiftUIMeasurementContainerStrategy = .automatic) - -> SwiftUIStylelessContentlessEpoxyableView - { - .init(behaviors: behaviors, sizing: sizing) - } -} - -// MARK: - SwiftUIEpoxyableView - -/// A SwiftUI `View` representing an `EpoxyableView` with content, behaviors, and style. -public struct SwiftUIEpoxyableView: MeasuringUIViewRepresentable, UIViewConfiguringSwiftUIView - where - View: EpoxyableView -{ - var content: View.Content - var style: View.Style - var behaviors: View.Behaviors? - public var sizing = SwiftUIMeasurementContainerStrategy.automatic - public var configurations: [(View) -> Void] = [] - - public func updateUIView(_ wrapper: SwiftUIMeasurementContainer, context: Context) { - let animated = context.transaction.animation != nil - - defer { - wrapper.view = self - - // We always update the view behaviors on every view update. - wrapper.uiView.setBehaviors(behaviors) - - for configuration in configurations { - configuration(wrapper.uiView) - } - } - - // We need to create a new view instance when the style is updated. - guard wrapper.view.style == style else { - let uiView = View(style: style) - uiView.setContent(content, animated: false) - uiView.setBehaviors(behaviors) - wrapper.uiView = uiView - return - } - - // We need to update the content of the existing view when the content is updated. - guard wrapper.view.content == content else { - wrapper.uiView.setContent(content, animated: animated) - wrapper.invalidateIntrinsicContentSize() - return - } - - // No updates required. - } - - public func makeUIView(context _: Context) -> SwiftUIMeasurementContainer { - let uiView = View(style: style) - uiView.setContent(content, animated: false) - // No need to set behaviors as `updateUIView` is called immediately after construction. - return SwiftUIMeasurementContainer(view: self, uiView: uiView, strategy: sizing) - } -} - -// MARK: - SwiftUIStylelessEpoxyableView - -/// A SwiftUI `View` representing an `EpoxyableView` with a `Never` `Style`. -public struct SwiftUIStylelessEpoxyableView: MeasuringUIViewRepresentable, UIViewConfiguringSwiftUIView - where - View: EpoxyableView, - View.Style == Never -{ - var content: View.Content - var behaviors: View.Behaviors? - public var sizing = SwiftUIMeasurementContainerStrategy.automatic - public var configurations: [(View) -> Void] = [] - - public func updateUIView(_ wrapper: SwiftUIMeasurementContainer, context: Context) { - let animated = context.transaction.animation != nil - - defer { - wrapper.view = self - - // We always update the view behaviors on every view update. - wrapper.uiView.setBehaviors(behaviors) - - for configuration in configurations { - configuration(wrapper.uiView) - } - } - - // We need to update the content of the existing view when the content is updated. - guard wrapper.view.content == content else { - wrapper.uiView.setContent(content, animated: animated) - wrapper.invalidateIntrinsicContentSize() - return - } - - // No updates required. - } - - public func makeUIView(context _: Context) -> SwiftUIMeasurementContainer { - let uiView = View() - uiView.setContent(content, animated: false) - // No need to set behaviors as `updateUIView` is called immediately after construction. - return SwiftUIMeasurementContainer(view: self, uiView: uiView, strategy: sizing) - } -} - -// MARK: - SwiftUIContentlessEpoxyableView - -/// A SwiftUI `View` representing an `EpoxyableView` with a `Never` `Content`. -public struct SwiftUIContentlessEpoxyableView: MeasuringUIViewRepresentable, UIViewConfiguringSwiftUIView - where - View: EpoxyableView, - View.Content == Never -{ - var style: View.Style - var behaviors: View.Behaviors? - public var sizing = SwiftUIMeasurementContainerStrategy.automatic - public var configurations: [(View) -> Void] = [] - - public func updateUIView(_ wrapper: SwiftUIMeasurementContainer, context _: Context) { - defer { - wrapper.view = self - - // We always update the view behaviors on every view update. - wrapper.uiView.setBehaviors(behaviors) - - for configuration in configurations { - configuration(wrapper.uiView) - } + public static func swiftUIView(behaviors: Behaviors? = nil) -> SwiftUIUIView { + SwiftUIUIView { + Self() } - - // We need to create a new view instance when the style is updated. - guard wrapper.view.style == style else { - let uiView = View(style: style) - uiView.setBehaviors(behaviors) - wrapper.uiView = uiView - return + .configure { context in + context.view.setBehaviors(behaviors) } - - // No updates required. - } - - public func makeUIView(context _: Context) -> SwiftUIMeasurementContainer { - let uiView = View(style: style) - // No need to set behaviors as `updateUIView` is called immediately after construction. - return SwiftUIMeasurementContainer(view: self, uiView: uiView, strategy: sizing) - } -} - -// MARK: - SwiftUIStylelessContentlessEpoxyableView - -/// A SwiftUI `View` representing an `EpoxyableView` with a `Never` `Style` and `Content`. -public struct SwiftUIStylelessContentlessEpoxyableView: MeasuringUIViewRepresentable, UIViewConfiguringSwiftUIView - where - View: EpoxyableView, - View.Content == Never, - View.Style == Never -{ - public var configurations: [(View) -> Void] = [] - var behaviors: View.Behaviors? - public var sizing = SwiftUIMeasurementContainerStrategy.automatic - - public func updateUIView(_ wrapper: SwiftUIMeasurementContainer, context _: Context) { - wrapper.view = self - wrapper.uiView.setBehaviors(behaviors) - - for configuration in configurations { - configuration(wrapper.uiView) - } - } - - public func makeUIView(context _: Context) -> SwiftUIMeasurementContainer { - let uiView = View() - // No need to set behaviors as `updateUIView` is called immediately after construction. - return SwiftUIMeasurementContainer(view: self, uiView: uiView, strategy: sizing) } } diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringUIViewRepresentable.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringUIViewRepresentable.swift index bcd20851..823cc3fc 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringUIViewRepresentable.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringUIViewRepresentable.swift @@ -14,10 +14,10 @@ import SwiftUI /// - SeeAlso: ``SwiftUIMeasurementContainer`` public protocol MeasuringUIViewRepresentable: UIViewRepresentable where - UIViewType == SwiftUIMeasurementContainer + UIViewType == SwiftUIMeasurementContainer { - /// The `UIView` that's being measured by the enclosing `SwiftUIMeasurementContainer`. - associatedtype View: UIView + /// The `UIView` content that's being measured by the enclosing `SwiftUIMeasurementContainer`. + associatedtype Content: UIView /// The sizing strategy of the represented view. /// diff --git a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift index 0fb97f28..2bdc9782 100644 --- a/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift +++ b/Sources/EpoxyCore/SwiftUI/LayoutUtilities/SwiftUIMeasurementContainer.swift @@ -12,13 +12,12 @@ import SwiftUI /// height through the `SwiftUISizingContext` binding. /// /// - SeeAlso: ``MeasuringUIViewRepresentable`` -public final class SwiftUIMeasurementContainer: UIView { +public final class SwiftUIMeasurementContainer: UIView { // MARK: Lifecycle - public init(view: SwiftUIView, uiView: UIViewType, strategy: SwiftUIMeasurementContainerStrategy) { - self.view = view - self.uiView = uiView + public init(content: Content, strategy: SwiftUIMeasurementContainerStrategy) { + self.content = content self.strategy = strategy // On iOS 15 and below, passing zero can result in a constraint failure the first time a view @@ -32,7 +31,7 @@ public final class SwiftUIMeasurementContainer: } super.init(frame: .init(origin: .zero, size: initialSize)) - addSubview(uiView) + addSubview(content) setUpConstraints() } @@ -43,12 +42,6 @@ public final class SwiftUIMeasurementContainer: // MARK: Public - /// The most recently updated SwiftUI `View` that constructed this measurement container, used to - /// perform comparisons with the previous fields, if needed. - /// - /// Has no side-effects when updated; purely available as a convenience. - public var view: SwiftUIView - /// The most recently measured fitting size of the `uiView` that fits within the current /// `proposedSize`. /// @@ -59,12 +52,12 @@ public final class SwiftUIMeasurementContainer: _measuredFittingSize ?? measureView() } - /// The `UIView` that's being measured by this container. - public var uiView: UIViewType { + /// The `UIView` content that's being measured by this container. + public var content: Content { didSet { - guard uiView !== oldValue else { return } + guard content !== oldValue else { return } oldValue.removeFromSuperview() - addSubview(uiView) + addSubview(content) // Invalidate the strategy since it's derived from this view. _resolvedStrategy = nil // Re-configure the constraints since they depend on the resolved strategy. @@ -154,11 +147,11 @@ public final class SwiftUIMeasurementContainer: case .automatic: // Perform an intrinsic size measurement pass, which gives us valid values for // `UILabel.preferredMaxLayoutWidth`. - let intrinsicSize = uiView.systemLayoutFittingIntrinsicSize() + 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, uiView.containsDoubleLayoutPassSubviews() { + if intrinsicSize.width > 0, content.containsDoubleLayoutPassSubviews() { resolved = .intrinsicHeightProposedWidth } else { let zero = CGFloat(0) @@ -180,19 +173,19 @@ public final class SwiftUIMeasurementContainer: case .intrinsicWidthProposedHeight: resolved = .intrinsicWidthProposedHeight case .intrinsic: - resolved = .intrinsic(uiView.systemLayoutFittingIntrinsicSize()) + resolved = .intrinsic(content.systemLayoutFittingIntrinsicSize()) } _resolvedStrategy = resolved return resolved } private func setUpConstraints() { - uiView.translatesAutoresizingMaskIntoConstraints = false + content.translatesAutoresizingMaskIntoConstraints = false - let leading = uiView.leadingAnchor.constraint(equalTo: leadingAnchor) - let top = uiView.topAnchor.constraint(equalTo: topAnchor) - let trailing = uiView.trailingAnchor.constraint(equalTo: trailingAnchor) - let bottom = uiView.bottomAnchor.constraint(equalTo: bottomAnchor) + 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, ] @@ -241,11 +234,11 @@ public final class SwiftUIMeasurementContainer: measuredSize = .noIntrinsicMetric case .intrinsicHeightProposedWidth: - measuredSize = uiView.systemLayoutFittingIntrinsicHeightFixedWidth(proposedSizeElseBounds.width) + measuredSize = content.systemLayoutFittingIntrinsicHeightFixedWidth(proposedSizeElseBounds.width) measuredSize.width = UIView.noIntrinsicMetric case .intrinsicWidthProposedHeight: - measuredSize = uiView.systemLayoutFittingIntrinsicWidthFixedHeight(proposedSizeElseBounds.height) + measuredSize = content.systemLayoutFittingIntrinsicWidthFixedHeight(proposedSizeElseBounds.height) measuredSize.height = UIView.noIntrinsicMetric case .intrinsic(let size): diff --git a/Sources/EpoxyCore/SwiftUI/SwiftUIUIView.swift b/Sources/EpoxyCore/SwiftUI/SwiftUIUIView.swift new file mode 100644 index 00000000..6448e1be --- /dev/null +++ b/Sources/EpoxyCore/SwiftUI/SwiftUIUIView.swift @@ -0,0 +1,124 @@ +// Created by eric_horacek on 9/8/22. +// Copyright © 2022 Airbnb Inc. All rights reserved. + +import SwiftUI + +// MARK: - SwiftUIUIView + +/// A `UIViewRepresentable` SwiftUI `View` that wraps its `Content` `UIView` within a +/// `SwiftUIMeasurementContainer`, used to size a UIKit view correctly within a SwiftUI view +/// hierarchy. +/// +/// Includes an optional generic `Storage` value, which can be used to compare old and new values +/// across state changes to prevent redundant view updates. +public struct SwiftUIUIView: MeasuringUIViewRepresentable, + UIViewConfiguringSwiftUIView +{ + + // MARK: Lifecycle + + /// Creates a SwiftUI representation of the content view with the given storage and the provided + /// `makeContent` closure to construct the content whenever `makeUIView(…)` is invoked. + init(storage: Storage, makeContent: @escaping () -> Content) { + self.storage = storage + self.makeContent = makeContent + } + + /// Creates a SwiftUI representation of the content view with the provided `makeContent` closure + /// to construct it whenever `makeUIView(…)` is invoked. + init(makeContent: @escaping () -> Content) where Storage == Void { + storage = () + self.makeContent = makeContent + } + + // MARK: Public + + public var configurations: [Configuration] = [] + + public var sizing: SwiftUIMeasurementContainerStrategy = .automatic + + // MARK: Private + + /// The current stored value, with the previous value provided to the configuration closure as + /// the `oldStorage`. + private var storage: Storage + + /// A closure that's invoked to construct the represented content view. + private var makeContent: () -> Content +} + +// MARK: UIViewRepresentable + +extension SwiftUIUIView { + public func makeUIView(context _: Context) -> SwiftUIMeasurementContainer { + SwiftUIMeasurementContainer(content: makeContent(), strategy: sizing) + } + + public func makeCoordinator() -> Coordinator { + Coordinator(storage: storage) + } + + public func updateUIView(_ uiView: SwiftUIMeasurementContainer, context: Context) { + let oldStorage = context.coordinator.storage + context.coordinator.storage = storage + + let configurationContext = ConfigurationContext( + oldStorage: oldStorage, + viewRepresentableContext: context, + container: uiView) + + for configuration in configurations { + configuration(configurationContext) + } + } +} + +// MARK: SwiftUIUIView.ConfigurationContext + +extension SwiftUIUIView { + /// The configuration context that's available to configure the `Content` view whenever the + /// `updateUIView()` method is invoked via a configuration closure. + public struct ConfigurationContext: ViewProviding { + /// The previous value for the `Storage` of this `SwiftUIUIView`, which can be used to store + /// values across state changes to prevent redundant view updates. + public var oldStorage: Storage + + /// The `UIViewRepresentable.Context`, with information about the transaction and environment. + public var viewRepresentableContext: Context + + /// The backing measurement container that contains the `Content`. + public var container: SwiftUIMeasurementContainer + + /// The `UIView` content that's being configured. + /// + /// Setting this to a new value updates the backing measurement container's `content`. + public var view: Content { + get { container.content } + nonmutating set { container.content = newValue } + } + + /// A convenience accessor indicating whether this content update should be animated. + public var animated: Bool { + viewRepresentableContext.transaction.animation != nil + } + } +} + +// MARK: SwiftUIUIView.Coordinator + +extension SwiftUIUIView { + /// A coordinator that stores the `storage` associated with this view, enabling the old storage + /// value to be accessed during the `updateUIView(…)`. + public final class Coordinator { + + // MARK: Lifecycle + + fileprivate init(storage: Storage) { + self.storage = storage + } + + // MARK: Internal + + fileprivate(set) var storage: Storage + } +} diff --git a/Sources/EpoxyCore/SwiftUI/UIView+SwiftUIView.swift b/Sources/EpoxyCore/SwiftUI/UIView+SwiftUIView.swift index 02c8bd8f..4ecdb669 100644 --- a/Sources/EpoxyCore/SwiftUI/UIView+SwiftUIView.swift +++ b/Sources/EpoxyCore/SwiftUI/UIView+SwiftUIView.swift @@ -13,8 +13,8 @@ extension UIViewProtocol { /// returned SwiftUI `View`: /// ``` /// MyUIView.swiftUIView(…) - /// .configure { (view: MyUIView) in - /// … + /// .configure { context in + /// context.view.doSomething() /// } /// ``` /// @@ -24,47 +24,11 @@ extension UIViewProtocol { /// MyView.swiftUIView(…).sizing(.intrinsicSize) /// ``` /// The sizing defaults to `.automatic`. - public static func swiftUIView(makeView: @escaping () -> Self) -> SwiftUIUIView { - SwiftUIUIView(makeView: makeView) + public static func swiftUIView(makeView: @escaping () -> Self) -> SwiftUIUIView { + SwiftUIUIView(makeContent: makeView) } } -// MARK: - SwiftUIUIView - -/// A `UIViewRepresentable` SwiftUI `View` that wraps its `Content` `UIView` within a -/// `SwiftUIMeasurementContainer`, used to size a UIKit view correctly within a SwiftUI view -/// hierarchy. -public struct SwiftUIUIView: MeasuringUIViewRepresentable, UIViewConfiguringSwiftUIView { - - // MARK: Public - - /// An array of closures that are invoked to configure the represented view. - public var configurations: [(View) -> Void] = [] - - /// The sizing context used to size the represented view. - public var sizing = SwiftUIMeasurementContainerStrategy.automatic - - public func makeUIView(context _: Context) -> SwiftUIMeasurementContainer { - SwiftUIMeasurementContainer( - view: self, - uiView: makeView(), - strategy: sizing) - } - - public func updateUIView(_ wrapper: SwiftUIMeasurementContainer, context _: Context) { - wrapper.view = self - - for configuration in configurations { - configuration(wrapper.uiView) - } - } - - // MARK: Internal - - /// A closure that's invoked to construct the represented view. - var makeView: () -> View -} - // MARK: - UIViewProtocol /// A protocol that all `UIView`s conform to, enabling extensions that have a `Self` reference. diff --git a/Sources/EpoxyCore/SwiftUI/UIViewConfiguringSwiftUIView.swift b/Sources/EpoxyCore/SwiftUI/UIViewConfiguringSwiftUIView.swift index 9d02aa1c..24c3d8d4 100644 --- a/Sources/EpoxyCore/SwiftUI/UIViewConfiguringSwiftUIView.swift +++ b/Sources/EpoxyCore/SwiftUI/UIViewConfiguringSwiftUIView.swift @@ -5,31 +5,35 @@ import SwiftUI // MARK: - UIViewConfiguringSwiftUIView -/// A protocol describing a SwiftUI `View` that can configure its `UIView` contents via an array of +/// A protocol describing a SwiftUI `View` that can configure its `UIView` content via an array of /// `configuration` closures. public protocol UIViewConfiguringSwiftUIView: View { - /// The `UIView` represented by this view. - associatedtype View: UIView + /// The context available to this configuration, which provides the `UIView` instance at a minimum + /// but can include additional context as needed. + associatedtype ConfigurationContext: ViewProviding - /// A mutable array of configuration closures that should each be invoked with the represented - /// `UIView` whenever `updateUIView` is called in a `UIViewRepresentable`. - var configurations: [(View) -> Void] { get set } + /// A closure that is invoked to configure the represented content view. + typealias Configuration = (ConfigurationContext) -> Void + + /// A mutable array of configuration closures that should each be invoked with the + /// `ConfigurationContext` whenever `updateUIView` is called in a `UIViewRepresentable`. + var configurations: [Configuration] { get set } } // MARK: Extensions extension UIViewConfiguringSwiftUIView { /// Returns a copy of this view updated to have the given closure applied to its represented view - /// whenever it is updated via the `updateUIView` method. - public func configure(_ configure: @escaping (View) -> Void) -> Self { + /// whenever it is updated via the `updateUIView(…)` method. + public func configure(_ configure: @escaping Configuration) -> Self { var copy = self copy.configurations.append(configure) return copy } /// Returns a copy of this view updated to have the given closures applied to its represented view - /// whenever it is updated via the `updateUIView` method. - public func configurations(_ configurations: [(View) -> Void]) -> Self { + /// whenever it is updated via the `updateUIView(…)` method. + public func configurations(_ configurations: [Configuration]) -> Self { var copy = self copy.configurations.append(contentsOf: configurations) return copy