注册
iOS

iOS - 数据存储

Bundle

简单理解就是资源文件包,会将许多图片、xib、文本文件组织在一起,打包成一个 Bundle 文件,这样可以在其他项目中引用包内的资源。

// 获取当前项目的Bundle
let bundle = Bundle.main

// 加载资源
let mp3 = Bundle.main.path(forResource: "xxx", ofType: "mp3")

沙盒

每一个 App 只能在自己的创建的文件系统(存储区域)中进行文件的操作,不能访问其他 App 的文件系统(存储区域),该文件系统(存储区域)被成为沙盒。所有的非代码文件都要保存在此,例如图像,图标,声音,plist,文本文件等。

沙盒机制保证了 App 的安全性,因为只能访问自己沙盒文件下的文件。

Home目录

沙盒的主目录,可以通过它查看沙盒目录的整体结构。

// 获取程序的Home目录
let homeDirectory = NSHomeDirectory()

Documents目录

保存应用程序运行时生成的持久化数据。可被iTunes备份,可备份到 iCloud。

// 方法1
let documentPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentPath = documentPaths[0]

// 方法2
let documentPath2 = NSHomeDirectory() + "/Documents"

上面的获取方式最后得到的是String,如果希望获取的是URL,可以通过下面的方式:

let manager = FileManager.default
let urlForDocument = manager.urls(for: .documentDirectory, in:.userDomainMask)
let url: URL = urlForDocument[0]

NSSearchPathForDirectoriesInDomains

  • 访问沙盒目录常用的函数,它返回值为一个数组,在 iOS 中由于只有一个唯一路径,所以直接取数组第一个元素即可。
func NSSearchPathForDirectoriesInDomains(
_ directory: FileManager.SearchPathDirectory,
_ domainMask: FileManager.SearchPathDomainMask,
_ expandTilde: Bool) -> [String]

  • directory:指定搜索的目录名称。
  • domainMask:搜索主目录的位置。userDomainMask 表示搜索的范围限制于当前应用的沙盒目录(参考定义注释)。
  • expandTilde:是否获取完整的路径。
let documentPaths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, false)
let documentPath = documentPaths[0] // ~/Documents

let documentPaths2 = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentPath2 = documentPaths2[0] // /Users/yangfan/Library/Developer/XCPGDevices/982B6CBA-747B-4831-9D87-F82160197333/data/Containers/Data/Application/56C657D5-B36B-449D-AC6C-E2417EA65D00/Documents

Library目录

存储程序的默认设置和其他信息,其下有两个重要目录:

  • Library/Preferences 目录:包含应用程序的偏好设置文件。不应该直接创建偏好设置文件,而是应该使用UserDefaults类来取得和设置应用程序的偏好。
  • Library/Caches 目录:主要存放缓存文件,此目录下文件不会在应用退出时删除。
// Library目录-方法1
let libraryPaths = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true)
let libraryPath = libraryPaths[0]

// Library目录-方法2
let libraryPath2 = NSHomeDirectory() + "/Library"

// Cache目录-方法1
let cachePaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
let cachePath = cachePaths[0]

// Cache目录-方法2
let cachePath2 = NSHomeDirectory() + "/Library/Caches"
  • tmp目录:存储临时文件,当在退出程序或设备重启时,文件会被清除。
// 方法1
let tmpDir = NSTemporaryDirectory()

// 方法2
let tmpDir2 = NSHomeDirectory() + "/tmp"

注意

每次编译代码会生成新的沙盒路径,所以模拟器运行同一个 App 时所得到的沙盒路径是不一样的,但上架的 App 在真机上运行不存在这种情况。

plist读写

class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 获取本地plist
let path = Bundle.main.path(forResource: "cityData", ofType: "plist")
if let path = path {
let root = NSDictionary(contentsOfFile: path) // 借助于NSDictionary
// print(root!.allKeys)
// print(root!.allKeys[31])
// 获取所有数据
let cities = root![root!.allKeys[31]] as! NSArray // 借助于NSArray
// print(cities)
// 沙盒路径
let documentDir = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first
let filePath = documentDir! + "/localData.plist"
// 写入沙盒
cities.write(toFile: filePath, atomically: true)
}
}
}

偏好设置

  • 一般用于保存如用户名、密码、版本等轻量级数据。
  • 通过UserDefaults来设置和读取偏好设置。
  • 偏好设置以key-value的方式进行读写操作。
  • 默认情况下数据自动以plist形式存储在沙盒的Library/Preferences目录。

