前言
在macOS app开发中需要将文件导入和导出,导入和导出都有指定的格式(本次用.configData做示例)。
1.在App菜单声明导入和导出菜单
swiftUI中添加如下的菜单命令,具体实现如下
/// 展示选取文件弹窗
@State private var isImportFile = false
@State private var isExportFile = false
WindowGroup {
…………………………
}
.commands {
CommandGroup(after: .importExport) {
Button("导入配置"){
self.isImportFile = true
}.fileImporter(isPresented:$isImportFile , allowedContentTypes: [.aesData, .data]) { result in
switch result{
case .success(let url):
print(url)
case .failure(let error):
print("文件选择错误:\(error)")
}
}
Button("导出配置"){
//读取文件 生成数据在弹窗导出窗口进行数据导出
}
.fileExporter(isPresented: $isExportFile,
document: TextFileDocument(content: ""),
contentType: .data,
defaultFilename: "requirementConfig") { result in
do{
let folderUrl = try result.get()
if let filePath = folderUrl.absoluteString.removingPercentEncoding {
print(filePath)
}
} catch {
print("文件选择错误:\(error)")
}
}
}
}
- fileImporter: 文件导入,该方法中包含对应的弹窗状态,支持选中的文件类型,是否允许多选等参数
- fileExporter: 文件导出,该方法包含对应的的弹窗状态,导出内存中的文档集合,导出文件类型,导出文件默认名称,导出回调(文件导出后会自动加上文件类型后缀)
fileExporter中需要自定义文档内存集合,常见的有text,data等数据类型。自定义文档类型需要遵守 FileDocument
协议,即实现以下方法(以 text为例)
struct TextFileDocument: FileDocument {
/// 支持的文件类型
static var readableContentTypes: [UTType] = [.plainText]
var content: String
init(content: String = "") {
self.content = content
}
// 创建并初始化文件
init(configuration: ReadConfiguration) throws {
if let data = configuration.file.regularFileContents {
content = String(decoding: data, as: UTF8.self)
}else{
content = ""
}
}
// 保存文件时 序列化数据的操作
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let data = Data(content.utf8)
return FileWrapper(regularFileWithContents: data)
}
}
2.自定义文件类型
上面定义的是系统支持的文件类型或者想自定义导出文件类型,可以实现FileDocument
协议,以下是自定义的设置data的FileDocument,同时对UTType也进行了拓展 自定义导出类型
extension UTType {
static let aesData = UTType(exportedAs:"com.zj.configData")
}
//MARK: 保存文件信息
struct DataFileDocument: FileDocument {
static var readableContentTypes: [UTType] = [.aesData]
var content: Data
init(content: Data = Data()) {
self.content = content
}
init(configuration: ReadConfiguration) throws {
if let data = configuration.file.regularFileContents {
self.content = data
}else{
self.content = Data()
}
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
return FileWrapper(regularFileWithContents: self.content)
}
}
如果只是对UTType进行了拓展,而没有对具体的类型做定义,xcode会报以下警告,导出的也不是自定义的文件类型
警告提示需要在plist 中定义并注册该export类型,在plist文件中添加一下内容
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.directory</string>
<string>public.data</string>
</array>
<key>UTTypeDescription</key>
<string>自定义文件类型</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>com.zj.configData</string> //自定义的typeIdentifier 和代码中的对应
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>configData</string> // 支持的后缀名
</array>
<key>public.mime-type</key> //上面支持的类型
<array/>
</dict>
</dict>
</array>
再次运行xcode警告消失,导出文件是指定的.configData 类型的文件
这样就可以在工程中通过这种方式导出和导入自定的数据,当然导出数据的时候需要对关键数据进行加密,在本文中使用的AES加密方式,swift也提供了常用的编辑API,自己封装一下即可
import CryptoKit
struct DYCryptoTool {
/// AES 加密
private static func aesEncrypt(data: Data, key: SymmetricKey) -> Data? {
do {
let sealedBox = try AES.GCM.seal(data, using: key)
return sealedBox.combined
} catch {
print("encrypt error:\(error.localizedDescription)")
}
return nil
}
/// AES 解密
private static func aesDecrypt(data: Data, key: SymmetricKey) -> Data? {
do {
let sealedBox = try AES.GCM.SealedBox(combined: data)
return try AES.GCM.open(sealedBox, using: key)
} catch {
print("decrypt error:\(error.localizedDescription)")
}
return nil
}
}
重点
由于对数据进行加解密需要定义一个SymmetricKey
,如果有服务端可以将对称加密密钥放在服务端,本地只是提供一个思路来进行加解密。
- 加密时定义一个不定长度的字符串
- 对原始数据进行data拼接
- 对称加密密钥拼接到原始数据中
- 返回的数据包括三部分 1.对称加密密钥 2.自定义密钥 3.原始数据
- 解密
- 读取文件中的数据
- 定义同样的对称密钥 获取其长度
- 自定义数据 获取其data长度
- 对文件中数据进行裁剪,获取最开始的文件数据
对称密钥和自定义data的位置可以任意设置,只要保证加密和解密是 拼接数据和移除数据时 顺序一致。