-
-
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 | |
} | |
} | |
} | |
} |
@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.
I have solved this issue by adding the following code to func
updateUIViewController
after the if else pair: