重构相册详情为信息卡片、Tab 筛选、排序、批量删除与底部上传栏;修复网格重叠、禁用按钮蒙层,并支持点击预览大图。同步扩展素材列表 API 与 ViewModel 分页逻辑,并优化有线传图缩略图与传输性能。 Co-authored-by: Cursor <cursoragent@cursor.com>
636 lines
20 KiB
Swift
636 lines
20 KiB
Swift
//
|
||
// TravelAlbumModels.swift
|
||
// suixinkan
|
||
//
|
||
// Created by Codex on 2026/6/29.
|
||
//
|
||
|
||
import Foundation
|
||
|
||
/// 旅拍相册用户摘要。
|
||
struct TravelAlbumUser: Decodable, Equatable, Hashable {
|
||
let id: Int
|
||
let phone: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id, phone
|
||
}
|
||
|
||
init(id: Int = 0, phone: String = "") {
|
||
self.id = id
|
||
self.phone = phone
|
||
}
|
||
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
id = container.lossyInt(forKey: .id)
|
||
phone = container.lossyString(forKey: .phone)
|
||
}
|
||
}
|
||
|
||
/// 旅拍相册任务实体。
|
||
struct TravelAlbumItem: Decodable, Identifiable, Equatable, Hashable {
|
||
let id: Int
|
||
let storeUserId: Int
|
||
let name: String
|
||
/// 1 先拍后买,2 先买后拍
|
||
let type: Int
|
||
let orderNumber: String
|
||
let materialNum: Int
|
||
let materialPrice: Int
|
||
let materialPackagePrice: Int
|
||
let photoPrice: Int
|
||
let coverURL: String
|
||
let userId: Int
|
||
let status: Int
|
||
let createdAt: String
|
||
let updatedAt: String
|
||
let user: TravelAlbumUser?
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case storeUserId = "store_user_id"
|
||
case name, type
|
||
case orderNumber = "order_number"
|
||
case materialNum = "material_num"
|
||
case materialPrice = "material_price"
|
||
case materialPackagePrice = "material_package_price"
|
||
case photoPrice = "photo_price"
|
||
case coverURL = "cover_url"
|
||
case userId = "user_id"
|
||
case status
|
||
case createdAt = "created_at"
|
||
case updatedAt = "updated_at"
|
||
case user
|
||
}
|
||
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
id = container.lossyInt(forKey: .id)
|
||
storeUserId = container.lossyInt(forKey: .storeUserId)
|
||
name = container.lossyString(forKey: .name)
|
||
type = container.lossyInt(forKey: .type)
|
||
orderNumber = container.lossyString(forKey: .orderNumber)
|
||
materialNum = container.lossyInt(forKey: .materialNum)
|
||
materialPrice = container.lossyInt(forKey: .materialPrice)
|
||
materialPackagePrice = container.lossyInt(forKey: .materialPackagePrice)
|
||
photoPrice = container.lossyInt(forKey: .photoPrice)
|
||
coverURL = container.lossyString(forKey: .coverURL)
|
||
userId = container.lossyInt(forKey: .userId)
|
||
status = container.lossyInt(forKey: .status)
|
||
createdAt = container.lossyString(forKey: .createdAt)
|
||
updatedAt = container.lossyString(forKey: .updatedAt)
|
||
user = try? container.decodeIfPresent(TravelAlbumUser.self, forKey: .user)
|
||
}
|
||
|
||
var displayPhone: String {
|
||
user?.phone ?? ""
|
||
}
|
||
}
|
||
|
||
/// 可绑定订单实体(买了再拍)。
|
||
struct TravelAlbumAvailableOrder: Decodable, Identifiable, Equatable, Hashable {
|
||
let projectName: String
|
||
let orderNumber: String
|
||
let userPhone: String
|
||
let userId: Int
|
||
|
||
var id: String { orderNumber }
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case projectName = "project_name"
|
||
case orderNumber = "order_number"
|
||
case userPhone = "user_phone"
|
||
case userId = "user_id"
|
||
}
|
||
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
projectName = container.lossyString(forKey: .projectName)
|
||
orderNumber = container.lossyString(forKey: .orderNumber)
|
||
userPhone = container.lossyString(forKey: .userPhone)
|
||
userId = container.lossyInt(forKey: .userId)
|
||
}
|
||
}
|
||
|
||
/// 旅拍相册素材实体。
|
||
struct TravelAlbumMaterial: Decodable, Identifiable, Equatable {
|
||
let id: Int
|
||
let userEquityTravelId: Int
|
||
let status: Int
|
||
let orderNumber: String
|
||
let userId: Int
|
||
let fileName: String
|
||
let fileType: Int
|
||
let fileURL: String
|
||
let fileSize: Int
|
||
let coverURL: String
|
||
let isPurchased: Bool
|
||
let createdAt: String
|
||
let updatedAt: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
case userEquityTravelId = "user_equity_travel_id"
|
||
case status
|
||
case orderNumber = "order_number"
|
||
case userId = "user_id"
|
||
case fileName = "file_name"
|
||
case fileType = "file_type"
|
||
case fileURL = "file_url"
|
||
case fileSize = "file_size"
|
||
case coverURL = "cover_url"
|
||
case isPurchased = "is_purchased"
|
||
case createdAt = "created_at"
|
||
case updatedAt = "updated_at"
|
||
}
|
||
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
id = container.lossyInt(forKey: .id)
|
||
userEquityTravelId = container.lossyInt(forKey: .userEquityTravelId)
|
||
status = container.lossyInt(forKey: .status)
|
||
orderNumber = container.lossyString(forKey: .orderNumber)
|
||
userId = container.lossyInt(forKey: .userId)
|
||
fileName = container.lossyString(forKey: .fileName)
|
||
fileType = container.lossyInt(forKey: .fileType)
|
||
fileURL = container.lossyString(forKey: .fileURL)
|
||
fileSize = container.lossyInt(forKey: .fileSize)
|
||
coverURL = container.lossyString(forKey: .coverURL)
|
||
isPurchased = container.lossyBool(forKey: .isPurchased)
|
||
createdAt = container.lossyString(forKey: .createdAt)
|
||
updatedAt = container.lossyString(forKey: .updatedAt)
|
||
}
|
||
|
||
/// 预览用大图 URL,优先原图地址。
|
||
var previewURLString: String {
|
||
if !fileURL.isEmpty { return fileURL }
|
||
return coverURL
|
||
}
|
||
|
||
/// 列表缩略图 URL,优先封面。
|
||
var thumbnailURLString: String {
|
||
if !coverURL.isEmpty { return coverURL }
|
||
return fileURL
|
||
}
|
||
}
|
||
|
||
/// 创建旅拍相册请求。
|
||
struct TravelAlbumCreateRequest: Encodable {
|
||
let name: String
|
||
let type: Int
|
||
let orderNumber: String?
|
||
let materialNum: Int?
|
||
let materialPrice: Double?
|
||
let materialPackagePrice: Double?
|
||
let photoPrice: Double?
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case name, type
|
||
case orderNumber = "order_number"
|
||
case materialNum = "material_num"
|
||
case materialPrice = "material_price"
|
||
case materialPackagePrice = "material_package_price"
|
||
case photoPrice = "photo_price"
|
||
}
|
||
}
|
||
|
||
/// 创建旅拍相册响应。
|
||
struct TravelAlbumCreateResponse: Decodable {
|
||
let id: Int
|
||
|
||
init(id: Int) {
|
||
self.id = id
|
||
}
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id
|
||
}
|
||
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
id = container.lossyInt(forKey: .id)
|
||
}
|
||
}
|
||
|
||
/// 上传素材登记请求。
|
||
struct TravelAlbumUploadMaterialRequest: Encodable {
|
||
let userEquityTravelId: Int
|
||
let fileName: String
|
||
let fileURL: String
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case userEquityTravelId = "user_equity_travel_id"
|
||
case fileName = "file_name"
|
||
case fileURL = "file_url"
|
||
}
|
||
}
|
||
|
||
/// 删除旅拍相册请求。
|
||
struct TravelAlbumDeleteRequest: Encodable {
|
||
let id: Int
|
||
}
|
||
|
||
/// 编辑旅拍相册请求。
|
||
struct TravelAlbumEditRequest: Encodable {
|
||
let id: Int
|
||
let name: String?
|
||
let materialPackagePrice: Double?
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case id, name
|
||
case materialPackagePrice = "material_package_price"
|
||
}
|
||
}
|
||
|
||
/// 删除素材请求。
|
||
struct TravelAlbumDeleteMaterialRequest: Encodable {
|
||
let id: Int
|
||
}
|
||
|
||
/// 小程序码响应。
|
||
struct TravelAlbumMpCodeResponse: Decodable {
|
||
let mpCodeOssURL: String
|
||
|
||
init(mpCodeOssURL: String) {
|
||
self.mpCodeOssURL = mpCodeOssURL
|
||
}
|
||
|
||
enum CodingKeys: String, CodingKey {
|
||
case mpCodeOssURL = "mp_code_oss_url"
|
||
}
|
||
|
||
init(from decoder: Decoder) throws {
|
||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||
mpCodeOssURL = container.lossyString(forKey: .mpCodeOssURL)
|
||
}
|
||
}
|
||
|
||
/// 有线传图页导航上下文。
|
||
struct WiredTransferContext: Hashable {
|
||
let albumId: Int
|
||
let albumName: String
|
||
let phone: String
|
||
let orderNumber: String
|
||
}
|
||
|
||
/// 有线传图上传状态。
|
||
enum WiredTransferUploadStatus: String, Codable, Equatable {
|
||
case uploaded = "UPLOADED"
|
||
case pending = "PENDING"
|
||
case transferring = "TRANSFERRING"
|
||
case uploading = "UPLOADING"
|
||
case failed = "FAILED"
|
||
|
||
var displayLabel: String {
|
||
switch self {
|
||
case .uploaded: "已上传"
|
||
case .pending: "待上传"
|
||
case .transferring: "传输中"
|
||
case .uploading: "上传中"
|
||
case .failed: "上传失败"
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 有线传图时间侧栏时间段。
|
||
struct WiredTransferTimeSlot: Identifiable, Equatable {
|
||
let id: String
|
||
let dateLabel: String
|
||
let slotStartLabel: String
|
||
let photoCount: Int
|
||
}
|
||
|
||
/// 有线传图时间侧栏日期分组。
|
||
struct WiredTransferDateGroup: Equatable {
|
||
let dateLabel: String
|
||
let slots: [WiredTransferTimeSlot]
|
||
}
|
||
|
||
/// 有线传图列表分组(30 分钟时间段)。
|
||
struct WiredTransferPhotoSection: Identifiable, Equatable {
|
||
var id: String { slotId }
|
||
let slotId: String
|
||
let headerTitle: String
|
||
let photos: [WiredTransferPhotoItem]
|
||
}
|
||
|
||
/// 有线传图列表项。
|
||
struct WiredTransferPhotoItem: Identifiable, Equatable {
|
||
let id: String
|
||
let sourceId: String
|
||
let fileName: String
|
||
/// 列表使用的缩略图 URL,优先指向本地小图或远程封面。
|
||
let thumbnailURL: String
|
||
/// 预览使用的大图 URL,优先指向本地原图。
|
||
let previewURL: String
|
||
let capturedAt: String
|
||
let fileSizeText: String
|
||
let resolutionText: String
|
||
let status: WiredTransferUploadStatus
|
||
let progress: Int
|
||
let errorMessage: String?
|
||
|
||
init(
|
||
id: String,
|
||
sourceId: String? = nil,
|
||
fileName: String,
|
||
thumbnailURL: String,
|
||
previewURL: String = "",
|
||
capturedAt: String,
|
||
fileSizeText: String,
|
||
resolutionText: String = "",
|
||
status: WiredTransferUploadStatus,
|
||
progress: Int = 0,
|
||
errorMessage: String? = nil
|
||
) {
|
||
self.id = id
|
||
self.sourceId = sourceId ?? id
|
||
self.fileName = fileName
|
||
self.thumbnailURL = thumbnailURL
|
||
self.previewURL = previewURL
|
||
self.capturedAt = capturedAt
|
||
self.fileSizeText = fileSizeText
|
||
self.resolutionText = resolutionText
|
||
self.status = status
|
||
self.progress = progress
|
||
self.errorMessage = errorMessage
|
||
}
|
||
|
||
var canSelectForSpecifyUpload: Bool {
|
||
status == .pending
|
||
}
|
||
|
||
var isNotUploaded: Bool {
|
||
status == .pending || status == .failed
|
||
}
|
||
|
||
/// 解析拍摄时间。
|
||
func capturedDate() -> Date? {
|
||
Self.capturedAtFormatter.date(from: capturedAt)
|
||
}
|
||
|
||
/// 是否为今日拍摄。
|
||
func isCapturedToday(calendar: Calendar = .current) -> Bool {
|
||
guard let date = capturedDate() else { return false }
|
||
return calendar.isDateInToday(date)
|
||
}
|
||
|
||
private static let capturedAtFormatter: DateFormatter = {
|
||
let formatter = DateFormatter()
|
||
formatter.locale = Locale(identifier: "zh_CN")
|
||
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||
return formatter
|
||
}()
|
||
}
|
||
|
||
/// 有线传图持久化记录。
|
||
struct WiredTransferPhotoRecord: Codable, Equatable {
|
||
let id: String
|
||
let sourceId: String
|
||
let fileName: String
|
||
let localPath: String
|
||
let thumbnailPath: String
|
||
let capturedAt: String
|
||
let fileSizeBytes: Int64
|
||
let status: String
|
||
let progress: Int
|
||
let errorMessage: String?
|
||
let albumId: Int
|
||
let userId: String
|
||
let remoteURL: String
|
||
let updatedAt: TimeInterval
|
||
|
||
func toPhotoItem() -> WiredTransferPhotoItem {
|
||
let thumbnailCandidates = [thumbnailPath, remoteURL]
|
||
let thumbnailURL = thumbnailCandidates.compactMap { path -> String? in
|
||
guard !path.isEmpty else { return nil }
|
||
if path.hasPrefix("http") { return path }
|
||
let preview = CameraDownloadStorage.previewURLString(from: path)
|
||
return preview.isEmpty ? nil : preview
|
||
}.first ?? ""
|
||
let previewCandidates = [localPath, remoteURL, thumbnailPath]
|
||
let previewURL = previewCandidates.compactMap { path -> String? in
|
||
guard !path.isEmpty else { return nil }
|
||
if path.hasPrefix("http") { return path }
|
||
let preview = CameraDownloadStorage.previewURLString(from: path)
|
||
return preview.isEmpty ? nil : preview
|
||
}.first ?? ""
|
||
|
||
return WiredTransferPhotoItem(
|
||
id: id,
|
||
sourceId: sourceId,
|
||
fileName: fileName,
|
||
thumbnailURL: thumbnailURL,
|
||
previewURL: previewURL,
|
||
capturedAt: capturedAt,
|
||
fileSizeText: Self.formatFileSize(fileSizeBytes),
|
||
status: WiredTransferUploadStatus(rawValue: status) ?? .pending,
|
||
progress: progress,
|
||
errorMessage: errorMessage
|
||
)
|
||
}
|
||
|
||
static func formatFileSize(_ bytes: Int64) -> String {
|
||
guard bytes > 0 else { return "--" }
|
||
let mb = Double(bytes) / 1024.0 / 1024.0
|
||
if mb >= 1024 {
|
||
return String(format: "%.2f GB", mb / 1024.0)
|
||
}
|
||
return String(format: "%.2f MB", mb)
|
||
}
|
||
}
|
||
|
||
extension WiredTransferPhotoItem {
|
||
/// 转为持久化记录。
|
||
func toRecord(
|
||
albumId: Int,
|
||
userId: String,
|
||
localPath: String,
|
||
thumbnailPath: String = "",
|
||
fileSizeBytes: Int64,
|
||
remoteURL: String = ""
|
||
) -> WiredTransferPhotoRecord {
|
||
WiredTransferPhotoRecord(
|
||
id: id,
|
||
sourceId: sourceId,
|
||
fileName: fileName,
|
||
localPath: localPath,
|
||
thumbnailPath: thumbnailPath,
|
||
capturedAt: capturedAt,
|
||
fileSizeBytes: fileSizeBytes,
|
||
status: status.rawValue,
|
||
progress: progress,
|
||
errorMessage: errorMessage,
|
||
albumId: albumId,
|
||
userId: userId,
|
||
remoteURL: remoteURL,
|
||
updatedAt: Date().timeIntervalSince1970
|
||
)
|
||
}
|
||
}
|
||
|
||
extension Array where Element == WiredTransferPhotoItem {
|
||
/// 按 Tab 过滤:0 全部,1 已上传,2 失败。
|
||
func filtered(byTabIndex index: Int) -> [WiredTransferPhotoItem] {
|
||
switch index {
|
||
case 1: filter { $0.status == .uploaded }
|
||
case 2: filter { $0.status == .failed }
|
||
default: self
|
||
}
|
||
}
|
||
|
||
/// 返回各 Tab 计数。
|
||
var transferTabCounts: [Int] {
|
||
[count, filter { $0.status == .uploaded }.count, filter { $0.status == .failed }.count]
|
||
}
|
||
|
||
/// 可指定上传的 photo id 集合。
|
||
var selectablePendingPhotoIDs: Set<String> {
|
||
Set(filter { $0.canSelectForSpecifyUpload }.map(\.id))
|
||
}
|
||
|
||
/// 按拍摄时间倒序排列,最新照片在最前。
|
||
func sortedByCaptureTimeDescending() -> [WiredTransferPhotoItem] {
|
||
sorted { lhs, rhs in
|
||
let leftDate = lhs.capturedDate() ?? .distantPast
|
||
let rightDate = rhs.capturedDate() ?? .distantPast
|
||
if leftDate != rightDate { return leftDate > rightDate }
|
||
return lhs.id > rhs.id
|
||
}
|
||
}
|
||
|
||
/// 构建 30 分钟时间段分组列表。
|
||
func buildPhotoSections() -> [WiredTransferPhotoSection] {
|
||
let grouped = Dictionary(grouping: compactMap { photo -> (Date, WiredTransferPhotoItem)? in
|
||
guard let date = photo.capturedDate() else { return nil }
|
||
return (WiredTransferTimeGrouping.halfHourSlotStart(for: date), photo)
|
||
}) { $0.0 }
|
||
|
||
return grouped.keys
|
||
.sorted(by: >)
|
||
.map { slotStart in
|
||
let photos = (grouped[slotStart] ?? [])
|
||
.map(\.1)
|
||
.sorted { ($0.capturedDate() ?? .distantPast) > ($1.capturedDate() ?? .distantPast) }
|
||
return WiredTransferPhotoSection(
|
||
slotId: WiredTransferTimeGrouping.slotID(for: slotStart),
|
||
headerTitle: WiredTransferTimeGrouping.sectionHeaderTitle(for: slotStart),
|
||
photos: photos
|
||
)
|
||
}
|
||
}
|
||
|
||
/// 构建时间侧栏分组。
|
||
func buildSidebarGroups() -> [WiredTransferDateGroup] {
|
||
var slotCounts: [Date: Int] = [:]
|
||
for photo in self {
|
||
guard let date = photo.capturedDate() else { continue }
|
||
let slotStart = WiredTransferTimeGrouping.halfHourSlotStart(for: date)
|
||
slotCounts[slotStart, default: 0] += 1
|
||
}
|
||
|
||
let calendar = Calendar.current
|
||
let groupedByDay = Dictionary(grouping: slotCounts.keys) { calendar.startOfDay(for: $0) }
|
||
|
||
return groupedByDay.keys
|
||
.sorted(by: >)
|
||
.map { day in
|
||
let slots = (groupedByDay[day] ?? [])
|
||
.sorted(by: >)
|
||
.compactMap { slotStart -> WiredTransferTimeSlot? in
|
||
guard let count = slotCounts[slotStart] else { return nil }
|
||
return WiredTransferTimeSlot(
|
||
id: WiredTransferTimeGrouping.slotID(for: slotStart),
|
||
dateLabel: WiredTransferTimeGrouping.dateLabel(for: slotStart),
|
||
slotStartLabel: WiredTransferTimeGrouping.timeLabel(for: slotStart),
|
||
photoCount: count
|
||
)
|
||
}
|
||
return WiredTransferDateGroup(
|
||
dateLabel: WiredTransferTimeGrouping.dateLabel(for: day),
|
||
slots: slots
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 有线传图 30 分钟时间段分组工具。
|
||
enum WiredTransferTimeGrouping {
|
||
/// 计算 30 分钟时间段起点。
|
||
static func halfHourSlotStart(for date: Date, calendar: Calendar = .current) -> Date {
|
||
var components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)
|
||
let minute = (components.minute ?? 0) / 30 * 30
|
||
components.minute = minute
|
||
components.second = 0
|
||
components.nanosecond = 0
|
||
return calendar.date(from: components) ?? date
|
||
}
|
||
|
||
/// 时间段唯一标识。
|
||
static func slotID(for slotStart: Date) -> String {
|
||
slotIDFormatter.string(from: slotStart)
|
||
}
|
||
|
||
/// 分组标题,例如「05月20日 12:00 - 12:30」。
|
||
static func sectionHeaderTitle(for slotStart: Date) -> String {
|
||
"\(dateLabel(for: slotStart)) \(timeLabel(for: slotStart)) - \(timeLabel(for: slotStart.addingTimeInterval(30 * 60)))"
|
||
}
|
||
|
||
/// 日期标签,例如「05月20日」。
|
||
static func dateLabel(for date: Date) -> String {
|
||
dateLabelFormatter.string(from: date)
|
||
}
|
||
|
||
/// 时间标签,例如「12:05」。
|
||
static func timeLabel(for date: Date) -> String {
|
||
timeLabelFormatter.string(from: date)
|
||
}
|
||
|
||
private static let slotIDFormatter: DateFormatter = {
|
||
let formatter = DateFormatter()
|
||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm"
|
||
return formatter
|
||
}()
|
||
|
||
private static let dateLabelFormatter: DateFormatter = {
|
||
let formatter = DateFormatter()
|
||
formatter.locale = Locale(identifier: "zh_CN")
|
||
formatter.dateFormat = "MM月dd日"
|
||
return formatter
|
||
}()
|
||
|
||
private static let timeLabelFormatter: DateFormatter = {
|
||
let formatter = DateFormatter()
|
||
formatter.locale = Locale(identifier: "zh_CN")
|
||
formatter.dateFormat = "HH:mm"
|
||
return formatter
|
||
}()
|
||
}
|
||
|
||
private extension KeyedDecodingContainer {
|
||
func lossyString(forKey key: Key) -> String {
|
||
if let value = try? decode(String.self, forKey: key) { return value }
|
||
if let value = try? decode(Int.self, forKey: key) { return "\(value)" }
|
||
if let value = try? decode(Double.self, forKey: key) { return "\(value)" }
|
||
return ""
|
||
}
|
||
|
||
func lossyInt(forKey key: Key) -> Int {
|
||
if let value = try? decode(Int.self, forKey: key) { return value }
|
||
if let value = try? decode(String.self, forKey: key), let intValue = Int(value) { return intValue }
|
||
if let value = try? decode(Double.self, forKey: key) { return Int(value) }
|
||
return 0
|
||
}
|
||
|
||
func lossyBool(forKey key: Key) -> Bool {
|
||
if let value = try? decode(Bool.self, forKey: key) { return value }
|
||
if let value = try? decode(Int.self, forKey: key) { return value != 0 }
|
||
if let value = try? decode(String.self, forKey: key) {
|
||
return value == "1" || value.lowercased() == "true"
|
||
}
|
||
return false
|
||
}
|
||
}
|