100、数据持久化与文件处理技术详解

数据持久化与文件处理技术详解

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框架是首选。

通过对这些数据存储和文件处理技术的了解和掌握,可以根据具体需求选择合适的方法,提高开发效率和应用性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值