阅读视图

发现新文章,点击刷新页面。

iOS Cursor 使用心得

最近在使用 Cursor 过程中,业务需求基本上 80% 代码由 AI 开发。

本文把过程中的一些心得记录分享一下。

基础配置

先介绍一下基本的配置。

个人使用的 Rules:

使用中文回复
每次生成代码前,先概述你的方案,等我确认后再生成代码
仅做最小改动,不要动任何我别的代码逻辑和注释,不调整注释的标点符号
不添加 log,只保留最关键的注释,注释使用中文

由于不同地方使用的风格不同,具体代码风格在 prompt 再单独指定。

自己记录在笔记中,方便快速粘贴,如:

public class SomeView: UIView {
    // 定义 UI 子控件,如
    let textLabel = UILabel()
    let imageView = UIImageView()

    override init(frame: CGRect) {
        super.init(frame: frame)

        // 对 UI 子控件进行布局
        textLabel.sk_added(to: self).then({ cur in
            // 此处设置 UI 展示属性
            cur.font = .systemFont(ofSize: 17, weight: .regular)
        }).sk_layout { make in
            // 此处设置布局代码,使用 SnapKit 语法
            make.left.equalTo(12)
            make.height.equalTo(24)
            make.top.equalTo(16)
            make.bottom.equalTo(-14)
        }
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Vibe Coding

图片展示了整个 AI 辅助编程的个人流程。

image.png

下面针对这个流程逐项再展开说明下。

0、前提掌握

上图是市面上常用的 AI 编程工具,按使用者的可控制程度排列。

虽然目前市面上已经有很多 0 基础 AI 编程工具,但是对于有准确性要求的商业项目开发,或者希望代码能够由人工交替维护,还是需要把控整体的架构。

所以哪部分知识是 Vibe Coding 下比较重要的部分?个人认为有以下几点:

  1. 某个领域的核心概念:拿 iOS 举例的话可能有 Swift 语法、UIKit 的使用方法、如何调试程序

  2. 最佳实践的把握(代码品味):SOP、模板、规范

而在 AI 的帮助下,我们也可以更快速地入门某个专门领域。

而代码品味则需要一定时间的工作沉淀,但在不同的领域里是可以共享的。

1、领域驱动,数据设计先行

image.png

在开发前,先确认好数据,包含以下维度

  • UI 需要用到的 ViewModel
  • 存储或与后台交互的 Model

当然,数据的设计也可以让 AI 进行,我们做一个 review 和调整。

这样做的好处是,后续 AI 将围绕固定的数据处理来进行编码,相当于提供了上下文和约束,会更容易得到想要的结果。

而对于已有的项目,则可以让 AI 先生成数据处理流程的文档,也方便自己快速了解逻辑。

这里的思路就是将步骤拆解成 SOP,每一步进行人工的确认,确认后作为下一步的前提

下面在逻辑开发过程中,也会遵照这一思路进行。

2、逻辑开发,Feature 维度

在 rules 中,有这样一句:“每次生成代码前,先概述你的方案,等我确认后再生成代码”。

这样做有两方面的好处:

  1. 对话中,AI 会将前文都带上,先输出方案作为后续代码生成的上下文,有利于代码准确
  2. 可以检查方案的可行性,如果不合适可以先进行调整

另外一点是以 Feature 的维度进行开发,特别是对于复杂的 controller。

具体含义是:实现某个功能的时候,先将相关的功能拆分出来,放入一个单独的 Feature 类,后续的开发限定在这个 Feature 类内进行。

这样做的好处主要是:

  1. 本身也是一种防止 Massive Controller 的实践,有利于代码的高内聚、低耦合
  2. 防止 AI 对其它无关代码进行修改(虽然 rules 说明了不要这样做,但 AI 还是会忍不住,特别是对于把注释的中文引号改成英文引号,claude 模型似乎有一种执念 😂)

3、Feature 完成,实时重构

在功能开发过程中,由于是通过多次和多轮对话进行的,AI 生成的代码难免会存在冗余的毛病。

在功能完成后,我们可以要求 AI 对 Feature 进行一次重构,让其优化代码风格、抽离重复的部分。

实际效果来看,AI 能出色地完成该任务。

这也体现了我们按 Feature 维度进行开发的好处,可以有效限制重构的范围。

Git Tips

勤提交,完成一个功能点后提交一下,防止 AI 后续改乱

但对于功能点过程中的多轮生成,每次都提交比较繁琐,提交也会过多而且不完整

个人比较习惯通过暂存区来阶段性提交,将已接受的和新生成的分离开

image.png

完成一轮之后,把接受的改动放入暂存区,此时 Cursor 也会自动算作 accept

再继续进行下一轮生成时,新的改动会在未暂存区,单独看这部分即可

参考

氛围编程 Vibe coding - 维基百科,自由的百科全书

AI 工具对比 I ranked every AI Coder: Bolt vs. Cursor vs. Replit vs Lovable

领域驱动 Domain Modeling: What you need to know before coding | Thoughtworks

Cursor 技巧 CursorHub教程

Cursor iOS 开发(供参考,个人还是 Cursor 生成、Xcode 运行比较顺手) How to use VSCode/Cursor for iOS development | by Thomas Ricouard | Medium

Podfile主要指令和参数

Podfile 中,主要用于定义 CocoaPods 配置的指令和参数。它是一个用 Ruby 编写的文件,包含了如何将依赖项(Pods)集成到 iOS 或 macOS 项目的详细信息。以下是 Podfile 中常见参数的详细解释:

1. platform

platform :ios, '10.0'
  • 含义:指定项目支持的最小 iOS 版本或其他平台版本。
  • 例子
    • platform :ios, '10.0' 表示项目最低支持 iOS 10.0 版本。
    • platform :osx, '10.13' 表示项目最低支持 macOS 10.13 版本。

2. target

target 'YourAppName' do
  # Pods for YourAppName
end
  • 含义:指定一个目标应用(Target),在该目标中集成相应的依赖。
  • 例子target 'MyApp' 指定了将要包含依赖的目标应用或目标框架。

3. pod

pod 'Alamofire', '~> 5.0'
  • 含义:指定依赖的具体库或框架及其版本。
  • 参数
    • 'Alamofire':库的名称。
    • ~> 5.0:表示 Alamofire 版本范围为 5.x(例如 5.1、5.2 等)。

还有以下配置含义

  • >0.1 表示大于 0.1 的任何版本,这样可以包含 0.2 或者 1.0
  • >=0.1 表示大于等于 0.1 的任何版本
  • <0.1 表示少于 0.1 的任何版本
  • <=0.1 表示少于等于 0.1 的任何版本

4. podspec

pod 'LibraryName', :podspec => 'https://example.com/LibraryName.podspec'
  • 含义:通过指定 .podspec 文件来集成自定义的 Pod 库,而不是从公共的 CocoaPods 仓库中获取。
  • 例子pod 'MyLibrary', :podspec => 'https://example.com/myLibrary.podspec'

5. use_frameworks!

use_frameworks!
  • 含义:启用动态框架(Frameworks)。如果不设置,CocoaPods 默认使用静态库(Static Libraries)。
  • 详细:设置 use_frameworks! 后,所有的 Pod 都会作为动态框架来集成,这对某些 Swift 项目尤为重要,因为 Swift 代码必须使用动态框架才能与其他 Swift 代码互相调用。

6. inherit!

inherit! :search_paths
  • 含义:控制如何继承父级 target 中的设置。常用于继承父目标的配置或路径。
  • 常见选项
    • :search_paths:继承父目标的搜索路径设置。
    • :complete:完全继承父目标的所有设置。

7. post_install

post_install do |installer|
  installer.pods_project.targets.each do |target|
    # 修改构建设置
  end
end
  • 含义:在安装完所有 Pods 后执行的脚本。通常用于对生成的 Xcode 项目进行自定义设置。
  • 例子:你可以在这里设置一些编译选项,或者在安装后做一些额外的配置,如自定义 Info.plist 或设置 Build Settings

8. pre_install

pre_install do |installer|
  # 安装之前做一些事情
end
  • 含义:在开始安装 Pods 之前执行的脚本,通常用于对安装前的环境进行调整。
  • 例子:可以用来检查是否满足某些条件,或者做一些清理工作。

9. target 中的 dependency

target 'App' do
  pod 'Alamofire'
  target 'AppTests' do
    pod 'Quick'
  end
end
  • 含义:在目标中嵌套定义其他子目标或依赖项。例如,如果你的应用有一个测试目标,你可以在 AppTests 中指定它的依赖。

10. configurations

configurations = ['Debug', 'Release']
  • 含义:指定哪些构建配置需要应用此 Podfile 中的配置。

11. source

source 'https://github.com/CocoaPods/Specs.git'
  • 含义:指定 Pod 依赖库的源仓库。默认情况下,CocoaPods 使用官方的 Specs 仓库,但你可以添加或更改为其他源。
  • 例子:如果使用私有的 Pod 库,你可以设置 source 指向你自己的仓库地址。

12. deployment_target

deployment_target = '10.0'
  • 含义:设置每个 Pod 的部署目标版本。它可以覆盖 platform 指定的版本,适用于某些特定库。
  • 例子deployment_target = '10.0' 表示设置该 Pod 的最低支持版本为 iOS 10.0。

13. :git:path

pod 'MyLibrary', :git => 'https://github.com/MyOrg/MyLibrary.git'
  • 含义:指定从 Git 仓库安装 Pod 或从本地路径安装 Pod。
  • 例子
    • :git 用于从 Git 仓库拉取代码。
    • :path 用于指定本地路径中的 Pod。

14. :branch, :tag, :commit

pod 'MyLibrary', :git => 'https://github.com/MyOrg/MyLibrary.git', :branch => 'develop'
  • 含义:当使用 :git 时,可以指定从哪个分支、标签或者提交记录拉取 Pod。
  • 例子
    • :branch => 'develop' 指定拉取 develop 分支。
    • :tag => 'v1.0.0' 指定拉取某个标签。
    • :commit => 'abcdef' 指定拉取某个特定提交。

15. :subspec

pod 'Alamofire/NetworkReachability'
  • 含义:如果 Pod 提供多个子模块,你可以只集成其中的一部分。子模块通常被称为 "subspecs"。
  • 例子pod 'Alamofire/NetworkReachability' 仅引入 Alamofire 中的 NetworkReachability 模块。

CocoaPods常见问题的处理方法,文章链接:

Xcode运行项目时安装cocoapods报错问题修复Couldn't determine repo type for URL:https://cdn.cocoapods.org/:

juejin.cn/post/725297…

CocoaPods常见问题:

nativesupport.dcloud.net.cn/UniMPDocs/F…

Swift中的析构函数deinit

析构函数

在 Swift 中,deinit(析构函数)只适用于 class 类型。因为只有类是引用类型,而不像结构体 struct 或枚举 enum 是值类型。

虽然 Swift 自动管理内存(ARC,Automatic Reference Counting),你不需要经常写 deinit,但在某些关键场景中非常有用。以下是最常见且实用的使用场景:

常用场景一:移除通知监听者

使用 NotificationCenter 添加观察者时,如果不手动移除,会导致内存泄漏或重复响应事件

class MyObserver {

    init() {
        NotificationCenter.default.addObserver(self, selector: #selector(handleEvent), name: .someEvent, object: nil)
    }

    @objc func handleEvent() {
        print("Event received")
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
        print("Observer deinitialized")
    }
}

常用场景二:取消定时器、任务、订阅等异步资源

比如 Timer、DispatchSourceTimer、Combine 的 AnyCancellable、URLSession task 等等。

class TimerHandler {
    var timer: Timer?

    init() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            print("Tick")
        }
    }

    deinit {
        timer?.invalidate()
        print("Timer invalidated")
    }
}

常用场景三:断开委托(Delegate)或弱引用检查

有时候你需要在 deinit 中把自己从其他类的 delegate 中移除,以避免无用引用。

class Child: SomeDelegate {
    init(parent: Parent) {
        parent.delegate = self
    }

