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

被折叠的 条评论
为什么被折叠?



