You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何获取SwiftUI按钮精确sourceRect以在UIKit中展示Popover

解决SwiftUI按钮Popover位置偏差问题

问题根源

  1. 你直接传入全局坐标系的frame,但Popover的sourceRect是基于sourceView(当前UIViewController的view)的本地坐标系,全局坐标会包含状态栏、导航栏等额外高度,导致位置偏移。
  2. 包裹按钮的GeometryReader会占据整个List单元格的空间,它的frame并非按钮的真实点击区域,而是单元格的大小,自然会让Popover指向错误位置。

解决方案

方案1:坐标转换修正(快速修改)

直接在UIViewController的showActivityVC方法中,把传入的全局坐标转换为当前view的本地坐标:

func showActivityVC(sourceRect: CGRect) {
    let activityVC = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
    if let popOver = activityVC.popoverPresentationController {
        popOver.sourceView = self.view!
        // 将全局坐标系的rect转换为当前view的本地坐标系
        let localRect = self.view.convert(sourceRect, from: nil)
        popOver.sourceRect = localRect
    }
    present(activityVC, animated: true)
}

方案2:获取按钮真实frame(更精准)

用PreferenceKey捕获按钮的实际frame,替代GeometryReader的单元格frame:

  1. 定义PreferenceKey:
struct ButtonFrameKey: PreferenceKey {
    static var defaultValue: CGRect = .zero
    
    static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
        value = nextValue()
    }
}
  1. 修改SwiftUI按钮代码(在EDWrapperView中添加@State var buttonFrame: CGRect?):
List {
    Section(header: Text("Export")) {
        Button(action: {
            guard let frame = buttonFrame else { return }
            // 转换为全局坐标后传给UIKit控制器
            let globalFrame = UIApplication.shared.windows.first?.convert(frame, from: hostingVC.view) ?? .zero
            exportDestinationVC.showActivityVC(sourceRect: globalFrame)
        }, label: {
            HStack(alignment: .firstTextBaseline) {
                Spacer()
                Image(systemName: "square.and.arrow.up")
                Text("Export")
                Spacer()
            }
        })
        .contentShape(Rectangle())
        // 用背景GeometryReader捕获按钮本地frame
        .background(
            GeometryReader { geometry in
                Color.clear
                    .preference(key: ButtonFrameKey.self, value: geometry.frame(in: .local))
            }
        )
        .onPreferenceChange(ButtonFrameKey.self) { newFrame in
            buttonFrame = newFrame
        }
    }
}

方案3:直接指定sourceView为HostingView(最优简化)

既然HostingView是UIViewController的子视图,直接把Popover的sourceView设为hostingVC.view,此时只需传入按钮在HostingView本地坐标系的frame:

// UIViewController中的方法
func showActivityVC(sourceRect: CGRect) {
    let activityVC = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
    if let popOver = activityVC.popoverPresentationController {
        popOver.sourceView = self.hostingVC.view
        popOver.sourceRect = sourceRect // 无需坐标转换,直接用按钮在HostingView的本地frame
    }
    present(activityVC, animated: true)
}

// SwiftUI按钮中直接传捕获到的buttonFrame
Button(action: {
    guard let frame = buttonFrame else { return }
    exportDestinationVC.showActivityVC(sourceRect: frame)
}, label: { /* ... */ })

内容的提问来源于stack exchange,提问作者vomi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 02:20:26