90、日历与地图开发全解析

日历与地图开发全解析

1. 日历事件与提醒设置

1.1 循环事件规则创建

循环事件规则可以通过 RRULE 进行定义,例如 RRULE:FREQ=YEARLY;INTERVAL=2;BYMONTH=1;BYDAY=SU 表示“每两年的一月的每个周日”。以下是创建该规则的代码示例:

let everySunday = EKRecurrenceDayOfWeek(.sunday)
let january = 1 as NSNumber
let recur = EKRecurrenceRule(
    recurrenceWith:.yearly, // every year
    interval:2, // no, every *two* years
    daysOfTheWeek:[everySunday],
    daysOfTheMonth:nil,
    monthsOfTheYear:[january],
    weeksOfTheYear:nil,
    daysOfTheYear:nil,
    setPositions: nil,
    end:nil)
let ev = EKEvent(eventStore:self.database)
ev.title = "Mysterious biennial Sunday-in-January morning ritual"
ev.addRecurrenceRule(recur)
ev.calendar = cal // assume we have our calendar
// need a start date and end date
let greg = Calendar(identifier:.gregorian)
var comp = DateComponents(year:2018, month:1, hour:10)
comp.weekday = 1 // Sunday
comp.weekdayOrdinal = 1 // *first* Sunday
ev.startDate = greg.date(from:comp)!
comp.hour = 11
ev.endDate = greg.date(from:comp)!
try self.database.save(ev, span:.futureEvents, commit:true)

在保存或删除循环事件时,需要指定 span 参数,其值可以是 .thisEvent .futureEvents ,对应日历界面中的两个按钮,分别表示仅影响当前事件或影响当前事件及未来所有重复事件。

1.2 提醒设置

提醒( EKReminder )与事件( EKEvent )类似,但 EKReminder 的 API 更现代。提醒具有日历、标题、备注、闹钟和循环规则,同时有开始日期、截止日期、完成日期和完成状态属性。以下是创建一个今日全天提醒的示例:

let cal = self.database.defaultCalendarForNewReminders()
let rem = EKReminder(eventStore:self.database)
rem.title = "Get bread"
rem.calendar = cal
let today = Date()
let greg = Calendar(identifier:.gregorian)
let comps : Set<Calendar.Component> = [.year, .month, .day]
rem.dueDateComponents = greg.dateComponents(comps, from:today)
try self.database.save(rem, commit:true)

1.3 proximity 闹钟设置

Proximity 闹钟(接近闹钟)会在用户接近或离开特定位置时触发,适用于提醒场景。以下是为提醒添加 proximity 闹钟的示例:

let alarm = EKAlarm()
let loc = EKStructuredLocation(title:"Trader Joe's")
loc.geoLocation = CLLocation(latitude:34.271848, longitude:-119.247714)
loc.radius = 10*1000 // meters
alarm.structuredLocation = loc
alarm.proximity = .enter // "geofence": we alarm when *arriving*
rem.addAlarm(alarm)

使用 proximity 闹钟需要位置服务授权,但此授权由提醒应用处理。如果提醒应用无法进行后台地理围栏,闹钟将不会触发(除非提醒应用处于前台)。

1.4 事件与提醒的获取

1.4.1 按标识符获取事件

可以通过事件的标识符( calendarItemIdentifier )快速唯一地获取事件,即使 EKEventStore 实例销毁,标识符仍然有效。

1.4.2 按谓词获取事件

可以通过匹配谓词( NSPredicate )从数据库中提取事件。首先需要指定开始和结束日期以及合格的日历数组,然后调用 EKEventStore predicateForEvents(withStart:end:calendars:) 方法。以下是一个示例:

let greg = Calendar(identifier:.gregorian)
let d = Date() // today
let d1 = greg.date(byAdding:DateComponents(year:-1), to:d)!
let d2 = greg.date(byAdding:DateComponents(year:2), to:d)!
let pred = self.database.predicateForEvents(withStart:
    d1, end:d2, calendars:[cal]) // assume we have our calendar
