当前位置: 首页 > news >正文

SwiftUI DatePicker实战:打造一个旅行计划App(含完整代码)

SwiftUI DatePicker实战:构建旅行计划App的进阶技巧

每次规划旅行时,最让人头疼的莫过于安排行程日期。作为iOS开发者,我们可以用SwiftUI的DatePicker组件为用户打造流畅的日期选择体验。不同于基础教程,本文将带你深入实战,通过构建一个功能完善的旅行计划App,掌握DatePicker的高级应用技巧。

1. 旅行App的日期选择架构设计

在开始编码前,合理的架构设计能避免后期大量重构。旅行App通常需要处理三类日期时间数据:

  • 出发日期:用户旅程开始的日期
  • 返回日期:旅程结束的日期(必须晚于出发日期)
  • 出发时间:具体出发时刻
struct TravelPlan: Identifiable { let id = UUID() var departureDate: Date var returnDate: Date var departureTime: Date var destination: String }

状态管理方案对比

方案优点缺点适用场景
@State简单直接难以跨视图共享单一视图简单状态
@ObservedObject可复用需要额外创建类中等复杂度组件
@EnvironmentObject全局访问过度使用会导致依赖混乱应用级共享状态

对于我们的旅行计划App,采用@State@Binding的组合最为合适:

struct TravelPlanView: View { @State private var departureDate = Date() @State private var returnDate = Calendar.current.date(byAdding: .day, value: 3, to: Date())! @State private var departureTime = Date() // 日期范围限制 private let minDate = Date() private let maxDate = Calendar.current.date(byAdding: .month, value: 6, to: Date())! }

2. 智能日期范围限制实现

基础日期范围限制使用in参数即可实现,但旅行场景需要更智能的逻辑:

DatePicker("出发日期", selection: $departureDate, in: minDate...maxDate, displayedComponents: .date) .onChange(of: departureDate) { newDate in // 自动调整返回日期,确保不早于出发日 if returnDate < newDate { returnDate = newDate } }

进阶技巧:节假日自动标记

func isHoliday(_ date: Date) -> Bool { let calendar = Calendar.current let components = calendar.dateComponents([.month, .day], from: date) // 示例节假日判断(实际应使用更完整的节假日库) return (components.month == 1 && components.day == 1) || // 元旦 (components.month == 10 && components.day == 1) // 国庆 } DatePicker("出发日期", selection: $departureDate) .datePickerStyle(.graphical) .environment(\.calendar, Calendar.current) .background { if isHoliday(departureDate) { Color.orange.opacity(0.2) } }

3. 多样式DatePicker的混合应用

不同场景适合不同的DatePicker样式,我们的旅行App可以这样组合:

出发日期选择- 图形日历式:

Section(header: Text("行程日期")) { DatePicker("出发日期", selection: $departureDate, displayedComponents: .date) .datePickerStyle(.graphical) DatePicker("返回日期", selection: $returnDate, in: departureDate...maxDate, displayedComponents: .date) .datePickerStyle(.graphical) }

出发时间选择- 滚轮式:

Section(header: Text("出发时间")) { DatePicker("选择时间", selection: $departureTime, displayedComponents: .hourAndMinute) .datePickerStyle(.wheel) .labelsHidden() }

快速选择预设- 紧凑式:

HStack { Text("行程时长") Spacer() DatePicker("", selection: $returnDate, in: departureDate...maxDate, displayedComponents: .date) .datePickerStyle(.compact) }

4. 实时响应与数据联动

优秀的日期选择体验应该实时反馈用户选择:

// 计算行程天数 var tripDuration: Int { Calendar.current.dateComponents([.day], from: departureDate, to: returnDate).day! + 1 } // 在界面中显示 Section(header: Text("行程摘要")) { LabeledContent("出发日期", value: departureDate.formatted(.dateTime.day().month().year())) LabeledContent("返回日期", value: returnDate.formatted(.dateTime.day().month().year())) LabeledContent("出发时间", value: departureTime.formatted(.dateTime.hour().minute())) LabeledContent("行程天数", value: "\(tripDuration)天") }

日期变化时的副作用处理

.onChange(of: departureDate) { [departureDate] newDate in let daysAdded = Calendar.current .dateComponents([.day], from: departureDate, to: newDate) .day! // 保持返回日期与出发日期的相对间隔 returnDate = Calendar.current .date(byAdding: .day, value: daysAdded, to: returnDate)! // 自动调整到合理时间范围 if returnDate > maxDate { returnDate = maxDate } }

5. 性能优化与用户体验提升

当日期选择器频繁更新时,需要注意性能问题:

防抖处理

@State private var debounceTimer: Timer? DatePicker("出发日期", selection: $departureDate) .onChange(of: departureDate) { newDate in debounceTimer?.invalidate() debounceTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { _ in // 实际处理逻辑 updateRelatedData(for: newDate) } }

大日期范围优化

// 对于长期行程规划,使用月份导航更高效 DatePicker("出发日期", selection: $departureDate, in: minDate...maxDate, displayedComponents: .date) .datePickerStyle(.graphical) .environment(\.calendar, Calendar(identifier: .gregorian))

无障碍支持

DatePicker("出发日期", selection: $departureDate) .accessibilityLabel("选择出发日期") .accessibilityHint("当前选择: \(departureDate.formatted())") .accessibilityAction(named: "今天") { departureDate = Date() }

6. 完整实现与扩展功能

将上述功能整合成完整视图:

struct TravelPlanView: View { @State private var departureDate = Date() @State private var returnDate = Calendar.current.date(byAdding: .day, value: 3, to: Date())! @State private var departureTime = Date() @State private var destination = "" private let minDate = Date() private let maxDate = Calendar.current.date(byAdding: .month, value: 6, to: Date())! var body: some View { NavigationStack { Form { destinationSection dateSelectionSection timeSelectionSection summarySection } .navigationTitle("旅行计划") .toolbar { Button("保存") { savePlan() } } } } private var destinationSection: some View { Section(header: Text("目的地")) { TextField("输入旅行目的地", text: $destination) } } private var dateSelectionSection: some View { Section(header: Text("行程日期")) { DatePicker("出发日期", selection: $departureDate, in: minDate...maxDate, displayedComponents: .date) .datePickerStyle(.graphical) DatePicker("返回日期", selection: $returnDate, in: departureDate...maxDate, displayedComponents: .date) .datePickerStyle(.graphical) } } private var timeSelectionSection: some View { Section(header: Text("出发时间")) { DatePicker("选择时间", selection: $departureTime, displayedComponents: .hourAndMinute) .datePickerStyle(.wheel) } } private var summarySection: some View { Section(header: Text("行程摘要")) { LabeledContent("目的地", value: destination.isEmpty ? "未设置" : destination) LabeledContent("出发日期", value: departureDate.formatted(.dateTime.day().month().year())) LabeledContent("返回日期", value: returnDate.formatted(.dateTime.day().month().year())) LabeledContent("出发时间", value: departureTime.formatted(.dateTime.hour().minute())) LabeledContent("行程天数", value: "\(tripDuration)天") } } private var tripDuration: Int { Calendar.current.dateComponents([.day], from: departureDate, to: returnDate).day! + 1 } private func savePlan() { let newPlan = TravelPlan( departureDate: departureDate, returnDate: returnDate, departureTime: departureTime, destination: destination ) // 保存逻辑... } }

扩展功能建议

  • 添加重复行程功能(每周/每月固定出行)
  • 集成天气API,显示选定日期的天气预报
  • 实现多城市行程规划,每个城市可设置不同日期
  • 添加分享功能,生成行程图片分享给同伴
http://www.jsqmd.com/news/629656/

相关文章:

  • Vue项目实战:基于Element-UI的El-Select-Tree树形下拉选择器封装指南
  • SenseVoice Small政务舆情:市民热线→情感分析+热点话题聚类展示
  • 最火推荐130个毕业设计微信小程序源码下载
  • ESP8266 OTA升级实战:基于巴法云的极简实现方案
  • GitHub 高效使用指南【实战篇】
  • 从零构建MMRotate旋转检测实战:自定义数据集制作与模型调优全解析
  • 基于Python的PC微信自动化探索:uiautomation+OpenCV+EasyOCR疾
  • 从工业质检到元宇宙捏脸:结构光三维测量技术是如何悄悄改变我们生活的?
  • 高质量的OPCClient_UA源码分享:基于C#的OPC客户端开发源码集(测试稳定、多行业应...
  • 别再手动移植FreeRTOS了!用STM32CubeMX 6.9.0一键生成工程(附串口打印调试技巧)
  • IOFILE结构体的介绍与House of orange时
  • 影子货币:商家跑路,储值卡变成废纸
  • 清华大学PPT模板终极指南:专业学术演示的完整解决方案
  • vscode-drawio:在VS Code中无缝集成专业图表设计的5大核心技术特性
  • EtherLab IGH1.6.5新版本发布:7年等待后的全面升级
  • 当语音合成开始“自主选择语调”:2026奇点大会揭示LLM-TTS融合新范式,5大行业适配模板今日起仅开放24小时下载
  • 避坑指南:Godot 4.4 中 Dialogue Manager 3 插件常见报错分析与解决(附正确加载姿势)
  • 如何在 Ubuntu 22.04 LTS 上部署 Jenkins 自动化服务器?
  • 如何3分钟解锁拯救者Y7000 BIOS隐藏功能:终极免费指南
  • 代码智能体基础:自动写代码、调试、运行、优化
  • .NET 磁盘BitLocker加密-技术选型迪
  • 从RC低通滤波器入手:5分钟搞懂波特图增益与相位曲线的实际意义
  • GoldHEN作弊管理器:构建高效PS4游戏修改系统的技术实践
  • Centos7防火墙高级策略:利用rich-rule实现精细化IP访问控制
  • 【STM32F4 HAL库实战】EC11旋转编码器的精准解码与抗干扰设计
  • 从CSP-J网络连接题看IP地址与端口号的实战校验
  • 如何通过3个简单步骤实现IDM永久免费使用:开源脚本技术全解析
  • Windows任务栏透明化终极指南:TranslucentTB让你的桌面焕然一新
  • Qoder IDE 如何重塑AI编程的上下文工程
  • 【OpenHarmony】RK3568平台OpenHarmony 4.1源码编译实战:从环境搭建到镜像生成