SwiftUI中提前执行数据获取,为何unlockDate始终显示当前日期?
问题描述
我在SwiftUI中为BoughtItemCard创建了BoughtItemViewModel,用于从Firebase获取已购物品的解锁日期,希望在BoughtItemOverlay中展示这个日期。但实际运行时,视图始终显示当前日期,而非从Firebase获取到的解锁日期。
相关代码如下:
BoughtItemCard 代码
enum SomeType { case song(Song) } struct BoughtItemCard: View { @State var type: SomeType @StateObject var vm = BoughtItemViewModel() var body: some View { switch type { case .song(let song): WebImage(url: URL(string: song.image)) .resizable() .frame(width: 150, height: 150) .overlay(BoughtItemOverlay(type: type) .environmentObject(vm) .onAppear() { vm.fetchUnlockDate(path: "songs", docId: song.id ?? "") }) } } }
BoughtItemViewModel 代码
class BoughtItemViewModel: ObservableObject { @Published var unlockDate = Date() func fetchUnlockDate(path: String, docId: String) { let uid = FirebaseManager.shared.auth.currentUser?.uid ?? "" FirebaseManager.shared.firestore.collection(path) .document(docId).collection("unlocked").document(uid).getDocument { docSnapshot, error in if let error = error { print(error) return } let data = docSnapshot?.data() self.unlockDate = (data?["unlockDate"] as? Date ?? Date()) } } }
BoughtItemOverlay 代码
struct BoughtItemOverlay: View { @State var showModal: Bool = false @State var type: SomeType @State var unlockDate = Date() @EnvironmentObject var vm: BoughtItemViewModel var body: some View { switch type { case .song(let song): VStack { Spacer().onAppear() { unlockDate = vm.unlockDate } Text("Purchased \(unlockDate.formatted(.dateTime.day().month(.defaultDigits).year()))") } } } }
问题原因
- 异步数据同步时机问题:Firebase的
getDocument是异步操作,当Spacer的onAppear执行时,ViewModel的unlockDate还没从Firebase获取到最新值,此时赋值的是初始的当前日期。而且onAppear只会执行一次,后续ViewModel的unlockDate更新后,本地的@State var unlockDate不会自动同步。 - 本地State冗余:你在
BoughtItemOverlay中声明了本地的@State var unlockDate,但没有监听ViewModel中@Published的unlockDate变化,导致ViewModel数据更新时,视图无法感知并刷新。
解决办法
1. 移除本地冗余State,直接绑定ViewModel的Published属性
删除BoughtItemOverlay中的@State var unlockDate,直接使用vm.unlockDate。因为@Published属性会在值变化时自动触发视图刷新,无需手动同步。
2. 调整onAppear的位置(可选但更合理)
将onAppear移到BoughtItemCard的WebImage或整个Overlay外层,确保视图出现时正确触发数据请求,避免嵌套层级导致的执行时机问题。
修正后的代码
BoughtItemOverlay 修正版
struct BoughtItemOverlay: View { @State var showModal: Bool = false let type: SomeType // 改为let,type是传入参数无需修改 @EnvironmentObject var vm: BoughtItemViewModel var body: some View { switch type { case .song(let song): VStack { Spacer() Text("Purchased \(vm.unlockDate.formatted(.dateTime.day().month(.defaultDigits).year()))") } } } }
BoughtItemCard 修正版(可选优化)
struct BoughtItemCard: View { let type: SomeType // 改为let,参数无需用@State修饰 @StateObject var vm = BoughtItemViewModel() var body: some View { switch type { case .song(let song): WebImage(url: URL(string: song.image)) .resizable() .frame(width: 150, height: 150) .overlay( BoughtItemOverlay(type: type) .environmentObject(vm) ) .onAppear { vm.fetchUnlockDate(path: "songs", docId: song.id ?? "") } } } }
额外说明
- 使用
@EnvironmentObject传递ViewModel时,确保父视图已经正确初始化并注入环境对象(这里@StateObject的使用是正确的)。 - 如果需要避免重复调用
fetchUnlockDate(比如视图重复出现时),可以在ViewModel中添加一个标记,判断是否已经获取过数据,防止重复请求。
内容的提问来源于stack exchange,提问作者AdamLeet
相关产品推荐
相关产品推荐