    deinit {
        print("Child released")
        // 这里通常不强制清空 delegate,但有时你想显式处理
    }
}

常用场景四:清理自定义资源或临时文件

当类负责写入临时文件或打开文件句柄等,需要在析构时清理。

class TempFileManager {
    let filePath: String

    init() {
        filePath = NSTemporaryDirectory() + UUID().uuidString
        FileManager.default.createFile(atPath: filePath, contents: nil)
    }

    deinit {
        try? FileManager.default.removeItem(atPath: filePath)
        print("Temporary file deleted")
    }
}

常用场景五:调试或资源释放追踪

打印 deinit 是常见的调试手段,能够帮助判断是否某对象已经被释放(尤其是在处理循环引用、Retain Cycle 的时候)。

deinit {
    print("MyViewController has been released!")
}

总结:deinit 适用的典型场景

场景 用途说明
通知监听移除 防止内存泄漏
定时器/任务取消 释放异步资源
文件或自定义资源清理 避免残留资源
delegate/数据源解绑(可选) 规范化资源管理
Combine/任务订阅取消 清理绑定或订阅
打印调试(检查是否释放) 非常实用的调试手段

在 SwiftUI 或 iOS 架构设计中,有些类如 ViewModel 或自定义对象,还是推荐善用 deinit 来做资源释放和问题定位



构造链

Swift 中 enum、struct 和 class 都可以拥有构造函数(initializer),但三者在构造行为和使用方式上有 明显差异,尤其是在以下几个地方:

• 默认构造器是否自动生成

• 成员变量初始化规则

• 继承与重写支持

• 可变性

• 引用与值语义

这里主要讨论下构造链。构造在 Swift 中,class 是引用类型,支持继承。而子类初始化时需要 保证父类的属性也被正确初始化,因此构造函数需要遵循一定的调用顺序,也就是所谓的“构造链”:

• 子类的 init 必须在某一阶段调用 super.init(...),以确保父类部分正确构造。

• 所有 stored property 初始化完后,才能调用 super.init()。

• Swift 编译器会严格检查构造顺序,避免未初始化状态下访问属性。

class Animal {
    var name: String
    init(name: String) {
        self.name = name
        print("Animal init")
    }
}

class Dog: Animal {
    var breed: String
    init(name: String, breed: String) {
        self.breed = breed              // 1️⃣ 先初始化子类属性
        super.init(name: name)         // 2️⃣ 调用父类构造器
        print("Dog init")
    }
}

// 调用
let dog = Dog(name: "Buddy", breed: "Labrador")

// 输出
# Animal init
# Dog init

对于 class类型:

1. 所有存储属性必须初始化。

2. 子类构造器必须在合适时机调用 super.init(...)。

3. 若父类没有默认构造器,子类必须手动调用父类指定构造器。

4. convenience init 只能调用本类的 designated init,不能直接调用 super.init。

5. 父类的 required init 必须被子类实现。

"struct 和 enum 没有继承, 它们只有构造器,不存在构造链,甚至构造器都可以用默认自动生成的

苹果3.2f倒计时结束后“死亡邮件”是啥样的?

前言

经历了苹果审核几轮清洗,有不少来询问3.2f破局以及被封号之后的情况,所以单独拿出来简单做个回复。

3.2f终止时间

从收到3.2f邮件到苹果正式封号,大概是30个自然日左右。正常情况下,未做任何解释以及回复一般就是足月嘎了。

如果是积极回复苹果,主动向苹果解释说明情况的大概率会比30天长一些。如果说也是30天到时候嘎了,那么就证明解释的内容完全没有被苹果认可,直接维持原判

3.2f死亡邮件

下面的图就是30天后收到死亡通牒,如图可见。

wechat_2025-05-16_174421_144.png

其中最关键的两点:

第一点:死刑立即执行!

本函作为您与Apple之间的《Apple开发者计划许可协议》(以下简称“ADP协议”)和《Apple开发者协议》(以下简称“开发者协议”)的终止通知,立即生效。

第二点:没救了不用挣扎了。

本函并非对此事事实的完整陈述,本函中的任何内容均不应被解释为放弃苹果可能拥有的任何权利或补救措施,所有这些权利或补救措施均在此保留

后续处理

最基本是测试设备不用想了,要贴标签做好设备隔离。避免重蹈覆辙!开发设备如果条件允许的情况下建议也处理掉了,以绝后患。

其次是将绑定的内购账号存档记录,不再使用。

最后就是代码了能不用就不能,能不从里面粘贴就别粘贴。苹果的算法已经不像之前那么简单了,别因为偷懒因小失大!也恳请各位管理层能多宽限些时间,避免技术编码时间不够不得不偷懒。

遵守规则,方得长治久安,最后祝大家大吉大利,今晚过审!

JSON Schema 表单规范

🧾 JSON Schema 表单规范 v1.0

该规范用于描述一个表单页面的 UI、字段规则、交互逻辑和校验要求。支持分组、动态显隐、联动、正则等高级能力。


JSON示例

{
  "title": "注册信息",
  "sections": [
    {
      "title": "基础信息",
      "fields": [
        {
          "key": "name",
          "label": "姓名",
          "type": "text",
          "required": true,
          "placeholder": "请输入姓名"
        },
        {
          "key": "gender",
          "label": "性别",
          "type": "radio",
          "required": true,
          "options": [
            { "label": "男", "value": "male" },
            { "label": "女", "value": "female" }
          ]
        },
        {
          "key": "militaryStatus",
          "label": "服兵役情况",
          "type": "text",
          "placeholder": "如已服役/未服役",
          "visibleIf": {
            "gender": "male"
          }
        }
      ]
    },
    {
      "title": "联系方式",
      "fields": [
        {
          "key": "phone",
          "label": "手机号码",
          "type": "text",
          "validators": [
            {
              "type": "regex",
              "pattern": "^1[0-9]{10}$",
              "message": "请输入有效手机号"
            }
          ]
        }
      ]
    }
  ]
}

Swift Demo

chatgpt.com/canvas/shar…

📌 顶层结构

{
  "title": "表单标题",
  "sections": [ FormSection, ... ]
}
字段 类型 含义
title string 整个表单的标题
sections array 分区数组,每个分区有一个标题与若干字段

📦 FormSection

{
  "title": "分区标题",
  "fields": [ FormField, ... ]
}
字段 类型 含义
title string 分区标题
fields array 表单字段列表

🧩 FormField 字段属性说明

{
  "key": "字段标识",
  "label": "显示名称",
  "type": "字段类型",
  "required": true,
  "placeholder": "提示文本",
  "editable": true,
  "hidden": false,
  "options": [ Option ],
  "visibleIf": { "gender": "male" },
  "validators": [ Validator ]
}
字段 类型 说明
key string 字段唯一标识(存值取值)
label string 显示名
type string 字段类型(见下表)
required boolean 是否必填
placeholder string 输入框提示文字
editable boolean 是否可编辑
hidden boolean 是否默认隐藏(不可见)
options array of Option 可选项,仅限 select/radio
visibleIf object {k: v} 显示条件(当 key==value 时显示)
validators array of Validator 校验规则

支持的字段类型(type):

类型 说明
text 普通文本输入框
textarea 多行文本
number 数字输入
select 下拉菜单
radio 单选组
checkbox 多选组(待扩展)
date 日期选择(可扩展)

🎛️ Option 配置(用于 select / radio)

{ "label": "男", "value": "male" }
字段 类型 含义
label string 用户可见文字
value string 实际提交值

✅ 校验 Validator

{
  "type": "regex",
  "pattern": "^1[0-9]{10}$",
  "message": "请输入有效手机号"
}
字段 类型 说明
type string 当前仅支持 "regex"
pattern string 正则表达式
message string 错误提示文本

🎯 visibleIf 条件控制说明

用于控制字段是否显示,格式如下:

"visibleIf": {
  "gender": "male"
}
  • 当 gender == "male" 时,该字段显示
  • 支持多个条件(与关系)
  • 可扩展支持数组值(如某字段 ∈ 多个值)

🧪 示例片段

{
  "key": "militaryStatus",
  "label": "服兵役情况",
  "type": "text",
  "visibleIf": {
    "gender": "male"
  }
}

🏁 约定建议(团队协作)

建议 说明
key 唯一 必须唯一,用于表单存储
所有文本请支持 i18n 协议 label / placeholder / message
支持 UI 端自定义字段类型扩展 可在 type 扩展为 imageUploader, mapPicker
使用 camelCase 命名字段 推荐风格一致

LazyViewContainer-iOS中节省内存的UI创建方式

背景

iOS工程中,创建自定义UI组件时有一个常见的写法:

