Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@ import SwiftUI
import SwiftUIKitView

struct SwiftUIwithUIKitView: View {
@State var integer: Int = 0

var body: some View {
NavigationView {
UIKitView() // <- This is a `UIKit` view.
.swiftUIView(layout: .intrinsic) // <- This is a SwiftUI `View`.
.set(\.title, to: "Hello, UIKit!")
.set(\.backgroundColor, to: UIColor(named: "swiftlee_orange"))
.fixedSize()
.navigationTitle("Use UIKit in SwiftUI")
VStack {
// Use UIKit inside SwiftUI like this:
UIViewContainer(UIKitView(), layout: .intrinsic)
.set(\.title, to: "Hello, UIKit \(integer)!")
.set(\.backgroundColor, to: UIColor(named: "swiftlee_orange"))
.fixedSize()
.navigationTitle("Use UIKit in SwiftUI")

Button("RANDOMIZED: \(integer)") {
integer = Int.random(in: 0..<300)
}
}
}
}
}
Expand All @@ -38,4 +46,3 @@ struct UILabelExample_Preview: PreviewProvider {
.previewDisplayName("UILabel Preview Example")
}
}

2 changes: 2 additions & 0 deletions Example/SwiftUIKitExample/UIKitView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ final class UIKitView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()

print("INIT")
}

required init?(coder: NSCoder) {
Expand Down
13 changes: 3 additions & 10 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,29 +1,22 @@
// swift-tools-version:5.3
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "SwiftUIKitView",
platforms: [
.iOS(.v13),
.iOS(.v14),
.macOS(.v10_15),
.tvOS(.v13),
.tvOS(.v14),
.watchOS(.v6)
],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SwiftUIKitView",
targets: ["SwiftUIKitView"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "SwiftUIKitView",
dependencies: []),
Expand Down
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SwiftUIKitView
![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/twitter-@Twannl-blue.svg?style=flat)](https://twitter.com/twannl)
![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/twitter-@Twannl-blue.svg?style=flat)](https://twitter.com/twannl)

Easily use UIKit views in SwiftUI.

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

## Examples

Using a `UIKit` view directly in SwiftUI:
### Using SwiftUIKitView in Production Code
Using a `UIKit` view directly in SwiftUI for production code requires you to use:

```swift
UIViewContainer(<YOUR UIKit View>, layout: <YOUR LAYOUT PREFERENCE>)
```

This is to prevent a UIKit view from being redrawn on every SwiftUI view redraw.

```swift
import SwiftUI
Expand All @@ -20,8 +27,7 @@ import SwiftUIKitView
struct SwiftUIwithUIKitView: View {
var body: some View {
NavigationView {
UILabel() // <- This can be any `UIKit` view.
.swiftUIView(layout: .intrinsic) // <- This is returning a SwiftUI `View`.
UIViewContainer(UILabel(), layout: .intrinsic) // <- This can be any `UIKit` view.
.set(\.text, to: "Hello, UIKit!") // <- Use key paths for updates.
.set(\.backgroundColor, to: UIColor(named: "swiftlee_orange"))
.fixedSize()
Expand All @@ -31,7 +37,16 @@ struct SwiftUIwithUIKitView: View {
}
```

Creating a preview provider for a `UIView`:
### Using `SwiftUIKitView` in Previews
Performance in Previews is less important, it's being redrawn either way.
Therefore, you can use of the more convenient `swiftUIView()` modifier:

```swift
UILabel() // <- This is a `UIKit` view.
.swiftUIView(layout: .intrinsic) // <- This is a SwiftUI `View`.
```

Creating a preview provider for a `UIView` looks as follows:

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

```swift
dependencies: [
.package(url: "https://github.com/AvdLee/SwiftUIKitView.git", .upToNextMajor(from: "1.0.0"))
.package(url: "https://github.com/AvdLee/SwiftUIKitView.git", .upToNextMajor(from: "2.0.0"))
]
```

Expand Down
69 changes: 69 additions & 0 deletions Sources/SwiftUIKitView/IntrinsicContentView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// IntrinsicContentView.swift
//
//
// Created by Antoine van der Lee on 23/07/2022.
//

import Foundation
import UIKit

public final class IntrinsicContentView<ContentView: UIView>: UIView {
let contentView: ContentView
let layout: Layout

init(contentView: ContentView, layout: Layout) {
self.contentView = contentView
self.layout = layout

super.init(frame: .zero)
backgroundColor = .clear
addSubview(contentView)
clipsToBounds = true
}

@available(*, unavailable) required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

private var contentSize: CGSize = .zero {
didSet {
invalidateIntrinsicContentSize()
}
}

public override var intrinsicContentSize: CGSize {
switch layout {
case .intrinsic:
return contentSize
case .fixedWidth(let width):
return .init(width: width, height: contentSize.height)
case .fixed(let size):
return size
}
}

public func updateContentSize() {
switch layout {
case .fixedWidth(let width):
// Set the frame of the cell, so that the layout can be updated.
var newFrame = contentView.frame
newFrame.size = CGSize(width: width, height: UIView.layoutFittingExpandedSize.height)
contentView.frame = newFrame

// Make sure the contents of the cell have the correct layout.
contentView.setNeedsLayout()
contentView.layoutIfNeeded()

// Get the size of the cell
let computedSize = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)

// 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.
contentSize = CGSize(width: width, height: ceil(computedSize.height))
case .fixed(let size):
contentSize = size
case .intrinsic:
contentSize = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
}
}
}
39 changes: 39 additions & 0 deletions Sources/SwiftUIKitView/ModifiedUIViewContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// ModifiedUIViewContainer.swift
//
//
// Created by Antoine van der Lee on 23/07/2022.
//

