-
-
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 | |
| } | |
| } | |
| } | |
| } |
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.
@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?