  1. UI组件初始化时,创建所有子组件实例;并将子组件添加到自定义组件层级中,再使用Autolayout做好约束布局
  2. 后续有数据时,直接为组件的每个子组件填充数据

这样的写法是会导致一定的内存浪费,比如

  • 有的场景可能仅在特定情况下才会拿到数据显示UI组件,如果没有数据,若仍然初始化了某个UI组件,则导致该组件占用了一部分长期不会用到的内存
  • 这样的浪费在像直播房等需要大量UI、非UI组件的场景下比较常见

当然,根本的解决方案也是很容易想到的:

  • 延迟加载:仅在需要某个UI组件时才去创建和使用

没错,这无疑可以从根本上解决该问题,但是,延迟加载写起来其实要更麻烦一些,所以为了方便快捷,最开始提到的写法其实更普遍

下面会提到一种折中的方案,能部分降低内存的浪费

LazyViewContainer

  • 红框为普通UI组件直接创建和内存分配过程;绿框中是LazyViewContainer工作原理
  • 普通UI组件初始化完后,内存中就有了该组件完整的对象
  • LazyViewContainer只是一个有很少轻量属性(3个)的普通类,所以LazyViewContainer初始化后,此时内存中的占用较小(约48 bytes)。仅当执行ensureView方法时,才会尝试创建实际的UI组件
  • 最右侧紫色框展示了不同情况下内存占用情况
  • 简单总结一下LazyViewContainer的工作原理,就是在实际UI组件创建之前添加了一个中间层(使用泛型和closure来做到这一点),尽可能延迟实际UI组件及子组件的创建

内存占用测试

使用了几个最常见的系统UI组件进行了一下内存占用的测试

