@ccwasden的解决方案非常有效。我通过让它更符合SwiftUI的“自然”风格来扩展他的工作。此版本还利用了 sizeThatFits
方法,因此您不必指定popover内容的大小。
struct PopoverViewModifier<PopoverContent>: ViewModifier where PopoverContent: View {
@Binding var isPresented: Bool
let onDismiss: (() -> Void)?
let content: () -> PopoverContent
func body(content: Content) -> some View {
content
.background(
Popover(
isPresented: self.$isPresented,
onDismiss: self.onDismiss,
content: self.content
)
)
}
}
extension View {
func popover<Content>(
isPresented: Binding<Bool>,
onDismiss: (() -> Void)? = nil,
content: @escaping () -> Content
) -> some View where Content: View {
ModifiedContent(
content: self,
modifier: PopoverViewModifier(
isPresented: isPresented,
onDismiss: onDismiss,
content: content
)
)
}
}
struct Popover<Content: View> : UIViewControllerRepresentable {
@Binding var isPresented: Bool
let onDismiss: (() -> Void)?
@ViewBuilder let content: () -> Content
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self, content: self.content())
}
func makeUIViewController(context: Context) -> UIViewController {
return UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
context.coordinator.host.rootView = self.content()
if self.isPresented, uiViewController.presentedViewController == nil {
let host = context.coordinator.host
host.preferredContentSize = host.sizeThatFits(in: CGSize(width: Int.max, height: Int.max))
host.modalPresentationStyle = UIModalPresentationStyle.popover
host.popoverPresentationController?.delegate = context.coordinator
host.popoverPresentationController?.sourceView = uiViewController.view
host.popoverPresentationController?.sourceRect = uiViewController.view.bounds
uiViewController.present(host, animated: true, completion: nil)
}
}
class Coordinator: NSObject, UIPopoverPresentationControllerDelegate {
let host: UIHostingController<Content>
private let parent: Popover
init(parent: Popover, content: Content) {
self.parent = parent
self.host = UIHostingController(rootView: content)
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
self.parent.isPresented = false
if let onDismiss = self.parent.onDismiss {
onDismiss()
}
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
}
UIDevice.current.userInterfaceIdiom == .phone
,如果为真,则应用自定义修饰符。 如果不是,它将使用弹出窗口的“本机”SwiftUI版本。我还必须重命名View函数扩展,以避免运行时命名冲突。 - undefined我使用自定义的UIViewRepresentable
让其能在iOS上运行。下面是使用方法:
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")
}
}
})
}
}
这是一个源代码链接,包含有关WithPopover
的信息。
对泛型类型 'WithPopover' 的引用需要 <...> 中的参数
。还有我得到了这个:在分离的视图控制器上呈现视图控制器是不鼓励的
。 - undefined这是@kou-ariga代码的改进版本。它还修复了一些问题,例如:
用法:
@State private var showInfo: Bool = false
// ...
Button {
showInfo = true
} label: {
Image(systemName: "info")
}
.alwaysPopover(isPresented: $showInfo) {
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam")
.font(.subheadline)
.multilineTextAlignment(.center)
.padding()
.frame(width: UIDevice.current.userInterfaceIdiom == .phone ? (UIScreen.screenWidth - 16 * 2) : 350)
.foregroundColor(Color.white)
.background(Color(.systemGray))
}
组件:
// MARK: - Extension
extension View {
func alwaysPopover<Content>(
isPresented: Binding<Bool>,
permittedArrowDirections: UIPopoverArrowDirection = [.up],
onDismiss: (() -> Void)? = nil,
content: @escaping () -> Content
) -> some View where Content: View {
self.modifier(AlwaysPopoverViewModifier(
isPresented: isPresented,
permittedArrowDirections: permittedArrowDirections,
onDismiss: onDismiss,
content: content
))
}
}
// MARK: - Modifier
struct AlwaysPopoverViewModifier<PopoverContent>: ViewModifier where PopoverContent: View {
@Binding var isPresented: Bool
let permittedArrowDirections: UIPopoverArrowDirection
let onDismiss: (() -> Void)?
let content: () -> PopoverContent
init(
isPresented: Binding<Bool>,
permittedArrowDirections: UIPopoverArrowDirection,
onDismiss: (() -> Void)? = nil,
content: @escaping () -> PopoverContent
) {
self._isPresented = isPresented
self.permittedArrowDirections = permittedArrowDirections
self.onDismiss = onDismiss
self.content = content
}
func body(content: Content) -> some View {
content
.background(
AlwaysPopover(
isPresented: self.$isPresented,
permittedArrowDirections: self.permittedArrowDirections,
onDismiss: self.onDismiss,
content: self.content
)
)
}
}
// MARK: - UIViewController
struct AlwaysPopover<Content: View>: UIViewControllerRepresentable {
@Binding var isPresented: Bool
let permittedArrowDirections: UIPopoverArrowDirection
let onDismiss: (() -> Void)?
@ViewBuilder let content: () -> Content
init(
isPresented: Binding<Bool>,
permittedArrowDirections: UIPopoverArrowDirection,
onDismiss: (() -> Void)?,
content: @escaping () -> Content
) {
self._isPresented = isPresented
self.permittedArrowDirections = permittedArrowDirections
self.onDismiss = onDismiss
self.content = content
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self, content: self.content())
}
func makeUIViewController(context: Context) -> UIViewController {
return UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
context.coordinator.host.rootView = self.content()
guard context.coordinator.lastIsPresentedValue != self.isPresented else { return }
context.coordinator.lastIsPresentedValue = self.isPresented
if self.isPresented {
let host = context.coordinator.host
if context.coordinator.viewSize == .zero {
context.coordinator.viewSize = host.sizeThatFits(in: UIView.layoutFittingExpandedSize)
}
host.preferredContentSize = context.coordinator.viewSize
host.modalPresentationStyle = .popover
host.popoverPresentationController?.delegate = context.coordinator
host.popoverPresentationController?.sourceView = uiViewController.view
host.popoverPresentationController?.sourceRect = uiViewController.view.bounds
host.popoverPresentationController?.permittedArrowDirections = self.permittedArrowDirections
if let presentedVC = uiViewController.presentedViewController {
presentedVC.dismiss(animated: true) {
uiViewController.present(host, animated: true, completion: nil)
}
} else {
uiViewController.present(host, animated: true, completion: nil)
}
}
}
class Coordinator: NSObject, UIPopoverPresentationControllerDelegate {
let host: UIHostingController<Content>
private let parent: AlwaysPopover
var lastIsPresentedValue: Bool = false
/// Content view size.
var viewSize: CGSize = .zero
init(parent: AlwaysPopover, content: Content) {
self.parent = parent
self.host = AlwaysPopoverUIHostingController(
rootView: content,
isPresented: self.parent.$isPresented,
onDismiss: self.parent.onDismiss
)
}
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
self.parent.isPresented = false
if let onDismiss = self.parent.onDismiss {
onDismiss()
}
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
}
// MARK: - UIHostingController
class AlwaysPopoverUIHostingController<Content: View>: UIHostingController<Content> {
@Binding private var isPresented: Bool
private let onDismiss: (() -> Void)?
init(rootView: Content, isPresented: Binding<Bool>, onDismiss: (() -> Void)?) {
self._isPresented = isPresented
self.onDismiss = onDismiss
super.init(rootView: rootView)
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidDisappear(_ animated: Bool) {
self.isPresented = false
if let onDismiss = self.onDismiss {
onDismiss()
}
}
}
仅限于macOS
以下是如何动态更改弹出窗口的框架...为了简单起见,没有包含动画效果,这取决于您。
struct TestCustomSizePopover: View {
@State var popover7 = false
var body: some View {
VStack {
Button("Popover") {
self.popover7.toggle()
}.popover(isPresented: self.$popover7, arrowEdge: .bottom) {
PopoverView()
}
}.frame(width: 800, height: 600)
}
}
struct PopoverView: View {
@State var adaptableHeight = CGFloat(100)
var body: some View {
VStack {
Text("Popover").padding()
Button(action: {
self.adaptableHeight = 300
}) {
Text("Button")
}
}
.frame(width: 100, height: adaptableHeight)
}
}
frame
修饰符对弹出窗口没有任何影响。 - undefined