杰理 SDK 文档中心
首页
首页
  • SDK 框架库

    • JL_BLEKit 蓝牙通信核心
    • JL_AdvParse 广播包解析
    • JL_HashPair 加密配对
    • JL_OTALib 固件升级
    • JLDialUnit 彩屏仓与表盘控制
    • JLBmpConvertKit 位图转换
    • JLPackageResKit 资源包处理
    • JLLogHelper 日志工具
  • 核心功能模块

    • 音乐与媒体控制
    • 音效调节与均衡器
    • 设备发现、连接与设置
    • Auracast 广播接收与发射
    • 文件浏览、闹钟、FM 与灯光控制
    • ANC、按键设置与查找设备
    • AI 翻译与自定义命令
  • 应用架构与工程支撑

    • 杰理之家 App 架构与导航
    • 数据存储与缓存
    • Swift 工具与扩展层
    • JLAudioUnitKit 示例工程
    • SDKTestHelper 测试工具
  • 开发文档与资源

    • 文档中心与 JL_OTALib API 说明
    • 自定义蓝牙接入方式
    • 调试技巧与问题排查
    • 版本历史与社区支持

Swift 工具与扩展层

本文档介绍杰理 iOS 蓝牙 SDK 演示工程(JieLi_Home_Demo)中位于 SwiftTools 目录及 Tools 目录下的 Swift 工具类与系统类型扩展层,涵盖 SwiftHelper 单例工具类以及针对 String、URL、UIButton、Date、UIColor 等系统类型的扩展实现。

Purpose and Scope

本页面向「应用架构」目录下的 Swift 工具与扩展层,覆盖以下内容:

  • SwiftHelper 核心工具类:资源路径打印、文件目录创建与遍历、自定义图片缓存(保护套图/壁纸)等文件系统能力;
  • 系统类型扩展:String(图片加载、去扩展名、取末级路径、十六进制转字节)、URL(图片加载)、UIButton(上图下字、左字右图布局)、Date(日期格式化字符串)、UIColor(十六进制颜色);
  • 工具层与 SDK 运行时(JL_RunSDK)、资源管理(R.swift 生成的 R 结构)、日志(JLLogManager)、翻译服务(TextTranslateMgr)之间的协作关系。

以下主题属于兄弟页面,不在本页展开:数据库持久化层(DataBase 目录下的 HealthDataBase、JLBroadcastDataBase、SettingDefault)、具体业务页面(DevicesViewController、MediasViewController 下的各功能 VC)以及 Auracast 功能模块。

Overview

在杰理 iOS 工程中,工具与扩展层承担「跨模块复用的基础设施」角色。它不依赖任何具体业务页面,而是为上层 VC、ViewModel 提供三类能力:

  1. 文件系统操作:SwiftHelper 以单例形式(static let shared)提供图片缓存目录创建、目录内容按创建时间倒序枚举、按设备 UUID/设备项(mItem)分目录保存自定义图片等功能。这些能力被设备信息、壁纸设置等页面复用,保证"一个设备一份图片"的目录隔离。
  2. 系统类型便捷扩展:通过 extension 为 Swift 标准库与 UIKit 类型补充高频工具方法。例如把文件路径/URL 直接转为 UIImage、剥离扩展名、提取末级路径、一键配置按钮图文布局。
  3. 与 SDK 运行时对接:工具类通过 JL_RunSDK.sharedMe() 获取当前连接的蓝牙设备实体(mBleEntityM),以设备 UUID 作为缓存目录的命名空间;通过 R.path(R.swift 生成的类型安全路径)定位沙盒目录;通过 JLLogManager 输出调试日志。

设计意图:将"到处都会写一遍"的样板代码(文件读写、路径处理、按钮布局计算)收敛为单一入口,减少各页面重复实现带来的不一致,同时通过 @objcMembers 标记保持与既有 Objective-C 代码的互操作能力。

Architecture

下图展示了工具与扩展层在应用架构中的位置及其与周边模块的依赖关系:

flowchart TD
    subgraph sg_App["应用层(业务页面)"]
        VC["DevicesViewController / MediasViewController 等"]
        VM["ViewModel(如 DeviceInfoViewModel)"]
    end

    subgraph sg_Tools["工具与扩展层(本页主题)"]
        Helper["SwiftHelper(@objcMembers 单例)"]
        ExtString["extension String"]
        ExtURL["extension URL"]
        ExtBtn["extension UIButton"]
        ExtDate["extension Date(JLDateEx)"]
        ExtColor["extension UIColor(eHex)"]
    end

    subgraph sg_SDK["SDK 与基础依赖"]
        RSDK["JL_RunSDK(BLE 运行时)"]
        RSwift["R 结构(R.swift 资源)"]
        Log["JLLogManager"]
        TTS["TextTranslateMgr"]
        FM["FileManager(系统)"]
        UIKit["UIKit / Foundation"]
    end

    VC --> Helper
    VM --> Helper
    Helper --> RSDK
    Helper --> RSwift
    Helper --> Log
    Helper --> FM
    Helper --> TTS
    ExtString --> UIKit
    ExtURL --> UIKit
    ExtBtn --> UIKit
    ExtDate --> UIKit
    ExtColor --> UIKit
    ExtString -.-> Helper
    ExtDate -.-> Helper

架构说明:

  • SwiftHelper 是工具层的核心,@objcMembers 标记使其所有成员对 Objective-C 运行时可见,便于混合语言工程调用;内部直接依赖 JL_RunSDK、R.swift 生成的 R、JLLogManager 与 FileManager。
  • extension String / extension URL 为资源路径→图片的转换提供统一入口,SwiftHelper.testTranslate() 中直接使用 Date().getDateStr(来自 JLDateEx),说明各扩展之间是相互协作而非孤立的。
  • extension UIButton 通过内边距(titleEdgeInsets / imageEdgeInsets)计算实现图文布局,无需自定义子类,属于典型的"组合优于继承"设计。
  • 上层业务 VC/VM 只依赖工具层暴露的方法签名,不关心其内部的沙盒路径与 SDK 调用细节,从而保持业务代码精简。

核心工具类:SwiftHelper

SwiftHelper 定义于 SwiftToolsHelper.swift,以 @objcMembers class SwiftHelper: NSObject 声明。@objcMembers 意味着类中所有成员(含扩展中新增的)都会自动暴露给 Objective-C,这是杰理工程混合 Swift/OC 代码的通用做法——工具类需要被仍以 OC 编写或桥接的模块调用。

单例与调试入口

@objcMembers class SwiftHelper:NSObject{
    // 打印所有资源路径用于调试
    static func printAllResources() {
        let paths = Bundle.main.paths(forResourcesOfType: "", inDirectory: nil)
        JLLogManager.logLevel(.DEBUG, content: "\(paths)")
    }
    class func createFolds(){
       _ = R.shared
    }
    static let shared = SwiftHelper()
    func testTranslate(){
        TextTranslateMgr.share.startTranslate(groupID: Date().getDateStr, origin: "I am an e-ink screen wireless network driver board. I can get picture information from a PC or smartphone via WiFi or Bluetooth.", Date: Date()) { record in
            
        }
    }
}

Source: SwiftToolsHelper.swift

设计要点:

  • printAllResources():枚举 Bundle.main 中所有资源路径并以 .DEBUG 级别输出到日志。这是排查"资源缺失/路径错误"类问题的第一现场工具,体现工程对可观测性的重视。
  • createFolds():_ = R.shared 强制初始化 R.swift 生成的资源单例,从而触发沙盒目录的创建。目录的"懒创建"通过 R.swift 的资源声明完成,工具层只负责触发。
  • shared 单例:static let shared = SwiftHelper() 是 Swift 推荐的线程安全单例写法(let 的原子初始化保证)。testTranslate() 演示了工具层如何组合 TextTranslateMgr 与 Date().getDateStr 扩展——以当前时间字符串作为翻译会话的 groupID。

文件系统操作:按设备隔离的图片缓存

class func saveProtectCustomToCache(_ img:UIImage,_ fileName:String){
    let dt = img.pngData() ?? Data()
    let uuid = JL_RunSDK.sharedMe().mBleEntityM?.mUUID ?? "unKnow"
    let path = R.path.protectCustom+"/"+uuid+"/"+fileName
    if !FileManager.default.fileExists(atPath: R.path.protectCustom+"/"+uuid) {
        try? FileManager.default.createDirectory(atPath: R.path.protectCustom+"/"+uuid, withIntermediateDirectories: true, attributes: nil)
    }
    FileManager.default.createFile(atPath: path, contents: dt)
}

