数据持久化与文件处理技术详解
1. JSON数据结构
在数据处理中,我们会遇到一种特定的JSON数据结构,它可以表示为一个
Outer
对象数组,每个
Outer
对象包含一个未知属性,该属性的值是一个
Inner
对象数组。以下是具体示例:
[
Outer(categoryName: "Trending",
unknown:
[Inner(category: "Trending",
price: 20.5,
isFavourite: Optional(true),
isWatchlist: nil)
]),
Outer(categoryName: "Comedy",
unknown:
[Inner(category: "Comedy",
price: 24.32,
isFavourite: nil,
isWatchlist: Optional(false))
])
]
这个数据结构是面向对象的,与原始JSON数据相对应,它清晰地展示了不同类别及其相关属性的层级关系。
2. SQLite数据库
2.1 简介
SQLite是一款轻量级、功能齐全的关系型数据库,可使用通用的数据库语言SQL与之交互。当数据以行和列(记录和字段)的形式存在,且需要快速搜索时,SQLite是一种合适的存储格式。此外,它不会将整个数据库加载到内存中,仅在需要时访问数据,这在内存有限的iOS设备环境中非常有价值。
2.2 使用步骤
2.2.1 导入和配置
要使用SQLite,需导入
SQLite3
。不过,与SQLite交互的C接口较为复杂,可使用轻量级前端库
fmdb
(https://github.com/ccgus/fmdb )。由于
fmdb
是用Objective - C编写的,所以需要一个桥接头文件,并在其中导入
FMDB.h
。
2.2.2 创建数据库和表
以下是创建数据库并添加
people
表的示例代码:
let db = FMDatabase(path:self.dbpath)
db.open()
do {
db.beginTransaction()
try db.executeUpdate(
"create table people (lastname text, firstname text)",
values:nil)
try db.executeUpdate(
"insert into people (firstname, lastname) values (?,?)",
values:["Matt", "Neuburg"])
try db.executeUpdate(
"insert into people (firstname, lastname) values (?,?)",
values:["Snidely", "Whiplash"])
try db.executeUpdate(
"insert into people (firstname, lastname) values (?,?)",
values:["Dudley", "Doright"])
db.commit()
} catch {
db.rollback()
}
上述代码首先打开数据库,然后开始事务,创建
people
表并插入三条记录,最后提交事务。若出现错误,则回滚事务。
2.2.3 读取数据
读取数据库数据的代码如下:
let db = FMDatabase(path:self.dbpath)
db.open()
if let rs = try? db.executeQuery("select * from people", values:nil) {
while rs.next() {
if let firstname = rs["firstname"], let lastname = rs["lastname"] {
print(firstname, lastname)
}
}
}
db.close()
此代码打开数据库,执行查询语句,遍历结果集并打印出每个人的名字,最后关闭数据库。
2.3 注意事项
若要在应用程序包中包含预先构建的SQLite文件,但不能直接在其中写入数据。解决方法是在开始使用之前,将其从应用程序包复制到其他位置,如
Documents
目录。
3. Core Data框架
3.1 概述
Core Data框架(
import CoreData
)提供了一种通用的方式来表达形成关系图的对象和属性。它内置了将这些对象存储在持久化存储中的功能,通常使用SQLite作为文件格式,并仅在需要时从存储中读取数据,从而有效利用内存。
3.2 特点与难点
Core Data并非入门级技术,使用和调试都有一定难度。它的表达方式冗长、僵化且晦涩,有其独特的操作方式,这使得之前关于对象集合中对象的创建、访问、修改或删除的知识变得不再适用。同时,它也不能完全替代真正的关系型数据库。
3.3 项目构建
3.3.1 模板选择
从头构建Core Data项目时,最简单的方法是选择
Master–Detail App
模板(或
Single View App
模板),并在第二步中勾选
Use Core Data
。这样会在应用程序委托类中提供构建Core Data持久化栈的模板代码,大多数情况下无需对其进行重大修改。
3.3.2 持久化栈
持久化栈由三个对象组成:
-
托管对象模型(
NSManagedObjectModel
)
:描述数据的结构。
-
托管对象上下文(
NSManagedObjectContext
)
:用于与数据通信。
-
持久存储协调器(
NSPersistentStoreCoordinator
)
:处理数据作为文件的实际存储。
从iOS 10开始,
NSPersistentContainer
对象会为我们创建整个持久化栈。以下是模板代码提供的
NSPersistentContainer
的懒加载初始化示例:
lazy var persistentContainer: NSPersistentContainer = {
let con = NSPersistentContainer(name: "PeopleGroupsCoreData")
con.loadPersistentStores { desc, err in
if let err = err {
fatalError("Unresolved error \(err)")
}
}
return con
}()
3.3.3 托管对象上下文
托管对象上下文是
NSPersistentContainer
的
viewContext
,是与Core Data交互的关键。要获取对象,从托管对象上下文获取;要创建对象,将其插入托管对象上下文;要保存数据,保存托管对象上下文。为了方便应用程序其他部分访问托管对象上下文,根视图控制器有一个
managedObjectContext
属性,应用程序委托的
application(_:didFinishLaunchingWithOptions:)
方法会将其配置为指向持久化容器的
viewContext
:
let nav = self.window!.rootViewController as! UINavigationController
let tvc = nav.topViewController as! GroupLister
tvc.managedObjectContext = self.persistentContainer.viewContext
3.4 数据模型设计
要描述构成数据模型(托管对象模型)的对象的结构和关系,需在数据模型文档中设计对象图。例如,在一个简单的对象图中,一个
Group
可以有多个
Person
。除了时间戳是日期类型,
Group
的UUID是UUID类型外,其他属性(类似于对象属性)均为字符串类型。
3.5 代码生成
Group
和
Person
不是类,而是实体名称,它们的属性也不是普通属性。所有Core Data模型对象都是
NSManagedObject
的实例,通过键值编码(KVC)动态处理属性。不过,可以在数据模型检查器中配置实体进行代码生成,这样在编译项目时,会为实体(如
Group
和
Person
)创建
NSManagedObject
的子类,并赋予与实体属性对应的属性。
3.6 视图控制器实现
3.6.1 GroupLister视图控制器
GroupLister
的职责是列出组并允许用户创建新组。它通过获取请求从Core Data获取组列表,在iOS中,Core Data模型对象常作为
UITableView
的模型数据,获取请求可通过
NSFetchedResultsController
方便地管理。以下是
GroupLister
中
NSFetchedResultsController
的初始化代码:
lazy var frc: NSFetchedResultsController<Group> = {
let req: NSFetchRequest<Group> = Group.fetchRequest()
req.fetchBatchSize = 20
let sortDescriptor = NSSortDescriptor(key:"timestamp", ascending:true)
req.sortDescriptors = [sortDescriptor]
let frc = NSFetchedResultsController(
fetchRequest:req,
managedObjectContext:self.managedObjectContext,
sectionNameKeyPath:nil, cacheName:nil)
frc.delegate = self
do {
try frc.performFetch()
} catch {
fatalError("Aborting with unresolved error")
}
return frc
}()
在
tableView
的数据源方法中,将
NSFetchedResultsController
作为模型数据:
override func numberOfSections(in tableView: UITableView) -> Int {
return self.frc.sections!.count
}
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
let sectionInfo = self.frc.sections![section]
return sectionInfo.numberOfObjects
}
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: self.cellID, for: indexPath)
cell.accessoryType = .disclosureIndicator
let group = self.frc.object(at:indexPath)
cell.textLabel!.text = group.name
return cell
}
当用户请求创建组时,会弹出一个提示框,输入组名后创建新的
Group
对象,保存到托管对象上下文,并导航到
PeopleLister
视图:
let context = self.frc.managedObjectContext
let group = Group(context: context)
group.name = av.textFields![0].text!
group.uuid = UUID()
group.timestamp = Date()
do {
try context.save()
} catch {
return
}
let pl = PeopleLister(group: group)
self.navigationController!.pushViewController(pl, animated: true)
3.6.2 PeopleLister视图控制器
PeopleLister
用于列出特定组中的所有人,其指定初始化方法为
init(group:)
。当从
GroupLister
视图导航到
PeopleLister
视图时,会实例化
PeopleLister
并将其推送到导航控制器栈中。
PeopleLister
也有一个
NSFetchedResultsController
属性,不过它只列出属于特定组的
Person
,通过设置获取请求的谓词来实现:
let pred = NSPredicate(format:"group = %@", self.group)
req.predicate = pred
PeopleLister
的界面是一个文本字段表,在
tableView(_:cellForRowAt:)
方法中,可使用
self.frc.object(at:indexPath)
获取
Person
对象,并设置文本字段的文本。当用户编辑文本字段时,会更新数据模型并保存托管对象上下文:
func textFieldDidEndEditing(_ textField: UITextField) {
var v : UIView = textField
repeat { v = v.superview! } while !(v is UITableViewCell)
let cell = v as! UITableViewCell
let ip = self.tableView.indexPath(for:cell)!
let object = self.frc.object(at:ip)
object.setValue(textField.text!, forKey: (
(textField.tag == 1) ? "firstName" : "lastName"))
do {
try object.managedObjectContext!.save()
} catch {
return
}
}
当用户请求创建新的
Person
时,会创建一个新的
Person
对象,配置其属性并保存到上下文。同时,通过实现
NSFetchedResultsController
的委托方法,使新的
Person
出现在表中:
@objc func doAdd(_:AnyObject) {
self.tableView.endEditing(true)
let context = self.frc.managedObjectContext
let person = Person(context:context)
person.group = self.group
person.lastName = ""
person.firstName = ""
person.timestamp = Date()
do {
try context.save()
} catch {
return
}
}
func controllerWillChangeContent(
_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.beginUpdates()
}
func controllerDidChangeContent(
_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
self.tableView.endUpdates()
}
func controller(
_ controller: NSFetchedResultsController<NSFetchRequestResult>,
didChange anObject: Any,
at indexPath: IndexPath?,
for type: NSFetchedResultsChangeType,
newIndexPath: IndexPath?) {
if type == .insert {
self.tableView.insertRows(at:[newIndexPath!], with: .automatic)
DispatchQueue.main.async {
let cell = self.tableView.cellForRow(at:newIndexPath!)!
let tf = cell.viewWithTag(1) as! UITextField
tf.becomeFirstResponder()
}
}
}
3.7 注意事项
Core Data文件不适合用作iCloud文档。若要将结构化数据反映到云端,
CloudKit
框架是更好的选择,它可以在线维护数据库,并同步不同设备间的数据变化。可参考Apple的
CloudKit Quick Start
指南获取更多信息。
3.8 流程总结
graph LR
A[选择模板创建项目] --> B[配置持久化栈]
B --> C[设计对象图]
C --> D[代码生成]
D --> E[实现视图控制器]
E --> F[处理数据操作]
4. PDF文件处理
4.1 简介
在iOS 11之前,显示PDF文件的方式较为复杂。从iOS 11开始,引入了PDF Kit(
import PDFKit
),它提供了一个原生的
UIView
子类
PDFView
,用于美观地显示PDF文件。
4.2 基本使用
基本使用
PDFView
很简单,只需初始化一个
PDFDocument
(可以从数据或文件URL初始化),并将其赋值给
PDFView
的
document
属性:
let v = PDFView(frame:self.view.bounds)
self.view.addSubview(v)
let url = Bundle.main.url(forResource: "notes", withExtension: "pdf")!
let doc = PDFDocument(url: url)
v.document = doc
4.3 其他配置
PDFView
还有许多可配置的方面,例如可以嵌入
UIPageViewController
用于PDF页面的布局和导航:
v.usePageViewController(true)
4.4 自定义PDF创建
可以通过创建
PDFPage
的子类来自定义PDF页面内容。以下是创建一个包含“Hello, world!”的PDF页面的示例:
class MyPage: PDFPage {
override func draw(with box: PDFDisplayBox, to context: CGContext) {
UIGraphicsPushContext(context)
context.saveGState()
let r = self.bounds(for: box)
let s = NSAttributedString(string: "Hello, world!", attributes: [
.font : UIFont(name: "Georgia", size: 80)!
])
let sz = s.boundingRect(with: CGSize(10000,10000),
options: .usesLineFragmentOrigin, context: nil)
context.translateBy(x: 0, y: r.height)
context.scaleBy(x: 1, y: -1)
s.draw(at: CGPoint(
(r.maxX - r.minX) / 2 - sz.width / 2,
(r.maxY - r.minY) / 2 - sz.height / 2
))
context.restoreGState()
UIGraphicsPopContext()
}
}
let v = PDFView(frame:self.view.bounds)
self.view.addSubview(v)
let doc = PDFDocument()
v.document = doc
doc.insert(MyPage(), at: 0)
4.5 页面判断
若文档中有多个自定义页面,可在
draw(with:to:)
方法中判断当前页面的索引:
let pagenum = self.document?.index(for: self)
4.6 其他功能
PDF Kit还提供了许多辅助类,可用于操作页面缩略图、选择、注释等。
4.7 流程总结
graph LR
A[初始化PDFView] --> B[初始化PDFDocument]
B --> C[赋值给PDFView]
C --> D[可选:配置导航等]
D --> E[可选:自定义页面内容]
5. 图像文件处理
5.1 简介
Image I/O框架提供了一种简单、统一的方式来打开图像文件、保存图像文件、转换图像文件格式,以及从标准图像文件格式中读取元数据,包括数码相机的EXIF和GPS信息。使用时需要导入
ImageIO
。该API是用C语言编写的,使用
CFTypeRefs
而非对象,没有Swift的面向对象封装,需要直接调用全局C函数,并在
CFTypeRefs
和其Foundation对应类型之间进行转换。
5.2 图像源的创建
使用Image I/O框架从图像文件获取元数据的步骤如下:
1. 获取图像文件的URL。
2. 设置选项,例如不缓存。
3. 使用
CGImageSourceCreateWithURL
创建图像源。
4. 使用
CGImageSourceCopyPropertiesAtIndex
获取图像的属性字典。
示例代码如下:
let url = Bundle.main.url(forResource:"colson", withExtension: "jpg")!
let opts : [AnyHashable:Any] = [kCGImageSourceShouldCache : false]
let src = CGImageSourceCreateWithURL(url as CFURL, opts as CFDictionary)!
let d = CGImageSourceCopyPropertiesAtIndex(src, 0, opts as CFDictionary) as! [AnyHashable:Any]
通过上述代码,在未将图像文件作为图像打开的情况下,就可以获得一个包含图像信息的字典,其中包括像素尺寸、分辨率、颜色模型、颜色深度、方向,以及数码相机拍摄的EXIF数据(如光圈、曝光、相机型号等)。
5.3 获取图像和缩略图
5.3.1 获取完整图像
可以使用
CGImageSourceCreateImageAtIndex
从图像源获取完整的
CGImage
。
5.3.2 获取缩略图
如果只是为了在界面中显示图像,获取缩略图是更好的选择,因为可以指定缩略图的大小,避免将大图像分配给小图像视图时造成的内存浪费。获取缩略图的步骤如下:
1. 获取图像文件的URL。
2. 设置初始选项,例如不缓存。
3. 创建图像源。
4. 根据设备屏幕比例和目标图像视图的宽度,设置缩略图的大小和其他选项。
5. 使用
CGImageSourceCreateThumbnailAtIndex
创建缩略图。
6. 将缩略图转换为
UIImage
并显示。
示例代码如下:
let url = Bundle.main.url(forResource:"colson", withExtension: "jpg")!
var opts : [AnyHashable:Any] = [kCGImageSourceShouldCache : false]
let src = CGImageSourceCreateWithURL(url as CFURL, opts as CFDictionary)!
let scale = UIScreen.main.scale
let w = self.iv.bounds.width * scale
opts = [
kCGImageSourceShouldAllowFloat : true,
kCGImageSourceCreateThumbnailWithTransform : true,
kCGImageSourceCreateThumbnailFromImageAlways : true,
kCGImageSourceShouldCacheImmediately : true,
kCGImageSourceThumbnailMaxPixelSize : w
]
let imref = CGImageSourceCreateThumbnailAtIndex(src, 0, opts as CFDictionary)!
let im = UIImage(cgImage: imref, scale: scale, orientation: .up)
self.iv.image = im
5.4 保存图像
保存图像时,需要创建一个图像目标。以下是将图像保存为TIFF格式的示例代码:
let url = Bundle.main.url(forResource:"colson", withExtension: "jpg")!
let opts : [AnyHashable:Any] = [kCGImageSourceShouldCache : false]
let src = CGImageSourceCreateWithURL(url as CFURL, opts as CFDictionary)!
let fm = FileManager.default
let suppurl = try! fm.url(for:.applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let tiff = suppurl.appendingPathComponent("mytiff.tiff")
let dest = CGImageDestinationCreateWithURL(tiff as CFURL, kUTTypeTIFF, 1, nil)!
CGImageDestinationAddImageFromSource(dest, src, 0, nil)
let ok = CGImageDestinationFinalize(dest)
上述代码直接从图像源将图像保存到图像目标,无需将图像作为图像打开。
5.5 流程总结
graph LR
A[获取图像URL] --> B[创建图像源]
B --> C{操作类型}
C -->|获取元数据| D[获取属性字典]
C -->|获取完整图像| E[获取CGImage]
C -->|获取缩略图| F[设置缩略图选项]
F --> G[创建缩略图]
G --> H[转换为UIImage显示]
C -->|保存图像| I[创建图像目标]
I --> J[添加图像到目标]
J --> K[完成保存]
6. 总结与对比
6.1 不同数据存储和文件处理方式对比
| 类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| SQLite | 轻量级、功能全、可快速搜索、不占大量内存 | C接口复杂 | 数据以行列形式存在且需快速搜索,如iOS设备本地存储 |
| Core Data | 通用表达对象关系、自动管理持久化存储、节省内存 | 难用难调试、表达晦涩、不能替代关系型数据库 | 处理复杂对象关系和持久化存储 |
| PDF Kit | 简单美观显示PDF、可自定义页面 | iOS 11 之前不可用 | 显示和处理PDF文件 |
| Image I/O | 统一处理图像文件、可获取元数据和缩略图 | C接口、需类型转换 | 图像文件的打开、保存、格式转换和元数据读取 |
6.2 选择建议
- 如果数据结构简单,需要快速搜索和本地存储,可选择SQLite。
- 对于复杂的对象关系和持久化管理,Core Data是一个不错的选择,但需要有一定的学习成本。
- 处理PDF文件时,使用PDF Kit可以方便地实现显示和自定义功能。
- 涉及图像文件的操作,如获取元数据、生成缩略图和保存图像,Image I/O框架是首选。
通过对这些数据存储和文件处理技术的了解和掌握,可以根据具体需求选择合适的方法,提高开发效率和应用性能。
超级会员免费看

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



