Skip to content

Instantly share code, notes, and snippets.

@auramagi
Created June 11, 2022 12:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save auramagi/59215d25b635cac79fa9817d57dae67b to your computer and use it in GitHub Desktop.
Save auramagi/59215d25b635cac79fa9817d57dae67b to your computer and use it in GitHub Desktop.
Drive UIView frame layout with SwiftUI Layout protocol
// The main layout logic
import SwiftUI
final class LayoutAdaptor: UIView {
var layout: (any Layout)? {
didSet { setNeedsLayout() }
}
override func layoutSubviews() {
super.layoutSubviews()
guard let layout = layout else { return }
let content = AnyLayout(layout) {
ForEach(subviews, id: \.objectIdentifier) { view in
UIViewProxyLayout(view: view)
}
}
.frame(width: bounds.width, height: bounds.height)
.ignoresSafeArea()
_ = ImageRenderer(content: content).uiImage // force render without showing on-screen
}
}
struct UIViewProxyLayout: Layout, View {
let view: UIView
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
view.sizeThatFits(proposal: proposal)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
view.frame = bounds
}
var body: some View {
self { Path() }
}
}
// This is mostly an untested shot in the dark, probably will fail to properly size more advanced views
extension UIView {
func sizeThatFits(proposal: ProposedViewSize) -> CGSize {
let widthTarget = target(for: proposal.width)
let heightTarget = target(for: proposal.height)
return systemLayoutSizeFitting(
CGSize(width: widthTarget.0, height: heightTarget.0),
withHorizontalFittingPriority: widthTarget.1,
verticalFittingPriority: heightTarget.1
)
}
func target(for proposal: CGFloat?) -> (CGFloat, UILayoutPriority) {
switch proposal {
case .none: return (UIView.layoutFittingCompressedSize.width, .fittingSizeLevel)
case .some(.zero): return (UIView.layoutFittingCompressedSize.width, .defaultHigh)
case .some(.infinity): return (UIView.layoutFittingExpandedSize.width, .defaultLow)
case let .some(value): return (value, .defaultHigh)
}
}
}
extension UIView {
var objectIdentifier: ObjectIdentifier {
.init(self)
}
}
// App for testing. Has a sample VC, a button to switches between 4 layouts.
import SwiftUI
@main
struct LayoutAdaptorApp: App {
@State var i = 0
var layoutType: LayoutType {
LayoutType.allCases[i % LayoutType.allCases.count]
}
var body: some Scene {
WindowGroup {
VStack {
TestVCRepresentable(layout: layoutType.layout)
.frame(width: 150, height: 150)
.border(.red)
.frame(maxWidth: .infinity, maxHeight: .infinity)
Button(
action: { i += 1 },
label: { Text("Change UIView layout") }
)
}
}
}
}
enum LayoutType: CaseIterable {
case hstack
case vstack
case zstack
case circle50
var layout: any Layout {
switch self {
case .vstack: return VStack()
case .hstack: return HStack()
case .zstack: return _ZStackLayout()
case .circle50: return _CircleLayout(radius: 50)
}
}
}
final class TestVC: UIViewController {
private lazy var adaptor = LayoutAdaptor()
private func makeLabel(text: String) -> UILabel {
let view = UILabel()
view.text = text
return view
}
private func makeImageView(systemImage: String) -> UIImageView {
UIImageView(image: .init(systemName: systemImage))
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(makeLabel(text: "Hello"))
view.addSubview(makeLabel(text: "World"))
view.addSubview(makeImageView(systemImage: "globe"))
setLayout(HStack())
}
override func loadView() {
view = adaptor
}
func setLayout(_ layout: any Layout, animated: Bool = false) {
func _setLayout(_ layout: any Layout) {
adaptor.layout = layout
adaptor.layoutIfNeeded()
}
if animated {
UIView.animate(withDuration: 0.3) { _setLayout(layout) }
} else {
_setLayout(layout)
}
}
}
struct TestVCRepresentable: UIViewControllerRepresentable {
var layout: any Layout
func makeUIViewController(context: Context) -> TestVC {
.init()
}
func updateUIViewController(_ uiViewController: TestVC, context: Context) {
uiViewController.setLayout(layout, animated: true)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment