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
相关产品推荐
相关产品推荐