DispatchQueue.global(qos:.default).async {
    self.database.enumerateEvents(matching:pred) { ev, stop in
        if ev.title.range(of:"nap") != nil {
            self.napid = ev.calendarItemIdentifier
            stop.pointee = true
        }
    }
}

获取的事件没有特定顺序,可以使用 compareStartDate(with:) 方法按开始日期排序。事件的重复项被视为单独的事件,同一事件的重复项具有相同的 calendarItemIdentifier ,按标识符获取事件时将得到最早的事件。

1.4.3 获取提醒

获取提醒与获取事件类似,但更简单。调用 fetchReminders(matching:completion:) 方法时,可能的谓词可以让你获取给定日历中的所有提醒、未完成提醒或已完成提醒。该方法会异步调用完成函数,无需在后台线程中调用。

1.5 日历界面

EventKit UI 框架提供了三个视图控制器类,用于让用户处理事件和日历:
- EKEventViewController :显示单个事件的描述,可能可编辑。
- EKEventEditViewController :允许用户创建或编辑事件。
- EKCalendarChooser :允许用户选择日历。

这些视图控制器会自动监听数据库的变化,并在需要时刷新编辑的信息。

1.5.1 EKEventViewController

EKEventViewController 以日历应用熟悉的方式显示事件,包括标题、日期和时间、日历、提醒和备注。使用时需要实例化它,为其提供数据库中的事件,分配委托,并将其推送到现有的导航控制器中:

let ev = self.database.calendarItem(withIdentifier:self.napid) as! EKEvent
let evc = EKEventViewController()
evc.event = ev
evc.delegate = self
self.navigationController?.pushViewController(evc, animated: true)

如果 allowsEditing true ,导航栏中将出现编辑按钮,用户可以编辑事件的各个方面,包括删除事件。用户删除事件时,委托方法 eventViewController(_:didCompleteWith:) 将被调用,需要在该方法中弹出导航控制器。

1.5.2 EKEventEditViewController

EKEventEditViewController 用于编辑事件。使用时需要设置其 eventStore editViewDelegate ,可选地设置事件,并将其作为呈现的视图控制器呈现。事件可以为 nil 表示全新事件,也可以是新创建或数据库中的现有事件。需要实现委托方法 eventEditViewController(_:didCompleteWith:) 以关闭呈现的视图控制器。

1.5.3 EKCalendarChooser

EKCalendarChooser 显示日历列表,用户可以通过点击选择日历。使用时需要使用其初始化器 init(selectionStyle:displayStyle:entityType:eventStore:) 进行实例化,设置委托,将其作为导航控制器的根视图控制器并呈现。有三个委托方法,分别是 calendarChooserDidFinish(_:) calendarChooserDidCancel(_:) calendarChooserSelectionDidChange(_:) ,在完成和取消方法中需要关闭呈现的视图控制器。以下是一个删除所选日历的示例:

@IBAction func deleteCalendar (_ sender: Any) {
    let choo = EKCalendarChooser(
        selectionStyle:.single, displayStyle:.allCalendars,
        entityType:.event, eventStore:self.database)
    choo.showsDoneButton = true
    choo.showsCancelButton = true
    choo.delegate = self
    choo.navigationItem.prompt = "Pick a calendar to delete:"
    let nav = UINavigationController(rootViewController: choo)
    self.present(nav, animated: true)
}
func calendarChooserDidCancel(_ choo: EKCalendarChooser) {
    self.dismiss(animated:true)
}
func calendarChooserDidFinish(_ choo: EKCalendarChooser) {
    let cals = choo.selectedCalendars
    guard cals.count > 0 else { self.dismiss(animated:true); return }
    let calsToDelete = cals.map {$0.calendarIdentifier}
    let alert = UIAlertController(title:"Delete selected calendar?",
        message:nil, preferredStyle:.actionSheet)
    alert.addAction(UIAlertAction(title:"Cancel", style:.cancel))
    alert.addAction(UIAlertAction(title:"Delete", style:.destructive) {_ in
        for id in calsToDelete {
            if let cal = self.database.calendar(withIdentifier:id) {
                try? self.database.removeCalendar(cal, commit: true)
            }
        }
        self.dismiss(animated:true) // dismiss *everything*
    })
    choo.present(alert, animated: true)
}

