Skip to content

Commit 56f2a1f

Browse files
authored
Production ready code for UIKit in SwiftUI (#6)
* First iteration * Second iteration * Iteration 3 * Start of iteration 4 * Create IntrinsicContentView * Fixed content size * Update readme, clean up code
1 parent 8f0239c commit 56f2a1f

File tree

10 files changed

+252
-105
lines changed

10 files changed

+252
-105
lines changed

Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,22 @@ import SwiftUI
99
import SwiftUIKitView
1010

1111
struct SwiftUIwithUIKitView: View {
12+
@State var integer: Int = 0
13+
1214
var body: some View {
1315
NavigationView {
14-
UIKitView() // <- This is a `UIKit` view.
15-
.swiftUIView(layout: .intrinsic) // <- This is a SwiftUI `View`.
16-
.set(\.title, to: "Hello, UIKit!")
17-
.set(\.backgroundColor, to: UIColor(named: "swiftlee_orange"))
18-
.fixedSize()
19-
.navigationTitle("Use UIKit in SwiftUI")
16+
VStack {
17+
// Use UIKit inside SwiftUI like this:
18+
UIViewContainer(UIKitView(), layout: .intrinsic)
19+
.set(\.title, to: "Hello, UIKit \(integer)!")
20+
.set(\.backgroundColor, to: UIColor(named: "swiftlee_orange"))
21+
.fixedSize()
22+
.navigationTitle("Use UIKit in SwiftUI")
23+
24+
Button("RANDOMIZED: \(integer)") {
25+
integer = Int.random(in: 0..<300)
26+
}
27+
}
2028
}
2129
}
2230
}
@@ -38,4 +46,3 @@ struct UILabelExample_Preview: PreviewProvider {
3846
.previewDisplayName("UILabel Preview Example")
3947
}
4048
}
41-

