TVOS SwiftUI中NavigationLink聚焦状态下多余背景问题
问题描述
开发tvOS应用时,在NavigationLink中嵌入自定义View后,View处于Focus状态时周围会显示不必要的背景。尝试设置.buttonStyle(.plain)无效,设置.buttonStyle(.borderless)则导致聚焦功能失效。
代码片段
主视图代码
ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: SizeScaler.width(16)) { ForEach(data.shows ?? [], id: \.self) { video in NavigationLink(destination: DetailView()) { PosterCard(video: video).buttonStyle(.plain) } .buttonStyle(.plain) } } .padding(.leading, SizeScaler.width(16)) }
PosterCard 自定义View代码
struct PosterCard: View { let video: CategoryShows private let width: CGFloat = SizeScaler.height(140) private var height: CGFloat { width * (3/2) } @State private var isImageLoading = true @FocusState private var isFocused: Bool var body: some View { VStack(alignment: .leading, spacing: SizeScaler.width(6)) { Spacer(height: 10) ZStack { if isImageLoading { Color.gray.opacity(0.3) .modifier(Shimmer()) .frame(width: width, height: height) .clipShape(RoundedRectangle(cornerRadius: 8)) } else { if let thumbnail = video.thumbnail { KFImage(URL(string: thumbnail)) .placeholder { Color.gray.opacity(0.3).modifier(Shimmer()) } .resizable() .scaledToFill() .frame(width: width, height: height) .clipShape(RoundedRectangle(cornerRadius: 8)) } else { Thumbnail_view(width: width) } } }.overlay { RoundedRectangle(cornerRadius: 8) .stroke(isFocused ? .white : .clear, lineWidth: 3).padding(-6) } .scaleEffect(isFocused ? 1.05 : 1.0) .animation(.easeInOut(duration: 0.18), value: isFocused) .focusable(true) .focused($isFocused) .background(.clear) Spacer(minLength: isFocused ? 5 : 0.000) Text(video.name ?? "") .font(AppFonts.monsterrat(14, weight: .semibold)) .foregroundColor(.white.opacity(0.5)) .lineLimit(2) .frame(width: width, alignment: .leading) Spacer(minLength: 0.5) } .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation { isImageLoading = false } } } } }
问题截图

解决方案
问题根源
NavigationLink在tvOS上默认自带系统样式的聚焦背景,而你在PosterCard中手动实现了聚焦UI(边框、缩放),两者叠加导致多余背景出现。同时,PosterCard内部设置的.focusable(true)会和NavigationLink的聚焦逻辑冲突,导致设置.borderless后聚焦失效。
修复步骤
移除PosterCard内部的手动聚焦管理
删除PosterCard中的@FocusState private var isFocused: Bool,以及.focusable(true)、.focused($isFocused)代码,让NavigationLink统一管理聚焦状态。通过环境变量获取聚焦状态
在PosterCard中添加环境变量,直接读取当前View的聚焦状态:@Environment(\.isFocused) private var isFocused清理NavigationLink的样式
给NavigationLink设置PlainButtonStyle并强制背景透明,彻底移除系统默认聚焦背景:NavigationLink(destination: DetailView()) { PosterCard(video: video) } .buttonStyle(PlainButtonStyle()) .background(.clear)保留原有UI动画逻辑
原有的边框高亮、缩放动画逻辑不变,只是聚焦状态改为从环境变量获取即可。
修复后代码示例
修复后的PosterCard
struct PosterCard: View { let video: CategoryShows @Environment(\.isFocused) private var isFocused private let width: CGFloat = SizeScaler.height(140) private var height: CGFloat { width * (3/2) } @State private var isImageLoading = true var body: some View { VStack(alignment: .leading, spacing: SizeScaler.width(6)) { Spacer(height: 10) ZStack { if isImageLoading { Color.gray.opacity(0.3) .modifier(Shimmer()) .frame(width: width, height: height) .clipShape(RoundedRectangle(cornerRadius: 8)) } else { if let thumbnail = video.thumbnail { KFImage(URL(string: thumbnail)) .placeholder { Color.gray.opacity(0.3).modifier(Shimmer()) } .resizable() .scaledToFill() .frame(width: width, height: height) .clipShape(RoundedRectangle(cornerRadius: 8)) } else { Thumbnail_view(width: width) } } }.overlay { RoundedRectangle(cornerRadius: 8) .stroke(isFocused ? .white : .clear, lineWidth: 3).padding(-6) } .scaleEffect(isFocused ? 1.05 : 1.0) .animation(.easeInOut(duration: 0.18), value: isFocused) Spacer(minLength: isFocused ? 5 : 0.000) Text(video.name ?? "") .font(AppFonts.monsterrat(14, weight: .semibold)) .foregroundColor(.white.opacity(0.5)) .lineLimit(2) .frame(width: width, alignment: .leading) Spacer(minLength: 0.5) } .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { withAnimation { isImageLoading = false } } } } }
修复后主视图
ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: SizeScaler.width(16)) { ForEach(data.shows ?? [], id: \.self) { video in NavigationLink(destination: DetailView()) { PosterCard(video: video) } .buttonStyle(PlainButtonStyle()) .background(.clear) } } .padding(.leading, SizeScaler.width(16)) }
内容的提问来源于stack exchange,提问作者steveSarsawa
相关产品推荐
相关产品推荐