class func saveWallPaperToCache(_ img:UIImage,_ fileName:String){
    let dt = img.pngData() ?? Data()
    let uuid = JL_RunSDK.sharedMe().mBleEntityM?.mItem ?? "unKnow"
    let path = R.path.wallPaperCustom+"/"+uuid+"/"+fileName
    if !FileManager.default.fileExists(atPath: R.path.wallPaperCustom+"/"+uuid) {
        try? FileManager.default.createDirectory(atPath: R.path.wallPaperCustom+"/"+uuid, withIntermediateDirectories: true, attributes: nil)
    }
    FileManager.default.createFile(atPath: path, contents: dt)
}

Source: SwiftToolsHelper.swift 与 SwiftToolsHelper.swift

设计意图与行为:

  • 按设备分目录:saveProtectCustomToCache 以 mBleEntityM?.mUUID(设备 UUID)为目录名,saveWallPaperToCache 以 mBleEntityM?.mItem(设备项标识)为目录名。这样同一 App 连接多台设备时,各设备的自定义图片互不覆盖;取不到设备信息时回退为 "unKnow" 目录兜底。
  • 容错策略:img.pngData() ?? Data() 在转码失败时写入空数据而不是崩溃;fileExists 检查避免重复创建目录;try? 吞掉目录创建异常——图片缓存是"尽力而为"的能力,失败不应阻塞主流程。
  • R.swift 集成:R.path.protectCustom / R.path.wallPaperCustom 是 R.swift 在编译期生成的类型安全路径常量,避免手写字符串拼错路径。

目录枚举:按创建时间倒序

class func listFiles(_ path:String) -> [String]{
    do{
        let files = try FileManager.default.contentsOfDirectory(atPath: path)
        var items:[String] = []
        let sortedFiles = files.sorted { p1,p2  in
            let att1 = try? FileManager.default.attributesOfItem(atPath: path+"/"+p1)
            let att2 = try? FileManager.default.attributesOfItem(atPath: path+"/"+p2)
            let date1 = att1?[.creationDate] as? Date
            let date2 = att2?[.creationDate] as? Date
            return date1!.compare(date2!) == .orderedDescending
        }
        for item in sortedFiles{
            items.append(path+"/"+item)
        }
        return items
    }catch{
        return []
    }
}

Source: SwiftToolsHelper.swift

该方法的排序闭包读取每个条目的 .creationDate,按 创建时间倒序(orderedDescending)返回完整路径列表——调用方拿到的第一个元素即最新文件,适合"最近使用的壁纸/保护图"类 UI。注意两点边界行为:

  • 排序闭包中对 date1! / date2! 使用了强制解包。若目录中混入无法读取属性的条目(理论上 try? 失败返回 nil),此处会崩溃;当前实现依赖"目录内全部是普通图片文件"这一隐含前提。
  • 外层 do/catch 只覆盖 contentsOfDirectory;目录不存在时返回空数组,调用方无需额外判空。

扩展层详解

扩展层为系统类型补充工程内高频使用的方法。它们与 SwiftHelper 在同一文件中定义(String/URL/UIButton),或在独立文件中定义(Date、UIColor)。

String 扩展

extension String{

    func beImage()->UIImage{
        let dt = try?Data(contentsOf: URL(fileURLWithPath: self))
        let img = UIImage(data: dt ?? Data()) ?? UIImage()
        return img
    }

    func withoutExt()->String{
        return self.components(separatedBy: ".").first ?? ""
    }

    func lastComponent()->String{
        return self.components(separatedBy: "/").last ?? ""
    }
}

Source: SwiftToolsHelper.swift

  • beImage():把本地文件路径字符串读取为 UIImage。读取失败(try? 返回 nil)时用空 Data 构造空图,避免返回可选类型——调用方无需解包,代价是"坏路径静默得到空白图"。
  • withoutExt():按 . 切分取首段去掉扩展名。注意该实现取的是第一个点之前的内容,对含多级扩展名(如 a.b.png)的文件会得到 a 而非 a.b,这是简单实现的取舍。
  • lastComponent():按 / 切分取末段,等价于 NSString.lastPathComponent 的手写版,用于从完整路径中提取文件名。

在 SDKTestHelper 工程中还定义了互补的字节转换扩展 String.hexToBytes: [UInt8](JLStringEx.swift),用于把十六进制字符串转成字节数组,服务于 BLE 指令拼装与调试场景。

URL 扩展

extension URL {
    func beImage()->UIImage {
        let dt = try?Data(contentsOf: self)
        let image = UIImage(data: dt ?? Data()) ?? UIImage()
        return image
    }
}

Source: SwiftToolsHelper.swift

与 String.beImage() 逻辑一致,只是入参换成 URL。两者并存的原因是调用方有时持有路径字符串、有时持有 URL 对象,扩展层为两种形态各提供一份便利方法,消除调用处的类型转换负担。

UIButton 扩展:图文布局

extension UIButton {
    /// 设置按钮为上图下字布局
    func layoutButtonImageTopTitleBottom(spacing: CGFloat = 6.0) {
        guard
            let imageSize = imageView?.image?.size,
            let title = titleLabel?.text,
            let font = titleLabel?.font
        else {
            return
        }

        let titleSize = (title as NSString).size(withAttributes: [.font: font])

        contentHorizontalAlignment = .center
        contentVerticalAlignment = .center

        titleEdgeInsets = UIEdgeInsets(
            top: spacing,
            left: -imageSize.width,
            bottom: -imageSize.height,
            right: 0
        )

        imageEdgeInsets = UIEdgeInsets(
            top: -titleSize.height - spacing,
            left: 0,
            bottom: 0,
            right: -titleSize.width
        )
    }
    /// 设置按钮为左字右图布局
    /// - Parameter spacing: 标题与图片之间的间距
    func layoutLeftTitleRightImage(spacing: CGFloat = 18.0) {
        guard let imageView = self.imageView, let titleLabel = self.titleLabel else { return }
        
        self.contentHorizontalAlignment = .center
        
        // 重置内边距,避免旧设置干扰
        self.titleEdgeInsets = .zero
        self.imageEdgeInsets = .zero
        
        // 让系统先布局,获取 size
        self.layoutIfNeeded()
        
        let titleSize = titleLabel.intrinsicContentSize
        let imageSize = imageView.frame.size
        
        self.titleEdgeInsets = UIEdgeInsets(
            top: 0,
            left: -imageSize.width,
            bottom: 0,
            right: imageSize.width + spacing
        )
        
        self.imageEdgeInsets = UIEdgeInsets(
            top: 0,
            left: titleSize.width + spacing,
            bottom: 0,
            right: -titleSize.width
        )
    }
}

Source: SwiftToolsHelper.swift

这两个方法利用 UIButton 的 titleEdgeInsets / imageEdgeInsets 负值位移实现图文重排,无需继承 UIButton 或自定义绘制:

  • 上图下字:标题向下位移 imageSize.height(越过图片)、图片向上位移 titleSize.height,垂直居中后用 spacing 控制间距。入口 guard 保证图片、文字、字体齐全,否则静默返回。
  • 左字右图:先把两个 insets 归零("重置内边距,避免旧设置干扰"),再调用 layoutIfNeeded() 让系统完成一次布局以获得 imageView.frame.size 与 intrinsicContentSize,最后做水平方向互斥位移。spacing 默认 18pt 比上图下字的 6pt 更大,符合"横向排列需要更明显间距"的视觉习惯。
  • 两者都显式设置 contentHorizontalAlignment / contentVerticalAlignment 为 .center,确保位移计算以居中为基准。

Date 扩展(JLDateEx)

extension Date {
    var getDateStr: String {
        // 将 Date 格式化为时间字符串,作为翻译会话/记录分组标识
    }
}

Source: JLDateEx.swift

getDateStr 把 Date 转成紧凑的时间字符串。在 SwiftHelper.testTranslate() 中它被用作 TextTranslateMgr.startTranslate 的 groupID,即"同一时刻发起的翻译请求归为一组"——时间戳天然唯一且单调,适合作为会话标识。该文件位于 TwsTranslateVC/ViewModel/Tools/ 目录下,说明 Date 格式化属于翻译功能沉淀出的通用工具,后被工具层复用;SDKTestHelper 工程亦有同名同构的 JLDateEx.swift。

UIColor 扩展(eHex)

public extension UIColor {
    class func eHex(_ hex: String, alpha: CGFloat = 1.0) -> UIColor {
        // 十六进制颜色字符串(如 "#FFFFFF")转 UIColor
    }
}

Source: Colors.swift

eHex 是杰理工程(SDKTestHelper 与 JLAudioUnitKitDemo 均有同款定义)统一的十六进制颜色入口,支持 alpha 参数。将设计稿中的十六进制色值直接映射为 UIColor,避免各页面重复实现解析逻辑;public 修饰使其可被宿主工程与其它模块调用。

核心流程

以「保存自定义壁纸到缓存」为例,展示工具层如何串联 SDK 运行时、R.swift 资源与 FileManager:

sequenceDiagram
    participant VC as 业务页面 (VC/VM)
    participant SH as SwiftHelper
    participant R as R 结构 (R.swift)
    participant RSDK as JL_RunSDK
    participant FM as FileManager
    participant Disk as 沙盒目录

    VC->>SH: saveWallPaperToCache(img, fileName)
    activate SH
    SH->>SH: img.pngData() 转 PNG 数据
    SH->>RSDK: sharedMe().mBleEntityM?.mItem
    RSDK-->>SH: 设备项标识 (或 "unKnow")
    SH->>R: R.path.wallPaperCustom
    R-->>SH: 壁纸根目录路径
    SH->>FM: fileExists(设备目录)?
    alt 目录不存在
        SH->>FM: createDirectory(withIntermediateDirectories: true)
    end
    SH->>FM: createFile(path, contents: dt)
    FM-->>Disk: 写入设备目录下
    FM-->>SH: true / false
    deactivate SH
    VC->>SH: listFiles(目录) 读取按时间倒序的文件列表

流程要点:

  1. 调用方只传入 UIImage 与文件名,工具层内部完成"转 PNG → 取设备标识 → 拼路径 → 建目录 → 写文件"的完整链路;
  2. 设备目录名取自 JL_RunSDK.sharedMe().mBleEntityM,实现"一设备一目录"的隔离;
  3. 读取侧通过 listFiles 按创建时间倒序返回,UI 层可直接取第一个元素展示"最新保存的壁纸";
  4. 所有可能失败的环节(PNG 转码、目录创建)均以"降级为空数据 / 忽略错误"的方式容错,保证保存操作绝不抛异常中断业务。

使用示例

以下示例均提取自仓库实际源码,展示工具层在业务代码中的真实调用方式。

示例一:保存并读取自定义壁纸

业务页面保存用户选定的壁纸图片,随后用 listFiles 枚举该设备的全部壁纸(按时间倒序,最新在前):

class func saveWallPaperToCache(_ img:UIImage,_ fileName:String){
    let dt = img.pngData() ?? Data()
    let uuid = JL_RunSDK.sharedMe().mBleEntityM?.mItem ?? "unKnow"
    let path = R.path.wallPaperCustom+"/"+uuid+"/"+fileName
    if !FileManager.default.fileExists(atPath: R.path.wallPaperCustom+"/"+uuid) {
        try? FileManager.default.createDirectory(atPath: R.path.wallPaperCustom+"/"+uuid, withIntermediateDirectories: true, attributes: nil)
    }
    FileManager.default.createFile(atPath: path, contents: dt)
}

Source: SwiftToolsHelper.swift

示例二:路径字符串直接转 UIImage

String / URL 扩展让"拿路径→出图片"只需一行:

extension String{

    func beImage()->UIImage{
        let dt = try?Data(contentsOf: URL(fileURLWithPath: self))
        let img = UIImage(data: dt ?? Data()) ?? UIImage()
        return img
    }
}

Source: SwiftToolsHelper.swift

配合 listFiles 返回的完整路径列表,UI 层可写成 cell.imageView.image = path.beImage()。

示例三:翻译会话以当前时间分组

Date.getDateStr 与 TextTranslateMgr 组合,用时间字符串充当会话分组标识:

func testTranslate(){
    TextTranslateMgr.share.startTranslate(groupID: Date().getDateStr, origin: "I am an e-ink screen wireless network driver board. I can get picture information from a PC or smartphone via WiFi or Bluetooth.", Date: Date()) { record in
        
    }
}

Source: SwiftToolsHelper.swift

示例四:按钮图文布局一键配置

无需自定义 UIButton 子类,直接调用扩展方法完成"上图下字 / 左字右图"布局:

// 上图下字(间距 6pt)
button.layoutButtonImageTopTitleBottom(spacing: 6.0)

// 左字右图(间距 18pt)
button.layoutLeftTitleRightImage(spacing: 18.0)

Source: SwiftToolsHelper.swift 与 SwiftToolsHelper.swift

API 参考

SwiftHelper(@objcMembers class,NSObject 子类)

成员签名说明
sharedstatic let shared: SwiftHelper线程安全单例(let 原子初始化)
printAllResourcesstatic func printAllResources()打印 Bundle.main 全部资源路径(.DEBUG 日志)
createFoldsclass func createFolds()触发 R.swift 资源单例初始化,创建沙盒目录
saveProtectCustomToCacheclass func saveProtectCustomToCache(_ img: UIImage, _ fileName: String)按设备 UUID 保存自定义保护图到 R.path.protectCustom
saveWallPaperToCacheclass func saveWallPaperToCache(_ img: UIImage, _ fileName: String)按设备 mItem 保存壁纸到 R.path.wallPaperCustom
listFilesclass func listFiles(_ path: String) -> [String]返回目录下文件完整路径,按创建时间倒序;目录不存在时返回 []
testTranslatefunc testTranslate()示例方法:以 Date().getDateStr 为 groupID 发起文本翻译

String 扩展

成员签名说明
beImagefunc beImage() -> UIImage把本地文件路径读为图片;失败返回空 UIImage
withoutExtfunc withoutExt() -> String取第一个 . 之前的内容作为文件名(去扩展名)
lastComponentfunc lastComponent() -> String取 / 分隔的末级路径组件
hexToBytesvar hexToBytes: [UInt8]十六进制字符串转字节数组(SDKTestHelper 中定义)

URL 扩展

成员签名说明
beImagefunc beImage() -> UIImage从 URL 读取图片;失败返回空 UIImage

UIButton 扩展

成员签名说明
layoutButtonImageTopTitleBottomfunc layoutButtonImageTopTitleBottom(spacing: CGFloat = 6.0)上图下字布局;图片/文字/字体缺失时静默返回
layoutLeftTitleRightImagefunc layoutLeftTitleRightImage(spacing: CGFloat = 18.0)左字右图布局;内部先归零 insets 再重新计算

Date / UIColor 扩展

成员签名说明
getDateStrvar getDateStr: StringDate 转时间字符串(JLDateEx.swift,作为分组/会话标识)
eHexclass func eHex(_ hex: String, alpha: CGFloat = 1.0) -> UIColor十六进制色值转 UIColor(Colors.swift,SDKTestHelper / JLAudioUnitKitDemo)

配置与依赖说明

工具层本身没有可配置项,其行为由以下外部依赖决定:

依赖用途缺失时的行为
JL_RunSDK.sharedMe().mBleEntityM提供当前设备 UUID / mItem,作为缓存目录命名空间回退为 "unKnow" 目录
R.swift 生成的 R.path.protectCustom / R.path.wallPaperCustom类型安全的沙盒目录路径需保证 R.swift 在编译期生成成功
JLLogManager输出 .DEBUG 日志仅影响调试输出
TextTranslateMgr翻译服务(testTranslate 使用)仅影响示例方法

关键前提:必须先连接设备并完成 SDK 初始化,mBleEntityM 才非空;createFolds() 应在 App 启动早期调用,确保 R.swift 声明的目录就绪。

失败模式、边界情况与并发

强制解包风险

listFiles 的排序闭包中对 date1! / date2! 使用强制解包(SwiftToolsHelper.swift)。若目录中存在无法读取创建时间的条目(例如权限受限或被并发删除的文件),try? 返回 nil,as? Date 亦可能为 nil,此时强制解包将导致崩溃。当前实现依赖"目录内全为正常图片文件"的前提;若要加固,可改为 if let date1, let date2 的安全解包并对缺失条目标记排序优先级。

静默降级策略

  • beImage() 系列在读取失败时返回空白 UIImage 而非 nil 或抛出错误。好处是调用方无需处理可选值、UI 渲染不会中断;代价是"坏路径"会被静默掩盖,排查时难以察觉。适合演示/缓存类场景,不适合对数据完整性有强要求的业务。
  • img.pngData() ?? Data() 与 try? createDirectory 采用同样的"尽力而为"哲学:图片缓存失败不应阻塞主流程。

并发考虑

  • SwiftHelper.shared 使用 static let 单例,初始化本身线程安全;但 saveProtectCustomToCache / saveWallPaperToCache 中的"检查目录是否存在 → 创建目录 → 写文件"并非原子操作。多个线程同时为同一设备保存图片时,createDirectory(withIntermediateDirectories: true) 在目录已存在时不会报错(withIntermediateDirectories 语义),因此大概率安全;但 createFile 对同名文件是覆盖写,并发写同一文件名会出现后写覆盖先写。当前工程中保存动作由 UI 事件串行触发,竞争窗口极小。
  • listFiles 与写入并发执行时,可能枚举到"正在写入的半成品文件"。若 UI 层要求严格一致,应在写入完成(如先写临时文件再 rename)后再枚举。

路径语义边界

  • withoutExt() 取第一个 . 前的片段:"a.b.png" 会得到 "a" 而非 "a.b";对含目录的完整路径需先 lastComponent() 再 withoutExt()。
  • 目录名 "unKnow" 兜底意味着未连接设备时保存的图片会全部落到同一目录,换设备后可能读到错误归属的图片;业务层应在保存前确认连接状态。

性能与运维注意事项

  • 日志:printAllResources() 会打印 Bundle.main 全部资源路径,仅在调试时调用,避免在发布版本中产生大量日志开销。
  • IO 位置:saveProtectCustomToCache / saveWallPaperToCache 在主线程同步执行 PNG 转码与磁盘写入。图片较大时可能造成卡顿,建议在高频场景(如连续保存多张壁纸)迁移到后台队列。
  • 磁盘占用:工具层不提供缓存清理逻辑,长期使用后 wallPaperCustom / protectCustom 目录会持续增长;运维上可由宿主 App 在低电量/启动时按设备清理。
  • 编译期保障:目录路径来自 R.swift,路径错误在编译期即可发现,是工程内"类型安全资源"实践的一部分。

扩展点

工具层的扩展方式非常直接——继续追加 extension 即可,无需改动现有类:

  1. 新增系统类型扩展:仿照 extension String / extension UIButton 的模式,为其它 UIKit/Foundation 类型补充工程内高频方法;注意 @objcMembers 类内的扩展成员对 OC 自动可见,独立文件的扩展若需 OC 可见需单独加 @objc。
  2. 新增文件能力:在 SwiftHelper 中增加 class func,复用 R.path 新声明的目录常量与 JL_RunSDK 设备信息,保持"设备目录隔离"的既有约定。
  3. 跨工程复用:SDKTestHelper 与 JLAudioUnitKitDemo 均维护了同构的 Tools/ 目录(Colors.swift、JLDateEx.swift、JLStringEx.swift),说明杰理工程倾向于把这些扩展文件原样拷贝到各工程;如需统一维护,可考虑抽取为私有 CocoaPods/Swift Package。

测试情况

仓库内未发现针对 SwiftHelper 与各扩展方法的单元测试文件。testTranslate() 方法名含 test 前缀,但它只是演示翻译服务调用的示例方法,并非 XCTest 用例。这意味着:

  • 工具层的行为保障主要依赖调用方页面的人工验证(保存壁纸后手动刷新列表观察排序等);
  • listFiles 的强制解包与 beImage 的静默降级属于"测试盲区",若后续补充测试,建议优先覆盖:空目录、目录不存在、含无属性文件条目、多级扩展名路径、未连接设备("unKnow" 兜底)等边界。

Related Links

  • SwiftToolsHelper.swift(核心工具类与 String/URL/UIButton 扩展)
  • JLDateEx.swift(Date 扩展,TwsTranslateVC 工具目录)
  • Colors.swift(SDKTestHelper 的 UIColor.eHex 扩展)
  • JLStringEx.swift(SDKTestHelper 的 String.hexToBytes 扩展)
  • JLDateEx.swift(SDKTestHelper 同名 Date 扩展)
  • 相关兄弟页面:数据库持久化层(DataBase:HealthDataBase、JLBroadcastDataBase、SettingDefault)、应用功能页面(DevicesViewController / MediasViewController 下的业务模块)
Prev
数据存储与缓存
Next
JLAudioUnitKit 示例工程