Skip to content

Instantly share code, notes, and snippets.

@rl-pavel
Last active August 17, 2024 10:39
Show Gist options
  • Save rl-pavel/13d9440306214fcf6d5f573deedd97c6 to your computer and use it in GitHub Desktop.
Save rl-pavel/13d9440306214fcf6d5f573deedd97c6 to your computer and use it in GitHub Desktop.
Resizable SwiftUI helper protocol that fixes the iOS 15 UIHostingController sizing bug.
import SwiftUI
struct SomeView: View, Resizable {
var updateSize: () -> Void?
var body: some View {
// Option 1: call the closure in the body.
let _ = updateSize?()
Text("Hello World")
// Option 2: call the closure using a SizeReader helper below.
// I found this to be more consistent in some situations, not sure why though.
.readSize { _ in updateSize?() }
}
}
import SwiftUI
import UIKit
/// This protocol defines a view whose size may change after the initial layout.
/// The purpose of this is to fix a (hopefully temporary) iOS 15 bug where the `UIHostingController`'s view
/// doesn't update it's size to fit the SwiftUI View content properly.
protocol Resizable {
var updateSize: (() -> Void)? { get set }
}
extension UIHostingController {
/// Sets the `Resizable.updateSize` closure to update view constraints on iOS 15.
/// NOTE: this needs to be called whenever `UIHostingController.rootView` changes,
/// e.g. if the `Content` is initially `nil`, or if the `rootView` is mutated.
func configureResizableViewIfNeeded() {
guard #available(iOS 15.0, *), var resizingView = rootView as? Resizable else { return }
resizingView.updateSize = { [weak self] in
self?.view?.setNeedsUpdateConstraints()
}
// swiftlint:disable:next force_cast
rootView = resizingView as! Content
}
}
// MARK: - Preview
#if DEBUG
struct Resizable_Previews: PreviewProvider {
struct Preview: View, Resizable {
@State var viewHeight: CGFloat = 100
var updateSize: VoidClosure?
var body: some View {
let _ = updateSize?()
VStack {
HStack {
Text("iOS \(UIDevice.current.systemVersion) - \(updateSize == nil ? "🤬" : "fixed")")
Spacer()
Text("Height: \(Int(viewHeight))")
}
Slider(value: $viewHeight, in: 100...300)
}
.padding(.horizontal)
.frame(height: viewHeight)
.background(Color.gray)
.padding(.horizontal)
}
}
struct IOS15SizeReproductionController: UIViewControllerRepresentable {
let updateSize: Bool
func makeUIViewController(context: Context) -> UIViewController {
let hostController = Host<Preview?>(nil)
hostController.view.backgroundColor = .red
hostController.rootView = .init()
if updateSize { hostController.configureResizableViewIfNeeded() }
let wrapperController = UIViewController()
wrapperController.addController(hostController)
hostController.view.snp.makeConstraints { $0.center.horizontal.equalToSuperview() }
return wrapperController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) { }
}
static var previews: some View {
VStack {
IOS15SizeReproductionController(updateSize: true)
IOS15SizeReproductionController(updateSize: false)
}
}
}
#endif
import SwiftUI
import UIKit
class Host<Content: View>: UIHostingController<Content> {
init(backgroundColor: UIColor = .clear, @ViewBuilder content: () -> Content) {
super.init(rootView: content())
view.backgroundColor = backgroundColor
configureResizableViewIfNeeded()
}
init(_ rootView: Content, backgroundColor: UIColor = .clear) {
super.init(rootView: rootView)
view.backgroundColor = backgroundColor
configureResizableViewIfNeeded()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
import SwiftUI
extension View {
func readSize(into size: Binding<CGSize>) -> some View {
self.modifier(SizeReader(size: size))
}
func readSize(changed: @escaping (() -> CGSize)) -> some View {
self.modifier(SizeReader(size: .init(get: { .zero }, set: changed)))
}
}
private struct SizeReader: ViewModifier {
@Binding var size: CGSize
func body(content: Content) -> some View {
content
.background(
GeometryReader { proxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: proxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self) { size = $0 }
}
}
private struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value currentValue: inout CGSize, nextValue: () -> CGSize) {
_ = nextValue()
}
}
// MARK: - Preview
#if DEBUG
struct SizeReader_Previews: PreviewProvider {
struct Preview: View {
@State var text: String = ""
@State var textSize: CGSize = .zero
var body: some View {
VStack {
TextField("type", text: $text)
.fixedSize()
.readSize(into: $textSize)
.background(Color.black.opacity(0.05))
Text("Size: w\(Int(textSize.width)) h\(Int(textSize.height))")
}
}
}
static var previews: some View {
Preview().padding()
}
}
#endif
@JoshuaHolme
Copy link

Getting the error cannot find type ‘Closure’ in scope

@JoshuaHolme
Copy link

Same with VoidClosure

@rl-pavel
Copy link
Author

@JoshuaHolme those are just helper typealiases I'm using - updated to use regular syntax.

typealias VoidClosure = () -> Void
typealias Closure<T> = (T) -> Void

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment