Last active
August 17, 2024 10:39
-
-
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.
This file contains 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
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?() } | |
} | |
} |
This file contains 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
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 |
This file contains 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
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") | |
} | |
} |
This file contains 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
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@JoshuaHolme those are just helper typealiases I'm using - updated to use regular syntax.