-
-
Save ccwasden/02cbe25b94eb6e844b43442427127e09 to your computer and use it in GitHub Desktop.
// -- Usage | |
struct Content: View { | |
@State var open = false | |
@State var popoverSize = CGSize(width: 300, height: 300) | |
var body: some View { | |
WithPopover( | |
showPopover: $open, | |
popoverSize: popoverSize, | |
content: { | |
Button(action: { self.open.toggle() }) { | |
Text("Tap me") | |
} | |
}, | |
popoverContent: { | |
VStack { | |
Button(action: { self.popoverSize = CGSize(width: 300, height: 600)}) { | |
Text("Increase size") | |
} | |
Button(action: { self.open = false}) { | |
Text("Close") | |
} | |
} | |
}) | |
} | |
} | |
// -- Source | |
struct WithPopover<Content: View, PopoverContent: View>: View { | |
@Binding var showPopover: Bool | |
var popoverSize: CGSize? = nil | |
let content: () -> Content | |
let popoverContent: () -> PopoverContent | |
var body: some View { | |
content() | |
.background( | |
Wrapper(showPopover: $showPopover, popoverSize: popoverSize, popoverContent: popoverContent) | |
.frame(maxWidth: .infinity, maxHeight: .infinity) | |
) | |
} | |
struct Wrapper<PopoverContent: View> : UIViewControllerRepresentable { | |
@Binding var showPopover: Bool | |
let popoverSize: CGSize? | |
let popoverContent: () -> PopoverContent | |
func makeUIViewController(context: UIViewControllerRepresentableContext<Wrapper<PopoverContent>>) -> WrapperViewController<PopoverContent> { | |
return WrapperViewController( | |
popoverSize: popoverSize, | |
popoverContent: popoverContent) { | |
self.showPopover = false | |
} | |
} | |
func updateUIViewController(_ uiViewController: WrapperViewController<PopoverContent>, | |
context: UIViewControllerRepresentableContext<Wrapper<PopoverContent>>) { | |
uiViewController.updateSize(popoverSize) | |
if showPopover { | |
uiViewController.showPopover() | |
} | |
else { | |
uiViewController.hidePopover() | |
} | |
} | |
} | |
class WrapperViewController<PopoverContent: View>: UIViewController, UIPopoverPresentationControllerDelegate { | |
var popoverSize: CGSize? | |
let popoverContent: () -> PopoverContent | |
let onDismiss: () -> Void | |
var popoverVC: UIViewController? | |
required init?(coder: NSCoder) { fatalError("") } | |
init(popoverSize: CGSize?, | |
popoverContent: @escaping () -> PopoverContent, | |
onDismiss: @escaping() -> Void) { | |
self.popoverSize = popoverSize | |
self.popoverContent = popoverContent | |
self.onDismiss = onDismiss | |
super.init(nibName: nil, bundle: nil) | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
} | |
func showPopover() { | |
guard popoverVC == nil else { return } | |
let vc = UIHostingController(rootView: popoverContent()) | |
if let size = popoverSize { vc.preferredContentSize = size } | |
vc.modalPresentationStyle = UIModalPresentationStyle.popover | |
if let popover = vc.popoverPresentationController { | |
popover.sourceView = view | |
popover.delegate = self | |
} | |
popoverVC = vc | |
self.present(vc, animated: true, completion: nil) | |
} | |
func hidePopover() { | |
guard let vc = popoverVC, !vc.isBeingDismissed else { return } | |
vc.dismiss(animated: true, completion: nil) | |
popoverVC = nil | |
} | |
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) { | |
popoverVC = nil | |
self.onDismiss() | |
} | |
func updateSize(_ size: CGSize?) { | |
self.popoverSize = size | |
if let vc = popoverVC, let size = size { | |
vc.preferredContentSize = size | |
} | |
} | |
} | |
} |
Ignore my above comment. I figured that out :)
Brilliant mate -- a real help to SwiftUI folks :-)
@dmallass - I'm having the same issue where the view isn't reflecting the underlying state changes. happen to remember what was causing that issue for you?
Yes, this didn't work on iPhone simulators on 15.2, but adding the function above (by @wassupdoc ) is what did it. Add it to the class, WrapperViewController.
@dmallass - I'm having the same issue where the view isn't reflecting the underlying state changes. happen to remember what was causing that issue for you?
I have solved this issue by adding the following code to func updateUIViewController
after the if else pair:
if let hostingController = uiViewController.popoverVC as? UIHostingController<PopoverContent> {
hostingController.rootView = popoverContent()
}
@sorin360 You should add that inside the UIPopoverPresentationControllerDelegate
you want it to act on. In this case, WrapperViewController
I encountered a weird bug which sometime caused the popover to reopen itself as a sheet (iOS). While I don't know the cause for it i suspected may be it's due to something happening between the controllerWillDismiss
and controllerDidDismiss
. Therefore i moved the content in presentationControllerWillDismiss
to presentationControllerDidDismiss
. It fixed the issue
When I use this within a SwiftUI List, opening the popover will display a warning, that “presenting view controller [UIHostingController] from detached view controller [WrapperViewController] is discouraged” (when used outside of List, it's fine). I have spent some time trying to figure out what might be causing this, but I can't find a reason why WrapperViewController would be detached. Does anyone know what might be causing this?
With iOS 16.4 - haven’t had a chance to test it myself but the new presentationCompactAdaptation(.none) modifier may work - Apples documentation - Apply the new .presentationCompactAdaptation(_:) modifier to the content of a modal presentation to control how it adapts to compact size classes on iPad and iPhone.
For example, the popover modifier presents a popover on iPad. By default, a popover adapts to the narrow horizontal size class on iPhone by showing as a sheet. In the example below, the .presentationCompactAdaptation(.none) modifier asks SwiftUI to show this as a popover on iPhone as well.
struct PopoverExample: View {
@State private var isShowingPopover = false
var body: some View {
Button("Show Popover") {
self.isShowingPopover = true
}
.popover(isPresented: $isShowingPopover) {
Text("Popover Content")
.padding()
.presentationCompactAdaptation(.none)
}
}
}
Use .presentationCompactAdaptation(horizontal:vertical:) to adapt differently in horizontally and vertically compact size classes. (103257577)
@INuvanda: Yes, there is this warning all the time and its getting worse with Xcode 15 beta: Here it's stated that this will be an error in future releases. I used these two nice extensions
extension UIWindow {
static var key: UIWindow? { // replacement of keyWindow
if #available(iOS 13, *) {
// from: https://stackoverflow.com/questions/57134259/how-to-resolve-keywindow-was-deprecated-in-ios-13-0
return UIApplication
.shared
.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.last { $0.isKeyWindow }
} else {
return UIApplication.shared.keyWindow
}
}
}
extension UIViewController {
// from https://stackoverflow.com/questions/26667009/get-top-most-uiviewcontroller
static var topMostViewController: UIViewController? {
var topMostViewController = UIWindow.key?.rootViewController // the starting vc
while let presentedViewController = topMostViewController?.presentedViewController {
topMostViewController = presentedViewController // loop over all presented vcs
}
return topMostViewController // and return the last/topmost one
}
}
to modify the function showPopover()
:
func showPopover() {
guard popoverVC == nil,
let topMostViewController = Self.topMostViewController else { return }
let vc = UIHostingController(rootView: popoverContent())
if let size = popoverSize { vc.preferredContentSize = size }
vc.modalPresentationStyle = UIModalPresentationStyle.popover
if let popover = vc.popoverPresentationController {
popover.sourceView = view
popover.delegate = self
}
popoverVC = vc
topMostViewController.present(vc, animated: true, completion: nil) // present from topmost VC
}
and the warning is gone.
@ccwasden If the popovercontent is getting modiifed while using it, it doesn't reflect in the view