From b03905ba051c9deeba037409ac3da3e97b3e0e41 Mon Sep 17 00:00:00 2001 From: Antoine van der Lee Date: Sat, 23 Jul 2022 11:52:37 +0200 Subject: [PATCH 1/7] First iteration --- .../SwiftUIwithUIKitView.swift | 70 +++++++++- Example/SwiftUIKitExample/UIKitView.swift | 2 + Package.swift | 4 +- Sources/SwiftUIKitView/UIViewContainer.swift | 121 +++++++++++------- 4 files changed, 142 insertions(+), 55 deletions(-) diff --git a/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift b/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift index 0e438d5..f67ae60 100644 --- a/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift +++ b/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift @@ -9,14 +9,24 @@ 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 { +// UIViewMaker { +// $0.text = "Hello no \(self.integer) from UIKit" +// } +// .fixedSize() + 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) + } + } } } } @@ -39,3 +49,51 @@ struct UILabelExample_Preview: PreviewProvider { } } +struct UIViewMaker: UIViewRepresentable { + + typealias UIViewType = ViewType + + var make: () -> ViewType = ViewType.init + var update: (ViewType) -> () + + func makeUIView(context: Context) -> ViewType { + return make() + } + + func updateUIView(_ uiView: ViewType, context: Context) { + update(uiView) + } +} + +struct ContentView: View { + @State var counter = 0 + + var body: some View { + VStack { + + Text("Hello no \(counter) from SwiftUI") + .padding() + + + UIViewMaker { + $0.text = "Hello no \(self.counter) from UIKit" + } + .fixedSize() + + } + .onAppear { + if counter == 0 { + schedule() + } + } + } + + func schedule() { + counter += 1 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.schedule() + } + } + +} + diff --git a/Example/SwiftUIKitExample/UIKitView.swift b/Example/SwiftUIKitExample/UIKitView.swift index b435077..637d2ae 100644 --- a/Example/SwiftUIKitExample/UIKitView.swift +++ b/Example/SwiftUIKitExample/UIKitView.swift @@ -48,6 +48,8 @@ final class UIKitView: UIView { override init(frame: CGRect) { super.init(frame: frame) setupView() + + print("INIT") } required init?(coder: NSCoder) { diff --git a/Package.swift b/Package.swift index a2a06c1..ebc3afe 100644 --- a/Package.swift +++ b/Package.swift @@ -6,9 +6,9 @@ import PackageDescription let package = Package( name: "SwiftUIKitView", platforms: [ - .iOS(.v13), + .iOS(.v14), .macOS(.v10_15), - .tvOS(.v13), + .tvOS(.v14), .watchOS(.v6) ], products: [ diff --git a/Sources/SwiftUIKitView/UIViewContainer.swift b/Sources/SwiftUIKitView/UIViewContainer.swift index cb9b47c..382b1a9 100644 --- a/Sources/SwiftUIKitView/UIViewContainer.swift +++ b/Sources/SwiftUIKitView/UIViewContainer.swift @@ -11,9 +11,9 @@ 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: Identifiable { +public struct UIViewContainer { //}: Identifiable { - public var id: UIView { view } +// public var id: UIView { view } /// The type of Layout to apply to the SwiftUI `View`. public enum Layout { @@ -28,59 +28,52 @@ public struct UIViewContainer: Identifiable { case fixed(size: CGSize) } - private let view: Child +// var view: Child! { +// get { coordinator.view } +// } + private let viewCreator: () -> 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) - } - } +// 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(_ view: @escaping @autoclosure () -> Child, layout: Layout = .intrinsic) { + self.viewCreator = view 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) - } +// 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. @@ -89,18 +82,49 @@ public struct UIViewContainer: Identifiable { .previewLayout(.sizeThatFits) .previewDisplayName(displayName) } + + public class Coordinator { + var view: Child! + var viewCreator: () -> Child + var modifiers: [(Child) -> Void] = [] + + init(_ viewCreator: @escaping () -> Child) { + self.viewCreator = viewCreator + } + } } // MARK: Preview + UIViewRepresentable @available(iOS 13, *) extension UIViewContainer: UIViewRepresentable { + public func makeCoordinator() -> Coordinator { + // Create an instance of Coordinator + return Coordinator(self.viewCreator) + } - public func makeUIView(context: Context) -> UIView { - return view + public func makeUIView(context: Context) -> Child { + context.coordinator.view = viewCreator() + context.coordinator.modifiers.forEach { modifier in + modifier(context.coordinator.view) + } + switch layout { + case .intrinsic: + return context.coordinator.view + case .fixed(let size): + context.coordinator.view.widthAnchor.constraint(equalToConstant: size.width).isActive = true + context.coordinator.view.heightAnchor.constraint(equalToConstant: size.height).isActive = true + case .fixedWidth(let width): + context.coordinator.view.widthAnchor.constraint(equalToConstant: width).isActive = true + } + return context.coordinator.view } - public func updateUIView(_ view: UIView, context: Context) {} + public func updateUIView(_ view: Child, context: Context) { + context.coordinator.modifiers.forEach { modifier in + modifier(view) + } + } } @available(iOS 13.0, *) @@ -108,7 +132,10 @@ extension UIViewContainer: KeyPathReferenceWritable { public typealias T = Child public func set(_ keyPath: ReferenceWritableKeyPath, to value: Value) -> Self { - view[keyPath: keyPath] = value + coordinator.modifiers.append({ view in + view[keyPath: keyPath] = value + }) + print("Modifiers is now \(coordinator.modifiers)") return self } } From 121979e71cac42b1861465b72951aeacce9c3ff4 Mon Sep 17 00:00:00 2001 From: Antoine van der Lee Date: Sat, 23 Jul 2022 11:53:01 +0200 Subject: [PATCH 2/7] Second iteration --- Sources/SwiftUIKitView/UIViewContainer.swift | 74 ++++++++++---------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/Sources/SwiftUIKitView/UIViewContainer.swift b/Sources/SwiftUIKitView/UIViewContainer.swift index 382b1a9..7f1bf53 100644 --- a/Sources/SwiftUIKitView/UIViewContainer.swift +++ b/Sources/SwiftUIKitView/UIViewContainer.swift @@ -28,36 +28,37 @@ public struct UIViewContainer { //}: Identifiable { case fixed(size: CGSize) } -// var view: Child! { -// get { coordinator.view } -// } + var view: Child! { + get { coordinator.view } + } private let viewCreator: () -> Child private let layout: Layout + private let coordinator: Coordinator /// - 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) -// } -// } + 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: @@ -66,6 +67,7 @@ public struct UIViewContainer { //}: Identifiable { public init(_ view: @escaping @autoclosure () -> Child, layout: Layout = .intrinsic) { self.viewCreator = view self.layout = layout + self.coordinator = Coordinator(self.viewCreator) } /// Applies the correct size to the SwiftUI `View` container. @@ -100,28 +102,28 @@ public struct UIViewContainer { //}: Identifiable { extension UIViewContainer: UIViewRepresentable { public func makeCoordinator() -> Coordinator { // Create an instance of Coordinator - return Coordinator(self.viewCreator) + coordinator } public func makeUIView(context: Context) -> Child { - context.coordinator.view = viewCreator() - context.coordinator.modifiers.forEach { modifier in - modifier(context.coordinator.view) + self.coordinator.view = viewCreator() + coordinator.modifiers.forEach { modifier in + modifier(self.coordinator.view) } switch layout { case .intrinsic: - return context.coordinator.view + return view case .fixed(let size): - context.coordinator.view.widthAnchor.constraint(equalToConstant: size.width).isActive = true - context.coordinator.view.heightAnchor.constraint(equalToConstant: size.height).isActive = true + self.view.widthAnchor.constraint(equalToConstant: size.width).isActive = true + self.view.heightAnchor.constraint(equalToConstant: size.height).isActive = true case .fixedWidth(let width): - context.coordinator.view.widthAnchor.constraint(equalToConstant: width).isActive = true + self.view.widthAnchor.constraint(equalToConstant: width).isActive = true } - return context.coordinator.view + return view } public func updateUIView(_ view: Child, context: Context) { - context.coordinator.modifiers.forEach { modifier in + coordinator.modifiers.forEach { modifier in modifier(view) } } From ebb2e1ab252b23389e1703d5b96e7e7e33493458 Mon Sep 17 00:00:00 2001 From: Antoine van der Lee Date: Sat, 23 Jul 2022 18:38:33 +0200 Subject: [PATCH 3/7] Iteration 3 --- .../SwiftUIwithUIKitView.swift | 5 + Sources/SwiftUIKitView/UIViewContainer.swift | 225 ++++++++++++------ 2 files changed, 151 insertions(+), 79 deletions(-) diff --git a/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift b/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift index f67ae60..720ac48 100644 --- a/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift +++ b/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift @@ -18,10 +18,15 @@ struct SwiftUIwithUIKitView: View { // $0.text = "Hello no \(self.integer) from UIKit" // } // .fixedSize() + UIViewContainer(UILabel(), layout: .intrinsic) + .set(\.text, to: "Hello, UIKit \(integer)!") + .set(\.backgroundColor, to: UIColor(named: "swiftlee_orange")) +// .fixedSize() UIViewContainer(UIKitView(), layout: .intrinsic) .set(\.title, to: "Hello, UIKit \(integer)!") .set(\.backgroundColor, to: UIColor(named: "swiftlee_orange")) .fixedSize() +// .fixedSize() .navigationTitle("Use UIKit in SwiftUI") Button("RANDOMIZED: \(integer)") { integer = Int.random(in: 0..<300) diff --git a/Sources/SwiftUIKitView/UIViewContainer.swift b/Sources/SwiftUIKitView/UIViewContainer.swift index 7f1bf53..a9cfb6a 100644 --- a/Sources/SwiftUIKitView/UIViewContainer.swift +++ b/Sources/SwiftUIKitView/UIViewContainer.swift @@ -9,48 +9,76 @@ import Foundation import UIKit 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 { //}: Identifiable { - -// public var id: UIView { view } +public class UIViewContainingCoordinator { + private(set) var view: Child? + private var viewCreator: () -> Child - /// The type of Layout to apply to the SwiftUI `View`. - public enum Layout { + var widthConstraint: NSLayoutConstraint? + var heightConstraint: NSLayoutConstraint? + private let layout: UIViewContainer.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) + init(_ viewCreator: @escaping () -> Child, layout: UIViewContainer.Layout) { + self.viewCreator = viewCreator + self.layout = layout } - - var view: Child! { - get { coordinator.view } + + func createView() { + guard view == nil else { return } + view = viewCreator() + view?.translatesAutoresizingMaskIntoConstraints = false + } + + func updateSize(for view: Child) { + switch layout { + case .intrinsic: + let size = self.size(for: view) + update(view: view, width: size.width, height: size.height) + case .fixed(let size): + update(view: view, width: size.width, height: size.height) + case .fixedWidth(let width): + update(view: view, width: width, height: nil) + } + view.setNeedsLayout() + view.layoutIfNeeded() + view.setNeedsUpdateConstraints() + view.updateConstraints() + } + + private func update(view: Child, width: CGFloat?, height: CGFloat?) { + if let width = width { + if let widthConstraint = widthConstraint { + widthConstraint.constant = width + } else { + widthConstraint = view.widthAnchor.constraint(equalToConstant: width) + widthConstraint?.isActive = true + } + } + if let height = height { + if let heightConstraint = heightConstraint { + heightConstraint.constant = height + } else { + heightConstraint = view.heightAnchor.constraint(equalToConstant: height) + heightConstraint?.isActive = true + } + } } - private let viewCreator: () -> Child - private let layout: Layout - private let coordinator: Coordinator /// - Returns: The `CGSize` to apply to the view. - private var size: CGSize { + func size(for view: Child) -> 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): @@ -59,85 +87,124 @@ public struct UIViewContainer { //}: Identifiable { return view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) } } +} + +/// 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 { //}: Identifiable { + +// public var id: UIView { view } + + /// The type of Layout to apply to the SwiftUI `View`. + public enum 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) + } + + @State public var coordinator: UIViewContainingCoordinator /// Initializes a `UIViewContainer` /// - Parameters: /// - view: `UIView` being previewed /// - layout: The layout to apply on the `UIView`. Defaults to `intrinsic`. - public init(_ view: @escaping @autoclosure () -> Child, layout: Layout = .intrinsic) { - self.viewCreator = view - self.layout = layout - self.coordinator = Coordinator(self.viewCreator) + public init(_ viewCreator: @escaping @autoclosure () -> Child, layout: Layout = .intrinsic) { + self.coordinator = UIViewContainingCoordinator(viewCreator, layout: layout) + } +} + +// MARK: Preview + UIViewRepresentable + +@available(iOS 13, *) +extension UIViewContainer: UIViewRepresentable { + public func makeCoordinator() -> UIViewContainingCoordinator { + // Create an instance of Coordinator + coordinator + } + + public func makeUIView(context: Context) -> Child { + context.coordinator.createView() + return context.coordinator.view! } + public func updateUIView(_ view: Child, context: Context) { + update(view) + view.setContentHuggingPriority(.defaultHigh, for: .vertical) + view.setContentHuggingPriority(.defaultHigh, for: .horizontal) + } + + public func update(_ uiView: Child) { + coordinator.updateSize(for: uiView) + } +} + +public protocol UIViewContaining: UIViewRepresentable { + associatedtype Child: UIView + var coordinator: UIViewContainingCoordinator { get } + func set(_ keyPath: ReferenceWritableKeyPath, to value: Value) -> ModifiedUIViewContainer + func update(_ uiView: Child) +} + +extension UIViewContaining { + public func set(_ keyPath: ReferenceWritableKeyPath, to value: Value) -> ModifiedUIViewContainer { + ModifiedUIViewContainer(child: self, keyPath: keyPath, value: value) + } + /// 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) +// public func fixedSizing() -> some View { +// if let view = coordinator.view { +// update(coordinator.view!) +// let size = coordinator.size(for: view) +// return self.frame(width: size.width, height: size.height, alignment: .topLeading) +// } else { +// return self +// } // } - + +} + +public extension View { /// 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 { + func preview(displayName: String? = nil) -> some View { return fixedSize() .previewLayout(.sizeThatFits) .previewDisplayName(displayName) } - - public class Coordinator { - var view: Child! - var viewCreator: () -> Child - var modifiers: [(Child) -> Void] = [] - - init(_ viewCreator: @escaping () -> Child) { - self.viewCreator = viewCreator - } - } } -// MARK: Preview + UIViewRepresentable +extension UIViewContainer: UIViewContaining { } +public struct ModifiedUIViewContainer: UIViewContaining where ChildContainer.Child == Child { + var child: ChildContainer + public var coordinator: UIViewContainingCoordinator { + child.coordinator + } + @State var keyPath: ReferenceWritableKeyPath + @State var value: Value -@available(iOS 13, *) -extension UIViewContainer: UIViewRepresentable { - public func makeCoordinator() -> Coordinator { - // Create an instance of Coordinator + public func makeCoordinator() -> UIViewContainingCoordinator { coordinator } public func makeUIView(context: Context) -> Child { - self.coordinator.view = viewCreator() - coordinator.modifiers.forEach { modifier in - modifier(self.coordinator.view) - } - switch layout { - case .intrinsic: - return view - 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 - } - return view + coordinator.createView() + return coordinator.view! } - - public func updateUIView(_ view: Child, context: Context) { - coordinator.modifiers.forEach { modifier in - modifier(view) - } + + public func updateUIView(_ uiView: Child, context: Context) { + update(uiView) } -} -@available(iOS 13.0, *) -extension UIViewContainer: KeyPathReferenceWritable { - public typealias T = Child - - public func set(_ keyPath: ReferenceWritableKeyPath, to value: Value) -> Self { - coordinator.modifiers.append({ view in - view[keyPath: keyPath] = value - }) - print("Modifiers is now \(coordinator.modifiers)") - return self + public func update(_ uiView: Child) { + uiView[keyPath: keyPath] = value + child.update(uiView) + coordinator.updateSize(for: uiView) } } From f6d253c1bde630bdf3b8b146f29d99b44c5d5db9 Mon Sep 17 00:00:00 2001 From: Antoine van der Lee Date: Sat, 23 Jul 2022 20:05:05 +0200 Subject: [PATCH 4/7] Start of iteration 4 --- .../SwiftUIwithUIKitView.swift | 6 +- .../SwiftUIKitView/IntrinsicContentView.swift | 51 ++++++++++ .../SwiftUIViewConvertable.swift | 4 +- Sources/SwiftUIKitView/UIViewContainer.swift | 94 ++++++++++--------- 4 files changed, 108 insertions(+), 47 deletions(-) create mode 100644 Sources/SwiftUIKitView/IntrinsicContentView.swift diff --git a/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift b/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift index 720ac48..37570a8 100644 --- a/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift +++ b/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift @@ -18,9 +18,9 @@ struct SwiftUIwithUIKitView: View { // $0.text = "Hello no \(self.integer) from UIKit" // } // .fixedSize() - UIViewContainer(UILabel(), layout: .intrinsic) - .set(\.text, to: "Hello, UIKit \(integer)!") - .set(\.backgroundColor, to: UIColor(named: "swiftlee_orange")) +// UIViewContainer(UILabel(), layout: .intrinsic) +// .set(\.text, to: "Hello, UIKit \(integer)!") +// .set(\.backgroundColor, to: UIColor(named: "swiftlee_orange")) // .fixedSize() UIViewContainer(UIKitView(), layout: .intrinsic) .set(\.title, to: "Hello, UIKit \(integer)!") diff --git a/Sources/SwiftUIKitView/IntrinsicContentView.swift b/Sources/SwiftUIKitView/IntrinsicContentView.swift new file mode 100644 index 0000000..472725f --- /dev/null +++ b/Sources/SwiftUIKitView/IntrinsicContentView.swift @@ -0,0 +1,51 @@ +// +// IntrinsicContentView.swift +// +// +// Created by Antoine van der Lee on 23/07/2022. +// + +import Foundation +import UIKit + +final class IntrinsicContentView: UIView { + var contentView: ContentView + let layout: Layout + + init(contentView: ContentView, layout: Layout) { + self.contentView = contentView + self.layout = layout + + super.init(frame: .zero) + backgroundColor = .clear + addSubview(contentView) + } + + @available(*, unavailable) required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var contentHeight: CGFloat = .zero { + didSet { invalidateIntrinsicContentSize() } + } + + override var intrinsicContentSize: CGSize { + .init(width: UIView.noIntrinsicMetric, height: contentHeight) + } + + override var frame: CGRect { + didSet { + guard frame != oldValue else { return } + + contentView.frame = self.bounds + contentView.layoutIfNeeded() + + let targetSize = CGSize(width: frame.width, height: UIView.layoutFittingCompressedSize.height) + + contentHeight = contentView.systemLayoutSizeFitting( + targetSize, + withHorizontalFittingPriority: .required, + verticalFittingPriority: .fittingSizeLevel).height + } + } +} diff --git a/Sources/SwiftUIKitView/SwiftUIViewConvertable.swift b/Sources/SwiftUIKitView/SwiftUIViewConvertable.swift index 152b47f..42df907 100644 --- a/Sources/SwiftUIKitView/SwiftUIViewConvertable.swift +++ b/Sources/SwiftUIKitView/SwiftUIViewConvertable.swift @@ -12,7 +12,7 @@ import UIKit @available(iOS 13.0, *) public protocol SwiftUIViewConvertable { associatedtype View: UIView - func swiftUIView(layout: UIViewContainer.Layout) -> UIViewContainer + func swiftUIView(layout: Layout) -> UIViewContainer } /// Add default protocol comformance for `UIView` instances. @@ -20,7 +20,7 @@ extension UIView: SwiftUIViewConvertable {} @available(iOS 13.0, *) public extension SwiftUIViewConvertable where Self: UIView { - func swiftUIView(layout: UIViewContainer.Layout) -> UIViewContainer { + func swiftUIView(layout: Layout) -> UIViewContainer { return UIViewContainer(self, layout: layout) } } diff --git a/Sources/SwiftUIKitView/UIViewContainer.swift b/Sources/SwiftUIKitView/UIViewContainer.swift index a9cfb6a..fbc8b35 100644 --- a/Sources/SwiftUIKitView/UIViewContainer.swift +++ b/Sources/SwiftUIKitView/UIViewContainer.swift @@ -9,15 +9,28 @@ import Foundation import UIKit import SwiftUI +/// The type of Layout to apply to the SwiftUI `View`. +public enum 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) +} + public class UIViewContainingCoordinator { private(set) var view: Child? private var viewCreator: () -> Child var widthConstraint: NSLayoutConstraint? var heightConstraint: NSLayoutConstraint? - private let layout: UIViewContainer.Layout + private let layout: Layout - init(_ viewCreator: @escaping () -> Child, layout: UIViewContainer.Layout) { + init(_ viewCreator: @escaping () -> Child, layout: Layout) { self.viewCreator = viewCreator self.layout = layout } @@ -31,17 +44,18 @@ public class UIViewContainingCoordinator { func updateSize(for view: Child) { switch layout { case .intrinsic: - let size = self.size(for: view) - update(view: view, width: size.width, height: size.height) +// let size = self.size(for: view) +// update(view: view, width: size.width, height: size.height) + break case .fixed(let size): update(view: view, width: size.width, height: size.height) case .fixedWidth(let width): update(view: view, width: width, height: nil) } - view.setNeedsLayout() - view.layoutIfNeeded() - view.setNeedsUpdateConstraints() - view.updateConstraints() +// view.setNeedsLayout() +// view.layoutIfNeeded() +// view.setNeedsUpdateConstraints() +// view.updateConstraints() } private func update(view: Child, width: CGFloat?, height: CGFloat?) { @@ -91,31 +105,18 @@ public class UIViewContainingCoordinator { /// 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 { //}: Identifiable { - -// public var id: UIView { view } +public struct UIViewContainer { - /// 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) - } - - @State public var coordinator: UIViewContainingCoordinator - /// Initializes a `UIViewContainer` /// - Parameters: /// - view: `UIView` being previewed /// - layout: The layout to apply on the `UIView`. Defaults to `intrinsic`. public init(_ viewCreator: @escaping @autoclosure () -> Child, layout: Layout = .intrinsic) { - self.coordinator = UIViewContainingCoordinator(viewCreator, layout: layout) + self.viewCreator = viewCreator + self.layout = layout } } @@ -125,30 +126,34 @@ public struct UIViewContainer { //}: Identifiable { extension UIViewContainer: UIViewRepresentable { public func makeCoordinator() -> UIViewContainingCoordinator { // Create an instance of Coordinator - coordinator + Coordinator(viewCreator, layout: layout) } public func makeUIView(context: Context) -> Child { + print("MAKE VIEW") context.coordinator.createView() return context.coordinator.view! } public func updateUIView(_ view: Child, context: Context) { - update(view) - view.setContentHuggingPriority(.defaultHigh, for: .vertical) - view.setContentHuggingPriority(.defaultHigh, for: .horizontal) + print("UPDATE VIEW") + update(view, coordinator: context.coordinator) + } - public func update(_ uiView: Child) { + public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator) { + print("UPDATE UIViewContainer") + uiView.setContentHuggingPriority(.defaultHigh, for: .vertical) + uiView.setContentHuggingPriority(.defaultHigh, for: .horizontal) coordinator.updateSize(for: uiView) + print(uiView.intrinsicContentSize) } } public protocol UIViewContaining: UIViewRepresentable { associatedtype Child: UIView - var coordinator: UIViewContainingCoordinator { get } func set(_ keyPath: ReferenceWritableKeyPath, to value: Value) -> ModifiedUIViewContainer - func update(_ uiView: Child) + func update(_ uiView: Child, coordinator: UIViewContainingCoordinator) } extension UIViewContaining { @@ -156,6 +161,12 @@ extension UIViewContaining { ModifiedUIViewContainer(child: self, keyPath: keyPath, value: value) } +// public func fixedSize() -> some View +// { +// print("FIXED SIZE") +// return self.background(Color.blue) +// } + /// Applies the correct size to the SwiftUI `View` container. /// - Returns: A `View` with the correct size applied. // public func fixedSizing() -> some View { @@ -183,28 +194,27 @@ public extension View { extension UIViewContainer: UIViewContaining { } public struct ModifiedUIViewContainer: UIViewContaining where ChildContainer.Child == Child { var child: ChildContainer - public var coordinator: UIViewContainingCoordinator { - child.coordinator - } + @State var keyPath: ReferenceWritableKeyPath @State var value: Value public func makeCoordinator() -> UIViewContainingCoordinator { - coordinator + child.makeCoordinator() as! UIViewContainingCoordinator } public func makeUIView(context: Context) -> Child { - coordinator.createView() - return coordinator.view! + context.coordinator.createView() + return context.coordinator.view! } public func updateUIView(_ uiView: Child, context: Context) { - update(uiView) + update(uiView, coordinator: context.coordinator) } - public func update(_ uiView: Child) { + public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator) { + print("UPDATE Modified \(keyPath)") uiView[keyPath: keyPath] = value - child.update(uiView) + child.update(uiView, coordinator: coordinator) coordinator.updateSize(for: uiView) } } From f15d620bad202d6c2d0a98d9d454c503cad10830 Mon Sep 17 00:00:00 2001 From: Antoine van der Lee Date: Sat, 23 Jul 2022 20:06:54 +0200 Subject: [PATCH 5/7] Create IntrinsicContentView --- .../SwiftUIKitView/IntrinsicContentView.swift | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftUIKitView/IntrinsicContentView.swift b/Sources/SwiftUIKitView/IntrinsicContentView.swift index 472725f..849c19e 100644 --- a/Sources/SwiftUIKitView/IntrinsicContentView.swift +++ b/Sources/SwiftUIKitView/IntrinsicContentView.swift @@ -9,7 +9,7 @@ import Foundation import UIKit final class IntrinsicContentView: UIView { - var contentView: ContentView + let contentView: ContentView let layout: Layout init(contentView: ContentView, layout: Layout) { @@ -25,12 +25,19 @@ final class IntrinsicContentView: UIView { fatalError("init(coder:) has not been implemented") } - private var contentHeight: CGFloat = .zero { + private var contentSize: CGSize = .zero { didSet { invalidateIntrinsicContentSize() } } override var intrinsicContentSize: CGSize { - .init(width: UIView.noIntrinsicMetric, height: contentHeight) + switch layout { + case .intrinsic: + return contentSize + case .fixedWidth(let width): + return .init(width: width, height: contentSize.height) + case .fixed(let size): + return size + } } override var frame: CGRect { @@ -42,10 +49,10 @@ final class IntrinsicContentView: UIView { let targetSize = CGSize(width: frame.width, height: UIView.layoutFittingCompressedSize.height) - contentHeight = contentView.systemLayoutSizeFitting( + contentSize = contentView.systemLayoutSizeFitting( targetSize, withHorizontalFittingPriority: .required, - verticalFittingPriority: .fittingSizeLevel).height + verticalFittingPriority: .fittingSizeLevel) } } } From 12e61a1b018f856170001c604761e060fc1d4207 Mon Sep 17 00:00:00 2001 From: Antoine van der Lee Date: Sat, 23 Jul 2022 20:37:22 +0200 Subject: [PATCH 6/7] Fixed content size --- .../SwiftUIKitView/IntrinsicContentView.swift | 77 ++++++++++++++++--- Sources/SwiftUIKitView/UIViewContainer.swift | 50 ++++++------ 2 files changed, 89 insertions(+), 38 deletions(-) diff --git a/Sources/SwiftUIKitView/IntrinsicContentView.swift b/Sources/SwiftUIKitView/IntrinsicContentView.swift index 849c19e..ee4706f 100644 --- a/Sources/SwiftUIKitView/IntrinsicContentView.swift +++ b/Sources/SwiftUIKitView/IntrinsicContentView.swift @@ -8,7 +8,7 @@ import Foundation import UIKit -final class IntrinsicContentView: UIView { +public final class IntrinsicContentView: UIView { let contentView: ContentView let layout: Layout @@ -19,6 +19,7 @@ final class IntrinsicContentView: UIView { super.init(frame: .zero) backgroundColor = .clear addSubview(contentView) + clipsToBounds = true } @available(*, unavailable) required init?(coder _: NSCoder) { @@ -26,10 +27,13 @@ final class IntrinsicContentView: UIView { } private var contentSize: CGSize = .zero { - didSet { invalidateIntrinsicContentSize() } + didSet { + invalidateIntrinsicContentSize() + print(#function) + } } - override var intrinsicContentSize: CGSize { + public override var intrinsicContentSize: CGSize { switch layout { case .intrinsic: return contentSize @@ -40,19 +44,68 @@ final class IntrinsicContentView: UIView { } } - override var frame: CGRect { - didSet { - guard frame != oldValue else { return } + public func updateContentSize() { + print(#function) + 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 - contentView.frame = self.bounds + // Make sure the contents of the cell have the correct layout. + contentView.setNeedsLayout() contentView.layoutIfNeeded() - let targetSize = CGSize(width: frame.width, height: UIView.layoutFittingCompressedSize.height) + // 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) + } + } + + public override var frame: CGRect { + didSet { + guard frame != oldValue || contentSize == .zero else { + return + } - contentSize = contentView.systemLayoutSizeFitting( - targetSize, - withHorizontalFittingPriority: .required, - verticalFittingPriority: .fittingSizeLevel) +// contentView.frame = self.bounds +// contentView.layoutIfNeeded() +// +// let targetSize = CGSize(width: frame.width, height: UIView.layoutFittingCompressedSize.height) +// +// contentSize = contentView.systemLayoutSizeFitting( +// targetSize, +// withHorizontalFittingPriority: .required, +// verticalFittingPriority: .fittingSizeLevel) + +// 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) +// } } } } diff --git a/Sources/SwiftUIKitView/UIViewContainer.swift b/Sources/SwiftUIKitView/UIViewContainer.swift index fbc8b35..631a8af 100644 --- a/Sources/SwiftUIKitView/UIViewContainer.swift +++ b/Sources/SwiftUIKitView/UIViewContainer.swift @@ -23,7 +23,7 @@ public enum Layout { } public class UIViewContainingCoordinator { - private(set) var view: Child? + private(set) var view: IntrinsicContentView? private var viewCreator: () -> Child var widthConstraint: NSLayoutConstraint? @@ -37,25 +37,19 @@ public class UIViewContainingCoordinator { func createView() { guard view == nil else { return } - view = viewCreator() - view?.translatesAutoresizingMaskIntoConstraints = false + let contentView = viewCreator() + view = IntrinsicContentView(contentView: contentView, layout: layout) } func updateSize(for view: Child) { - switch layout { - case .intrinsic: -// let size = self.size(for: view) +// switch layout { +// case .intrinsic: +// break +// case .fixed(let size): // update(view: view, width: size.width, height: size.height) - break - case .fixed(let size): - update(view: view, width: size.width, height: size.height) - case .fixedWidth(let width): - update(view: view, width: width, height: nil) - } -// view.setNeedsLayout() -// view.layoutIfNeeded() -// view.setNeedsUpdateConstraints() -// view.updateConstraints() +// case .fixedWidth(let width): +// update(view: view, width: width, height: nil) +// } } private func update(view: Child, width: CGFloat?, height: CGFloat?) { @@ -129,19 +123,19 @@ extension UIViewContainer: UIViewRepresentable { Coordinator(viewCreator, layout: layout) } - public func makeUIView(context: Context) -> Child { + public func makeUIView(context: Context) -> IntrinsicContentView { print("MAKE VIEW") context.coordinator.createView() return context.coordinator.view! } - public func updateUIView(_ view: Child, context: Context) { + public func updateUIView(_ view: IntrinsicContentView, context: Context) { print("UPDATE VIEW") - update(view, coordinator: context.coordinator) + update(view.contentView, coordinator: context.coordinator, updateContentSize: true) } - public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator) { + public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator, updateContentSize: Bool) { print("UPDATE UIViewContainer") uiView.setContentHuggingPriority(.defaultHigh, for: .vertical) uiView.setContentHuggingPriority(.defaultHigh, for: .horizontal) @@ -153,7 +147,7 @@ extension UIViewContainer: UIViewRepresentable { public protocol UIViewContaining: UIViewRepresentable { associatedtype Child: UIView func set(_ keyPath: ReferenceWritableKeyPath, to value: Value) -> ModifiedUIViewContainer - func update(_ uiView: Child, coordinator: UIViewContainingCoordinator) + func update(_ uiView: Child, coordinator: UIViewContainingCoordinator, updateContentSize: Bool) } extension UIViewContaining { @@ -202,19 +196,23 @@ public struct ModifiedUIViewContainer } - public func makeUIView(context: Context) -> Child { + public func makeUIView(context: Context) -> IntrinsicContentView { context.coordinator.createView() return context.coordinator.view! } - public func updateUIView(_ uiView: Child, context: Context) { - update(uiView, coordinator: context.coordinator) + public func updateUIView(_ uiView: IntrinsicContentView, context: Context) { + update(uiView.contentView, coordinator: context.coordinator, updateContentSize: true) } - public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator) { + public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator, updateContentSize: Bool) { print("UPDATE Modified \(keyPath)") uiView[keyPath: keyPath] = value - child.update(uiView, coordinator: coordinator) + child.update(uiView, coordinator: coordinator, updateContentSize: false) coordinator.updateSize(for: uiView) + + if updateContentSize { + coordinator.view?.updateContentSize() + } } } From 598f973ac7508bc6acb040089f430acbd9f827b5 Mon Sep 17 00:00:00 2001 From: Antoine van der Lee Date: Sat, 23 Jul 2022 21:05:47 +0200 Subject: [PATCH 7/7] Update readme, clean up code --- .../SwiftUIwithUIKitView.swift | 60 +----- Package.swift | 9 +- README.md | 27 ++- .../SwiftUIKitView/IntrinsicContentView.swift | 42 ----- .../ModifiedUIViewContainer.swift | 39 ++++ .../SwiftUIViewConvertable.swift | 4 + Sources/SwiftUIKitView/UIViewContainer.swift | 171 +----------------- Sources/SwiftUIKitView/UIViewContaining.swift | 32 ++++ .../UIViewContainingCoordinator.swift | 47 +++++ 9 files changed, 149 insertions(+), 282 deletions(-) create mode 100644 Sources/SwiftUIKitView/ModifiedUIViewContainer.swift create mode 100644 Sources/SwiftUIKitView/UIViewContaining.swift create mode 100644 Sources/SwiftUIKitView/UIViewContainingCoordinator.swift diff --git a/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift b/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift index 37570a8..b2815a1 100644 --- a/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift +++ b/Example/SwiftUIKitExample/SwiftUIwithUIKitView.swift @@ -14,20 +14,13 @@ struct SwiftUIwithUIKitView: View { var body: some View { NavigationView { VStack { -// UIViewMaker { -// $0.text = "Hello no \(self.integer) from UIKit" -// } -// .fixedSize() -// UIViewContainer(UILabel(), layout: .intrinsic) -// .set(\.text, to: "Hello, UIKit \(integer)!") -// .set(\.backgroundColor, to: UIColor(named: "swiftlee_orange")) -// .fixedSize() + // Use UIKit inside SwiftUI like this: UIViewContainer(UIKitView(), layout: .intrinsic) .set(\.title, to: "Hello, UIKit \(integer)!") .set(\.backgroundColor, to: UIColor(named: "swiftlee_orange")) .fixedSize() -// .fixedSize() .navigationTitle("Use UIKit in SwiftUI") + Button("RANDOMIZED: \(integer)") { integer = Int.random(in: 0..<300) } @@ -53,52 +46,3 @@ struct UILabelExample_Preview: PreviewProvider { .previewDisplayName("UILabel Preview Example") } } - -struct UIViewMaker: UIViewRepresentable { - - typealias UIViewType = ViewType - - var make: () -> ViewType = ViewType.init - var update: (ViewType) -> () - - func makeUIView(context: Context) -> ViewType { - return make() - } - - func updateUIView(_ uiView: ViewType, context: Context) { - update(uiView) - } -} - -struct ContentView: View { - @State var counter = 0 - - var body: some View { - VStack { - - Text("Hello no \(counter) from SwiftUI") - .padding() - - - UIViewMaker { - $0.text = "Hello no \(self.counter) from UIKit" - } - .fixedSize() - - } - .onAppear { - if counter == 0 { - schedule() - } - } - } - - func schedule() { - counter += 1 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.schedule() - } - } - -} - diff --git a/Package.swift b/Package.swift index ebc3afe..ffaa154 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// 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 @@ -12,18 +12,11 @@ let package = Package( .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: []), diff --git a/README.md b/README.md index 36b8008..6b027a8 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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(, layout: ) +``` + +This is to prevent a UIKit view from being redrawn on every SwiftUI view redraw. ```swift import SwiftUI @@ -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() @@ -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 @@ -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")) ] ``` diff --git a/Sources/SwiftUIKitView/IntrinsicContentView.swift b/Sources/SwiftUIKitView/IntrinsicContentView.swift index ee4706f..c3db762 100644 --- a/Sources/SwiftUIKitView/IntrinsicContentView.swift +++ b/Sources/SwiftUIKitView/IntrinsicContentView.swift @@ -29,7 +29,6 @@ public final class IntrinsicContentView: UIView { private var contentSize: CGSize = .zero { didSet { invalidateIntrinsicContentSize() - print(#function) } } @@ -45,7 +44,6 @@ public final class IntrinsicContentView: UIView { } public func updateContentSize() { - print(#function) switch layout { case .fixedWidth(let width): // Set the frame of the cell, so that the layout can be updated. @@ -68,44 +66,4 @@ public final class IntrinsicContentView: UIView { contentSize = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) } } - - public override var frame: CGRect { - didSet { - guard frame != oldValue || contentSize == .zero else { - return - } - -// contentView.frame = self.bounds -// contentView.layoutIfNeeded() -// -// let targetSize = CGSize(width: frame.width, height: UIView.layoutFittingCompressedSize.height) -// -// contentSize = contentView.systemLayoutSizeFitting( -// targetSize, -// withHorizontalFittingPriority: .required, -// verticalFittingPriority: .fittingSizeLevel) - -// 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) -// } - } - } } diff --git a/Sources/SwiftUIKitView/ModifiedUIViewContainer.swift b/Sources/SwiftUIKitView/ModifiedUIViewContainer.swift new file mode 100644 index 0000000..e3532fd --- /dev/null +++ b/Sources/SwiftUIKitView/ModifiedUIViewContainer.swift @@ -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: UIViewContaining where ChildContainer.Child == Child { + + let child: ChildContainer + let keyPath: ReferenceWritableKeyPath + let value: Value + + public func makeCoordinator() -> UIViewContainingCoordinator { + child.makeCoordinator() as! UIViewContainingCoordinator + } + + public func makeUIView(context: Context) -> IntrinsicContentView { + context.coordinator.createView() + } + + public func updateUIView(_ uiView: IntrinsicContentView, context: Context) { + update(uiView.contentView, coordinator: context.coordinator, updateContentSize: true) + } + + public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator, updateContentSize: Bool) { + uiView[keyPath: keyPath] = value + child.update(uiView, coordinator: coordinator, updateContentSize: false) + + if updateContentSize { + coordinator.view?.updateContentSize() + } + } +} + diff --git a/Sources/SwiftUIKitView/SwiftUIViewConvertable.swift b/Sources/SwiftUIKitView/SwiftUIViewConvertable.swift index 42df907..179d859 100644 --- a/Sources/SwiftUIKitView/SwiftUIViewConvertable.swift +++ b/Sources/SwiftUIKitView/SwiftUIViewConvertable.swift @@ -21,6 +21,10 @@ extension UIView: SwiftUIViewConvertable {} @available(iOS 13.0, *) public extension SwiftUIViewConvertable where Self: UIView { func swiftUIView(layout: Layout) -> UIViewContainer { + 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(, layout: layout)` instead." + ) return UIViewContainer(self, layout: layout) } } diff --git a/Sources/SwiftUIKitView/UIViewContainer.swift b/Sources/SwiftUIKitView/UIViewContainer.swift index 631a8af..8693002 100644 --- a/Sources/SwiftUIKitView/UIViewContainer.swift +++ b/Sources/SwiftUIKitView/UIViewContainer.swift @@ -9,94 +9,6 @@ import Foundation import UIKit import SwiftUI -/// The type of Layout to apply to the SwiftUI `View`. -public enum 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) -} - -public class UIViewContainingCoordinator { - private(set) var view: IntrinsicContentView? - private var viewCreator: () -> Child - - var widthConstraint: NSLayoutConstraint? - var heightConstraint: NSLayoutConstraint? - private let layout: Layout - - init(_ viewCreator: @escaping () -> Child, layout: Layout) { - self.viewCreator = viewCreator - self.layout = layout - } - - func createView() { - guard view == nil else { return } - let contentView = viewCreator() - view = IntrinsicContentView(contentView: contentView, layout: layout) - } - - func updateSize(for view: Child) { -// switch layout { -// case .intrinsic: -// break -// case .fixed(let size): -// update(view: view, width: size.width, height: size.height) -// case .fixedWidth(let width): -// update(view: view, width: width, height: nil) -// } - } - - private func update(view: Child, width: CGFloat?, height: CGFloat?) { - if let width = width { - if let widthConstraint = widthConstraint { - widthConstraint.constant = width - } else { - widthConstraint = view.widthAnchor.constraint(equalToConstant: width) - widthConstraint?.isActive = true - } - } - if let height = height { - if let heightConstraint = heightConstraint { - heightConstraint.constant = height - } else { - heightConstraint = view.heightAnchor.constraint(equalToConstant: height) - heightConstraint?.isActive = true - } - } - } - - /// - Returns: The `CGSize` to apply to the view. - func size(for view: Child) -> 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) - } - } -} - /// 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 { @@ -124,95 +36,18 @@ extension UIViewContainer: UIViewRepresentable { } public func makeUIView(context: Context) -> IntrinsicContentView { - print("MAKE VIEW") context.coordinator.createView() - return context.coordinator.view! } public func updateUIView(_ view: IntrinsicContentView, context: Context) { - print("UPDATE VIEW") update(view.contentView, coordinator: context.coordinator, updateContentSize: true) } - - public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator, updateContentSize: Bool) { - print("UPDATE UIViewContainer") - uiView.setContentHuggingPriority(.defaultHigh, for: .vertical) - uiView.setContentHuggingPriority(.defaultHigh, for: .horizontal) - coordinator.updateSize(for: uiView) - print(uiView.intrinsicContentSize) - } -} - -public protocol UIViewContaining: UIViewRepresentable { - associatedtype Child: UIView - func set(_ keyPath: ReferenceWritableKeyPath, to value: Value) -> ModifiedUIViewContainer - func update(_ uiView: Child, coordinator: UIViewContainingCoordinator, updateContentSize: Bool) -} - -extension UIViewContaining { - public func set(_ keyPath: ReferenceWritableKeyPath, to value: Value) -> ModifiedUIViewContainer { - ModifiedUIViewContainer(child: self, keyPath: keyPath, value: value) - } - -// public func fixedSize() -> some View -// { -// print("FIXED SIZE") -// return self.background(Color.blue) -// } - - /// Applies the correct size to the SwiftUI `View` container. - /// - Returns: A `View` with the correct size applied. -// public func fixedSizing() -> some View { -// if let view = coordinator.view { -// update(coordinator.view!) -// let size = coordinator.size(for: view) -// return self.frame(width: size.width, height: size.height, alignment: .topLeading) -// } else { -// return self -// } -// } - -} - -public extension View { - /// Creates a preview of the `UIViewContainer` with the right size applied. - /// - Returns: A preview of the container. - func preview(displayName: String? = nil) -> some View { - return fixedSize() - .previewLayout(.sizeThatFits) - .previewDisplayName(displayName) - } } -extension UIViewContainer: UIViewContaining { } -public struct ModifiedUIViewContainer: UIViewContaining where ChildContainer.Child == Child { - var child: ChildContainer - - @State var keyPath: ReferenceWritableKeyPath - @State var value: Value - - public func makeCoordinator() -> UIViewContainingCoordinator { - child.makeCoordinator() as! UIViewContainingCoordinator - } - - public func makeUIView(context: Context) -> IntrinsicContentView { - context.coordinator.createView() - return context.coordinator.view! - } - - public func updateUIView(_ uiView: IntrinsicContentView, context: Context) { - update(uiView.contentView, coordinator: context.coordinator, updateContentSize: true) - } - +extension UIViewContainer: UIViewContaining { public func update(_ uiView: Child, coordinator: UIViewContainingCoordinator, updateContentSize: Bool) { - print("UPDATE Modified \(keyPath)") - uiView[keyPath: keyPath] = value - child.update(uiView, coordinator: coordinator, updateContentSize: false) - coordinator.updateSize(for: uiView) - - if updateContentSize { - coordinator.view?.updateContentSize() - } + guard updateContentSize else { return } + coordinator.view?.updateContentSize() } } diff --git a/Sources/SwiftUIKitView/UIViewContaining.swift b/Sources/SwiftUIKitView/UIViewContaining.swift new file mode 100644 index 0000000..c9071bf --- /dev/null +++ b/Sources/SwiftUIKitView/UIViewContaining.swift @@ -0,0 +1,32 @@ +// +// UIViewContaining.swift +// +// +// Created by Antoine van der Lee on 23/07/2022. +// + +import Foundation +import UIKit +import SwiftUI + +public protocol UIViewContaining: UIViewRepresentable { + associatedtype Child: UIView + func set(_ keyPath: ReferenceWritableKeyPath, to value: Value) -> ModifiedUIViewContainer + func update(_ uiView: Child, coordinator: UIViewContainingCoordinator, updateContentSize: Bool) +} + +extension UIViewContaining { + public func set(_ keyPath: ReferenceWritableKeyPath, to value: Value) -> ModifiedUIViewContainer { + ModifiedUIViewContainer(child: self, keyPath: keyPath, value: value) + } +} + +public extension View { + /// Creates a preview of the `UIViewContainer` with the right size applied. + /// - Returns: A preview of the container. + func preview(displayName: String? = nil) -> some View { + return fixedSize() + .previewLayout(.sizeThatFits) + .previewDisplayName(displayName) + } +} diff --git a/Sources/SwiftUIKitView/UIViewContainingCoordinator.swift b/Sources/SwiftUIKitView/UIViewContainingCoordinator.swift new file mode 100644 index 0000000..cc75276 --- /dev/null +++ b/Sources/SwiftUIKitView/UIViewContainingCoordinator.swift @@ -0,0 +1,47 @@ +// +// UIViewContainingCoordinator.swift +// +// +// Created by Antoine van der Lee on 23/07/2022. +// + +import Foundation +import UIKit +import SwiftUI + +/// The type of Layout to apply to the SwiftUI `View`. +public enum 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) +} + +public class UIViewContainingCoordinator { + private(set) var view: IntrinsicContentView? + private var viewCreator: () -> Child + + var widthConstraint: NSLayoutConstraint? + var heightConstraint: NSLayoutConstraint? + private let layout: Layout + + init(_ viewCreator: @escaping () -> Child, layout: Layout) { + self.viewCreator = viewCreator + self.layout = layout + } + + func createView() -> IntrinsicContentView { + if let view = view { + return view + } else { + let contentView = IntrinsicContentView(contentView: viewCreator(), layout: layout) + view = contentView + return contentView + } + } +}