You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI中如何实现支持多粒度的艺术品日期选择器?

艺术品编目多精度日期UI实现方案

针对SwiftUI中处理多精度艺术品日期(含范围)的需求,以下是一套兼顾易用性和有效性的实现方案:

核心思路

  1. 用枚举明确区分不同精度的日期类型,与数据库模型直接对应,避免歧义
  2. 动态加载UI组件:根据用户选择的日期精度,显示对应的Picker组合,从根源避免无效日期(如年月模式不提供日选择)
  3. 内置日期范围校验,确保结束日期不早于开始日期

完整代码实现

1. 日期模型定义

先定义一个枚举来封装所有可能的日期类型:

enum ArtworkDate: Equatable, Comparable {
    case year(Int)
    case yearMonth(year: Int, month: Int)
    case fullDate(Date)
    case range(ArtworkDate, ArtworkDate)
    
    // 实现比较协议,用于范围校验
    static func < (lhs: ArtworkDate, rhs: ArtworkDate) -> Bool {
        func toComponents(_ date: ArtworkDate) -> DateComponents {
            switch date {
            case .year(let y): return DateComponents(year: y)
            case .yearMonth(let y, let m): return DateComponents(year: y, month: m)
            case .fullDate(let d): return Calendar.current.dateComponents([.year, .month, .day], from: d)
            case .range: return DateComponents()
            }
        }
        
        guard let lhsDate = Calendar.current.date(from: toComponents(lhs)),
              let rhsDate = Calendar.current.date(from: toComponents(rhs)) else {
            return false
        }
        return lhsDate < rhsDate
    }
}

2. UI组件实现

struct ArtworkDateInputView: View {
    @Binding var artworkDate: ArtworkDate
    @State private var selectedPrecision: DatePrecision = .fullDate
    @State private var isRangeMode = false
    @State private var startDate: ArtworkDate = .year(2000)
    @State private var endDate: ArtworkDate = .year(2000)
    
    enum DatePrecision {
        case year, yearMonth, fullDate
    }
    
    var body: some View {
        VStack(spacing: 16) {
            // 切换范围/单日期模式
            Toggle("日期范围", isOn: $isRangeMode)
                .toggleStyle(SwitchToggleStyle())
                .padding(.horizontal)
            
            if isRangeMode {
                HStack(spacing: 12) {
                    dateInputColumn(title: "开始日期", date: $startDate)
                    Text("-")
                        .font(.title)
                    dateInputColumn(title: "结束日期", date: $endDate)
                }
                // 自动校正范围顺序
                .onChange(of: startDate) { newValue in
                    if endDate < newValue {
                        endDate = newValue
                    }
                }
            } else {
                dateInputColumn(title: "创作日期", date: $artworkDate)
            }
        }
        .onAppear {
            // 初始化时同步单日期状态到范围组件
            if case .range(let start, let end) = artworkDate {
                isRangeMode = true
                startDate = start
                endDate = end
            } else {
                startDate = artworkDate
                endDate = artworkDate
            }
        }
        .onDisappear {
            // 保存范围模式的结果到绑定变量
            if isRangeMode {
                artworkDate = .range(startDate, endDate)
            }
        }
    }
    
    // 单个日期输入列
    private func dateInputColumn(title: String, date: Binding<ArtworkDate>) -> some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(title)
                .font(.caption)
                .foregroundColor(.secondary)
            
            // 精度选择器
            Picker("精度", selection: $selectedPrecision) {
                Text("仅年份").tag(DatePrecision.year)
                Text("年月").tag(DatePrecision.yearMonth)
                Text("完整日期").tag(DatePrecision.fullDate)
            }
            .pickerStyle(.segmented)
            
            // 根据精度显示对应选择器
            switch selectedPrecision {
            case .year:
                yearPicker(for: date)
            case .yearMonth:
                yearMonthPicker(for: date)
            case .fullDate:
                fullDatePicker(for: date)
            }
        }
        .frame(maxWidth: .infinity)
    }
    
    // 仅年份选择器
    private func yearPicker(for date: Binding<ArtworkDate>) -> some View {
        Picker("年份", selection: Binding(
            get: {
                switch date.wrappedValue {
                case .year(let y): return y
                case .yearMonth(let y, _): return y
                case .fullDate(let d): return Calendar.current.component(.year, from: d)
                case .range: return 2000
                }
            },
            set: { date.wrappedValue = .year($0) }
        )) {
            ForEach(1000...Calendar.current.component(.year, from: Date()), id: \.self) { year in
                Text("\(year)年").tag(year)
            }
        }
        .pickerStyle(.wheel)
    }
    
    // 年月选择器
    private func yearMonthPicker(for date: Binding<ArtworkDate>) -> some View {
        let yearBinding = Binding(
            get: {
                switch date.wrappedValue {
                case .year(let y): return y
                case .yearMonth(let y, _): return y
                case .fullDate(let d): return Calendar.current.component(.year, from: d)
                case .range: return 2000
                }
            },
            set: { newYear in
                let currentMonth = switch date.wrappedValue {
                case .yearMonth(_, let m): m
                default: 1
                }
                date.wrappedValue = .yearMonth(year: newYear, month: currentMonth)
            }
        )
        
        let monthBinding = Binding(
            get: {
                switch date.wrappedValue {
                case .yearMonth(_, let m): return m
                case .fullDate(let d): return Calendar.current.component(.month, from: d)
                default: 1
                }
            },
            set: { newMonth in
                date.wrappedValue = .yearMonth(year: yearBinding.wrappedValue, month: newMonth)
            }
        )
        
        HStack(spacing: 8) {
            Picker("年份", selection: yearBinding) {
                ForEach(1000...Calendar.current.component(.year, from: Date()), id: \.self) { year in
                    Text("\(year)年").tag(year)
                }
            }
            .pickerStyle(.wheel)
            
            Picker("月份", selection: monthBinding) {
                ForEach(1...12, id: \.self) { month in
                    Text(Calendar.current.monthSymbols[month-1]).tag(month)
                }
            }
            .pickerStyle(.wheel)
        }
    }
    
    // 完整日期选择器(系统自带,自动处理有效性)
    private func fullDatePicker(for date: Binding<ArtworkDate>) -> some View {
        DatePicker("日期", selection: Binding(
            get: {
                switch date.wrappedValue {
                case .fullDate(let d): return d
                case .year(let y): return Calendar.current.date(from: DateComponents(year: y)) ?? Date()
                case .yearMonth(let y, let m): return Calendar.current.date(from: DateComponents(year: y, month: m)) ?? Date()
                case .range: return Date()
                }
            },
            set: { date.wrappedValue = .fullDate($0) }
        ), displayedComponents: .date)
        .datePickerStyle(.wheel)
        .labelsHidden()
    }
}

方案优势

  • 无无效日期:不同精度对应不同选择器,年月模式不提供日选择,完整日期依赖系统DatePicker自动校验合法性
  • 状态同步:切换精度时自动保留已有日期信息(比如从完整日期切到年月,会保留对应的年和月)
  • 范围支持:内置范围校验,确保结束日期不会早于开始日期,且支持不同精度的范围组合(如2023年 - 2024年5月)
  • 扩展性强:可根据需求调整年份范围、添加自定义精度(如世纪),或针对特定艺术品类型预设默认精度

内容的提问来源于stack exchange,提问作者xskull.greymonx

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 05:55:44