Skip to content

Instantly share code, notes, and snippets.

@ipedro
Last active August 18, 2022 23:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ipedro/7531b2513eff84ce7610927ecfa0a898 to your computer and use it in GitHub Desktop.
Save ipedro/7531b2513eff84ce7610927ecfa0a898 to your computer and use it in GitHub Desktop.
UIKit -> SwiftUI
import SwiftUI
// MARK: - UIKit.UIView -> SwiftUI.View
public extension NSObjectProtocol where Self: UIView {
/// Creates a wrapper for a UIKit view that you use to integrate that view into your
/// SwiftUI view hierarchy.
///
/// For best results set sensible values for [content compression resistance](https://developer.apple.com/documentation/uikit/uiview/1622526-setcontentcompressionresistancep),
/// and [content hugging priority](https://developer.apple.com/documentation/uikit/uiview/1622485-setcontenthuggingpriority) in your content view and it's immediate subviews.
///
/// - Warning: Although Auto Layout does its best to give views the space they need
/// based on their intrinsic content sizes, all views also have a content compression
/// resistance priority and a content hugging priority that determine how much it
/// fights to retain its intrinsic content size when available space is less than
/// or greater than it needs, respectively.
///
/// - Note: When the state of your app changes, SwiftUI updates the portions of your
/// interface affected by those changes. The update handler is called for any
/// changes affecting the corresponding UIKit view. Use it to update the size
/// of your view to match the new state information provided in the `context`
/// parameter of the update handler.
///
/// - Parameter layout: Determines the optimal size of the view based on the specified content fitting priorities. Default is **sizeThatFits**.
/// - Parameter onUpdate: (Optional) A closure that gets called by SwiftUI when changes affect
/// the corresponding UIKit view. Default value is **nil**.
/// - Returns: An opaque SwiftUI view.
func asView(
layout: UIViewLayout = .sizeThatFits,
onUpdate: SwiftUIView<Self, Void>.UpdateHandler? = nil
) -> some View {
SwiftUIView(content: self, coordinator: (), layout: layout, onUpdate: onUpdate)
}
/// Creates a wrapper for a UIKit view that you use to integrate that view into your
/// SwiftUI view hierarchy.
///
/// For best results set sensible values for [content compression resistance](https://developer.apple.com/documentation/uikit/uiview/1622526-setcontentcompressionresistancep),
/// and [content hugging priority](https://developer.apple.com/documentation/uikit/uiview/1622485-setcontenthuggingpriority) in your content view and it's immediate subviews.
///
/// - Warning: Although Auto Layout does its best to give views the space they need
/// based on their intrinsic content sizes, all views also have a content compression
/// resistance priority and a content hugging priority that determine how much it
/// fights to retain its intrinsic content size when available space is less than
/// or greater than it needs, respectively.
///
/// - Note: When the state of your app changes, SwiftUI updates the portions of your
/// interface affected by those changes. The update handler is called for any
/// changes affecting the corresponding UIKit view. Use it to update the size
/// of your view to match the new state information provided in the `context`
/// parameter of the update handler.
///
/// - Parameter onUpdate: (Optional) A closure that gets called by SwiftUI when changes affect
/// the corresponding UIKit view. Default value is **nil**.
/// - Parameter coordinator: A type to coordinate with the view.
/// - Parameter layout: Determines the optimal size of the view based on the specified content fitting priorities. Default is **sizeThatFits**.
/// - Returns: An opaque SwiftUI view.
func asView<Coordinator>(
coordinator: Coordinator,
layout: UIViewLayout = .sizeThatFits,
onUpdate: SwiftUIView<Self, Coordinator>.UpdateHandler? = nil
) -> some View {
SwiftUIView(content: self, coordinator: coordinator, layout: layout, onUpdate: onUpdate)
}
/// Convenience method to display a SwiftUI preview for your UIView.
/// - Parameter displayName: (Optional) Sets a user visible name to show in the canvas for a preview. Default value is **nil**.
/// - Parameter layout: (Optional) Overrides the size of the container for the preview. Default value is **.sizeThatFits**.
/// - Returns: An opaque SwiftUI view.
func asPreview(displayName: String? = nil, layout: PreviewLayout = .sizeThatFits) -> some View {
asView()
.previewDisplayName(displayName)
.previewLayout(layout)
}
}
// MARK: - SwiftUI Wrapper View
/// A generic wrapper designed to conveniently use UIKit views into your SwiftUI view hierarchy.
public struct SwiftUIView<Content: UIView, Coordinator>: UIViewRepresentable {
public typealias UpdateHandler = (Content, Context) -> Void
let content: Content
let coordinator: Coordinator
let layout: UIViewLayout
let onUpdate: UpdateHandler?
init(
content: Content,
coordinator: Coordinator,
layout: UIViewLayout,
onUpdate: UpdateHandler?
) {
self.content = content
self.coordinator = coordinator
self.layout = layout
self.onUpdate = onUpdate
}
/// Creates the custom instance that you use to communicate changes from
/// your view to other parts of your SwiftUI interface.
public func makeCoordinator() -> Coordinator {
coordinator
}
/// Creates the view object and configures its initial state.
public func makeUIView(context: Context) -> Container<Content> {
Container(content: content, layout: layout)
}
/// Updates the state of the specified view with new information from
/// SwiftUI.
public func updateUIView(_ container: Container<Content>, context: Context) {
onUpdate?(container.content, context)
}
}
// MARK: - UIView Layout
/// A size constraint for UIKit views displayed in a SwiftUI hierarchy.
public struct UIViewLayout {
/// The size that you prefer for the view.
public var targetSize: CGSize
/// The priority for horizontal constraints. Higher priorities will influence the width to be as close as possible to the width value of targetSize.
public var horizontalPriority: UILayoutPriority
/// The priority for vertical constraints. Higher priorities will influence the height to be as close as possible to the height value of targetSize.
public var verticalPriority: UILayoutPriority
public init(
targetSize: CGSize,
horizontalPriority: UILayoutPriority,
verticalPriority: UILayoutPriority
) {
self.targetSize = targetSize
self.horizontalPriority = horizontalPriority
self.verticalPriority = verticalPriority
}
/// The option to use the smallest possible size.
public static let sizeThatFits = UIViewLayout(
targetSize: UIView.layoutFittingCompressedSize,
horizontalPriority: .defaultHigh,
verticalPriority: .defaultHigh
)
/// The option to use the largest possible size.
public static let expandedSize = UIViewLayout(
targetSize: UIView.layoutFittingExpandedSize,
horizontalPriority: .defaultHigh,
verticalPriority: .defaultHigh
)
/// Center the preview in a container the size of the device on which the
/// preview is running.
public static let device = UIViewLayout(
targetSize: UIScreen.main.bounds.size,
horizontalPriority: .required,
verticalPriority: .required
)
}
// MARK: - SwiftUI Host Container
public extension SwiftUIView {
/// A container designed for presenting a UIKit view in a SwiftUI view hierarchy.
///
/// This wrapper will replicate the view's content [compression resistance](https://developer.apple.com/documentation/uikit/uiview/1622526-setcontentcompressionresistancep), and [hugging](https://developer.apple.com/documentation/uikit/uiview/1622485-setcontenthuggingpriority) priorities, and if the view declares it's own instrinsic content size, use that. If not, then calculates the component size trying to fit to the main screen's width with high compression.
final class Container<Content: UIView>: UIView {
let content: Content
let layout: UIViewLayout
// MARK: Init
init(content: Content, layout: UIViewLayout) {
self.content = content
self.layout = layout
super.init(frame: content.frame)
setup()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
content.translatesAutoresizingMaskIntoConstraints = false
addSubview(content)
leadingAnchor.constraint(equalTo: content.leadingAnchor).isActive = true
topAnchor.constraint(equalTo: content.topAnchor).isActive = true
trailingAnchor.constraint(equalTo: content.trailingAnchor).isActive = true
bottomAnchor.constraint(equalTo: content.bottomAnchor).isActive = true
}
// MARK: Overrides
/// Returns the priority with which the view's content resists being made larger than its intrinsic size.
public override func contentHuggingPriority(for axis: NSLayoutConstraint.Axis) -> UILayoutPriority {
content.contentHuggingPriority(for: axis)
}
/// Returns the priority with which the view's content resists being made smaller than its intrinsic size.
public override func contentCompressionResistancePriority(for axis: NSLayoutConstraint.Axis) -> UILayoutPriority {
content.contentCompressionResistancePriority(for: axis)
}
/// The natural size, considering only properties of the view itself.
///
/// Custom views typically have content that they display of which the layout system
/// is unaware. Setting this property allows a custom view to communicate to the
/// layout system what size it would like to be based on its content.
public override var intrinsicContentSize: CGSize {
let intrinsicContentSize = content.intrinsicContentSize
// If the view has a meaningful value for it's intrinsic size we return it.
if intrinsicContentSize != .noIntrinsicMetric {
return intrinsicContentSize
}
// or else we calculate it based on the main screen width.
return content.systemLayoutSizeFitting(
layout.targetSize,
withHorizontalFittingPriority: layout.horizontalPriority,
verticalFittingPriority: layout.verticalPriority
)
}
}
}
private extension CGSize {
static let noIntrinsicMetric = CGSize(
width: UIView.noIntrinsicMetric,
height: UIView.noIntrinsicMetric
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment