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

tvOS:如何在ScrollView中正确加载更多项并优化焦点体验

tvOS SwiftUI 横向滚动列表加载更多焦点优化方案

你的思路可行性分析

你提到的拆分数组、动态切换加载按钮的思路是可行的,但需要维护多组数据和按钮状态,代码会更繁琐,后期维护成本高,不推荐作为首选方案。另外,@FocusState 并不会破坏 NavigationLink,只要正确使用,两者可以共存。

更优解决方案

方案1:记录并恢复焦点

通过 @FocusState 跟踪点击加载更多前的焦点卡片,加载完成后将焦点切回原位置,避免跳到列表末尾。

代码示例:

// 定义焦点状态变量
@FocusState private var focusedGameId: String?

if (!viewModel.previousGames.isEmpty) {
    VStack (alignment: .leading, spacing: 8) {
        Text(languageManager.localizedString(for: "Past"))
            .font(.title2)
            .foregroundColor(Color.white)
        ScrollView(.horizontal, showsIndicators: false) {
            HStack (alignment: .top, spacing: 48) {
                ForEach(viewModel.previousGames) { game in
                    NavigationLink(destination: MagazineGameView(viewModel: FollowGameAPIController(), gameID: game.gameID)) {
                        GameCardView(game: game, showGameResults: showGameResults)
                    }
                    .buttonStyle(.borderless)
                    .focused($focusedGameId, equals: game.gameID)
                }
                Button(action: {
                    // 记录当前焦点的游戏ID
                    let currentFocusedId = focusedGameId
                    getMoreItems {
                        // 加载完成后恢复焦点
                        focusedGameId = currentFocusedId
                    }
                }) {
                    MoreItemsView()
                }
                .buttonStyle(.borderless)
                .disabled(loading) // 加载时禁用按钮,防止重复点击
            }
        }
    }
}

// 修改getMoreItems,添加完成回调
private func getMoreItems(completion: @escaping () -> Void) {
    loading = true
    viewModel.getMemberCalendarGames(isPrevious: true, lastGameId: viewModel.previousGames.last?.gameID) { result in
        switch result {
        case .success(let newGames):
            print("Successfully fetched more games: \(newGames)")
        case .failure(let error):
            print("Failed to fetch more games: \(error)")
        }
        loading = false
        completion()
    }
}

方案2:结合ScrollViewReader固定滚动位置

使用 ScrollViewReader 记录加载前的滚动位置,加载完成后滚动回原位置,同时配合焦点管理效果更好。

代码示例:

@FocusState private var focusedGameId: String?
@State private var scrollTarget: String?

if (!viewModel.previousGames.isEmpty) {
    VStack (alignment: .leading, spacing: 8) {
        Text(languageManager.localizedString(for: "Past"))
            .font(.title2)
            .foregroundColor(Color.white)
        ScrollViewReader { proxy in
            ScrollView(.horizontal, showsIndicators: false) {
                HStack (alignment: .top, spacing: 48) {
                    ForEach(viewModel.previousGames) { game in
                        NavigationLink(destination: MagazineGameView(viewModel: FollowGameAPIController(), gameID: game.gameID)) {
                            GameCardView(game: game, showGameResults: showGameResults)
                        }
                        .buttonStyle(.borderless)
                        .focused($focusedGameId, equals: game.gameID)
                        .id(game.gameID) // 给每个卡片设置唯一ID
                    }
                    Button(action: {
                        // 记录当前焦点的游戏ID
                        scrollTarget = focusedGameId
                        getMoreItems {
                            // 滚动到原焦点卡片位置
                            if let target = scrollTarget {
                                proxy.scrollTo(target, anchor: .center)
                                focusedGameId = target
                            }
                        }
                    }) {
                        MoreItemsView()
                    }
                    .buttonStyle(.borderless)
                    .disabled(loading)
                }
            }
        }
    }
}

// 同样修改getMoreItems添加回调
private func getMoreItems(completion: @escaping () -> Void) {
    loading = true
    viewModel.getMemberCalendarGames(isPrevious: true, lastGameId: viewModel.previousGames.last?.gameID) { result in
        switch result {
        case .success(let newGames):
            print("Successfully fetched more games: \(newGames)")
        case .failure(let error):
            print("Failed to fetch more games: \(error)")
        }
        loading = false
        completion()
    }
}

关键注意点

  • @FocusState 与 NavigationLink 兼容:只要不为 NavigationLink 本身设置冲突的焦点绑定,就不会影响跳转功能,上述示例中焦点绑定在卡片内部,不会破坏导航。
  • 加载状态控制:添加 loading 状态禁用按钮,避免用户重复触发加载请求。
  • 滚动锚点选择:使用 .center 锚点可以让原焦点卡片保持在屏幕中间,体验更自然。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 09:25:14