SwiftUI中如何实现.contextMenu预览点击全屏展开功能?
需求描述
想要在ForEach列表的SwiftUI视图中,复刻苹果Stocks应用的交互逻辑:长按新闻项弹出ContextMenu预览,点击预览可全屏展开网页内容。当前代码的ContextMenu预览无法交互,需要修改实现这一功能。
当前代码
列表核心代码
if !vm.StockNews.isEmpty { ForEach(vm.StockNews, id: \.self) { news in if let url = URL(string: news.link) { VStack(alignment: .leading) { Text("\(news.title)") .font(.title) .fontWeight(.semibold) .padding(EdgeInsets(top: 0, leading: 5, bottom: 5, trailing: 5)) .lineLimit(4) Text("\(news.description)") .font(.headline) .padding(5) .lineLimit(3) .fixedSize(horizontal: false, vertical: true) Text("\(vm.formattedTimeAgo(from: news.pubDate))") .padding(EdgeInsets(top: 5, leading: 5, bottom: 0, trailing: 5)) } .fixedSize(horizontal: false, vertical: true) .onTapGesture { isPresentingWebView = true } .fullScreenCover(isPresented: $isPresentingWebView) { SafariWebView(url: url) .ignoresSafeArea() } .contextMenu { Button(action: { UIPasteboard.general.string = news.link }) { Text("Kopier lenke") Image(systemName: "link") } } preview: { SafariWebView(url: url) } } else { VStack(alignment: .leading) { Text("\(news.title)") .font(.title) .fontWeight(.semibold) .padding(5) .lineLimit(3) Text("\(news.description)") .font(.headline) .padding(5) .lineLimit(3) .fixedSize(horizontal: false, vertical: true) Text("\(news.pubDate)") .padding(5) } .fixedSize(horizontal: false, vertical: true) } } } else { HStack { Spacer() Text("Ingen nylige nyheter") .font(.title2.bold()) Spacer() } .padding(.top, -15) .padding(.bottom, 30) .listRowSeparator(.hidden) }
SafariWebView代码
import SwiftUI import SafariServices struct SafariWebView: UIViewControllerRepresentable { let url: URL func makeUIViewController(context: Context) -> SFSafariViewController { let config = SFSafariViewController.Configuration() return SFSafariViewController(url: url, configuration: config) } func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) { } }
解决方案
默认的ContextMenu预览视图不可交互,需通过以下步骤改造:
- 替换状态变量:将单个布尔值
isPresentingWebView改为可选URL变量,用于跟踪当前要全屏展示的新闻链接:
@State private var selectedNewsURL: URL?
- 给预览添加点击交互:在ContextMenu的预览视图上绑定点击手势,点击时设置目标URL并关闭ContextMenu:
.contextMenu { Button(action: { UIPasteboard.general.string = news.link }) { Text("Kopier lenke") Image(systemName: "link") } } preview: { SafariWebView(url: url) .onTapGesture { selectedNewsURL = url // 强制关闭ContextMenu UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } }
- 更新全屏展示逻辑:将
fullScreenCover绑定到selectedNewsURL,利用item绑定自动关联对应URL,关闭时重置变量:
.fullScreenCover(item: $selectedNewsURL) { _ in SafariWebView(url: url) .ignoresSafeArea() }
- 调整列表项点击逻辑:原列表项的点击手势同步更新为设置
selectedNewsURL:
.onTapGesture { selectedNewsURL = url }
修改后的核心代码片段
// 顶部新增状态变量 @State private var selectedNewsURL: URL? // ... 其他代码 ... if !vm.StockNews.isEmpty { ForEach(vm.StockNews, id: \.self) { news in if let url = URL(string: news.link) { VStack(alignment: .leading) { // 新闻标题、描述、时间代码保持不变 } .fixedSize(horizontal: false, vertical: true) .onTapGesture { selectedNewsURL = url } .fullScreenCover(item: $selectedNewsURL) { _ in SafariWebView(url: url) .ignoresSafeArea() } .contextMenu { Button(action: { UIPasteboard.general.string = news.link }) { Text("Kopier lenke") Image(systemName: "link") } } preview: { SafariWebView(url: url) .onTapGesture { selectedNewsURL = url UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } } } else { // 无效URL的视图代码保持不变 } } } else { // 无新闻的视图代码保持不变 }
关键说明
- 使用
item绑定的fullScreenCover更适配多列表项场景,能自动关联对应URL,避免单个布尔值的状态冲突。 - 调用
resignFirstResponder可强制关闭ContextMenu,防止预览视图与全屏视图同时显示。 - 预览视图的
onTapGesture会覆盖默认预览的无交互状态,实现点击触发全屏的效果。
内容的提问来源于stack exchange,提问作者Kristoffer Melen
相关产品推荐
相关产品推荐

