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

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

问题截图

tvOS聚焦时多余背景


解决方案

问题根源

NavigationLink在tvOS上默认自带系统样式的聚焦背景,而你在PosterCard中手动实现了聚焦UI(边框、缩放),两者叠加导致多余背景出现。同时,PosterCard内部设置的.focusable(true)会和NavigationLink的聚焦逻辑冲突,导致设置.borderless后聚焦失效。

修复步骤

  1. 移除PosterCard内部的手动聚焦管理
    删除PosterCard中的@FocusState private var isFocused: Bool,以及.focusable(true)、.focused($isFocused)代码,让NavigationLink统一管理聚焦状态。

  2. 通过环境变量获取聚焦状态
    在PosterCard中添加环境变量,直接读取当前View的聚焦状态:

    @Environment(\.isFocused) private var isFocused
    
  3. 清理NavigationLink的样式
    给NavigationLink设置PlainButtonStyle并强制背景透明,彻底移除系统默认聚焦背景:

    NavigationLink(destination: DetailView()) {
        PosterCard(video: video)
    }
    .buttonStyle(PlainButtonStyle())
    .background(.clear)
    
  4. 保留原有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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.11 21:43:13