import Foundation
import SwiftUI
import UIKit

public struct ModifiedUIViewContainer<ChildContainer: UIViewContaining, Child, Value>: UIViewContaining where ChildContainer.Child == Child {

let child: ChildContainer
let keyPath: ReferenceWritableKeyPath<Child, Value>
let value: Value

public func makeCoordinator() -> UIViewContainingCoordinator<Child> {
child.makeCoordinator() as! UIViewContainingCoordinator<Child>
}

public func makeUIView(context: Context) -> IntrinsicContentView<Child> {
context.coordinator.createView()
}

public func updateUIView(_ uiView: IntrinsicContentView<Child>, context: Context) {
update(uiView.contentView, coordinator: context.coordinator, updateContentSize: true)
}

public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator<Child>, updateContentSize: Bool) {
uiView[keyPath: keyPath] = value
child.update(uiView, coordinator: coordinator, updateContentSize: false)

if updateContentSize {
coordinator.view?.updateContentSize()
}
}
}

8 changes: 6 additions & 2 deletions Sources/SwiftUIKitView/SwiftUIViewConvertable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,19 @@ import UIKit
@available(iOS 13.0, *)
public protocol SwiftUIViewConvertable {
associatedtype View: UIView
func swiftUIView(layout: UIViewContainer<View>.Layout) -> UIViewContainer<View>
func swiftUIView(layout: Layout) -> UIViewContainer<View>
}

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

@available(iOS 13.0, *)
public extension SwiftUIViewConvertable where Self: UIView {
func swiftUIView(layout: UIViewContainer<Self>.Layout) -> UIViewContainer<Self> {
func swiftUIView(layout: Layout) -> UIViewContainer<Self> {
assert(
ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1",
"This method is designed to use in previews only and is not performant for production code. Use `UIViewContainer(<YOUR VIEW>, layout: layout)` instead."
)
return UIViewContainer(self, layout: layout)
}
}
99 changes: 19 additions & 80 deletions Sources/SwiftUIKitView/UIViewContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,104 +11,43 @@ import SwiftUI

/// A container for UIKit `UIView` elements. Conforms to the `UIViewRepresentable` protocol to allow conversion into SwiftUI `View`s.
@available(iOS 13.0, *)
public struct UIViewContainer<Child: UIView>: Identifiable {

public var id: UIView { view }
public struct UIViewContainer<Child: UIView> {

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

/// Uses the size returned by .`systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)`.
case intrinsic

/// Uses an intrinsic height combined with a fixed width.
case fixedWidth(width: CGFloat)

/// A fixed width and height is used.
case fixed(size: CGSize)
}

private let view: Child
private let layout: Layout

/// - Returns: The `CGSize` to apply to the view.
private var size: CGSize {
switch layout {
case .fixedWidth(let width):
// Set the frame of the cell, so that the layout can be updated.
var newFrame = view.frame
newFrame.size = CGSize(width: width, height: UIView.layoutFittingExpandedSize.height)
view.frame = newFrame

// Make sure the contents of the cell have the correct layout.
view.setNeedsLayout()
view.layoutIfNeeded()

// Get the size of the cell
let computedSize = view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)

// 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.
return CGSize(width: width, height: ceil(computedSize.height))
case .fixed(let size):
return size
case .intrinsic:
return view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
}
}

/// Initializes a `UIViewContainer`
/// - Parameters:
/// - view: `UIView` being previewed
/// - layout: The layout to apply on the `UIView`. Defaults to `intrinsic`.
public init(_ view: @autoclosure () -> Child, layout: Layout = .intrinsic) {
self.view = view()
public init(_ viewCreator: @escaping @autoclosure () -> Child, layout: Layout = .intrinsic) {
self.viewCreator = viewCreator
self.layout = layout

switch layout {
case .intrinsic:
return
case .fixed(let size):
self.view.widthAnchor.constraint(equalToConstant: size.width).isActive = true
self.view.heightAnchor.constraint(equalToConstant: size.height).isActive = true
case .fixedWidth(let width):
self.view.widthAnchor.constraint(equalToConstant: width).isActive = true
}
}

/// Applies the correct size to the SwiftUI `View` container.
/// - Returns: A `View` with the correct size applied.
public func fixedSize() -> some View {
let size = self.size
return frame(width: size.width, height: size.height, alignment: .topLeading)
}

/// Creates a preview of the `UIViewContainer` with the right size applied.
/// - Returns: A preview of the container.
public func preview(displayName: String? = nil) -> some View {
return fixedSize()
.previewLayout(.sizeThatFits)
.previewDisplayName(displayName)
}
}

// MARK: Preview + UIViewRepresentable

@available(iOS 13, *)
extension UIViewContainer: UIViewRepresentable {
public func makeCoordinator() -> UIViewContainingCoordinator<Child> {
// Create an instance of Coordinator
Coordinator(viewCreator, layout: layout)
}

public func makeUIView(context: Context) -> UIView {
return view
public func makeUIView(context: Context) -> IntrinsicContentView<Child> {
context.coordinator.createView()
}

public func updateUIView(_ view: UIView, context: Context) {}
public func updateUIView(_ view: IntrinsicContentView<Child>, context: Context) {
update(view.contentView, coordinator: context.coordinator, updateContentSize: true)

}
}

@available(iOS 13.0, *)
extension UIViewContainer: KeyPathReferenceWritable {
public typealias T = Child

public func set<Value>(_ keyPath: ReferenceWritableKeyPath<Child, Value>, to value: Value) -> Self {
view[keyPath: keyPath] = value
return self
extension UIViewContainer: UIViewContaining {
public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator<Child>, updateContentSize: Bool) {
guard updateContentSize else { return }
coordinator.view?.updateContentSize()
}
}
Loading