SwiftUI使用.overlay()修饰符导致卡片数据重复显示异常
问题根因
这不是系统bug,是代码逻辑存在两个核心错误:
- 每个
TopActivityCardView实例内部都通过@StateObject var vm = ViewModel()创建了独立的ViewModel实例,且视图内部自带ScrollView+ForEach(vm.fetch())遍历全量接口数据,等于生成6张卡片时,每一张卡片都把所有6条活动数据完整渲染了一遍,多层内容堆叠后最上层的内容挡住了下层,视觉上就表现为所有卡片数据完全一致。 - 父子视图各自初始化独立的ViewModel实例,没有实现单一数据源,之前更换
@EnvironmentObject/@ObservedObject/@StateObject没有效果,是因为始终没有给子视图传入对应单条卡片的数据,还是让子视图自己拉取全量数据渲染。
修复方案
按以下三点调整即可:
- 只在最上层父视图初始化一次ViewModel,作为全局唯一数据源,禁止子视图自行初始化ViewModel
- 改造
TopActivityCardView,移除内部的全量数据遍历逻辑,改为接收单个活动条目作为入参,只负责渲染单张卡片的内容 - 调整布局层级,用
ZStack层叠实现UI效果,替代容易出现内容堆叠的多层.overlay()嵌套,移除子视图内部多余的ScrollView避免内容错位。
修正后的代码
TopActivityCardView 改造
import SwiftUI // TopActivityCardView 只负责单张卡片内容展示,不持有数据源 struct TopActivityCardView: View { // 入参:单条活动数据,由父视图传入 let item: ActivityItem var body: some View { RoundedRectangle(cornerRadius: 20) .foregroundColor(Color.darkBlue) .frame(height: 120, alignment: .center) .overlay( ActivityTitleView(activityTitle: item.title) ) .overlay( ActivityHoursView(workHours: item.timeframes.weekly.current) ) .overlay( ActivityEllipisView() ) .overlay( TimePeriodView( timePeriod: "Last Week", weeklyHrs: item.timeframes.weekly.previous ) ) } } struct TopActivityCardView_Previews: PreviewProvider { static var previews: some View { // 预览时传入测试数据即可 TopActivityCardView(item: ActivityItem( title: "Work", timeframes: TimeFrame(weekly: (current: 32, previous: 36)), category: "work" )) } }
RectCardView 改造
import SwiftUI struct RectCardView: View { // 唯一数据源:只在这里初始化一次ViewModel @StateObject var vm = ViewModel() // 卡片配置:颜色、图标、对应的数据分类标识 private let cardConfigs: [(color: Color, icon: String, category: String)] = [ (Color.work, "icon-work", "work"), (Color.play, "icon-play", "play"), (Color.study, "icon-study", "study"), (Color.exercise, "icon-exercise", "exercise"), (Color.social, "icon-social", "social"), (Color.selfCare, "icon-self-care", "selfCare") ] var body: some View { ScrollView { VStack(spacing: 20) { ForEach(cardConfigs, id: \.category) { config in // 匹配当前卡片对应的单条活动数据 if let matchedItem = vm.fetch().first(where: { $0.category == config.category }) { ZStack(alignment: .top) { // 背景层:彩色底+右上角图标 RoundedRectangle(cornerRadius: 20) .foregroundColor(config.color) .frame(height: 110) .overlay( Image(config.icon) .brightness(-0.2) .frame(width: 60, height: 20, alignment: .topTrailing) .offset(x: -16, y: -10) ) .clipped() // 内容层:深色内容卡片,用offset实现设计稿的错位效果 TopActivityCardView(item: matchedItem) .offset(y: 40) } .frame(height: 160) .clipped() } } } .padding(.horizontal) } } } struct RectCardView_Previews: PreviewProvider { static var previews: some View { RectCardView() } }
注:上述代码中用到的
ActivityItem、TimeFrame是和接口返回字段对齐的数据模型,如果你已有对应模型,直接替换成自己的模型即可,只要保证能通过分类标识匹配到对应卡片的数据。
避坑提示
- 不要在
ForEach循环内部创建@StateObject实例,会导致SwiftUI复用视图时出现状态错乱、数据不一致问题 - 纯展示类的子视图不要持有ViewModel,直接接收父视图传入的展示数据即可,保证整个页面只有一个数据源
- 复杂层叠布局优先用
ZStack实现,不要无限制嵌套.overlay(),否则很容易出现布局层级不可控、内容堆叠遮挡的问题
内容的提问来源于stack exchange,提问作者RachelRadford
相关产品推荐
相关产品推荐