案例

  • 记住密码
class ViewController: UIViewController {
@IBOutlet weak var username: UITextField!
@IBOutlet weak var password: UITextField!
@IBOutlet weak var swit: UISwitch!
// UserDefaults
let userDefaults = UserDefaults.standard

override func viewDidLoad() {
super.viewDidLoad()

// 取出存储的数据
let name = userDefaults.string(forKey: "name")
let pwd = userDefaults.string(forKey: "pwd")
let isOn = userDefaults.standard.bool(forKey: "isOn")
// 填充输入框
username.text = name
password.text = pwd
// 设置开关状态
swit.isOn = isOn
}

@IBAction func login(_ sender: Any) {
print("密码已经记住")
}

@IBAction func remember(_ sender: Any) {
let swit = sender as! UISwitch
// 如果记住密码开关打开
if swit.isOn {
let name = username.text
let pwd = password.text
// 存储用户名和密码
userDefaults.set(name, forKey: "name")
userDefaults.set(pwd, forKey: "pwd")
// 同时存储开关的状态
userDefaults.set(swit.isOn, forKey: "isOn")
// 最后进行同步
userDefaults.synchronize()
}
}
}
  • 新特性界面
class SceneDelegate: UIResponder, UIWindowSceneDelegate { 
var window: UIWindow?
// 当前版本号
var currentVersion: Double!
// UserDefaults
let userDefaults = UserDefaults.standard

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(windowScene: windowScene)
if isNewVersion {
// 新特性界面
let newVC = UIViewController()
newVC.view.backgroundColor = .green
window?.rootViewController = newVC
// 存储当前版本号
userDefaults.set(currentVersion, forKey: "localVersion")
userDefaults.synchronize()
} else {
// 主界面
let mainVC = UIViewController()
mainVC.view.backgroundColor = .red
window?.rootViewController = mainVC
}

window?.makeKeyAndVisible()
}
}

extension SceneDelegate {
// 是否新版本
private var isNewVersion: Bool {
// 获取当前版本号
currentVersion = Double(Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)!
// 本地版本号
let localVersion = userDefaults.double(forKey: "localVersion")
// 比较大小
return currentVersion > localVersion
}
}

默认值

如果需要在使用时设置 UserDefaults 的默认值,可以使用register方法。

enum Keys: String {
case name // 名字
case isRem // 记住密码
}

// 设置默认值
UserDefaults.standard.register(defaults: [
Keys.name.rawValue: "UserA",
Keys.isRem.rawValue: false
])

注意:在设置默认值后如果修改了其中的属性值,即使再次执行register方法也不会重置。

跨域

一般情况下使用UserDefaults.standard没有太大问题,但当 App 足够复杂时就会产生几个问题:

  • 需要保证设置数据 key 具有唯一性,防止产生冲突。
  • 同一个 plist 文件越来越大造成的读写效率降低。
  • 无法便捷的清除特定的偏好设置数据。

因此还有另外一种获取 UserDefaults 对象的方法:UserDefaults(suiteName: String?),可以根据传入的 suiteName 参数进行处理:

  • 传入 nil:等同于UserDefaults.standard
  • 传入 App Groups 的 ID:操作共享目录中的 plist 文件,以便在跨 App 或宿主 App 与扩展应用之间(如 App 与 Widget)共享数据。
  • 传入其他值:操作Documents/Library/Preferences目录下以suiteName命名的 plist 文件。

可以通过如下的方式删除指定suiteName的 plist 文件里的全部数据。

let userDefaults = UserDefaults(suiteName: "abc")
userDefaults?.removePersistentDomain(forName: "abc")

归档与反归档

  • 归档(序列化)是把对象转为Data,反归档(反序列化)是从Data还原出对象。
  • 可以存储自定义数据。
  • 存储的数据需要继承自NSObject并遵循NSSecureCoding协议。

案例

  • 自定义对象
class Person: NSObject, NSSecureCoding {   
var name:String?
var age:Int?

override init() {
}

static var supportsSecureCoding: Bool = true

// 编码- 归档调用
func encode(with aCoder: NSCoder) {
aCoder.encode(age, forKey: "age")
aCoder.encode(name, forKey: "name")
}

// 解码-反归档调用
required init?(coder aDecoder: NSCoder) {
super.init()
age = aDecoder.decodeObject(forKey: "age") as? Int
name = aDecoder.decodeObject(forKey: "name") as? String
}
}
  • 归档与反归档
