https://twitter.com/auramagi/status/1535600662922158080?s=21&t=3PdsJRuqGzznCk3_ahrblQ
Created
June 11, 2022 12:43
-
-
Save auramagi/59215d25b635cac79fa9817d57dae67b to your computer and use it in GitHub Desktop.
Drive UIView frame layout with SwiftUI Layout protocol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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