1.6 日历相关操作总结

操作 描述 相关方法/类
创建循环事件 根据 RRULE 定义循环规则,设置事件属性并保存 EKRecurrenceRule EKEvent
创建提醒 设置提醒的标题、日历、截止日期等属性并保存 EKReminder
添加 proximity 闹钟 创建闹钟和结构化位置,设置闹钟属性并添加到提醒中 EKAlarm EKStructuredLocation
获取事件 按标识符或谓词获取事件,可对事件进行排序 EKEventStore NSPredicate
获取提醒 调用 fetchReminders 方法获取提醒 EKEventStore
日历界面操作 使用 EKEventViewController EKEventEditViewController EKCalendarChooser 处理事件和日历 EKEventViewController EKEventEditViewController EKCalendarChooser

1.7 日历操作流程图

graph TD;
    A[开始] --> B[选择操作类型];
    B --> C{创建事件};
    B --> D{创建提醒};
    B --> E{获取事件};
    B --> F{获取提醒};
    B --> G{日历界面操作};
    C --> H[定义循环规则];
    C --> I[设置事件属性];
    C --> J[保存事件];
    D --> K[设置提醒属性];
    D --> L[保存提醒];
    E --> M[按标识符或谓词获取];
    E --> N[事件排序];
    F --> O[调用 fetchReminders 方法];
    G --> P[选择视图控制器];
    G --> Q[设置相关属性和委托];
    G --> R[呈现视图控制器];
    J --> S[结束];
    L --> S;
    N --> S;
    O --> S;
    R --> S;

2. 地图显示

2.1 地图基础

应用可以模仿地图应用,通过 Map Kit 框架显示地图界面并在地图上放置注释和覆盖层。需要导入 MapKit 框架,用于描述经纬度的类来自 Core Location 框架,但如果已经导入 Map Kit 框架,则无需显式导入。

2.2 地图显示方式

2.2.1 地图类型

地图有多种类型( MKMapType ),常见的有:
- .standard :标准地图。
- .satellite :卫星地图。
- .hybrid :混合地图。
还有 .mutedStandard 类型,会淡化地图元素,使添加的内容更突出。

2.2.2 地图区域

地图显示的区域是 MKCoordinateRegion ,它由中心坐标( CLLocationCoordinate2D )和跨度( MKCoordinateSpan )组成。以下是初始化地图显示特定区域的示例:

let loc = CLLocationCoordinate2DMake(34.927752,-120.217608)
let span = MKCoordinateSpan(latitudeDelta: 0.015, longitudeDelta: 0.015)
let reg = MKCoordinateRegion(center:loc, span:span)
self.map.region = reg

如果已知区域的米制尺寸,可以使用 MKCoordinateRegion init(center:latitudinalMeters:longitudinalMeters:) 方法进行转换。

2.2.3 使用 MKMapRect 描述地图区域

还可以使用 MKMapRect 描述地图区域,它由 MKMapPoint MKMapSize 组成。可以通过 MKMapPoint(_:) 方法将经纬度坐标转换为地图点,通过 coordinate 属性将地图点转换为经纬度坐标,还可以使用 distance(to:) 方法计算地图点之间的距离,使用 MKMetersPerMapPointAtLatitude(_:) MKMapPointsPerMeterAtLatitude(_:) 方法获取点与米的比例。以下是使用 MKMapRect 显示大致相同区域的示例:

let loc = CLLocationCoordinate2DMake(34.927752,-120.217608)
let pt = MKMapPoint(loc)
let w = MKMapPointsPerMeterAtLatitude(loc.latitude) * 1200
self.map.visibleMapRect =
    MKMapRect(x:pt.x - w/2.0, y:pt.y - w/2.0, width:w, height:w)

为地图视图的 region visibleMapRect 属性赋值时,地图视图会优化显示而不扭曲地图比例,可以通过 regionThatFits(_:) mapRectThatFits(_:) mapRectThatFits(_:edgePadding:) 方法在代码中进行相同的优化。

