在SwiftUI中更改弹出窗口大小

4
我正在尝试设置弹出窗口的特定大小或使其适应其内容。
我尝试更改弹出窗口视图的框架,但似乎没有起作用。
Button("Popover") {
        self.popover7.toggle()
}.popover(isPresented: self.$popover7, arrowEdge: .bottom) {
         PopoverView().frame(width: 100, height: 100, alignment: .center)
}

我希望实现在iPad的日历应用中发现的这种行为。

enter image description here


Akshay,这与问题无关,问题是关于SwiftUI的。 - undefined
4个回答

11

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

1
非常出色的改进。我本来也打算这么做,但是后来看到你在下面的回复 :) - undefined
1
谢谢,@Jensie!我对updateUIViewController方法进行了一个小的修改。在单个视图中使用多个弹出窗口时,之前的版本存在内部状态不一致的问题。希望能有所帮助。 - undefined
1
感谢@ccwasden和@kou_ariga! 我根据自己的需求进行了一些小修改...在iPad上创建一个填充整个屏幕宽度的视图。 因此,我更新了PopoverViewModifier的body()以检查UIDevice.current.userInterfaceIdiom == .phone,如果为真,则应用自定义修饰符。 如果不是,它将使用弹出窗口的“本机”SwiftUI版本。我还必须重命名View函数扩展,以避免运行时命名冲突。 - undefined

7

我使用自定义的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
在模拟器中对我来说运行良好,没有出现故障。从Gist中获取了整个内容,并根据上面的片段使用它。预览功能无法正常工作,只有在点击按钮时显示一个空白屏幕。但是没有显示任何错误。Xcode版本为11.4 beta 3 (11N132i)。 - undefined
@ccwasden在代码片段中忘记包含adapativePresentationStyle函数了。func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { return .none // 这是强制在iPhone上显示弹出窗口的方法 } - undefined
这个不行。iOS15模拟器iPhone 12。总是占据整个屏幕。 - undefined

2

这是@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()
        }
    }
}


0

仅限于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)
    }
}

1
你试过这段代码吗?它不起作用,弹出窗口的大小没有改变。 - undefined
确实,这是从测试项目中复制/粘贴的。Xcode 11.2.1。啊...抱歉,我是为了MacOS目标做的。稍后我会检查一下iOS。 - undefined
是的,在MacOS上设置弹出窗口的框架效果很好,但在iOS上,frame修饰符对弹出窗口没有任何影响。 - undefined
根据我的发现,目前内容跟踪 .frame 是正确的,并且已经发生了变化,但系统弹出窗口无法追踪内容变化(在iOS上)。 - undefined
目前似乎无法更改弹出窗口的框架。 我创建了错误报告FB7465491,建议您也这样做。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接