  • 测试环境:
    • iPhone 13 mini/iOS 18.1.1, Release环境
  • 测试方法:
    • 使用不同UI组件,使用不同创建方式,验证对应情况下,看单个UI组件实例实际的内存占用

不同UI组件的创建逻辑代码如下所示:

// UILabel
let label = UILabel()
label.text = "Test Label"
label.font = .systemFont(ofSize: 14)
label.textColor = .red
label.backgroundColor = .blue
label.frame = CGRect(x: 100, y: 100, width: 100, height: 20)
// UIButton
let button = UIButton(type: .system)
button.setTitle("Test Button", for: .normal)
button.backgroundColor = .green
button.frame = CGRect(x: 100, y: 200, width: 100, height: 40)
// UIImageView
let imageView = UIImageView()
imageView.backgroundColor = .yellow
imageView.frame = CGRect(x: 100, y: 300, width: 100, height: 100)
组件类型 创建方式 单个实例内存(bytes) 节省内存(bytes) 节省比例
UILabel 直接创建 896 - -
Lazy创建 48 768 94.64%
Lazy Ensure 944 -48 -5.36%
UIButton 直接创建 768 - -
Lazy创建 48 1792 93.75%
Lazy Ensure 816 -48 -6.25%
UIImageView 直接创建 512 - -
Lazy创建 48 384 90.62%
Lazy Ensure 560 -48 -9.38%

根据测试结果,总结一下结论:

