SwiftUI中如何在Firebase的Map数组内为指定日期添加时间?
在SwiftUI中向Firebase数组内对象的指定字段添加元素的正确实现方法
问题背景
Firebase中存储的数据结构如下:
"booking_days": [ { "date": "__Timestamp__2023-11-05T05:11:54.301Z", "day_off": false, "time": [ "11:30" ] }, { "date": "__Timestamp__2023-11-04T05:20:17.640Z", "day_off": false, "time": [ "14:30" ] } ]
需求:当请求日期与booking_days数组中某个元素的date匹配时,向该元素的time数组添加新时间。预期结果如下:
"booking_days": [ { "date": "__Timestamp__2023-11-05T05:11:54.301Z", "day_off": false, "time": [ "11:30", "12:30" ] }, { "date": "__Timestamp__2023-11-04T05:20:17.640Z", "day_off": false, "time": [ "14:30" ] } ]
尝试的代码无法实现预期效果:
func setBookingTimeForExistingDay(userId: String, bookingDays: BookingDay, existingDay: Date) async throws { guard let bookingDay = try? encoder.encode(bookingDays) else { throw URLError(.badURL) } let orderBookingTime: [String : Any] = [DBPortfolioModel.CodingKeys.bookingDays.rawValue : FieldValue.arrayUnion([bookingDay])] try await portfolioUserDocument(userId: userId).setData(orderBookingTime, mergeFields: ["\(existingDay)"]) }
问题分析
原代码存在两个核心错误:
- 使用
FieldValue.arrayUnion([bookingDay])会尝试将整个BookingDay对象添加到booking_days数组中,而非修改已有对象的time数组 mergeFields参数传入"\(existingDay)"完全无效,该参数需指定要合并的字段路径,而非日期字符串
正确实现方案
Firebase无法直接通过条件匹配更新数组内的嵌套对象,需先定位到目标元素,再更新其内部字段。以下提供两种可行方案:
方案一:直接通过索引路径更新
先查询文档找到目标日期在数组中的索引,再通过精确路径更新time数组:
func addBookingTimeToExistingDay(userId: String, targetDate: Date, newTime: String) async throws { // 获取用户文档引用 let docRef = portfolioUserDocument(userId: userId) let snapshot = try await docRef.getDocument() // 提取booking_days数组数据 guard var bookingDays = snapshot.data()?[DBPortfolioModel.CodingKeys.bookingDays.rawValue] as? [[String: Any]] else { throw NSError(domain: "FirebaseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "未找到booking_days数据"]) } // 解析日期并找到匹配的元素索引 let dateFormatter = ISO8601DateFormatter() dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] let calendar = Calendar.current var targetIndex: Int? for (index, day) in bookingDays.enumerated() { guard let dateString = day["date"] as? String, let cleanDateString = dateString.components(separatedBy: "__Timestamp__").last, let dayDate = dateFormatter.date(from: cleanDateString) else { continue } // 比较日期(忽略时间,仅匹配同一天) if calendar.isDate(dayDate, inSameDayAs: targetDate) { targetIndex = index break } } guard let index = targetIndex else { throw NSError(domain: "FirebaseError", code: -2, userInfo: [NSLocalizedDescriptionKey: "未找到匹配的日期"]) } // 构建更新路径,添加新时间(自动去重) let updatePath = "\(DBPortfolioModel.CodingKeys.bookingDays.rawValue)[\(index)].time" let updates: [String: Any] = [ updatePath: FieldValue.arrayUnion([newTime]) ] // 执行更新 try await docRef.updateData(updates) }
方案二:使用自定义模型处理
通过解码为BookingDay模型修改后再编码写回,代码可读性更高:
// 自定义BookingDay模型,适配Firebase的日期格式 struct BookingDay: Codable { var date: Date var day_off: Bool var time: [String] enum CodingKeys: String, CodingKey { case date, day_off, time } // 解码处理__Timestamp__前缀 init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let dateString = try container.decode(String.self, forKey: .date) let cleanDateString = dateString.replacingOccurrences(of: "__Timestamp__", with: "") let dateFormatter = ISO8601DateFormatter() dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] guard let date = dateFormatter.date(from: cleanDateString) else { throw DecodingError.dataCorruptedError(forKey: .date, in: container, debugDescription: "日期格式无效") } self.date = date self.day_off = try container.decode(Bool.self, forKey: .day_off) self.time = try container.decode([String].self, forKey: .time) } // 编码添加__Timestamp__前缀 func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) let dateFormatter = ISO8601DateFormatter() dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] let dateString = "__Timestamp__\(dateFormatter.string(from: date))" try container.encode(dateString, forKey: .date) try container.encode(day_off, forKey: .day_off) try container.encode(time, forKey: .time) } } // 更新方法 func addBookingTimeToExistingDay(userId: String, targetDate: Date, newTime: String) async throws { let docRef = portfolioUserDocument(userId: userId) let snapshot = try await docRef.getDocument() guard let data = snapshot.data(), let bookingDaysData = data[DBPortfolioModel.CodingKeys.bookingDays.rawValue] as? [[String: Any]] else { throw NSError(domain: "FirebaseError", code: -1, userInfo: [NSLocalizedDescriptionKey: "未找到booking_days数据"]) } // 解码为模型数组 var bookingDays: [BookingDay] = try bookingDaysData.map { let jsonData = try JSONSerialization.data(withJSONObject: $0) return try JSONDecoder().decode(BookingDay.self, from: jsonData) } // 找到目标日期并添加时间 let calendar = Calendar.current if let index = bookingDays.firstIndex(where: { calendar.isDate($0.date, inSameDayAs: targetDate) }) { // 可选:去重避免重复时间 if !bookingDays[index].time.contains(newTime) { bookingDays[index].time.append(newTime) } // 编码回字典数组 let updatedBookingDays: [[String: Any]] = try bookingDays.map { let jsonData = try JSONEncoder().encode($0) return try JSONSerialization.jsonObject(with: jsonData) as! [String: Any] } // 更新文档 try await docRef.updateData([ DBPortfolioModel.CodingKeys.bookingDays.rawValue: updatedBookingDays ]) } else { throw NSError(domain: "FirebaseError", code: -2, userInfo: [NSLocalizedDescriptionKey: "未找到匹配的日期"]) } }
注意事项
- 日期比较时建议使用
Calendar.current.isDate(_:inSameDayAs:),避免时区或时间戳精度导致的匹配失败 - 使用
FieldValue.arrayUnion会自动忽略重复的时间值,无需额外去重(方案一);方案二中可手动判断避免重复 - 确保
portfolioUserDocument(userId:)方法返回正确的Firestore文档引用
内容的提问来源于stack exchange,提问作者Dmitry
相关产品推荐
相关产品推荐