class ViewController: UIViewController {
var data: Data!
var origin: Person!

override func viewDidLoad() {
super.viewDidLoad()
}

// 归档
@IBAction func archiver(_ sender: Any) {
let p = Person()
p.age = 20
p.name = "zhangsan"

do {
try data = NSKeyedArchiver.archivedData(withRootObject: p, requiringSecureCoding: true)
} catch {
print(error)
}
}

// 反归档
@IBAction func unarchiver(_ sender: Any) {
do {
try origin = NSKeyedUnarchiver.unarchivedObject(ofClass: Person.self, from: data)
print(origin!.age!)
print(origin!.name!)
} catch {
print(error)
}
}
}

数据库—sqlite3

由于 Swift 直接操作 sqlite3 非常不方便,所以借助于SQLite.swift的框架。

  • Model
struct Person {    
var name : String = ""
var phone : String = ""
var address : String = ""
}
  • DBTools
import SQLite

struct DBTools {
// 数据库路径
let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/person.db"
// 数据库连接
var db: Connection!
// 表名与字段
let personTable = Table("t_person") // 表名
let personID = Expression<Int>("id") // id
let personName = Expression<String>("name") // name
let personPhone = Expression<String>("phone") // phone
let personAddress = Expression<String>("address") // address

// MARK: - 构造函数,数据库有则连接 没有就创建后连接
init() {
do {
db = try Connection(dbPath)
print("数据库创建/连接成功")
} catch {
print("数据库创建/连接失败")
}
}

// MARK: - 创建表格,表若存在不会再次创建,直接进入catch
func createTable() {
// 创表
do {
try db.run(personTable.create(block: { t in
t.column(personID, primaryKey: .autoincrement)
t.column(personName)
t.column(personPhone)
t.column(personAddress)
}))
print("数据表创建成功")
} catch {
print("数据表创建失败")
}
}

// MARK: - 插入数据
func insertPerson(person: Person) {
let insert = personTable.insert(personName <- person.name, personPhone <- person.phone, personAddress <- person.address)
// 插入
do {
try db.run(insert)
print("插入数据成功")
} catch {
print("插入数据失败")
}
}

// MARK: - 删除数据
func deletePerson(name: String) {
// 筛选数据
let p = personTable.filter(personName == name)
// 删除
do {
let row = try db.run(p.delete())
if row == 0 {
print("暂无数据删除")
} else {
print("数据删除成功")
}
} catch {
print("删除数据失败")
}
}

// MARK: - 更新数据
func updatePerson(person: Person) {
// 筛选数据
let p = personTable.filter(personName == person.name)
// 更新
do {
let row = try db.run(p.update(personPhone <- person.phone, personAddress <- person.address))
if row == 0 {
print("暂无数据更新")
} else {
print("数据更新成功")
}
} catch {
print("数据更新失败")
}
}

// MARK: - 查询数据
func selectPerson() -> [Person]? {
// 保存查询结果
var response: [Person] = []
// 查询
do {
let select = try db.prepare(personTable)
for person in select {
let p = Person(name: person[personName], phone: person[personPhone], address: person[personAddress])
response.append(p)
}

if !response.isEmpty {
print("数据查询成功")
} else {
print("对不起,暂无数据")
}
return response
} catch {
print("数据查询失败")
return nil
}
}
}
  • ViewController
class ViewController: UIViewController {    
var dbTools: DBTools?

override func viewDidLoad() {
super.viewDidLoad()
}

@IBAction func createDB(_ sender: Any) {
dbTools = DBTools()
}

@IBAction func createTab(_ sender: Any) {
dbTools?.createTable()
}

@IBAction func insertData(_ sender: Any) {
let p = Person(name: "zhangsan", phone: "18888888888", address: "AnHuiWuhu")
dbTools?.insertPerson(person: p)
}

@IBAction func deleteData(_ sender: Any) {
dbTools?.deletePerson(name: "zhangsan")
}

@IBAction func updateData(_ sender: Any) {
let p = Person(name: "zhangsan", phone: "17777777777", address: "JiangSuNanJing")
dbTools?.updatePerson(person: p)
}

@IBAction func selectData(_ sender: Any) {
let person = dbTools?.selectPerson()
if let person = person {
for p in person {
print(p)
}
}
}
}

0 个评论

要回复文章请先登录注册