SwiftUI Charts Architecture & DataFlow

Swift 相关知识点学习

2025-05-08  本文已影响0人  Tuberose

Swift 相关知识点学习

一、Main Actor 隔离

.task {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            uploadData()
        }

        group.addTask {
            // Disable Idle Timer to avoid auto-locking screen  
            UIApplication.shared.isIdleTimerDisabled = true
        }
    }
}

遇到的这个编译错误:

Main actor-isolated class property 'shared' can not be mutated from a Sendable closure

是因为在 .task 这个 modifier 的异步闭包中访问了 UIApplication.shared.isIdleTimerDisabled = true,而这个属性 受 Main Actor 隔离(Main actor-isolated),不能在一个并发上下文中(比如 TaskGroup 内部)直接修改。


⚠️ 原因分析:


✅ 解决方案:

方法一:在主线程中设置 isIdleTimerDisabled

你可以把它包裹到 MainActor.run 中运行:

group.addTask {
    await MainActor.run {
        UIApplication.shared.isIdleTimerDisabled = true
    }
}

方法二:把 UIApplication.shared.isIdleTimerDisabled = true 放到 .onAppearDispatchQueue.main.async

其实完全可以避免放在 taskgroup 中,像这样写也可以:

.onAppear {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
        self.onboarding()
        UIApplication.shared.isIdleTimerDisabled = true
    }
}

或者直接:

.onAppear {
    UIApplication.shared.isIdleTimerDisabled = true
}

✅ 总结建议

由于设置 isIdleTimerDisabled 本身并不耗时,也不需要并发处理,直接放在 .onAppear 中或者使用 MainActor.run 包裹,确保线程安全即可。


一、先解释:@MainActor 是什么?

👉 它的作用:

@MainActorSwift 的一种 actor 隔离机制,用于确保标记的变量或方法 只能在主线程上被访问或修改,适用于:

@MainActor
var title: String  // 只能在主线程读取或写入

在原来的代码里,不用加 @MainActor 也可以,只要保证在主线程上更新 UI 状态或平台变量即可



同步耗时操作阻塞主线程


init() {
    // Check if UserDefaults contains platform key
    if let platform = UserDefaults.standard.string(forKey: "Platform"), let platform = Platform(rawValue: platform) {
        self.platform = platform
    } else {
        self.platform = MyCommand.shared.searchPlatform()
        UserDefaults.standard.set(self.platform.rawValue, forKey: "Platform")
    }

    Task {
        switch self.platform {
        case .platform10xx:
            let _ = MyCommand.shared.initXXXListener()
        case .platform20xx:
            let _ = MyCommand.shared.initXXXEvent()
        }
    }
}

这个 MyCommand.shared.searchPlatform() 是执行命令,返回平台是platform10xx还是 platform20xx。可能比较耗时,使用异步线程调用就解决了这个问题。它为什么会影响 view 的启动。


🧠 一、为什么 MyCommand.shared.searchPlatform() 会影响 view 启动?

✅ 原因归结为一点:它是一个同步耗时操作”,放在 初始化阶段(init)阻塞主线程,间接破坏了 AVKitUI 的初始化时机。

🔍 具体影响路径:

  1. init() 是在主线程上调用的,如果其中执行了耗时操作(如 searchPlatform() 是同步执行 shell 命令或 I/O 的话)——
  2. 阻塞主线程,导致整个 app UI 构建、系统服务(如 AVAudioSessionAVPlayerAVPiPController)初始化延迟甚至错过系统期望的生命周期阶段。
  3. 特别是 PiP view初始化涉及的系统服务需要在合适的生命周期点启动,且依赖主线程的“响应性”,此时如果主线程卡住,就会出现系统组件初始化失败,或者系统认为“你不支持 PiP”。

✅ 将 searchPlatform() 放到异步线程后,系统组件终于能按时正常初始化,自然也就不会影响 PiP 启动了。

init() {
    Task {
        await self.setupPlatformt()
    }
}

private func setupPlatform() async {
    // 1. Get platform (cache first)
    let detectedPlatform: Platform
    if let cached = UserDefaults.standard.string(forKey: "Platform"),
       let cachedPlatform = Platform(rawValue: cached) {
        detectedPlatform = cachedPlatform
    } else {
        detectedPlatform = await withCheckedContinuation { continuation in
            DispatchQueue.global(qos: .userInitiated).async {
                let result = MyCommand.shared.searchPlatform()
                continuation.resume(returning: result)
            }
        }
        
        // Caching platform values
        UserDefaults.standard.set(detectedPlatform.rawValue, forKey: "Platform")
    }
    
    // 2. Update local variables 
    self.platform = detectedPlatform
    
    // 3. Execute initialization commands according to the platform
    switch detectedPlatform {
        case .platform10xx:
            let _ = MyCommand.shared.initXXXListener()
        case .platform20xx:
            let _ = MyCommand.shared.initXXXEvent()
    }
}