  1. LazyViewContainer单个实例(在没有通过ensureView触发实际UI组件创建时)占用的内存约为 48 bytes
  2. 基于结论1,所以当LazyViewContainer真的要创建实际UI组件时,会比直接创建组件占用的内存更多,但也仅多48 bytes,至于精确的内存占比可以参考表格中“节省比例”列的绿字数据
  3. 同时也能预见到,当自定义UI组件越复杂时,“仅Lazy创建”的方式下,带来的内存节省比例更显著

LazyViewContainer使用场景

首先要明确两点:

  1. 这并不是创建UI组件的最节省内存的方式,只是一种在编码便捷性和内存性能之间的一种折中手段
  2. 不是所有情况下都推荐使用LazyViewContainer

那么,什么场景下推荐使用?

  • 用户可能长时间进行操作的、复杂UI层级的页面(如语音、视频、直播房间)
    • 这样的页面支持的功能比较多,但又不是需要同时运行这些功能,当多数用户、多数情况下仅使用一小部分功能时,那么如果仍然在进入页面时创建所有功能的业务或UI组件,就是会造成比较严重的内存浪费
  • 现有代码中业务逻辑太复杂,不宜短时间做大重构时,适合这样的折中方案
  • 复杂的自定义UI组件
    • 子组件越多,UI层级越复杂,当该UI组件使用率低的时候,所带来的内存节省效果越明显

什么场景下不推荐使用?

