Skip to content

Commit

Permalink
[SwiftUI] Simplify UIKit in SwiftUI briding (#125)
Browse files Browse the repository at this point in the history
* [SwiftUI] Simplify UIKit in SwiftUI briding

We can remove all of the `EpoxyableView` flavors of `MeasuringUIViewRepresentable` in favor of a single shared `SwiftUIUIView` that supports a generic `Storage`. This has the added benefit of fixing some crashes we were seeing with Xcode previews.
  • Loading branch information
erichoracek authored Sep 9, 2022
1 parent 882e46f commit b67f7e6
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 269 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
249 changes: 60 additions & 189 deletions Sources/EpoxyCore/SwiftUI/EpoxyableView+SwiftUIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
/// }
/// ```
///
Expand All @@ -26,9 +26,27 @@ extension StyledView where Self: ContentConfigurableView & BehaviorsConfigurable
content: Content,
style: Style,
behaviors: Behaviors? = nil)
-> SwiftUIEpoxyableView<Self>
-> SwiftUIUIView<Self, (content: Content, style: Style)>
{
.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)
}
}
}

Expand All @@ -43,8 +61,8 @@ extension StyledView
/// returned SwiftUI `View`:
/// ```
/// MyView.swiftUIView(…)
/// .configure { (view: MyView) in
///
/// .configure { context in
/// context.view.doSomething()
/// }
/// ```
///
Expand All @@ -56,9 +74,22 @@ extension StyledView
public static func swiftUIView(
content: Content,
behaviors: Behaviors? = nil)
-> SwiftUIStylelessEpoxyableView<Self>
-> SwiftUIUIView<Self, Content>
{
.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)
}
}
}

Expand All @@ -73,8 +104,8 @@ extension StyledView
/// returned SwiftUI `View`:
/// ```
/// MyView.swiftUIView(…)
/// .configure { (view: MyView) in
///
/// .configure { context in
/// context.view.doSomething()
/// }
/// ```
///
Expand All @@ -87,9 +118,19 @@ extension StyledView
public static func swiftUIView(
style: Style,
behaviors: Behaviors? = nil)
-> SwiftUIContentlessEpoxyableView<Self>
-> SwiftUIUIView<Self, Style>
{
.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)
}
}
}

Expand All @@ -105,8 +146,8 @@ extension StyledView
/// returned SwiftUI `View`:
/// ```
/// MyView.swiftUIView(…)
/// .configure { (view: MyView) in
///
/// .configure { context in
/// context.view.doSomething()
/// }
/// ```
///
Expand All @@ -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<Self>
{
.init(behaviors: behaviors, sizing: sizing)
}
}

// MARK: - SwiftUIEpoxyableView

/// A SwiftUI `View` representing an `EpoxyableView` with content, behaviors, and style.
public struct SwiftUIEpoxyableView<View>: 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<Self, View>, 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<Self, View> {
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<View>: 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<Self, View>, 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<Self, View> {
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<View>: 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<Self, View>, 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<Self, Void> {
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<Self, View> {
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<View>: 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<Self, View>, context _: Context) {
wrapper.view = self
wrapper.uiView.setBehaviors(behaviors)

for configuration in configurations {
configuration(wrapper.uiView)
}
}

public func makeUIView(context _: Context) -> SwiftUIMeasurementContainer<Self, View> {
let uiView = View()
// No need to set behaviors as `updateUIView` is called immediately after construction.
return SwiftUIMeasurementContainer(view: self, uiView: uiView, strategy: sizing)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import SwiftUI
/// - SeeAlso: ``SwiftUIMeasurementContainer``
public protocol MeasuringUIViewRepresentable: UIViewRepresentable
where
UIViewType == SwiftUIMeasurementContainer<Self, View>
UIViewType == SwiftUIMeasurementContainer<Content>
{
/// 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.
///
Expand Down
Loading

0 comments on commit b67f7e6

Please sign in to comment.