✅ 1. init() 方法一般用来做什么?

💡 init() 的本质职责:

构造出有效可用的对象初始状态,不建议在其中执行副作用重的逻辑,尤其是:

❗为什么?


✅ 合理的 init() 用法 ✅

合法的行为 说明
设置默认值 初始化本地属性,比如 self.platform = .unknown
从内存中恢复状态 例如 UserDefaults / 本地缓存
创建依赖对象 比如 Logger()URLSession()
做轻量判断逻辑 不会阻塞线程

❌ 不推荐的 init() 行为 ❌

不推荐的行为 原因
执行 shell 命令 / I/O 操作 易阻塞主线程,且不易捕获异常
配置系统服务(如 AVAudioSession 初始化时系统组件可能尚未准备好
创建 AVPlayer / PiP Controller 等系统资源 这些往往依赖系统生命周期和 UI 状态
启动异步任务并期望依赖其结果 初始化结束后没法等结果完成,不可预测

🧠 2. 那耗时或异步操作放在哪更合适?



二、private(set) 和 didSet

这是 Swift 中一种非常简洁且实用的语法组合

完整语法结构分析

private(set) var currentSession: MySession? {
    didSet {
        if let currentSession {
            // ...
        }
    }
}

1. private(set):访问权限控制


2. var currentSession: MySession?


3. didSet 是属性观察器


4. if let currentSession简化版的 Optional Binding

if let currentSession {
    // ...
}

等价于:

if let currentSession = currentSession {
    // ...
}

Swift 5.7+ 中支持这种 简写绑定形式


5. didSet 内的逻辑说明

self.sessionStart = Date()
self.logPath = currentSession.folderPath + "/myLog.txt"

📌 小结:这是一个“状态属性 + 观察器 + 封装写权限”的典型用法

它实现了:


📚 可借鉴的场景



三、串行队列 DispatchQueue

let queue = DispatchQueue(label: "com.test.queue", qos: .userInitiated)

这个代码中的 DispatchQueue 是一个串行队列(serial queue)。在这行代码中,DispatchQueue(label: "com.test.queue", qos: .userInitiated) 创建了一个串行队列,但它的使用是否异步取决于如何在队列中调度任务。

解释:

  1. 串行队列(Serial Queue):

    • 串行队列按顺序执行任务,一个任务在前一个任务完成后才会执行下一个任务。即使将多个任务放入队列,它们也会逐一执行,而不会并行。
    • 通过传递 label 来创建一个串行队列。这里的 com.test.queue 只是队列的名称,qos: .userInitiated 是队列的质量服务(Quality of Service)级别,表示任务是用户发起的,应该优先执行。
  2. 为什么不是异步的

    • 当创建了一个队列,但队列的调度模式是决定异步还是同步的。在这行代码中,队列本身没有指定任务的调度方式。
    • 如果在队列中调度任务时使用的是 async,那么任务就是异步执行的;如果使用的是 sync,则是同步执行。

例如:

let queue = DispatchQueue(label: "com.test.queue", qos: .userInitiated)

// 异步调度任务
queue.async {
    // 异步任务
    print("This is async task")
}

// 同步调度任务
queue.sync {
    // 同步任务
    print("This is sync task")
}

总结:



四、stride(from:through:by:) 函数学习

stride(from:through:by:)Swift 中用来生成一个数值序列的函数。它通过指定起始值、结束值和步长来创建一个有规律的数值序列。你可以使用它来创建一个范围内的数值集合,步长可以是正数也可以是负数。

函数签名:

func stride(from start: T, through end: T, by step: T) -> StrideTo<T> where T : Strideable

参数说明:

返回值:

示例代码:

  1. 正向步进(递增)
    生成从 0 到 10(包括 10)之间的数,步长为 2。

    let numbers = stride(from: 0, through: 10, by: 2)
    for number in numbers {
        print(number)
    }
    

    输出:

    0
    2
    4
    6
    8
    10
    
  2. 负向步进(递减)
    生成从 10 到 0(包括 0)之间的数,步长为 -2。

    let numbers = stride(from: 10, through: 0, by: -2)
    for number in numbers {
        print(number)
    }
    

    输出:

    10
    8
    6
    4
    2
    0
    
  3. 通过 ... 创建闭区间
    stride(from:through:by:) 是一个闭区间(through),意味着它会包含结束值。

使用场景:

stride 适用于需要创建有规律的数值序列时,例如:

总结:

stride(from:through:by:) 函数是 Swift 提供的一个非常灵活的工具,可以用于生成带有特定步长的数值序列。它非常适合需要精确控制范围和步长的场景。



五、split(separator:maxSplits:omittingEmptySubsequences:)

这个方法用来把一个字符串分割成多个子字符串([Substring]),根据指定的“分隔符”来进行切割。

🧾 函数签名:

func split(
    separator: Character,
    maxSplits: Int = .max,
    omittingEmptySubsequences: Bool = true
) -> [Substring]

📌 参数说明:


🔍 这个例子的解析:

let components = content.split(separator: "?", maxSplits: 1)

等价于:

let components = content.split(separator: "?", maxSplits: 1, omittingEmptySubsequences: true)

🧪 示例:

例 1:正常分割

let content = "abc?def?ghi"
let parts = content.split(separator: "?", maxSplits: 1)
// parts = ["abc", "def?ghi"]

只分一次。第一次遇到 ? 就切一刀,剩下的部分原样保留。


例 2:分割多次(默认值)

let content = "a?b?c?d"
let parts = content.split(separator: "?")
// parts = ["a", "b", "c", "d"]

默认是 maxSplits: .max,会尽可能多地切。


例 3:保留空子串

let content = "a??b"
let parts = content.split(separator: "?", omittingEmptySubsequences: false)
// parts = ["a", "", "b"]

🧠 总结记忆法:



六、Swift 中使用 static

在 Swift 中频繁使用 static 有什么问题?

static 方法是类型方法,它们属于类型(class/struct/enum),而不属于某个实例对象。频繁使用 static 方法有一些 潜在问题

❌ 可能的问题

  1. 无法访问实例属性

    • static 方法无法访问 self 或实例变量,这意味着所有需要实例数据的逻辑都不能放在 static 方法中
    • 例如:
      class Example {
          var name = "Swift"
      
          static func printName() {
              print(name) // ❌ 编译错误,不能访问实例属性
          }
      }
      
    • 解决方案:如果方法需要访问实例数据,就不应该使用 static,应该用实例方法
  2. 不适用于需要继承的情况

    • static 方法不能被子类重写,而 class func 可以。
    • 例如:
      class Parent {
          static func greet() {
              print("Hello from Parent")
          }
      }
      
      class Child: Parent {
          override static func greet() {  // ❌ 报错,static 方法不能被 override
              print("Hello from Child")
          }
      }
      
    • 解决方案
      • 如果方法需要允许子类重写,请使用 class func 代替 static func
  3. 容易导致全局状态污染

    • 过多 static 方法会让类逐渐变成工具类(Utility Class),但大部分时候,面向对象设计更鼓励使用实例方法

    • 例如:

      class Utility {
          static func format(date: Date) -> String { ... }
          static func log(message: String) { ... }
          static func validateEmail(_ email: String) -> Bool { ... }
      }
      
    • 问题

      • 这些方法全是 static,导致 Utility 变成了一个全局工具类
      • 缺乏面向对象的封装性,无法在不同实例间存储状态。
      • 难以扩展:如果需要不同的 log 级别、不同的 date format,就必须增加参数或新增方法,代码维护困难。
    • 解决方案

      • 如果方法属于特定的对象实例,尽量用实例方法
      • 如果方法需要继承,使用 class func

什么时候适合使用 static

虽然 static 可能带来上述问题,但在以下场景中使用 static 是合适的

✅ 1. 工具方法(Utility Methods)

✅ 2. 单例模式

✅ 3. 枚举中的静态常量

✅ 4. 类型级计算属性

✅ 5. static 作为工厂方法


什么时候不适合用 static

场景 static 适合? 推荐做法
需要访问实例属性 ❌ 不适合 用实例方法
可能需要被子类重写 ❌ 不适合 class func
可能存储状态 ❌ 不适合 用实例方法
需要创建对象(工厂方法) ✅ 适合 可用 static func
纯工具方法 ✅ 适合 可用 static func

代码如何优化?

ReportConfigModel 里,频繁使用 static,但它们大部分是不依赖实例的数据解析逻辑,所以static 是合适的

✅ 但如果以后 booleanValue 需要根据实例的设定解析数据,建议改成实例方法:

func booleanValue(forKey key: String, in dictionary: NSDictionary) -> Bool {
    (dictionary[key] as? Bool) ?? ((dictionary[key] as? String)?.lowercased() == "true")
}

这样更灵活,避免滥用 static


总结


✅ 结论:
ReportConfigModel 里,booleanValue 目前是工具方法,static 没问题
但如果未来它需要访问 self,就应该改成实例方法。合理使用 static,避免让代码变成全是工具函数的“过程式编程”



七、数字系统及其转换

1 基本概念

2 数字转换方法

3 应用场景



八、Xcode 调试工具:LLDB、Console 和 OSLog

1 LLDB

2 Console

3 OSLog


上一篇 下一篇

猜你喜欢

热点阅读