  • 列表页面
    • 列表滚动时,伴随多个cell的复用,虽然每个cell的数据不尽相同,有的UI元素可能不需要显示。但可能会被复用,所以此情况下,cell初始化时将所有子UI组件都创建,合理性更强,这样会更方便滚动时的复用

源码与使用方式

源码:

//
//  LazyViewContainer.swift
//
//  Created by songgeb on 2025/1/1.
//  Copyright © 2025 songgeb. All rights reserved.
//

import UIKit
/// 将创建自定义UIView逻辑延迟到数据到来时,同时避免当数据无效时仍创建自定义UIView,导致无意义的内存占用问题
final class LazyViewContainer<T: UIView> {
    private(set) var view: T?
    private let createView: () -> T
    private weak var parentView: UIView?

    init(createView: @escaping () -> T) {
        self.createView = createView
    }

    init() {
        createView = { T() }
    }

    /// 获取或创建视图,交给调用方处理布局
    /// - Parameters:
    ///   - parent: 父视图
    ///   - customAddition: 自定义添加视图逻辑
    ///   - onFirstAddition: 首次创建、同时添加到parentView的时机
    /// - Returns: 自定义视图
    @discardableResult
    func ensureView(
        in parent: UIView,
        customAddition: ((T) -> Void)? = nil,
        onFirstAddition: ((T) -> Void)? = nil) -> T
    {
        if let existingView = view {
            return existingView
        }

        let newView = Thread.isMainThread ? createView() : DispatchQueue.main.sync { createView() }

        // 使用自定义添加方式或默认 addSubview
        if let customAddition {
            customAddition(newView)
        } else {
            parent.addSubview(newView)
        }

        onFirstAddition?(newView)
        view = newView
        parentView = parent
        return newView
    }