Example/SwiftUIKitExample/UIKitView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ final class UIKitView: UIView {
4848
override init(frame: CGRect) {
4949
super.init(frame: frame)
5050
setupView()
51+
52+
print("INIT")
5153
}
5254

5355
required init?(coder: NSCoder) {

Package.swift

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,22 @@
1-
// swift-tools-version:5.3
1+
// swift-tools-version:5.5
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

44
import PackageDescription
55

66
let package = Package(
77
name: "SwiftUIKitView",
88
platforms: [
9-
.iOS(.v13),
9+
.iOS(.v14),
1010
.macOS(.v10_15),
11-
.tvOS(.v13),
11+
.tvOS(.v14),
1212
.watchOS(.v6)
1313
],
1414
products: [
15-
// Products define the executables and libraries a package produces, and make them visible to other packages.
1615
.library(
1716
name: "SwiftUIKitView",
1817
targets: ["SwiftUIKitView"]),
1918
],
20-
dependencies: [
21-
// Dependencies declare other packages that this package depends on.
22-
// .package(url: /* package url */, from: "1.0.0"),
23-
],
2419
targets: [
25-
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
26-
// Targets can depend on other targets in this package, and on products in packages this package depends on.
2720
.target(
2821
name: "SwiftUIKitView",
2922
dependencies: []),

README.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# SwiftUIKitView
2-
![Swift Version](https://img.shields.io/badge/Swift-5.3-F16D39.svg?style=flat) ![Dependency frameworks](https://img.shields.io/badge/Supports-_Swift_Package_Manager-F16D39.svg?style=flat) [![Twitter](https://img.shields.io/badge/[email protected]?style=flat)](https://twitter.com/twannl)
2+
![Swift Version](https://img.shields.io/badge/Swift-5.5-F16D39.svg?style=flat) ![Dependency frameworks](https://img.shields.io/badge/Supports-_Swift_Package_Manager-F16D39.svg?style=flat) [![Twitter](https://img.shields.io/badge/[email protected]?style=flat)](https://twitter.com/twannl)
33

44
Easily use UIKit views in SwiftUI.
55

@@ -11,7 +11,14 @@ You can read more about [Getting started with UIKit in SwiftUI and visa versa](h
1111

1212
## Examples
1313

14-
Using a `UIKit` view directly in SwiftUI:
14+
### Using SwiftUIKitView in Production Code
15+
Using a `UIKit` view directly in SwiftUI for production code requires you to use:
16+
17+
```swift
18+
UIViewContainer(<YOUR UIKit View>, layout: <YOUR LAYOUT PREFERENCE>)
19+
```
20+
21+
This is to prevent a UIKit view from being redrawn on every SwiftUI view redraw.
1522

1623
```swift
1724
import SwiftUI
@@ -20,8 +27,7 @@ import SwiftUIKitView
2027
struct SwiftUIwithUIKitView: View {
2128
var body: some View {
2229
NavigationView {
23-
UILabel() // <- This can be any `UIKit` view.
24-
.swiftUIView(layout: .intrinsic) // <- This is returning a SwiftUI `View`.
30+
UIViewContainer(UILabel(), layout: .intrinsic) // <- This can be any `UIKit` view.
2531
.set(\.text, to: "Hello, UIKit!") // <- Use key paths for updates.
2632
.set(\.backgroundColor, to: UIColor(named: "swiftlee_orange"))
2733
.fixedSize()
@@ -31,7 +37,16 @@ struct SwiftUIwithUIKitView: View {
3137
}
3238
```
3339

34-
Creating a preview provider for a `UIView`:
40+
### Using `SwiftUIKitView` in Previews
41+
Performance in Previews is less important, it's being redrawn either way.
42+
Therefore, you can use of the more convenient `swiftUIView()` modifier:
43+
44+
```swift
45+
UILabel() // <- This is a `UIKit` view.
46+
.swiftUIView(layout: .intrinsic) // <- This is a SwiftUI `View`.
47+
```
48+
49+
Creating a preview provider for a `UIView` looks as follows:
3550

3651
```swift
3752
import SwiftUI
@@ -96,7 +111,7 @@ Once you have your Swift package set up, adding the SDK as a dependency is as ea
96111

97112
```swift
98113
dependencies: [
99-
.package(url: "https://github.com/AvdLee/SwiftUIKitView.git", .upToNextMajor(from: "1.0.0"))
114+
.package(url: "https://github.com/AvdLee/SwiftUIKitView.git", .upToNextMajor(from: "2.0.0"))
100115
]
101116
```
102117

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// IntrinsicContentView.swift
3+
//
4+
//
5+
// Created by Antoine van der Lee on 23/07/2022.
6+
//
7+
8+
import Foundation
9+
import UIKit
10+
11+
public final class IntrinsicContentView<ContentView: UIView>: UIView {
12+
let contentView: ContentView
13+
let layout: Layout
14+
15+
init(contentView: ContentView, layout: Layout) {
16+
self.contentView = contentView
17+
self.layout = layout
18+
19+
super.init(frame: .zero)
20+
backgroundColor = .clear
21+
addSubview(contentView)
22+
clipsToBounds = true
23+
}
24+
25+
@available(*, unavailable) required init?(coder _: NSCoder) {
26+
fatalError("init(coder:) has not been implemented")
27+
}
28+
29+
private var contentSize: CGSize = .zero {
30+
didSet {
31+
invalidateIntrinsicContentSize()
32+
}
33+
}
34+
35+
public override var intrinsicContentSize: CGSize {
36+
switch layout {
37+
case .intrinsic:
38+
return contentSize
39+
case .fixedWidth(let width):
40+
return .init(width: width, height: contentSize.height)
41+
case .fixed(let size):
42+
return size
43+
}
44+
}
45+
46+
public func updateContentSize() {
47+
switch layout {
48+
case .fixedWidth(let width):
49+
// Set the frame of the cell, so that the layout can be updated.
50+
var newFrame = contentView.frame
51+
newFrame.size = CGSize(width: width, height: UIView.layoutFittingExpandedSize.height)
52+
contentView.frame = newFrame
53+
54+
// Make sure the contents of the cell have the correct layout.
55+
contentView.setNeedsLayout()
56+
contentView.layoutIfNeeded()
57+
58+
// Get the size of the cell
59+
let computedSize = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
60+
61+
// Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes." We use ceil to make sure we get rounded numbers and no half pixels.
62+
contentSize = CGSize(width: width, height: ceil(computedSize.height))
63+
case .fixed(let size):
64+
contentSize = size
65+
case .intrinsic:
66+
contentSize = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
67+
}
68+
}
69+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// ModifiedUIViewContainer.swift
3+
//
4+
//
5+
// Created by Antoine van der Lee on 23/07/2022.
6+
//
7+
8+
import Foundation
9+
import SwiftUI
10+
import UIKit
11+
12+
public struct ModifiedUIViewContainer<ChildContainer: UIViewContaining, Child, Value>: UIViewContaining where ChildContainer.Child == Child {
13+
14+
let child: ChildContainer
15+
let keyPath: ReferenceWritableKeyPath<Child, Value>
16+
let value: Value
17+
18+
public func makeCoordinator() -> UIViewContainingCoordinator<Child> {
19+
child.makeCoordinator() as! UIViewContainingCoordinator<Child>
20+
}
21+
22+
public func makeUIView(context: Context) -> IntrinsicContentView<Child> {
23+
context.coordinator.createView()
24+
}
25+
26+
public func updateUIView(_ uiView: IntrinsicContentView<Child>, context: Context) {
27+
update(uiView.contentView, coordinator: context.coordinator, updateContentSize: true)
28+
}
29+
30+
public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator<Child>, updateContentSize: Bool) {
31+
uiView[keyPath: keyPath] = value
32+
child.update(uiView, coordinator: coordinator, updateContentSize: false)
33+
34+
if updateContentSize {
35+
coordinator.view?.updateContentSize()
36+
}
37+
}
38+
}
39+

Sources/SwiftUIKitView/SwiftUIViewConvertable.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ import UIKit
1212
@available(iOS 13.0, *)
1313
public protocol SwiftUIViewConvertable {
1414
associatedtype View: UIView
15-
func swiftUIView(layout: UIViewContainer<View>.Layout) -> UIViewContainer<View>
15+
func swiftUIView(layout: Layout) -> UIViewContainer<View>
1616
}
1717

1818
/// Add default protocol comformance for `UIView` instances.
1919
extension UIView: SwiftUIViewConvertable {}
2020

2121
@available(iOS 13.0, *)
2222
public extension SwiftUIViewConvertable where Self: UIView {
23-
func swiftUIView(layout: UIViewContainer<Self>.Layout) -> UIViewContainer<Self> {
23+
func swiftUIView(layout: Layout) -> UIViewContainer<Self> {
24+
assert(
25+
ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1",
26+
"This method is designed to use in previews only and is not performant for production code. Use `UIViewContainer(<YOUR VIEW>, layout: layout)` instead."
27+
)
2428
return UIViewContainer(self, layout: layout)
2529
}
2630
}

Sources/SwiftUIKitView/UIViewContainer.swift

Lines changed: 19 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -11,104 +11,43 @@ import SwiftUI
1111

1212
/// A container for UIKit `UIView` elements. Conforms to the `UIViewRepresentable` protocol to allow conversion into SwiftUI `View`s.
1313
@available(iOS 13.0, *)
14-
public struct UIViewContainer<Child: UIView>: Identifiable {
15-
16-
public var id: UIView { view }
14+
public struct UIViewContainer<Child: UIView> {
1715

18-
/// The type of Layout to apply to the SwiftUI `View`.
19-
public enum Layout {
16+
let viewCreator: () -> Child
17+
let layout: Layout
2018

21-
/// Uses the size returned by .`systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)`.
22-
case intrinsic
23-
24-
/// Uses an intrinsic height combined with a fixed width.
25-
case fixedWidth(width: CGFloat)
26-
27-
/// A fixed width and height is used.
28-
case fixed(size: CGSize)
29-
}
30-
31-
private let view: Child
32-
private let layout: Layout
33-
34-
/// - Returns: The `CGSize` to apply to the view.
35-
private var size: CGSize {
36-
switch layout {
37-
case .fixedWidth(let width):
38-
// Set the frame of the cell, so that the layout can be updated.
39-
var newFrame = view.frame
40-
newFrame.size = CGSize(width: width, height: UIView.layoutFittingExpandedSize.height)
41-
view.frame = newFrame
42-
43-
// Make sure the contents of the cell have the correct layout.
44-
view.setNeedsLayout()
45-
view.layoutIfNeeded()
46-
47-
// Get the size of the cell
48-
let computedSize = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
49-
50-
// Apple: "Only consider the height for cells, because the contentView isn't anchored correctly sometimes." We use ceil to make sure we get rounded numbers and no half pixels.
51-
return CGSize(width: width, height: ceil(computedSize.height))
52-
case .fixed(let size):
53-
return size
54-
case .intrinsic:
55-
return view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
56-
}
57-
}
58-
5919
/// Initializes a `UIViewContainer`
6020
/// - Parameters:
6121
/// - view: `UIView` being previewed
6222
/// - layout: The layout to apply on the `UIView`. Defaults to `intrinsic`.
63-
public init(_ view: @autoclosure () -> Child, layout: Layout = .intrinsic) {
64-
self.view = view()
23+
public init(_ viewCreator: @escaping @autoclosure () -> Child, layout: Layout = .intrinsic) {
24+
self.viewCreator = viewCreator
6525
self.layout = layout
66-
67-
switch layout {
68-
case .intrinsic:
69-
return
70-
case .fixed(let size):
71-
self.view.widthAnchor.constraint(equalToConstant: size.width).isActive = true
72-
self.view.heightAnchor.constraint(equalToConstant: size.height).isActive = true
73-
case .fixedWidth(let width):
74-
self.view.widthAnchor.constraint(equalToConstant: width).isActive = true
75-
}
76-
}
77-
78-
/// Applies the correct size to the SwiftUI `View` container.
79-
/// - Returns: A `View` with the correct size applied.
80-
public func fixedSize() -> some View {
81-
let size = self.size
82-
return frame(width: size.width, height: size.height, alignment: .topLeading)
83-
}
84-
85-
/// Creates a preview of the `UIViewContainer` with the right size applied.
86-
/// - Returns: A preview of the container.
87-
public func preview(displayName: String? = nil) -> some View {
88-
return fixedSize()
89-
.previewLayout(.sizeThatFits)
90-
.previewDisplayName(displayName)
9126
}
9227
}
9328

9429
// MARK: Preview + UIViewRepresentable
9530

9631
@available(iOS 13, *)
9732
extension UIViewContainer: UIViewRepresentable {
33+
public func makeCoordinator() -> UIViewContainingCoordinator<Child> {
34+
// Create an instance of Coordinator
35+
Coordinator(viewCreator, layout: layout)
36+
}
9837

99-
public func makeUIView(context: Context) -> UIView {
100-
return view
38+
public func makeUIView(context: Context) -> IntrinsicContentView<Child> {
39+
context.coordinator.createView()
10140
}
10241

103-
public func updateUIView(_ view: UIView, context: Context) {}
42+
public func updateUIView(_ view: IntrinsicContentView<Child>, context: Context) {
43+
update(view.contentView, coordinator: context.coordinator, updateContentSize: true)
44+
45+
}
10446
}
10547

106-
@available(iOS 13.0, *)
107-
extension UIViewContainer: KeyPathReferenceWritable {
108-
public typealias T = Child
109-
110-
public func set<Value>(_ keyPath: ReferenceWritableKeyPath<Child, Value>, to value: Value) -> Self {
111-
view[keyPath: keyPath] = value
112-
return self
48+
extension UIViewContainer: UIViewContaining {
49+
public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator<Child>, updateContentSize: Bool) {
50+
guard updateContentSize else { return }
51+
coordinator.view?.updateContentSize()
11352
}
11453
}

0 commit comments

Comments
 (0)