2.3 地图交互与控制

2.3.1 用户交互

默认情况下,用户可以使用常规手势缩放和滚动地图,可以通过设置地图视图的 isZoomEnabled isScrollEnabled 属性来关闭这些功能。通常会将它们都设置为 true false ,还可以使用 UIGestureRecognizer 进一步自定义地图视图对触摸的响应。

2.3.2 编程方式改变地图显示区域

可以通过调用以下方法以编程方式改变地图显示的区域,可选地添加动画效果:
- setRegion(_:animated:)
- setCenter(_:animated:)
- setVisibleMapRect(_:animated:)
- setVisibleMapRect(_:edgePadding:animated:)

2.3.3 地图加载和区域变化通知

地图视图的委托( MKMapViewDelegate )会在地图加载和区域变化(包括编程触发的变化)时收到通知,相关方法包括:
- mapViewWillStartLoadingMap(_:)
- mapViewDidFinishLoadingMap(_:)
- mapViewDidFailLoadingMap(_:withError:)
- mapViewDidChangeVisibleRegion(_:)
- mapView(_:regionWillChangeAnimated:)
- mapView(_:regionDidChangeAnimated:)

2.4 地图组件显示

MKMapView 有一些布尔属性,如 showsCompass showsScale showsTraffic ,用于控制相应地图组件的显示。从 iOS 11 开始,指南针和比例尺可以作为独立视图( MKCompassButton MKScaleView )显示,使用时可能需要将相应的布尔属性设置为 false ,以避免出现两个指南针或比例尺。这两个视图的可见性由 compassVisibility scaleVisibility 属性控制,其值可以是:
- .hidden :隐藏。
- .visible :可见。
- .adaptive :自适应(默认),指南针仅在地图旋转时可见,比例尺仅在地图缩放时可见。

初次显示指南针或比例尺视图时可能会出现闪烁问题,解决方法是在将视图添加到界面之前将其 isHidden 属性设置为 true

2.5 3D 地图显示

可以启用地图的 3D 视图( pitchEnabled ),并且有强大的 API 可以控制 3D 视图。从 iOS 9 开始,有 3D 飞越地图类型 .satelliteFlyover .hybridFlyover

2.6 地图相关操作总结

操作 描述 相关方法/类
显示地图 选择地图类型,设置地图区域 MKMapView MKMapType MKCoordinateRegion
地图交互控制 控制用户缩放和滚动,编程改变显示区域 MKMapView UIGestureRecognizer
地图加载和区域变化通知 委托方法接收通知 MKMapViewDelegate
地图组件显示 控制指南针、比例尺和交通信息显示 MKMapView MKCompassButton MKScaleView
3D 地图显示 启用 3D 视图 MKMapView

2.7 地图操作流程图

graph TD;
    A[开始] --> B[创建 MKMapView];
    B --> C[选择地图类型];
    C --> D[设置地图区域];
    D --> E[用户交互设置];
    E --> F[编程改变显示区域];
    E --> G[设置委托接收通知];
    D --> H[地图组件显示设置];
    D --> I[3D 视图设置];
    F --> J[结束];
    G --> J;
    H --> J;
    I --> J;

综上所述,日历和地图开发涉及到多个方面的知识和操作,通过合理运用相关的类和方法,可以实现丰富的功能和良好的用户体验。在开发过程中,需要注意各种属性和方法的使用,以及可能出现的问题和解决方法。

3. 日历与地图开发的综合应用

3.1 结合日历事件与地图位置

在实际应用中,我们可以将日历事件与地图位置结合起来。例如,为日历中的事件添加一个关联的地图位置,当用户查看事件时,可以直接在地图上显示该位置。以下是一个简单的示例,展示如何在创建事件时关联地图位置:

// 创建事件
let ev = EKEvent(eventStore: self.database)
ev.title = "Meeting at a specific location"
ev.startDate = Date()
ev.endDate = Date().addingTimeInterval(3600) // 1 hour later
ev.calendar = cal