    /// 移除视图
    /// - Parameter customRemoval: 自定义移除视图逻辑
    func removeView(using customRemoval: ((T) -> Void)? = nil) {
        guard let view = view, view.superview != nil else { return }
        func doRemoving() {
            if let customRemoval {
                customRemoval(view)
            } else {
                view.removeFromSuperview()
            }
        }
        if Thread.isMainThread {
            doRemoving()
        } else {
            DispatchQueue.main.async { doRemoving() }
        }
        self.view = nil
        parentView = nil
    }

    // 检查视图是否创建
    var isCreated: Bool {
        view != nil
    }

    deinit {
        removeView()
    }
}

使用方式:

class TestViewController: UIViewController {
    private lazy var leftVerticalStack: UIStackView = {
        let vstack = UIStackView(frame: .zero)
        vstack.axis = .vertical
        vstack.alignment = .leading
        vstack.distribution = .equalSpacing
        return vstack
    }()
    
    private lazy var detailLabelLoader = LazyViewContainer<UILabel> {
        let label = UILabel()
        label.font = .systemFont(withSize: 14)
        label.textColor = .bSecondary
        return label
    }

    private func updateDetailLabel(_ shouldShowDetail: Bool) {
        if shouldShowDetail {
            detailLabelLoader.ensureView(in: leftVerticalStack, customAddition: { [weak leftVerticalStack] label in
                guard let leftVerticalStack else { return }
                leftVerticalStack.addArrangedSubview(label)
            })
            detailLabelLoader.view?.text = "abc"

        } else {
            detailLabelLoader.removeView { [weak self] label in
                guard let self else { return }
                leftVerticalStack.removeArrangedSubview(label)
            }
        }
    }
}
❌