Skip to content

Commit b67f7e6

Browse files
authored
[SwiftUI] Simplify UIKit in SwiftUI briding (#125)
* [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.
1 parent 882e46f commit b67f7e6

File tree

8 files changed

+230
-269
lines changed

8 files changed

+230
-269
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased](https://github.com/airbnb/epoxy-ios/compare/0.8.0...HEAD)
88

9+
### Changed
10+
- Remove all of the `EpoxyableView` flavors of `MeasuringUIViewRepresentable` in favor of a
11+
single shared `SwiftUIUIView` that supports a generic `Storage`, which has the added benefit of
12+
fixing some Xcode preview crashes.
13+
914
### Fixed
1015
- Improved double layout pass heuristics for views that have intrinsic size dimensions below 1 or
1116
for views that have double layout pass subviews that aren't horizontally constrained to the edges.

Example/EpoxyExample/ViewControllers/SwiftUI/EpoxyInSwiftUIViewController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ struct EpoxyInSwiftUIView: View {
2727
TextRow.swiftUIView(
2828
content: .init(title: "Row \(index)", body: BeloIpsum.sentence(count: 1, wordCount: index)),
2929
style: .small)
30-
.configure { row in
30+
.configure { context in
3131
// swiftlint:disable:next no_direct_standard_out_logs
32-
print("Configuring \(row)")
32+
print("Configuring \(context.view)")
3333
}
3434
.onTapGesture {
3535
// swiftlint:disable:next no_direct_standard_out_logs

Sources/EpoxyCore/SwiftUI/EpoxyableView+SwiftUIView.swift

Lines changed: 60 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ extension StyledView where Self: ContentConfigurableView & BehaviorsConfigurable
1212
/// returned SwiftUI `View`:
1313
/// ```
1414
/// MyView.swiftUIView(…)
15-
/// .configure { (view: MyView) in
16-
///
15+
/// .configure { context in
16+
/// context.view.doSomething()
1717
/// }
1818
/// ```
1919
///
@@ -26,9 +26,27 @@ extension StyledView where Self: ContentConfigurableView & BehaviorsConfigurable
2626
content: Content,
2727
style: Style,
2828
behaviors: Behaviors? = nil)
29-
-> SwiftUIEpoxyableView<Self>
29+
-> SwiftUIUIView<Self, (content: Content, style: Style)>
3030
{
31-
.init(content: content, style: style, behaviors: behaviors)
31+
SwiftUIUIView(storage: (content: content, style: style)) {
32+
let view = Self(style: style)
33+
view.setContent(content, animated: false)
34+
return view
35+
}
36+
.configure { context in
37+
// We need to create a new view instance when the style changes.
38+
if context.oldStorage.style != style {
39+
context.view = Self(style: style)
40+
context.view.setContent(content, animated: context.animated)
41+
}
42+
// Otherwise, if the just the content changes, we need to update it.
43+
else if context.oldStorage.content != content {
44+
context.view.setContent(content, animated: context.animated)
45+
context.container.invalidateIntrinsicContentSize()
46+
}
47+
48+
context.view.setBehaviors(behaviors)
49+
}
3250
}
3351
}
3452

@@ -43,8 +61,8 @@ extension StyledView
4361
/// returned SwiftUI `View`:
4462
/// ```
4563
/// MyView.swiftUIView(…)
46-
/// .configure { (view: MyView) in
47-
///
64+
/// .configure { context in
65+
/// context.view.doSomething()
4866
/// }
4967
/// ```
5068
///
@@ -56,9 +74,22 @@ extension StyledView
5674
public static func swiftUIView(
5775
content: Content,
5876
behaviors: Behaviors? = nil)
59-
-> SwiftUIStylelessEpoxyableView<Self>
77+
-> SwiftUIUIView<Self, Content>
6078
{
61-
.init(content: content, behaviors: behaviors)
79+
SwiftUIUIView(storage: content) {
80+
let view = Self()
81+
view.setContent(content, animated: false)
82+
return view
83+
}
84+
.configure { context in
85+
// We need to update the content of the existing view when the content is updated.
86+
if context.oldStorage != content {
87+
context.view.setContent(content, animated: context.animated)
88+
context.container.invalidateIntrinsicContentSize()
89+
}
90+
91+
context.view.setBehaviors(behaviors)
92+
}
6293
}
6394
}
6495

@@ -73,8 +104,8 @@ extension StyledView
73104
/// returned SwiftUI `View`:
74105
/// ```
75106
/// MyView.swiftUIView(…)
76-
/// .configure { (view: MyView) in
77-
///
107+
/// .configure { context in
108+
/// context.view.doSomething()
78109
/// }
79110
/// ```
80111
///
@@ -87,9 +118,19 @@ extension StyledView
87118
public static func swiftUIView(
88119
style: Style,
89120
behaviors: Behaviors? = nil)
90-
-> SwiftUIContentlessEpoxyableView<Self>
121+
-> SwiftUIUIView<Self, Style>
91122
{
92-
.init(style: style, behaviors: behaviors)
123+
SwiftUIUIView(storage: style) {
124+
Self(style: style)
125+
}
126+
.configure { context in
127+
// We need to create a new view instance when the style changes.
128+
if context.oldStorage != style {
129+
context.view = Self(style: style)
130+
}
131+
132+
context.view.setBehaviors(behaviors)
133+
}
93134
}
94135
}
95136

@@ -105,8 +146,8 @@ extension StyledView
105146
/// returned SwiftUI `View`:
106147
/// ```
107148
/// MyView.swiftUIView(…)
108-
/// .configure { (view: MyView) in
109-
///
149+
/// .configure { context in
150+
/// context.view.doSomething()
110151
/// }
111152
/// ```
112153
///
@@ -116,182 +157,12 @@ extension StyledView
116157
/// MyView.swiftUIView(…).sizing(.intrinsicSize)
117158
/// ```
118159
/// The sizing defaults to `.automatic`.
119-
public static func swiftUIView(
120-
behaviors: Behaviors? = nil,
121-
sizing: SwiftUIMeasurementContainerStrategy = .automatic)
122-
-> SwiftUIStylelessContentlessEpoxyableView<Self>
123-
{
124-
.init(behaviors: behaviors, sizing: sizing)
125-
}
126-
}
127-
128-
// MARK: - SwiftUIEpoxyableView
129-
130-
/// A SwiftUI `View` representing an `EpoxyableView` with content, behaviors, and style.
131-
public struct SwiftUIEpoxyableView<View>: MeasuringUIViewRepresentable, UIViewConfiguringSwiftUIView
132-
where
133-
View: EpoxyableView
134-
{
135-
var content: View.Content
136-
var style: View.Style
137-
var behaviors: View.Behaviors?
138-
public var sizing = SwiftUIMeasurementContainerStrategy.automatic
139-
public var configurations: [(View) -> Void] = []
140-
141-
public func updateUIView(_ wrapper: SwiftUIMeasurementContainer<Self, View>, context: Context) {
142-
let animated = context.transaction.animation != nil
143-
144-
defer {
145-
wrapper.view = self
146-
147-
// We always update the view behaviors on every view update.
148-
wrapper.uiView.setBehaviors(behaviors)
149-
150-
for configuration in configurations {
151-
configuration(wrapper.uiView)
152-
}
153-
}
154-
155-
// We need to create a new view instance when the style is updated.
156-
guard wrapper.view.style == style else {
157-
let uiView = View(style: style)
158-
uiView.setContent(content, animated: false)
159-
uiView.setBehaviors(behaviors)
160-
wrapper.uiView = uiView
161-
return
162-
}
163-
164-
// We need to update the content of the existing view when the content is updated.
165-
guard wrapper.view.content == content else {
166-
wrapper.uiView.setContent(content, animated: animated)
167-
wrapper.invalidateIntrinsicContentSize()
168-
return
169-
}
170-
171-
// No updates required.
172-
}
173-
174-
public func makeUIView(context _: Context) -> SwiftUIMeasurementContainer<Self, View> {
175-
let uiView = View(style: style)
176-
uiView.setContent(content, animated: false)
177-
// No need to set behaviors as `updateUIView` is called immediately after construction.
178-
return SwiftUIMeasurementContainer(view: self, uiView: uiView, strategy: sizing)
179-
}
180-
}
181-
182-
// MARK: - SwiftUIStylelessEpoxyableView
183-
184-
/// A SwiftUI `View` representing an `EpoxyableView` with a `Never` `Style`.
185-
public struct SwiftUIStylelessEpoxyableView<View>: MeasuringUIViewRepresentable, UIViewConfiguringSwiftUIView
186-
where
187-
View: EpoxyableView,
188-
View.Style == Never
189-
{
190-
var content: View.Content
191-
var behaviors: View.Behaviors?
192-
public var sizing = SwiftUIMeasurementContainerStrategy.automatic
193-
public var configurations: [(View) -> Void] = []
194-
195-
public func updateUIView(_ wrapper: SwiftUIMeasurementContainer<Self, View>, context: Context) {
196-
let animated = context.transaction.animation != nil
197-
198-
defer {
199-
wrapper.view = self
200-
201-
// We always update the view behaviors on every view update.
202-
wrapper.uiView.setBehaviors(behaviors)
203-
204-
for configuration in configurations {
205-
configuration(wrapper.uiView)
206-
}
207-
}
208-
209-
// We need to update the content of the existing view when the content is updated.
210-
guard wrapper.view.content == content else {
211-
wrapper.uiView.setContent(content, animated: animated)
212-
wrapper.invalidateIntrinsicContentSize()
213-
return
214-
}
215-
216-
// No updates required.
217-
}
218-
219-
public func makeUIView(context _: Context) -> SwiftUIMeasurementContainer<Self, View> {
220-
let uiView = View()
221-
uiView.setContent(content, animated: false)
222-
// No need to set behaviors as `updateUIView` is called immediately after construction.
223-
return SwiftUIMeasurementContainer(view: self, uiView: uiView, strategy: sizing)
224-
}
225-
}
226-
227-
// MARK: - SwiftUIContentlessEpoxyableView
228-
229-
/// A SwiftUI `View` representing an `EpoxyableView` with a `Never` `Content`.
230-
public struct SwiftUIContentlessEpoxyableView<View>: MeasuringUIViewRepresentable, UIViewConfiguringSwiftUIView
231-
where
232-
View: EpoxyableView,
233-
View.Content == Never
234-
{
235-
var style: View.Style
236-
var behaviors: View.Behaviors?
237-
public var sizing = SwiftUIMeasurementContainerStrategy.automatic
238-
public var configurations: [(View) -> Void] = []
239-
240-
public func updateUIView(_ wrapper: SwiftUIMeasurementContainer<Self, View>, context _: Context) {
241-
defer {
242-
wrapper.view = self
243-
244-
// We always update the view behaviors on every view update.
245-
wrapper.uiView.setBehaviors(behaviors)
246-
247-
for configuration in configurations {
248-
configuration(wrapper.uiView)
249-
}
160+
public static func swiftUIView(behaviors: Behaviors? = nil) -> SwiftUIUIView<Self, Void> {
161+
SwiftUIUIView {
162+
Self()
250163
}
251-
252-
// We need to create a new view instance when the style is updated.
253-
guard wrapper.view.style == style else {
254-
let uiView = View(style: style)
255-
uiView.setBehaviors(behaviors)
256-
wrapper.uiView = uiView
257-
return
164+
.configure { context in
165+
context.view.setBehaviors(behaviors)
258166
}
259-
260-
// No updates required.
261-
}
262-
263-
public func makeUIView(context _: Context) -> SwiftUIMeasurementContainer<Self, View> {
264-
let uiView = View(style: style)
265-
// No need to set behaviors as `updateUIView` is called immediately after construction.
266-
return SwiftUIMeasurementContainer(view: self, uiView: uiView, strategy: sizing)
267-
}
268-
}
269-
270-
// MARK: - SwiftUIStylelessContentlessEpoxyableView
271-
272-
/// A SwiftUI `View` representing an `EpoxyableView` with a `Never` `Style` and `Content`.
273-
public struct SwiftUIStylelessContentlessEpoxyableView<View>: MeasuringUIViewRepresentable, UIViewConfiguringSwiftUIView
274-
where
275-
View: EpoxyableView,
276-
View.Content == Never,
277-
View.Style == Never
278-
{
279-
public var configurations: [(View) -> Void] = []
280-
var behaviors: View.Behaviors?
281-
public var sizing = SwiftUIMeasurementContainerStrategy.automatic
282-
283-
public func updateUIView(_ wrapper: SwiftUIMeasurementContainer<Self, View>, context _: Context) {
284-
wrapper.view = self
285-
wrapper.uiView.setBehaviors(behaviors)
286-
287-
for configuration in configurations {
288-
configuration(wrapper.uiView)
289-
}
290-
}
291-
292-
public func makeUIView(context _: Context) -> SwiftUIMeasurementContainer<Self, View> {
293-
let uiView = View()
294-
// No need to set behaviors as `updateUIView` is called immediately after construction.
295-
return SwiftUIMeasurementContainer(view: self, uiView: uiView, strategy: sizing)
296167
}
297168
}

Sources/EpoxyCore/SwiftUI/LayoutUtilities/MeasuringUIViewRepresentable.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ import SwiftUI
1414
/// - SeeAlso: ``SwiftUIMeasurementContainer``
1515
public protocol MeasuringUIViewRepresentable: UIViewRepresentable
1616
where
17-
UIViewType == SwiftUIMeasurementContainer<Self, View>
17+
UIViewType == SwiftUIMeasurementContainer<Content>
1818
{
19-
/// The `UIView` that's being measured by the enclosing `SwiftUIMeasurementContainer`.
20-
associatedtype View: UIView
19+
/// The `UIView` content that's being measured by the enclosing `SwiftUIMeasurementContainer`.
20+
associatedtype Content: UIView
2121

2222
/// The sizing strategy of the represented view.
2323
///

0 commit comments

Comments
 (0)