SwiftUI 自定义文件类型(UTType)

1,084 阅读3分钟

前言

在macOS app开发中需要将文件导入和导出,导入和导出都有指定的格式(本次用.configData做示例)。

1.在App菜单声明导入和导出菜单

截屏2024-07-02 11.52.02.png

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会报以下警告,导出的也不是自定义的文件类型

截屏2024-07-02 14.14.07.png

警告提示需要在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 类型的文件

截屏2024-07-02 14.28.57.png

这样就可以在工程中通过这种方式导出和导入自定的数据,当然导出数据的时候需要对关键数据进行加密,在本文中使用的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的位置可以任意设置,只要保证加密和解密是 拼接数据和移除数据时 顺序一致。