// 创建关联的地图位置
let loc = EKStructuredLocation(title: "Meeting Place")
loc.geoLocation = CLLocation(latitude: 34.0522, longitude: -118.2437) // Los Angeles
loc.radius = 100 // meters
ev.structuredLocation = loc

// 保存事件
try self.database.save(ev, span:.thisEvent, commit: true)

3.2 根据日历事件在地图上标记

我们可以根据日历中的事件信息,在地图上标记出事件的发生地点。以下是一个示例代码,展示如何从日历中获取事件,并在地图上标记出事件的位置:

// 获取日历中的事件
let greg = Calendar(identifier:.gregorian)
let d = Date() // today
let d1 = greg.date(byAdding: DateComponents(year: -1), to: d)!
let d2 = greg.date(byAdding: DateComponents(year: 2), to: d)!
let pred = self.database.predicateForEvents(withStart: d1, end: d2, calendars: [cal])
self.database.enumerateEvents(matching: pred) { ev, stop in
    if let location = ev.structuredLocation, let geoLocation = location.geoLocation {
        let annotation = MKPointAnnotation()
        annotation.title = ev.title
        annotation.coordinate = geoLocation.coordinate
        self.map.addAnnotation(annotation)
    }
}

3.3 综合应用操作总结

操作 描述 相关方法/类
事件关联位置 在创建事件时设置关联的地图位置 EKEvent EKStructuredLocation
地图标记事件 从日历中获取事件,在地图上标记事件位置 EKEventStore MKPointAnnotation MKMapView

3.4 综合应用流程图

graph TD;
    A[开始] --> B[创建日历事件];
    B --> C[关联地图位置];
    C --> D[保存事件到日历];
    D --> E[从日历获取事件];
    E --> F[检查事件位置];
    F --> G{有位置信息};
    G -->|是| H[创建地图标记];
    G -->|否| I[跳过标记];
    H --> J[在地图上添加标记];
    I --> K[结束];
    J --> K;

4. 开发注意事项与技巧

4.1 权限问题

  • 日历权限 :在使用日历相关功能时,需要请求日历访问权限。可以使用 EKEventStore requestAccess(to:completion:) 方法请求权限。示例代码如下:
let eventStore = EKEventStore()
eventStore.requestAccess(to:.event) { (granted, error) in
    if granted {
        // 权限已授予,可以进行日历操作
    } else {
        // 权限未授予,处理错误
    }
}
  • 位置权限 :使用 proximity 闹钟或地图相关功能时,需要请求位置访问权限。可以使用 CLLocationManager 来请求位置权限。

4.2 性能优化

  • 事件获取优化 :在获取大量事件时,尽量使用谓词进行筛选,避免获取不必要的事件。
  • 地图加载优化 :可以在地图加载完成后再进行标记添加等操作,避免影响地图加载性能。

4.3 错误处理

在进行日历和地图操作时,可能会出现各种错误,如权限错误、保存失败等。需要在代码中进行适当的错误处理,例如:

do {
    try self.database.save(ev, span:.thisEvent, commit: true)
} catch {
    print("Error saving event: \(error)")
}

4.4 开发注意事项总结

注意事项 描述 解决方法
权限问题 日历和位置权限需要请求 使用相应的方法请求权限
性能优化 避免不必要的事件获取和地图操作 使用谓词筛选事件,优化地图加载顺序
错误处理 操作可能出现各种错误 在代码中进行适当的错误处理

4.5 开发注意事项流程图

graph TD;
    A[开始开发] --> B[检查权限];
    B --> C{权限是否授予};
    C -->|是| D[进行操作];
    C -->|否| E[请求权限];
    E --> B;
    D --> F[性能优化];
    F --> G[执行操作];
    G --> H{操作是否成功};
    H -->|是| I[结束];
    H -->|否| J[错误处理];
    J --> D;

通过以上的介绍,我们了解了日历和地图开发的各个方面,包括事件和提醒的创建、获取,地图的显示和交互,以及它们的综合应用。同时,我们也掌握了开发过程中的注意事项和技巧。在实际开发中,我们可以根据具体需求,灵活运用这些知识,开发出功能丰富、性能良好的应用程序。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值