普通视图

发现新文章,点击刷新页面。
今天 — 2025年3月13日掘金 iOS

【手游出海】你知道吗?除了30%的“苹果税”,你还交了不少地区销售税

作者 CocoaKier
2025年3月12日 19:26

一、Google是如何处理地区税率的?

做过Google出海的朋友可能知道,有些国家会有额外的“税率”,下文统称“地区销售税”。如下图,Google Play后台填写内购商品价格时,可以看到每个地区的税率,以及有一个选项“默认价格含税”。
Google Play后台计费点税率

这个商品你的收入 = 商品价格 - 税款 - Google抽成

你可以通过勾选和取消勾选“默认价格含税”选项框,来改变用户看到的价格和谁来付税款。
默认价格含税,表示你给的定价是包含了税款的,用户实际支付的金额 = 定价,这笔税款你出。一般情况下,用户实际支付的金额和App内看到的价格一致,用户体验比较好;
默认价格不含税,表示你给的定价是不包含税款的,用户实际支付的金额 = 定价+税款,这笔税款用户出。一般情况下,用户实际支付的金额会比App内看到的价格多出一个税款

定价选默认含税还是默认不含税,主要取决于你的运营策略以及考虑当地用户的支付习惯。

二、苹果定价默认包含了地区销售税

可能有些人(特别是开发)以为销售额减去苹果抽成,剩下的就是打款收入了。其实不是这样的,苹果也有地区销售税

你的收入 = 商品价格 - 税款 - 苹果抽成(普通30%、小型开发者15%)

与Google做法不同,苹果没有给你选择的权利,苹果后台你设置的商品定价,默认是包含了地区销售税的,即默认价格含税。而且,苹果后台没有地方可以查看地区税率,只能自己根据“价格”和“收入”计算出地区税率(下图苹果列出的收入就是你最后实际到手的钱,这部分是减去了税款和抽成的,也考虑到了小型开发者少抽成的部分)。
苹果后台定价

我们以韩国和日本为例,我们随便找一个计费点计算,例如 1人民币 对应:

韩国 (KRW),价格 ₩1,100.00,收入 ₩700.00,收入占比=700/1100≈64%,其中苹果抽成30%,那么韩国地区税率=(1-64%)-30%=6%;

日本 (JPY),价格 ¥150.00,收入 ¥105.00,收入占比=105/150≈70%,其中苹果抽成30%,那么日本地区税率=(1-70%)-30%=0%,日本没有税率;

如果你是小型开发者,上面苹果抽成部分则按15%计算。无论你是不是小型开发者,地区税率一般情况下计算出来是一样的。

下面是我计算出的苹果地区税率,供参考,后续可能会变(2025.3.12):
中国大陆(1%)
香港(0%)
澳门(0%)
台湾(5%)
日本(0%)
韩国(8%)
泰国(5%)
马来西亚(6%)
新加坡(7%)
越南(8.5%)
菲律宾(0%)
印度尼西亚(8.5%)
缅甸(0%)
柬埔寨(8%)
老挝(0%)
文莱(0%)

参考文献
【苹果官方】设置税务类别
【苹果官方】下载财务报告
还不懂苹果的税收政策及结算问题?看这篇就够了!

iOS Swift UIKit 编程规范指南

作者 ZRD1112
2025年3月12日 16:48

总结

控制器命名规范

  1. 使用描述性名称
    • 控制器的名称应该清晰地描述其功能和职责。例如,PersonViewController 表示该控制器用于管理与人员信息相关的视图。

方法命名规范

  1. 使用动宾结构
    • 方法名应该是一个动词或动词短语,描述该方法执行的操作。例如,loadData()saveUserInfo()
  • 清晰性
  • 可读性:新成员快速理解方法作用
  • 可维护性:减少因命名歧义引发的 Bug
  • 一致性:团队协作风格统一

通过良好的命名,可以让代码更加易于理解、扩展和维护。

UI从上往下添加,初始化UI,add,添加约束。这样在相邻上下文中就能找到,好阅读。

不使用过多if else判断,后期维护很麻烦。


好的,用户问的是在使用MVC架构做一个iOS项目时,项目的层次应该如何组织。我需要详细分析这个问题,确保覆盖MVC的各个层面以及实际项目中的常见结构。

首先,用户可能已经了解MVC的基本概念,但需要具体的项目结构示例。他们可能希望了解如何将不同的组件(模型、视图、控制器)组织到不同的目录中,以及如何处理服务、工具类等其他部分。

接下来,我得考虑用户可能的背景。可能是刚接触MVC的开发者,或者在团队协作中需要统一结构,所以需要明确且规范的目录划分。用户可能关心如何避免Massive View Controller,如何分离业务逻辑,数据持久化如何处理,网络请求放在哪里等等。

然后,我需要回顾标准的MVC结构,并扩展到实际项目中常见的子目录。例如,模型可能分为实体、DTO、Core Data模型等。视图部分可能需要自定义UI组件,使用xib或纯代码。控制器可能按功能模块分组,同时需要处理服务层、工具类、资源管理等。

还需要考虑依赖管理,比如第三方库的位置,常量、扩展方法的组织方式。测试部分的目录结构也是重要的一环,确保单元测试和UI测试有明确的归属。

另外,用户可能希望目录结构示例能够直观展示,所以提供一个树状结构的例子会很有帮助。同时,需要解释每个目录的作用,比如Services用于网络请求和数据持久化,Utilities放置工具类和扩展,Resources管理资源文件等。

最后,要强调一些最佳实践,比如避免在控制器中堆积业务逻辑,合理使用扩展和子类化,保持模块化以便于维护和协作。可能还需要提到常见的陷阱,如过度依赖控制器,或者模型层过于臃肿,以及如何通过协议和依赖注入来解耦。

总结来说,用户需要一个清晰、可维护的项目结构示例,以及每个部分的职责说明,同时结合MVC的最佳实践,帮助他们组织代码,提高项目的可维护性和扩展性。

以下是一个基于 MVC 模式 的 iOS 项目标准层次结构,适用于中大型项目。通过清晰的模块划分和职责分离,可大幅提升代码可维护性和团队协作效率:


项目目录结构示例

ProjectName/
├── Application/         # 应用全局配置
│   ├── AppDelegate.swift
│   └── SceneDelegate.swift
│
├── Modules/             # **核心模块**(按功能划分)
│   ├── Home/            # 首页模块
│   │   ├── Controller/
│   │   │   └── HomeViewController.swift
│   │   ├── Model/
│   │   │   ├── HomeDataModel.swift      # 数据模型
│   │   │   └── HomeAPIService.swift     # 网络请求服务
│   │   └── View/
│   │       ├── HomeHeaderView.swift     # 自定义视图
│   │       └── HomeCollectionViewCell.swift
│   │
│   ├── Product/         # 商品模块
│   └── User/            # 用户模块
│
├── Common/              # 公共资源
│   ├── Extensions/      # 扩展方法
│   │   └── UIView+Extensions.swift
│   ├── Utilities/       # 工具类
│   │   ├── DateFormatter.swift
│   │   └── ImageCacheManager.swift
│   ├── Protocols/       # 协议定义
│   │   └── ReusableView.swift
│   └── Constants/       # 常量
│       └── AppConstants.swift
│
├── Resources/           # 静态资源
│   ├── Assets.xcassets  # 图片/图标
│   ├── Localizable.strings  # 多语言
│   └── Colors.swift     # 颜色常量
│
├── Services/            # 全局服务
│   ├── Network/         # 网络层
│   │   ├── APIClient.swift
│   │   └── APIError.swift
│   └── Database/        # 数据库层
│       └── CoreDataManager.swift
│
└── SupportingFiles/     # 工程配置文件
    ├── Info.plist
    └── LaunchScreen.storyboard

核心模块内部结构详解(以 Home 模块为例)

1. Controller 层

  • 职责:处理视图生命周期、用户交互、数据传递
  • 最佳实践
    // HomeViewController.swift
    class HomeViewController: UIViewController {
        // MARK: - Properties
        private var dataModel: HomeDataModel!
        private let apiService = HomeAPIService()
    
        // MARK: - UI Components
        private lazy var collectionView: UICollectionView = { ... }()
    
        // MARK: - Lifecycle
        override func viewDidLoad() {
            super.viewDidLoad()
            setupUI()
            loadData()
        }
    
        // MARK: - Data Loading
        private func loadData() {
            apiService.fetchHomeData { [weak self] result in
                self?.dataModel = HomeDataModel(result)
                self?.collectionView.reloadData()
            }
        }
    }
    
    // MARK: - UICollectionViewDataSource
    extension HomeViewController: UICollectionViewDataSource { ... }
    

2. Model 层

  • 职责:数据解析、业务逻辑处理
  • 代码示例
    // HomeDataModel.swift
    struct HomeDataModel {
        let banners: [Banner]
        let products: [Product]
    
        init(_ apiResponse: APIResponse) {
            // 数据转换逻辑
            banners = apiResponse.banners.map { Banner(dto: $0) }
            products = apiResponse.products.map { Product(dto: $0) }
        }
    }
    
    // HomeAPIService.swift
    class HomeAPIService {
        func fetchHomeData(completion: @escaping (Result<APIResponse, Error>) -> Void) {
            APIClient.shared.request(.home, completion: completion)
        }
    }
    

3. View 层

  • 职责:UI 组件定义、布局、样式
  • 代码示例
    // HomeHeaderView.swift
    class HomeHeaderView: UIView {
        private lazy var titleLabel: UILabel = {
            let label = UILabel()
            label.font = .systemFont(ofSize: 18, weight: .bold)
            return label
        }()
    
        // SnapKit 布局
        private func setupLayout() {
            addSubview(titleLabel)
            titleLabel.snp.makeConstraints { make in
                make.edges.equalToSuperview().inset(16)
            }
        }
    }
    

关键分层原则

  1. 严格职责分离

    • Controller:仅协调数据和视图,不处理业务逻辑
    • Model:独立处理数据转换、验证、存储
    • View:只关注 UI 展示和布局
  2. 依赖关系方向

    View → Controller ← Model
          ↑
          Services/Network
    
    • 禁止 View 直接访问 Model
    • 禁止 Model 依赖 Controller
  3. 模块化开发

    • 每个功能模块独立成文件夹
    • 使用 @_exported import ModuleName 实现模块间隔离(Swift 5.9+)

进阶扩展建议

  1. 依赖注入

    protocol DataServiceProtocol {
        func fetchData() -> [Data]
    }
    
    class HomeViewController {
        private let dataService: DataServiceProtocol
    
        init(dataService: DataServiceProtocol = DefaultDataService()) {
            self.dataService = dataService
        }
    }
    
  2. 路由导航

    // Router.swift
    enum AppRoute {
        case productDetail(id: String)
        case userProfile(userId: String)
    }
    
    class Router {
        static func navigate(to route: AppRoute, from context: UIViewController) {
            switch route {
            case .productDetail(let id):
                let vc = ProductDetailViewController(productId: id)
                context.navigationController?.pushViewController(vc, animated: true)
            // 其他路由处理...
            }
        }
    }
    
  3. 状态管理

    class HomeState {
        var isLoading: Bool = false
        var data: HomeDataModel?
        var error: Error?
    }
    
    // 在 Controller 中观察状态变化
    private func observeState() {
        homeState.$isLoading
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isLoading in
                self?.loadingIndicator.isHidden = !isLoading
            }
            .store(in: &cancellables)
    }
    

典型文件命名规范

类型 命名模式 示例
Controller [功能]ViewController ProductListViewController
Model [功能]Model UserProfileModel
Service [功能]Service PaymentService
View [功能][元素类型]View ProductCardView

通过这种结构化的组织方式,您的项目将具备以下优势:

  • 快速定位:新人可快速找到对应模块代码
  • 低耦合:模块间依赖清晰,易于单元测试
  • 高扩展性:新增功能只需添加模块目录
  • 团队协作:多人并行开发不冲突

以下是一个基于 MVC 模式纯代码 UISnapKit 约束 的完整示例,涵盖用户列表展示场景:


1. Model 层

// User.swift
struct User {
    let id: String
    let name: String
    let email: String
}

2. View 层

自定义 UITableViewCell

// UserTableViewCell.swift
class UserTableViewCell: UITableViewCell {
    // MARK: - UI Elements
    private lazy var nameLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 16, weight: .bold)
        return label
    }()
    
    private lazy var emailLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 14)
        label.textColor = .gray
        return label
    }()
    
    // MARK: - Initialization
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupUI()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - UI Setup
    private func setupUI() {
        [nameLabel, emailLabel].forEach { contentView.addSubview($0) }
        
        // SnapKit 约束
        nameLabel.snp.makeConstraints { make in
            make.top.left.equalToSuperview().inset(16)
            make.right.lessThanOrEqualToSuperview().offset(-16)
        }
        
        emailLabel.snp.makeConstraints { make in
            make.top.equalTo(nameLabel.snp.bottom).offset(8)
            make.left.bottom.equalToSuperview().inset(16)
        }
    }
    
    // MARK: - Data Binding
    func configure(with user: User) {
        nameLabel.text = user.name
        emailLabel.text = user.email
    }
}

3. Controller 层

ViewController

// UserListViewController.swift
class UserListViewController: UIViewController {
    // MARK: - Properties
    private var users: [User] = []
    
    // MARK: - UI Elements
    private var tableView: UITableView! 
  
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        loadData()
    }
    
    // MARK: - Data Loading
    private func loadData() {
        // 模拟网络请求
        UserService.fetchUsers { [weak self] result in
            guard let self = self else { return }
            
            switch result {
            case .success(let users):
                self.users = users
                DispatchQueue.main.async {
                    self.tableView.reloadData()
                }
            case .failure(let error):
                print("Error: \(error.localizedDescription)")
            }
        }
    }
    
  // MARK: - Actions


    
    // MARK: - UI Setup (代码太多放在最后)
    private func setupUI() {
        view.backgroundColor = .white
        tableView = UITableView()
        tableView.register(UserTableViewCell.self, forCellReuseIdentifier: "UserCell")
        tableView.delegate = self
        tableView.dataSource = self
        view.addSubview(tableView)
        
        // SnapKit 约束
        tableView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
    }
}

// MARK: - UITableViewDataSource & Delegate
extension UserListViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return users.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath) as? UserTableViewCell else {
            return UITableViewCell()
        }
        cell.configure(with: users[indexPath.row])
        return cell
    }
}

4. Service 层

// UserService.swift
class UserService {
    static func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) {
        // 模拟异步请求
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
            let mockUsers = [
                User(id: "1", name: "Alice", email: "alice@example.com"),
                User(id: "2", name: "Bob", email: "bob@example.com")
            ]
            DispatchQueue.main.async {
                completion(.success(mockUsers))
            }
        }
    }
}

关键点说明

  1. MVC 分层清晰

    • Model: User 结构体表示数据
    • View: UserTableViewCell 处理 UI 展示
    • Controller: UserListViewController 协调数据和视图
  2. 纯代码 UI

    • 使用 init(style:reuseIdentifier:) 初始化 Cell
    • 通过 snp.makeConstraints 添加 SnapKit 约束
  3. 避免循环引用

    • 在闭包中使用 [weak self]
    • 使用 guard let self = self else { return } 安全解包
  4. 异步处理

    • 使用 DispatchQueue 模拟网络请求
    • 主线程更新 UI
  5. 代码可维护性

    • 使用 MARK 注释分隔代码块
    • 将 Service 逻辑独立到单独类

效果预览

Simulator Screen Recording转存失败,建议直接上传图片文件 (实际运行后会显示包含 Alice 和 Bob 的用户列表)


这种结构符合 Apple 推荐的 MVC 模式,同时通过 SnapKit 简化了 Auto Layout 代码,适合中大型项目的可维护性要求。


给控制器 方法命名 业务明确、不易混淆

好的,我现在需要帮用户解决如何给控制器的方法命名的问题。用户之前已经询问了关于iOS Swift UIKit的编程规范,特别是MVC架构下按钮点击事件的处理位置。现在他们进一步想知道控制器方法的命名规范。

首先,我得回顾之前的对话,确保这次的回答和之前的指南一致。之前的回答中提到了命名规范,比如类型和协议使用大驼峰,变量和函数用小驼峰,布尔属性以is、has等开头。同时,在控制器处理按钮点击事件时,方法名如didTapButton,遵循了清晰的动作描述。

接下来,我需要确定用户的具体需求。他们可能希望了解控制器中的方法应该如何命名,特别是不同职责的方法,比如生命周期方法、事件处理、数据加载、导航等。命名规范对于代码的可读性和可维护性非常重要,尤其是在团队协作中。

用户可能想知道如何区分私有方法和公有方法,是否需要使用特定前缀或后缀。此外,是否有常见的命名模式或Apple的官方建议可以参考。比如,Apple的文档中通常使用动词开头的方法名,如viewDidLoad, prepare(for:sender:),这些都是标准的生命周期方法。

还要考虑方法的作用域。私有方法是否需要使用下划线或其他标记?在Swift中,通常使用private关键字,并且方法名不需要下划线,但有些团队可能有自己的规范。另外,是否需要使用// MARK:来分组方法,提高代码的可读性。

此外,事件处理方法如按钮点击、手势识别等,通常以didTap、handle等动词开头,明确表示用户交互触发的动作。数据相关的方法可能以load、fetch、update、delete等动词开头,表明其功能。

可能需要举一些具体的例子,涵盖不同场景下的命名,比如初始化UI、处理按钮点击、网络请求回调、导航跳转等。同时,要强调一致性的重要性,避免混合使用不同的命名风格。

另外,用户可能在处理复杂逻辑时,如何命名辅助方法。比如,验证表单的方法可以命名为validateForm,显示警告的方法为showErrorAlert,这样方法名即表达了其功能。

还要注意避免使用模糊的命名,比如doSomething或processData,这样的名称无法清晰传达方法的作用。应该使用更具描述性的名称,比如fetchUserData或updateProfileImage。

最后,可能需要提到一些工具,如SwiftLint,来强制执行命名规范,确保团队代码风格统一。同时,参考Apple的API设计指南,遵循Swift的命名惯例,使用自然语言和清晰的表达方式。

以下是针对 控制器方法命名 的规范化指南,结合 Apple 官方建议和 Swift 社区最佳实践:


1. 核心原则

  • 清晰性优先:方法名应直接反映其行为或意图(如 updateUI 优于 refresh
  • 动词开头:表示动作(fetchhandlenavigate
  • 避免缩写configureCell 优于 configCell
  • 作用域标识:私有方法使用 private 修饰,无需添加 _ 前缀

2. 分类命名规范

(1) 生命周期方法

直接使用 Apple 预设方法名,禁止修改:

override func viewDidLoad() { ... }
override func viewWillAppear(_ animated: Bool) { ... }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { ... }

(2) UI 初始化与布局

  • 场景:创建视图、设置约束、配置样式
  • 命名模式setup[元素名]configure[功能]
// 初始化所有 UI 元素
private func setupUI() {
    setupTableView()
    setupNavigationBar()
}

// 配置特定组件
private func setupTableView() {
    tableView.register(Cell.self, forCellReuseIdentifier: "Cell")
    // SnapKit 约束
    tableView.snp.makeConstraints { ... }
}

// 动态样式配置
private func configureDarkMode() {
    view.backgroundColor = .systemBackground
}

(3) 用户交互事件

  • 场景:按钮点击、手势、输入框回调
  • 命名模式didTap[元素名]handle[动作]
// 按钮点击(明确目标)
@objc private func didTapSubmitButton() {
    validateForm()
}

// 手势识别(通用处理)
@objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
    // 处理拖拽逻辑
}

// 输入框回调(带参数)
@objc private func textFieldDidChange(_ textField: UITextField) {
    guard let text = textField.text else { return }
    search(keyword: text)
}

(4) 数据操作

  • 场景:网络请求、本地数据加载、缓存处理
  • 命名模式load/fetch/update/delete[数据对象]
// 加载本地数据
private func loadCachedData() {
    guard let data = cacheManager.getData() else { return }
    process(data)
}

// 发起网络请求
private func fetchUserProfile() {
    APIService.fetchProfile { [weak self] result in
        self?.handleProfileResult(result)
    }
}

// 处理请求结果(私有方法)
private func handleProfileResult(_ result: Result<User, Error>) {
    switch result {
    case .success(let user): updateUI(with: user)
    case .failure(let error): showErrorAlert(message: error.localizedDescription)
    }
}

(5) 导航与页面跳转

  • 场景:跳转到下一页、模态弹窗、关闭页面
  • 命名模式navigateTo[目标]show/dismiss[页面]
// 跳转到详情页
private func navigateToDetailPage(with item: Item) {
    let vc = DetailViewController(item: item)
    navigationController?.pushViewController(vc, animated: true)
}

// 显示模态弹窗
private func showFilterOptions() {
    let filterVC = FilterViewController()
    filterVC.delegate = self
    present(filterVC, animated: true)
}

// 关闭当前页
@objc private func dismissCurrentPage() {
    dismiss(animated: true)
}

(6) 工具方法

  • 场景:数据验证、格式化、工具函数
  • 命名模式validate[对象] / format[数据类型]
// 表单验证
private func validateForm() -> Bool {
    guard !usernameTextField.text!.isEmpty else {
        showValidationError("用户名不能为空")
        return false
    }
    return true
}

// 日期格式化
private func formatDate(_ date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    return formatter.string(from: date)
}

3. 进阶技巧

(1) 使用 // MARK: 分组方法

提升代码可读性:

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource { ... }

// MARK: - Network Handling
private extension ViewController {
    func fetchData() { ... }
    func handleNetworkError(_ error: Error) { ... }
}

(2) 闭包参数命名

若方法接受闭包参数,需明确其用途:

func loadData(completion: @escaping (Result<Data, Error>) -> Void) { ... }

(3) 避免冗余前缀

Swift 的命名空间机制不需要类名前缀:

// ✅ 正确
func configureHeaderView() { ... }

// ❌ 避免
func vc_configureHeaderView() { ... }

4. 典型错误示例与修正

错误命名 问题 修正方案
func get() { ... } 无意义动词 func fetchUserData() { ... }
func update() { ... } 缺少操作对象 func updateProfileImage() { ... }
func btnClick() { ... } 非标准缩写 func didTapSubmitButton() { ... }
func process() { ... } 模糊职责 func handleImageUpload() { ... }

5. 命名检查工具

  • SwiftLint 规则:启用 identifier_name 规则限制命名风格
  • Xcode 重构工具:右键方法名 → Refactor → Rename 安全重命名

通过遵循这些规范,您的控制器代码将具备以下优势:

  • 可读性:新成员快速理解方法作用
  • 可维护性:减少因命名歧义引发的 Bug
  • 一致性:团队协作风格统一

以下是针对 控制器(ViewController)命名 的规范化指南,结合 Apple 官方建议和 Swift 社区最佳实践:


1. 核心原则

  • 明确职责:名称应直接反映其功能或管理的视图内容(如 LoginViewController 管理登录页)
  • 使用 ViewController 后缀:统一标识控制器类型
  • 避免冗余前缀:Swift 的模块化特性无需添加项目缩写(如 XXUserVC 不符合规范)
  • 大驼峰式(UpperCamelCase):首字母大写,无下划线或连字符

2. 具体场景命名规范

(1) 基础页面控制器

  • 模式[功能]ViewController
  • 示例
    // ✅ 正确
    class HomeViewController: UIViewController { ... }      // 首页
    class ProfileViewController: UIViewController { ... }  // 用户资料页
    class SettingsViewController: UIViewController { ... } // 设置页
    

(2) 列表/集合视图控制器

  • 模式[数据对象]ListViewController[功能]CollectionViewController
  • 示例
    class ProductListViewController: UITableViewController { ... }       // 商品列表
    class PhotoCollectionViewController: UICollectionViewController { ... } // 图片集合视图
    

(3) 详情页控制器

  • 模式[主对象]DetailViewController
  • 示例
    class ArticleDetailViewController: UIViewController { ... }  // 文章详情页
    class OrderDetailViewController: UIViewController { ... }    // 订单详情页
    

(4) 容器类控制器

  • 模式[布局类型]ContainerViewController
  • 示例
    class TabContainerViewController: UITabBarController { ... }    // Tab 容器
    class PageContainerViewController: UIPageViewController { ... } // 分页容器
    

(5) 弹窗/浮层控制器

  • 模式[功能]PopupViewController[动作]ActionSheetViewController
  • 示例
    class FilterPopupViewController: UIViewController { ... }       // 筛选弹窗
    class ShareActionSheetViewController: UIViewController { ... }  // 分享动作面板
    

3. 高级场景命名

(1) 组合功能控制器

使用连词明确多职责:

class SearchAndFilterViewController: UIViewController { ... }  // 集成搜索+筛选功能

(2) 子模块控制器

通过路径层级体现关系:

// 用户模块的子页面
class UserNotificationSettingsViewController: UIViewController { ... }  // 用户通知设置页

(3) 通用组件控制器

强调复用性:

class ImagePickerViewController: UIImagePickerController { ... }  // 通用图片选择器

4. 错误命名示例与修正

错误命名 问题 修正方案
class VC1: UIViewController 无意义缩写 class LoginViewController
class UserVC: UIViewController 冗余缩写 class UserProfileViewController
class MyHomePage: UIViewController 缺少后缀 class HomeViewController
class product_list_vc: UIViewController 蛇形命名法 class ProductListViewController

5. 特殊类型控制器命名

控制器类型 命名规范 示例
UITableViewController 保持 ViewController 后缀 class OrderListViewController: UITableViewController
UICollectionViewController 同上 class GalleryViewController: UICollectionViewController
子视图控制器 体现父子关系 class ProfileHeaderViewController: UIViewController (作为 Profile 页的子控制器)
通用基类控制器 添加 Base 前缀 class BaseViewController: UIViewController

6. 配套文件命名规范

保持与控制器类名一致,方便检索:

  • Storyboard/XIB 文件LoginViewController.storyboard
  • ViewModelLoginViewModel.swift (MVVM 模式)
  • 单元测试类LoginViewControllerTests.swift

7. 工具辅助

  • SwiftLint 规则:通过 type_name 规则强制后缀检测
    # .swiftlint.yml
    type_name:
      validates_start_with_lowercase: false
      allowed_symbols: "ViewController"
    
  • Xcode 模板:自定义 File Template 统一生成规范名称

遵循这些规范将带来以下优势:

  • 快速定位文件:通过名称直接判断控制器功能
  • 减少命名冲突:清晰的层级和模块划分
  • 提升团队协作:统一命名风格降低沟通成本

我先来分析用户的问题。用户在之前的对话中提到了使用MVC设计模式、代码添加UI以及使用SnapKit进行约束布局的例子,现在进一步询问如何给控制器以及里面的方法命名。这表明用户对代码规范和可读性有较高的关注,可能希望在实际开发中写出清晰、易维护的代码。

我需要先回顾之前的回答内容,确保上下文连贯。之前的回答已经给出了一个完整的MVC示例,包括模型、视图和控制器的实现。用户现在的问题更聚焦在控制器和方法的命名上,这意味着我需要从命名规范的角度去回答,而不是重复之前的代码实现。

接下来,我思考如何组织回答。命名规范是一个比较常见的话题,我需要从几个方面来展开,比如控制器的命名、方法的命名,以及如何让名字清晰、准确地表达其功能。我还需要结合MVC的背景,说明控制器和方法在MVC中的角色,这样用户能更好地理解为什么这样命名。

在具体回答时,我会先从控制器的命名开始,因为控制器是用户提问的第一部分。控制器的命名需要体现其职责,比如管理哪个模块或功能。然后,我会转到方法的命名,强调动宾结构和语义清晰的重要性。最后,我会补充一些通用的命名建议,比如避免模糊词汇、保持一致性等。

在语气上,我会保持专业但不失亲切,尽量用通俗易懂的语言来解释,同时提供一些具体的例子,帮助用户更好地理解和应用这些建议。

总结一下,我的回答会围绕以下几个点展开:控制器命名的规则、方法命名的技巧、以及一些通用的命名建议。这些内容既要符合规范,又要结合实际开发场景,确保用户能学以致用。

在iOS开发中,给控制器和方法命名时,应该遵循一些通用的命名规范和最佳实践,以确保代码的可读性、可维护性和一致性。以下是一些具体的建议:

控制器命名规范

  1. 使用描述性名称

    • 控制器的名称应该清晰地描述其功能和职责。例如,PersonViewController 表示该控制器用于管理与人员信息相关的视图。
  2. 遵循命名约定

    • 控制器类名通常以 "ViewController" 结尾,例如 SettingsViewControllerProfileViewController
  3. 使用驼峰命名法

    • 类名采用大驼峰命名法(PascalCase),即每个单词的首字母大写,例如 ProductDetailViewController
  4. 避免使用模糊的名称

    • 避免使用模糊或不具体的名称,如 MainViewControllerDataViewController,除非它们确实对应于应用中的主视图或数据视图。

方法命名规范

  1. 使用动宾结构

    • 方法名应该是一个动词或动词短语,描述该方法执行的操作。例如,loadData()saveUserInfo()
  2. 遵循命名约定

    • 方法名采用小驼峰命名法(camelCase),即第一个单词的首字母小写,后续单词的首字母大写,例如 fetchRemoteData()updateUI()
  3. 参数命名清晰

    • 方法的参数名也应该清晰描述其用途,例如 configure(with:)handleEvent(for:at:)
  4. 避免使用模糊的名称

    • 避免使用模糊或不具体的名称,如 doSomething(),除非方法确实执行一个非常通用的操作。

示例

以下是一个遵循上述命名规范的控制器和方法的示例:

import UIKit

// 模型
struct Person {
    var name: String
    var age: Int
}

// 视图
class PersonView: UIView {
    // 视图相关代码
}

// 控制器
class PersonViewController: UIViewController {
    
    var person: Person?
    let personView = PersonView()
    
    override func loadView() {
        view = personView
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "Person Information"
        setupActions()
        updateView()
    }
    
    // 设置动作
    func setupActions() {
        personView.saveButton.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
    }
    
    // 更新视图
    func updateView() {
        if let person = person {
            personView.nameTextField.text = person.name
            personView.ageTextField.text = String(person.age)
        }
    }
    
    // 保存按钮点击处理
    @objc func saveButtonTapped() {
        guard let name = personView.nameTextField.text, !name.isEmpty,
              let ageText = personView.ageTextField.text, let age = Int(ageText) else {
            showAlert(message: "Please enter valid name and age.")
            return
        }
        
        person = Person(name: name, age: age)
        showAlert(message: "Person information saved successfully!")
    }
    
    // 显示警告
    func showAlert(message: String) {
        let alert = UIAlertController(title: "Alert", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        present(alert, animated: true, completion: nil)
    }
}

总结

  • 控制器命名:使用描述性名称,遵循 "ViewController" 后缀约定,采用大驼峰命名法。
  • 方法命名:使用动宾结构,遵循小驼峰命名法,参数命名清晰,避免模糊名称。

通过遵循这些命名规范,可以使代码更加清晰、易读和易维护,有助于团队协作和长期项目的可持续发展。


命名控制器及其方法时,遵循清晰、简洁和易于理解的原则非常重要。下面是一些关于控制器和方法命名的建议:

1. 控制器命名

控制器的命名应根据其所管理的视图或功能进行命名。通常情况下,控制器的命名采用 <功能/视图>ViewController 的格式。以下是一些常见的命名模式:

  • 基于功能的命名: 如果控制器的功能比较清晰,可以直接使用功能来命名控制器。例如:

    • LoginViewController:用于处理登录界面。
    • SettingsViewController:用于处理设置界面。
    • ProfileViewController:用于处理用户资料页面。
    • HomeViewController:用于展示首页内容。
  • 基于视图的命名: 如果控制器管理的是某个具体的视图,可以结合视图进行命名:

    • LoginScreenViewController:表示登录屏幕。
    • DashboardViewController:表示仪表盘页面。
    • MainMenuViewController:表示主菜单页面。

命名时应该避免使用过于宽泛的名称,如 ViewControllerMainController,因为这些名称过于通用,可能会导致代码的可读性下降。

2. 方法命名

方法的命名应该根据它所执行的操作来命名,确保能够清楚地表明方法的功能。以下是一些常见的命名规则:

  • 事件或动作相关的命名: 如果方法是用来响应用户操作或事件的,方法名称通常以 handleperform 开头,接着是描述行为的名词。例如:

    • handleLoginButtonTapped():处理登录按钮点击事件。
    • performSearch():执行搜索操作。
    • handleLogout():处理用户注销。
    • performAction(for:):执行特定操作,参数描述行为的对象或参数。
  • 状态更新相关的命名: 如果方法用于更新 UI 或数据状态,可以使用 updatesetrefresh 等词。例如:

    • updateProfileInformation():更新用户资料信息。
    • setUsername(_:):设置用户名。
    • refreshData():刷新数据。
  • 获取数据的方法命名: 如果方法是用来获取某些数据,通常命名为 fetchget,并且加上适当的描述。例如:

    • fetchUserData():获取用户数据。
    • getWeatherData():获取天气数据。
    • fetchPosts():获取帖子数据。
  • 生命周期相关的命名: 这些方法通常与控制器的生命周期(如视图的加载和销毁)相关。比如:

    • viewDidLoad():视图加载时调用。
    • viewWillAppear():视图即将显示时调用。
    • viewDidAppear():视图已经显示时调用。

3. 命名示例

假设你正在开发一个包含登录界面的应用,以下是一些命名示例:

控制器命名:

  • LoginViewController:处理登录界面相关逻辑。
  • SignupViewController:处理注册界面相关逻辑。
  • ForgotPasswordViewController:处理忘记密码界面。

方法命名:

  • handleLoginButtonTapped():处理登录按钮点击事件。
  • validateInputFields():验证用户输入的用户名和密码。
  • showError(message:):显示错误信息。
  • performLogin():执行登录操作。
  • navigateToHomeScreen():导航到首页屏幕。

4. 代码示例

LoginViewController.swift

class LoginViewController: UIViewController {
    
    // UI 元素
    private var usernameTextField: UITextField!
    private var passwordTextField: UITextField!
    private var loginButton: UIButton!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }

    // 设置 UI
    private func setupUI() {
        // 这里代码添加 UI 元素
    }

    // 处理登录按钮点击事件
    @objc func handleLoginButtonTapped() {
        // 获取输入内容
        let username = usernameTextField.text ?? ""
        let password = passwordTextField.text ?? ""
        
        // 验证输入内容
        if validateInputFields(username: username, password: password) {
            performLogin(username: username, password: password)
        } else {
            showError(message: "Invalid input")
        }
    }

    // 验证输入字段
    private func validateInputFields(username: String, password: String) -> Bool {
        return !username.isEmpty && !password.isEmpty
    }

    // 执行登录
    private func performLogin(username: String, password: String) {
        // 登录逻辑
        print("Logging in with username: \(username) and password: \(password)")
        navigateToHomeScreen()
    }

    // 显示错误信息
    private func showError(message: String) {
        print(message)
    }

    // 导航到首页
    private func navigateToHomeScreen() {
        // 导航到首页逻辑
        print("Navigating to Home Screen")
    }
}

5. 总结

  • 控制器命名:应根据功能或视图命名,例如 LoginViewControllerHomeViewController,避免使用过于模糊的名称。
  • 方法命名:应清晰表达方法的功能,使用动词来描述操作,确保方法名具备可读性和语义清晰性。
  • 命名习惯:遵循一致性和可读性的原则,使代码在团队合作时容易理解与维护。

通过良好的命名,可以让代码更加易于理解、扩展和维护。


以下是一份针对 iOS Swift UIKit 开发的编程规范指南,涵盖代码结构、命名、最佳实践和可维护性等方面:


1. 命名规范

  • 类型和协议:使用大驼峰式(UpperCamelCase)。
    class MyViewController: UIViewController { ... }
    protocol DataFetchable { ... }
    
  • 变量和函数:使用小驼峰式(lowerCamelCase)。
    var userNameLabel: UILabel!
    func fetchDataFromAPI() { ... }
    
  • 布尔属性:以 ishasshould 等开头。
    var isUserLoggedIn: Bool = false
    
  • 缩写词:全大写(如 URLID)。
    let apiURL: URL
    var userID: String
    

2. 代码结构

  • 遵循 MVC/MVVM
    • ViewController 保持轻量化,仅处理视图生命周期和用户交互。
    • 业务逻辑和网络请求抽离到 ViewModelService 类中。
  • 使用扩展(Extension):按功能拆分代码块。
    // MARK: - UITableViewDataSource
    extension MyViewController: UITableViewDataSource { ... }
    
    // MARK: - Private Methods
    private extension MyViewController {
        func setupUI() { ... }
    }
    
  • 避免 Massive ViewController:复杂 UI 拆分为子视图或自定义 UIView 子类。

3. UI 开发规范

  • 使用 Interface Builder 或纯代码
    • 若使用 Storyboard/XIB,确保约束清晰,避免 Magic Number。
    • 纯代码布局推荐 SnapKit 或原生 Auto Layout。
  • 复用组件:通用 UI 组件(如按钮、表格单元格)封装为独立类。
  • 适配深色模式:通过 UIColor(dynamicProvider:) 处理颜色和图片。
    let backgroundColor = UIColor { trait in
        trait.userInterfaceStyle == .dark ? .black : .white
    }
    

4. 内存管理与闭包

  • 避免循环引用:在闭包中使用 [weak self][unowned self]
    networkService.fetchData { [weak self] result in
        guard let self = self else { return }
        self.updateUI()
    }
    
  • 懒加载属性:使用 lazy var 延迟初始化视图。
    private lazy var tableView: UITableView = {
        let table = UITableView()
        table.delegate = self
        return table
    }()
    

5. 错误处理与异步

  • 使用 Result 类型:处理网络请求结果。
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) { ... }
    
  • 主线程更新 UI:确保 UI 操作在 DispatchQueue.main.async 中执行。
    DispatchQueue.global().async {
        // 后台处理
        DispatchQueue.main.async {
            self.label.text = "Updated"
        }
    }
    

6. 代码可维护性

  • 注释与文档
    • 复杂逻辑添加注释。
    • 公共 API 使用文档注释(///)。
    /// 根据用户ID获取用户信息
    /// - Parameter userId: 用户唯一标识
    func fetchUser(by userId: String) { ... }
    
  • 代码格式化:使用 SwiftFormat 或 SwiftLint 统一风格。
  • 单元测试:核心逻辑编写测试用例。
    func testLoginSuccess() {
        let viewModel = LoginViewModel()
        viewModel.login(username: "test", password: "123")
        XCTAssertTrue(viewModel.isLoggedIn)
    }
    

7. 兼容性与版本控制

  • API 可用性检查:使用 @available#available
    if #available(iOS 15.0, *) {
        tableView.sectionHeaderTopPadding = 0
    }
    
  • 第三方库管理:推荐 Swift Package Manager (SPM),避免过度依赖。

8. 其他最佳实践

  • 避免强制解包:使用 if letguard let 处理可选值。
    guard let data = data else {
        print("Data is nil")
        return
    }
    
  • 常量集中管理:使用 enumstruct 组织常量。
    enum Constants {
        static let apiKey = "ABC123"
        static let maxRetryCount = 3
    }
    
  • 通知名称:使用扩展统一管理。
    extension Notification.Name {
        static let userDidLogin = Notification.Name("UserDidLoginNotification")
    }
    

总结

遵循规范可提升代码可读性、可维护性和团队协作效率。实际开发中可根据团队需求调整,但需保持一致性。建议结合工具(如 SwiftLint)自动化检查代码风格。

Swift flatMap 和 compactMap

作者 ZRD1112
2025年3月12日 15:01

理解 flatMapcompactMap 的关键在于它们的 核心目的适用场景。以下是清晰的对比和实际示例,帮助你彻底掌握它们:


一、核心区别总结

方法 核心功能 输入类型 输出类型 典型场景
map 一对一转换 [A][B] 直接转换后的数组 简单类型转换
flatMap 转换 + 展平嵌套 [[A]][B] 展平后的单层数组 处理嵌套集合
compactMap 转换 + 过滤 nil [A?][B] 非空值数组 处理可选值

二、详细解释 + 实际示例

1. flatMap:处理嵌套集合

核心作用:先对每个元素做转换,再将所有结果合并成一个单层数组。

// 示例 1:处理二维数组
let numbers = [[1, 2], [3, 4], [5]]
let flattened = numbers.flatMap { $0 } // [1, 2, 3, 4, 5]

// 示例 2:转换并展平
let strings = ["Hello", "World"]
let characters = strings.flatMap { $0.map { String($0) } }
// ["H", "e", "l", "l", "o", "W", "o", "r", "l", "d"]

2. compactMap:处理可选值

核心作用:转换时自动过滤 nil 值,返回非空数组。

// 示例 1:转换并过滤 nil
let optionalNumbers: [Int?] = [1, nil, 3, nil, 5]
let validNumbers = optionalNumbers.compactMap { $0 } // [1, 3, 5]

// 示例 2:安全类型转换
let mixedValues: [Any] = ["A", 2, 3.14, "B"]
let strings = mixedValues.compactMap { $0 as? String } // ["A", "B"]

三、常见误区解析

误区 1:误用 flatMap 处理可选值

// ❌ 旧代码(Swift 4.1 之前)
let numbers = ["1", "2", "three"]
let ints = numbers.flatMap { Int($0) } // 返回 [1, 2]

// ✅ 正确做法(Swift 4.1+)
let ints = numbers.compactMap { Int($0) } // 明确表达过滤 nil 的意图

误区 2:混淆嵌套层级

// 输入是 [[[Int]]] 时
let deepNested = [[[1], [2]], [[3]]]
let wrong = deepNested.flatMap { $0 } // 结果仍是 [[[1], [2]], [[3]]]

// ✅ 需要多次展平
let fullyFlattened = deepNested.flatMap { $0 }.flatMap { $0 } // [1, 2, 3]

四、实际应用场景

场景 1:处理网络请求结果

// 从多个 API 获取用户数据(可能失败)
let apiResponses: [Result<User, Error>] = [...]

// 用 compactMap 提取成功结果
let validUsers = apiResponses.compactMap { try? $0.get() }

场景 2:解析混合数据源

// 从多个来源获取联系人(可能有空数据)
let contactSources: [[CNContact]?] = [...]

// 用 flatMap + compactMap 合并有效数据
let allContacts = contactSources
    .compactMap { $0 }       // 过滤 nil 数组
    .flatMap { $0 }          // 展平二维数组

场景 3:构建复合数据结构

// 将多个部门的员工列表合并
let departments = [
    Department(employees: ["Alice", "Bob"]),
    Department(employees: ["Charlie"])
]

let allEmployees = departments.flatMap { $0.employees } // ["Alice", "Bob", "Charlie"]

五、性能优化技巧

  1. 惰性计算:对大数据集使用 lazy

    let bigData = (0..<1_000_000).map { $0 }
    let processed = bigData.lazy
        .compactMap { $0 % 2 == 0 ? nil : $0 }
        .flatMap { [$0, $0] } // 按需计算
    
  2. 链式操作顺序:先 compactMapflatMap 减少处理量

    // 优化前
    data.flatMap { $0 }.compactMap { ... }
    
    // 优化后
    data.compactMap { ... }.flatMap { $0 }
    

六、终极判断流程图

当不确定该用哪个方法时,按以下步骤判断:

               开始
                ↓
        需要处理可选值吗?
          /           \
        是             否
        ↓               ↓
  使用 compactMap   需要展平嵌套吗?
                        /           \
                      是             否
                      ↓               ↓
                使用 flatMap      使用 map

通过理解这些核心概念和实际用例,你应该能游刃有余地使用这两个强大的集合操作方法了!

好的,我现在需要理解用户的问题,用户问的是“我怎么才能弄懂 flatMap和compactMap”。用户之前已经询问过关于Swift中处理通讯录群组和联系人的问题,现在转向了Swift中的高阶函数。用户可能在使用这些方法时遇到了困惑,或者在阅读之前的优化代码时看到了flatMapcompactMap,想要深入理解它们的区别和用法。

首先,我需要确认用户的基础知识。他们可能已经了解mapfilterreduce等基础的高阶函数,但对于flatMapcompactMap不太清楚。用户可能在使用数组或可选值时遇到过需要处理嵌套结构或过滤nil值的情况,这正是这两个方法的用武之地。

接下来,我需要考虑如何清晰地解释这两个概念。可能需要从基本定义入手,比较它们的区别,并通过具体的例子来展示它们在实际中的应用场景。比如,flatMap可以展平嵌套的数组,而compactMap则在转换的同时过滤掉nil值。同时,用户可能在之前的代码中看到过compactMap用于处理可选值,例如在获取群组成员ID时过滤无效的ID。

还要注意Swift版本的变化。在Swift 4.1之后,flatMap被拆分为compactMap来处理可选值的情况,这一点需要明确指出,避免用户混淆。可能用户之前看到过旧的代码使用flatMap过滤nil,现在需要更新到使用compactMap

另外,用户可能对何时使用flatMapcompactMap感到困惑。例如,当处理数组中的数组时,应该使用flatMap来展平结构;而当转换数组元素并需要过滤nil时,应该使用compactMap。这些用例需要通过例子来阐明。

此外,用户可能希望了解这两个方法的性能或效率,但暂时可能更关注基础概念和正确使用方式。可能需要提到在处理大数据量时,正确选择方法可以提高效率,但当前阶段以理解为主。

还需要考虑用户可能遇到的常见错误,比如错误地在数组转换中使用flatMap而实际上需要的是compactMap,或者在处理可选值时没有正确使用compactMap导致未过滤nil值。通过指出这些错误情况,帮助用户避免常见的陷阱。

最后,总结两者的区别,并给出使用建议,帮助用户在实际编码中做出正确的选择。可能需要提供一个对比表格,或者在示例代码中明确标注两者的不同应用场景。

在解释过程中,要使用简单明了的语言,避免过于技术化的术语,确保用户能够逐步理解。同时,通过实际代码示例来加深理解,让用户能够动手实践,观察不同方法的效果,从而更好地掌握它们的用法。

昨天 — 2025年3月12日掘金 iOS

Appstore被拒4.3(a)并不可怕,可怕的没有遇到对的方案破局!

作者 iOS阿玮
2025年3月11日 19:37

Appstore问题或咨询,请公众号留言,不免费,不闲聊。iOS研究院

前言

提到Appstore审核,关于Guideline 4.3(a) - Design - Spam简直是所有开发的梦魇。

作为在从16年入行至今都活跃iOSer的我来说,做得越久发觉一条真理。

至少对于Appstore来讲, 代码并不值钱,值钱的是好的点子,以及如何上架正规市场!

当然这里所指的技术不值钱,单纯是从狭隘的说劳动力价值,也就是工资薪酬

客观来讲,技术是有价的,毕竟是买劳动者的时间。创意无价,好的创意会创造无限可能。

从体验来说,如果觉得自己体验做得好,那么会有体验做得更好。

从交互来说,如果觉得自己交互前卫,那么用不了几天市面上就是会有雷同。

当然,上面的铺垫,主要想表达现在已经知识付费的时代。关于过审的方案,就好比一个好的点子。

只要方法对,破局并不是复杂的事情。

所以,单纯是上来就套路方案,套话的客户,大可不必了。大家都是成年人,不是小孩子过家家。

关于最近的2单4.3(a)

无论是哪种产品,为了保护客户们的隐私这里只提及非常笼统的大类目。

游戏类案例截图

通过整改方案,耗时半个月修改,今天成功上架Appstore。特别说明:老板非常大气在费用上一口价,没有多犹豫。这种老板注定发大财!

游戏类(1)

游戏类(2)

工具类案例截图

因为是独立开发者而且是一个小的工具类,考虑到大环境不景气。 收费并不高,只是简单整改了3天就顺利通过了审核。

工具类

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

本文使用 markdown.com.cn 排版

音视频学习笔记十三——渲染与滤镜之着色器基础

2025年3月11日 13:40

题记:本章会开始写一些滤镜了,LearnOpenGL CN中都以3D效果为主,此系列只处理2D图片效果,涉及视频、相机等。音视频学习Demo有OpenGL在相机、视频方面的应用,文章或代码若有错误,也希望大佬不吝赐教。

一、着色器

在OpenGL中,可编程管线允许开发者控制图形渲染流程中的特定阶段。这些自定义的控制程序称为着色器(Shader)。着色器运行在GPU上,使用特定的着色器语言(GLSL)编写,开发者可以根据需求实现各种复杂的算法和处理逻辑。

渲染流程.jpg

OpenGL ES对细分着色器和几何着色器支持有限,本系列只讨论顶点着色器和片元着色器。

  • 顶点着色器(Vertex Shader)
    • 处理顶点的位置、法线、纹理坐标等属性。
    • 常用于顶点位移、骨骼动画等。
  • 片段着色器(Fragment Shader)
    • 处理像素(片段)的颜色、透明度等属性。
    • 纹理采样、光照计算、颜色混合等。

关于着色器的处理流程,可以参考前面的文章,本章讲述语言的语法细节。

1.1 GLSL(OpenGL Shading Language)

1.1.1 数据类型

GLSL是一种类C语言,除了voidboolintfloat外,也支持一些对向量和矩阵的类型。

  • 特有类型

    • 向量:vec2vec3vec4
      • 支持floatintbool类型
      • vec2float类型2维向量;ivec3int类型3维向量;bvec4bool类型4维向量;
    • 矩阵:mat3mat4
      • 仅支持浮点类型,mat4表示4x4的浮点矩阵
    • 纹理:sampler2DsamplerCube
  • 精度限定符

    • highp(高)、mediump(中)、lowp(低),修饰intfloat类型等
    • lowp的float类型范围是[-2, 2],在数据转换中需要特别注意
    • 顶点着色器有默认类型 float 默认为 highpint 默认为 mediump
    • 片元着色器float无默认类型,int类型默认mediump

    所以一般看Shader时会发现同一对象的定义,片元需要显式说明

    // 顶点着色器中定义
    varying vec2 textureCoordinate;
    // 同一对象在片元着色器定义
    varying highp vec2 textureCoordinate;
    

    或者在片元着色器开始添加定义

    // 片段着色器必须声明默认浮点精度
    precision mediump float;
    
  • 变量限定符有两个版本

    • 旧版(OpenGL ES 2.0)
      • uniform:从CPU传递的全局常量。
      • attribute(只用于顶点着色器):逐顶点的输入数据。
      • varying:顶点着色器向片段着色器传递插值数据
    • 新版(OpenGL ES 3.0+,兼容旧版)
      • uniform:不变
      • in:输入变量,替代旧版 attribute 和 varying
      • out:输出变量,替代旧版 varying
      • inout:函数参数修饰符,表示参数既是输入也是输出,仅用于函数参数

    OpenGL ES 3.0一般写成

    /// 顶点着色器
    #version 300 es
    in vec3 aPosition;     // 输入顶点位置(替代attribute)
    in vec2 aTexCoord;     // 输入纹理坐标
    out vec2 vTexCoord;    // 输出到片段着色器(替代varying)
    
    void main() {
        gl_Position = vec4(aPosition, 1.0);
        vTexCoord = aTexCoord;
    }
    /// 片元着色器
    #version 300 es
    precision mediump float;
    in vec2 vTexCoord;       // 输入来自顶点着色器
    uniform sampler2D uTexture;
    out vec4 FragColor;      // 输出颜色(替代gl_FragColor)
    
    void main() {
        FragColor = texture(uTexture, vTexCoord);
    }
    

1.1.2 内置函数与常量

  • 数学函数

    函数 描述 示例 典型应用
    abs(x) 绝对值 abs(-5.0) → 5.0 距离计算、法线方向处理
    floor(x)/ceil(x) 向下/向上取整 floor(3.7) → 3.0 像素对齐、离散化操作
    round(x) 四舍五入 round(2.3) → 2.0 纹理坐标量化
    mod(x, y) 取模运算 mod(5.2, 3.0) → 2.2 周期性效果(如平铺纹理)
    clamp(x, min, max) 将值限制在 [min, max] 范围内 clamp(1.5, 0.0, 1.0) → 1.0 避免数值溢出
    mix(a, b, t) 线性插值:a*(1-t) + b*t mix(0.0, 10.0, 0.3) → 3.0 颜色渐变、动画过渡
    step(edge, x) 阶跃函数:x >= edge ? 1.0 : 0.0 step(0.5, 0.7) → 1.0 条件分支的无分支替代,(OpenGL避免用if)
    smoothstep(a, b, x) 平滑过渡的插值(S 形曲线) smoothstep(0.0, 1.0, 0.5) → 0.5 抗锯齿、柔和边缘效果
    length(v) 向量长度 length(vec2(3.0, 4.0)) → 5.0 距离计算、归一化
    distance(a, b) 两点间距离 distance(vec3(0.0), vec3(1.0)) → 1.732 碰撞检测、光照衰减
    dot(a, b) 点积 dot(vec3(1,0,0), vec3(0,1,0)) → 0.0 光照强度、投影计算
    cross(a, b) 叉积(仅适用于三维向量) cross(vec3(1,0,0), vec3(0,1,0)) → (0,0,1) 法线计算、旋转轴确定
    normalize(v) 向量归一化 normalize(vec3(2,0,0)) → (1,0,0) 方向向量处理
    reflect(I, N) 反射向量:I - 2*dot(N,I)*N reflect(lightDir, normal) 镜面反射、环境映射
    refract(I, N, eta) 折射向量 refract(lightDir, normal, 1.5) 透明材质(水、玻璃)
    sin(x)/cos(x)/tan(x) 基本三角函数(输入为弧度) sin(radians(90.0)) → 1.0 波形动画、旋转矩阵计算
    asin(x)/acos(x) 反三角函数 acos(0.5) → 1.047(≈60°) 反射向量计算
    pow(x, y) 幂运算:x^y pow(2.0, 3.0) → 8.0 光照衰减、非线性颜色空间转换
    exp(x)/log(x) 指数函数和自然对数 exp(1.0) → e ≈ 2.718 复杂数学模型(如体积渲染)
    sqrt(x) 平方根 sqrt(4.0) → 2.0 向量长度归一化
  • 内置常量

    常量                    值   说明                     
    gl_MaxVertexAttribs ≥16 顶点属性最大数量(如位置、法线、纹理坐标等)。
    gl_MaxTextureUnits  ≥16 支持的纹理单元数量。             
    gl_MaxVertexOutput  ≥16 顶点着色器可输出的向量数量。

1.2 顶点着色器

顶点着色器最重要的是构建裁剪空间的坐标点gl_Position,绘制点时也可以设置gl_PointSize。具体可参考OpenGL基础一坐标系,处理坐标系变换:

// vec4类型
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vertexPosition;

在顶点着色器中还有两个只读的变量。

内置变量            数据类型    作用说明         
gl_VertexID   int   顶点索引,表示正在处理的是第几个顶点       
gl_InstanceID int   实例索引,多次绘制时很有用

1.3 片元着色器

片元着色器最重要的是构建gl_FragColor(OpenGL 3以后out vec4 FragColor替代),可以直接赋值颜色,也可以通过采样纹理获得。

/// 颜色,赋值为红色
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
/// 纹理采样,inputImageTexture为纹理单元,textureCoordinate为纹理坐标
gl_FragColor = texture2D(inputImageTexture, textureCoordinate);

这里需要说明的是,片元着色器会对每个像素点执行一次,在顶点没有指向的地方是通过插值完成的(无论是直接赋值颜色还是纹理采样)。例子中分别对3个顶点赋值了不同的颜色,可以看到下列类似iOS CAGradientLayer图层的效果。

float vertices[] = {
    // positions         // colors
     0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // bottom right
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // bottom left
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f   // top 
};
image.png
  • 内置变量
内置变量             数据类型    作用说明  
gl_FragCoord   vec4  窗口坐标
gl_FrontFacing bool  判断当前片段是否属于正面
gl_PointCoord  vec2  点的纹理坐标,仅当渲染点时有效
gl_FragDepth   float 手动设置片段深度值(默认使用 gl_FragCoord.z
  • discard用法

此外,绘制纹理时,特别是透明通道时,使用discard会很有用(混合也可以),可参考learnOpenGL

image.png
void main()
{
    vec4 texColor = texture(texture1, TexCoords);
    // 如果纹理的透明度小于某个阈值,则丢弃该片元
    if(texColor.a < 0.1)
        discard;
    FragColor = texColor;
}

二、着色器示例

经过上面的介绍,可以动手做点着色器了,下面给过一些例子和解释。顶点着色器和片元着色器都是配合使用的,但有不同效果处理重点在不同的着色器上。

2.1 顶点示例

顶点着色器的基本用法就是构建图形,如经典的金字塔图形顶点着色器基本就是模型变换输出。

// 顶点着色
attribute vec4 position;
attribute vec4 color;
varying vec4 fragColor;
uniform mat4 modelViewProjectionMatrix;
void main() {
    gl_Position = modelViewProjectionMatrix * position;
    fragColor = color;
}
// 片元着色
precision mediump float;
varying vec4 fragColor;
void main() {
    gl_FragColor = fragColor;
}\
image.png
  • 构建顶点

    • 构建面顶点
    const GLfloat pyramidVertices[] = {
          // 正面
          0.0f,  1.0f,  0.0f,    1.0f, 0.0f, 0.0f,  // 顶点
          -1.0f, -1.0f, 1.0f,    1.0f, 0.0f, 0.0f,  // 左下
          1.0f, -1.0f, 1.0f,     1.0f, 0.0f, 0.0f,  // 右下
    
          // 右面
          0.0f,  1.0f,  0.0f,    0.0f, 1.0f, 0.0f,
          1.0f, -1.0f, 1.0f,     0.0f, 1.0f, 0.0f,
          1.0f, -1.0f, -1.0f,    0.0f, 1.0f, 0.0f,
    
          // 后面
          0.0f,  1.0f,  0.0f,    0.0f, 0.0f, 1.0f,
          1.0f, -1.0f, -1.0f,    0.0f, 0.0f, 1.0f,
          -1.0f, -1.0f, -1.0f,   0.0f, 0.0f, 1.0f,
    
          // 左面
          0.0f,  1.0f,  0.0f,    1.0f, 1.0f, 0.0f,
          -1.0f, -1.0f, -1.0f,   1.0f, 1.0f, 0.0f,
          -1.0f, -1.0f, 1.0f,    1.0f, 1.0f, 0.0f
          ...
    };
    
    • 复制到GPU VBO
    glGenBuffers(1, &_vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(pyramidVertices), pyramidVertices, GL_STATIC_DRAW);
    
  • 纹理赋值

    • uniform类型,获取layout,使用glUniformxxx进行赋值
    // 设置MVP矩阵,设置shader中的uniform modelViewProjectionMatrix
    xxx
    GLint modelViewProjectionMatrixUniform = glGetUniformLocation(_program, "modelViewProjectionMatrix");
    glUniformMatrix4fv(modelViewProjectionMatrixUniform, 1, 0, modelViewProjectionMatrix.m);
    
    • attribute类型,获取layout,打开glEnableVertexAttribArray,使用glVertexAttribPointer赋值
    // 设置顶点属性,获取attribute的layout
    GLuint positionAttribute = glGetAttribLocation(_program, "position");
    GLuint colorAttribute = glGetAttribLocation(_program, "color");
    // 默认属性关闭,这里必须打开
    glEnableVertexAttribArray(positionAttribute);
    glEnableVertexAttribArray(colorAttribute);
    // attribute赋值
    glVertexAttribPointer(positionAttribute, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, 0);
    glVertexAttribPointer(colorAttribute, 4, GL_FLOAT, GL_FALSE, sizeof(GLfloat) * 6, (void*)(sizeof(GLfloat) * 3));
    

2.2 片元示例

片元着色器基本用法绘制片段颜色,例如在图片处理中,顶点着色器几乎不处理,片元着色器处理变换。(美颜的形变可以在片元处理,也可以在顶点处理)。这里举出灰度图和马赛克作为练手。

2.2.1 灰度图

把图片显示效果变成灰度图是滤镜中常用到的效果。

image.png

RGB转灰度公式如下:

// BT.709
Gray=0.2125×R+0.7154×G+0.0721×B

所以,可以把灰度图frag shader写成如下,vec4(gray, gray, gray, a)

precision highp float;
varying vec2 textureCoordinate;
uniform sampler2D inputImageTexture;
const highp vec3 W = vec3(0.2125, 0.7154, 0.0721);

void main()
{
   lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);
   float luminance = dot(textureColor.rgb, W);
   gl_FragColor = vec4(vec3(luminance), textureColor.a);
}

2.2.2 马赛克效果

再来看下马赛克效果:

image.png

马赛克效果简单理解就是一小块区域取一个颜色(中间或者左下都可以),根据需要分成不同粒度,图中是宽高都等分成50份,所以下列就是一个floor(TextureCoordsVarying.x / unit)取整操作(去掉小数位)。

precision highp float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;

void main (void) {
    vec2 maskXY = TextureCoordsVarying;
    float unit = 1.0 / 50.0;
    maskXY.x = float(floor(TextureCoordsVarying.x / unit)) * unit;
    maskXY.y = float(floor(TextureCoordsVarying.y / unit)) * unit;
    vec4 mask = texture2D(Texture, maskXY);
    gl_FragColor = vec4(mask.rgb, 1.0);
}

iOS常用三方库记录

2025年3月11日 10:35
1.基础
pod 'TKBaseKit'                       #通用基础库,使用时最好指定某个版本
pod 'TKPermissionKit'                #权限管理   ,按需导入   
pod 'TKCrashNilSafe'                 #iOS防奔溃处理!
pod 'TKMultipleDelegate'            #iOS 多delegate,多代理,多委托实现方案
pod 'TKAnimationKit'                  #动画-Demo    ->  test
pod 'TKUIKit'                            #一些常用的UI控件与工具类集成    ->  test
pod 'IQKeyboardManager'           #键盘 OC
pod 'IQKeyboardManagerSwift'     #键盘 Swift
pod 'JKCategories'                     #一个有用的Objective-C类别的集合
pod 'SwifterSwift'                       #包含 500 多个原生 Swift 扩展的便捷集合,可提高您的工作效率。
pod 'SPPermissions                    #使用现成的界面询问权限。您可以检查状态权限以及之前是否已请求。支持 SwiftUI。 -> 需要按需加载
pod 'CTMediator'                       #组件化

2.响应式
pod 'RxSwift',                      #Swift响应式编程
pod 'RxCocoa', 
pod 'ReactiveCocoa'
pod 'Epoxy'                          #一套声明式 UI API,用于在 Swift 中构建 UIKit 应用程序
componentkit                        #一个受 React 启发的 iOS 视图框架。
3.布局,约束
pod 'Masonry'                                           #布局约束           推荐
pod 'SnapKit'                                            #布局约束   Swift   推荐
pod 'SDAutoLayout'                                    #布局约束
pod 'SnapKitExtend'                                   #对SnapKit的补充,同时补充九宫格布局方式,对Arry的设置和对等间距排列的布局等
pod 'CHTCollectionViewWaterfallLayout'        #瀑布流库
pod 'LXMWaterfallLayout'                            #瀑布流库       Swift
pod 'CollectionViewPagingLayout'                  #一个简单但高度可定制的 UICollectionViewLayout -> 支持SwiftUI  -> 推荐
UITableView-FDTemplateLayoutCell                #用于自动计算 UITableViewCell 高度的模板自动布局单元格  -> https://github.com/forkingdog/UITableView-FDTemplateLayoutCell
pod 'IBPCollectionViewCompositionalLayout'    #UICollectionView组合布局
pod 'LxGridView'    
4.网络,蓝牙
pod 'Alamofire'                               #Swift
pod 'Moya'                                     #用Swift 编写的网络抽象层。即对网络请求工具(如Alamofire)的抽象 ->  有多个版本
pod 'AFNetworking'                         #OC
pod 'YTKNetwork'                            #一个基于 AFNetworking 的高级请求工具。
pod 'Reachability'                            #替代Apple Reachability,  fix
pod 'SocketRocket'                          #WebSocket  OC      推荐
pod 'CocoaAsyncSocket'                  #WebSocket
pod 'Starscream'                            #WebSocket  Swift   推荐
pod 'SwiftWebSocket'                      #WebSocket  Swift
pod 'PusherSwift'                           #websocket  Swift
pod 'Socket.IO-Client-Swift'             # Socket.IO
pod 'ZYNetworkAccessibity'             #iOS网络权限的监控和判断
pod 'CocoaHTTPServer'                   #HTTP服务器
pod 'GCDWebServer'                       # HTTP server for iOS, macOS & tvOS
pod 'Telegraph'                              #适用于 iOS、tvOS 和 macOS 的安全 Web 服务器
pod 'XMPPFramework'                     #
pod 'MQTTClient'                             #MQTT Client Framework
pod 'BabyBluetooth'                         #一个非常容易使用的蓝牙库,适用于ios和os
pod 'BluetoothKit'                            #使用 BLE 在 iOS/OSX 设备之间轻松通信
5.Model,Json,XML,HTML,Yaml,Markdown解析; JS交互
pod 'YYModel'                   #json OC    推荐
pod 'JSONModel'                 #json OC
pod 'MJExtension'               #json OC
pod 'ExCodable'                 #json Swift -> 推荐 -> 对Codable协议做了大量优化(作者说,自己还没有看)
pod 'Codextended'               #json Swift -> 可推荐 -> 对Codable做了一些简化
                                #就Swift Json to Model工具,系统自带的Codable协议就能完成常见的需求量,就是繁琐了一点;可推荐使用ExCodable
pod 'KakaJSON'                  #json Swift 推荐
pod 'SwiftyJSON',               #json Swift
pod 'HandyJSON',                #json Swift 可推荐 fix
pod 'CleanJSON'                 #json Swift NO USE
pod 'ObjectMapper'              #json swift NO USE
pod 'Ono'                       #xml,html OC
pod 'Kanna'                     #xml,html
pod 'SwiftSoup'                 #html,css,jquery,DOM
pod 'SWXMLHash'                 #xml
pod 'SwiftyXMLParser'           #xml
pod 'Yaml'                      #yaml -> 该库存在严重bug
pod 'Yams'                      #yaml Swift 推荐使用
scout                           #JSON、Plist、YAML、XML。Swift -> 可以 ->  并且提供二进制工具
scout                           #https://github.com/ABridoux/scout
yaml-to-swift                   #JSON、Plist、YAML、XML。 在线转Swift代码 -> https://jsonformatter.org/yaml-to-swift
https://yaml.org                #各种语言yaml解析库
CommonMarkAttributedString      #markdown
CommonMark                      #markdown https://github.com/SwiftDocOrg/CommonMark
MarkdownUI                      #markdown SwiftUI
pod 'WebViewJavascriptBridge'   #OC与js交互
KKQuickDraw                     #iOS实现HTML H5秒开、拦截请求替换资源、优化HTML加载速度
6.指示器,进度条,弹窗,菜单Menu
pod 'M13ProgressSuite'          #带动画的进度条
pod 'MBProgressHUD'             #OC
pod 'JGProgressHUD'             #OC
pod 'SVProgressHUD'             #OC
pod 'WSProgressHUD'             #OC
pod 'ProgressHUD'               #Swift  iOS 13.0+
pod 'KRProgressHUD'             #Swift
pod 'PKHUD'                     #Swift
pod 'ZKProgressHUD'             #Swift
pod 'APESuperHUD'               #Swift
pod 'VHUD'                      #Swift
pod 'SPAlert'                   #来自 Apple Music 和反馈的本机警报。包含 Done、Heart & Message 和其他预设。支持 SwiftUI。
pod 'KLCPopup'                  #一个自定义弹出窗口(old)
pod 'FFDropDownMenu'            #下拉菜单,你可以自定义菜单样式,自由性高。 
pod 'GKCover'                   #一行代码实现遮罩视图,让你的弹窗更easy
pod 'PanModal'                  #An elegant and highly customizable presentation API for constructing bottom sheet modals on iOS.
UIContextMenuInteraction        #Apple system 上下文菜单
UIMenuController                #Apple system 编辑菜单
UIActivityViewController        #Apple system 分享
LNPopupController        # 弹窗
HWPanModal               # 弹窗
YBPopupMenu              # 弹窗
LNPopupController        # 菜单
YBPopupMenu              # 弹窗
7.数据,缓存,加密,解压,数据库,sqlite
#https://www.modb.pro/db/224238
pod 'FMDB'                      #有多个版本,按需选择 -> 关于加密操作:https://www.jianshu.com/p/36609966490e
pod 'LKDBHelper'                #FMDB的二次封装,支持ORM -> https://github.com/li6185377/LKDBHelper-SQLite-ORM
pod 'BGFMDB'
pod 'SQLite.swift'              #纯Swift SQLite数据库,使用比较简单
pod 'WCDB'                      #微信封装的sqlite库   OC      推荐使用微信的WCDB
pod 'WCDB.swift'                #微信封装的sqlite库   Swift
pod 'Realm'                     #Core Data与SQLite的替代品   OC
pod 'RealmSwift'                #Core Data与SQLite的替代品   Swift
pod "EJDB2"                     #一个json通用数据库框架 -> https://github.com/Softmotions/ejdb
    说明:
        1.WCDB和FMDB都是基于Sqlite;
        2.WCDB,Realm支持ORM,FMDB不支持ORM。
        3.WCDB与FMDB都有基于SQLCipher的加密功能。
        4.WCDB,Realm都有Swift版本
        5.推荐等级:WCDB > FMDB > Realm


pod 'YYCache'                   #缓存
pod 'CocoaSecurity'             #数据加密  OC
pod 'CryptoSwift'               #数据加密  Swift  推荐
pod 'SSZipArchive'              #压缩包 -> 压缩,解压   OC
pod 'Zip'                       #压缩包 -> 压缩,解压   Swift
pod 'DataCompression'           #Data -> 压缩,解压
pod 'GZIP'                      #Data -> 压缩,解压
pod 'GzipSwift'                 #Data -> 压缩,解压
GTMBase64                       #GTMBase64加解密 优化版本 -> https://github.com/yanglei3kyou/GTMBase64
pod 'UICKeyChainStore'          #Keychain   OC
pod 'KeychainAccess'            #Keychain   Swift
pod 'SSKeychain'                #Keychain   OC
pod 'TKKeychain'                #Keychain
8.View
pod 'QMUIKit'                   #致力于提高项目 UI 开发效率的解决方案
pod 'SDWebImage'                #网络图片展示
pod 'SDWebImageFLPlugin'        #gif
pod 'Kingfisher'                #Swift   ->  一个轻量级的纯 Swift 库,用于从 Web 下载和缓存图像。
pod 'FLAnimatedImage'           #适用于 iOS 的高性能动画 GIF 引擎
pod 'SVGKit'                    #SVG图片加载
pod 'AlamofireImage'            #是Alamofire的图像组件库
pod 'MJRefresh'                 #刷新控件
pod 'KafkaRefresh'              #内置多种动画、可自定义和灵活的iOS下拉刷新框架

pod 'SDCycleScrollView'         #Banner轮播       OC
pod 'FSPagerView'               #Banner轮播       Swift
pod 'iCarousel'                 #高度可定制的3D轮播图
pod 'GKCycleScrollView'         #Banner轮播

pod 'JXCategoryView'            #分段选择器 OC   ->  还有多种样式,具体需要查看作者首页
pod 'JXSegmentedView'           #分段选择器 Swift
pod 'JXPagingView/Pager'        #联动-多页面嵌套   OC
pod 'JXPagingView/Paging'       #联动-多页面嵌套   Swift
pod 'JXBottomSheetView'         #UITableView向上拖动嵌套
pod 'SGPagingView'              #分段,联动
pod 'GKPageScrollView'          #分段,联动,类似微博、抖音、网易云等个人详情页滑动嵌套效果
pod 'Tabman'                    #一个功能强大的分页视图控制器,带交互式指示器栏
pod 'GSKStretchyHeaderView'     #一个简单的Header下拉放大
pod 'ParallaxHeader'            #Header下拉放大

pod 'GKNavigationBarViewController'     #iOS自定义导航栏-导航栏联动
pod 'GKNavigationBar'                   #iOS自定义导航栏-导航栏联动
pod 'WRNavigationBar'                   #一行代码设置状态栏、导航栏按钮、标题、颜色、透明度,移动等    ->   不错
pod 'HBDNavigationBar'                  #

pod 'PYSearch'                  #搜索控制器
pod 'SGAdvertScrollView'        #文字广告轮播,跑马灯
pod 'JXMarqueeView'             #跑马灯
pod 'FXBlurView'                #模糊处理
pod 'UUMarqueeView'             #跑马灯
pod 'YFRollingLabel'            #跑马灯

pod 'OCBarrage'                    #弹幕
pod 'BarrageRenderer'              #弹幕
pod 'HJDanmakuDemo'                #弹幕

StarRate                        #星星评分控件 -> https://github.com/chenjie1219/StarRate

pod 'DropDown'                  #适用于 iOS 的“材质设计”下拉列表
GHDropMenu                      #仿京东/美团电商筛选菜单  -> https://github.com/shabake/GHDropMenuDemo

pod 'LTMorphingLabel'           #可变形、动态的UILabel。  OC、Swift、SwiftUI

pod 'UITextView-WZB'            #一个强大的UITextView分类,三大功能,让系统TextView自带placeholder属性、自动高度、支持输入图片
pod 'UITextView+Placeholder'    #placeholder for UITextView

pod 'TORoundedButton'           #高性能的圆角Button

pod 'TORoundedTableView'        #UITableView分组样式(即iOS新增的UITableViewStyleInsetGrouped样式), 支持10.0+ 
pod 'TOInsetGroupedTableView'   #UITableView分组样式(即iOS新增的UITableViewStyleInsetGrouped样式), 支持11.0+ 
pod 'UICountingLabel'           #为 UILabel 添加动画计数支持。
pod 'JKCountDownButton'         #IOS倒计时按钮,常常用于注册等发送验证码的时候进行倒计时操作
pod 'EFAutoScrollLabel'         #当文本长度超过标签宽度时可以滚动的标签。
RealtimeGradientText            #渐变文本       ->  https://github.com/kevinzhow/RealtimeGradientText
pod 'TOScrollBar'               #https://github.com/TimOliver/TOScrollBar
pod 'TOSplitViewController'     #https://github.com/TimOliver/TOSplitViewController

pod 'WMDragView'                #WMDragView致力于让任意View都可以自由悬浮拖曳(可拖动,悬浮按钮),类似于iOS的AssistiveTouch效果,微信浮窗。

VVeboTableViewDemo              #如何进行TableView流畅度优化的。  -> https://github.com/johnil/VVeboTableViewDemo

LGApplications                  #小demo集合:①ActionSheet; ②PickerView; ③ScrollView嵌套; ④渐变文字Label; ⑤tableView实现单选; -> https://github.com/MrLee767201403/LGApplications
9.UITableView/UICollectionView
pod 'ESTabBarController-swift'          #用于自定义UI、徽章和向选项卡项添加动画。支持lottie! ->  Swift  -> 推荐    
pod 'SwipeableTabBarController'        #UITabBar控制器,其选项卡之间具有滑动交互功能。
pod 'MCTabBarController'                 #快速定制TabBar中间按钮凸起   -> 也有Swift版本    ->  https://github.com/Ccalary/MCTabBarController
pod 'AxcAE_TabBar'                         #多种样式的TabBar        ->  https://github.com/axclogo/AxcAE_TabBar
pod 'FancyTabBar'                           #可扩展且可自定义的选项卡栏
pod 'CYLTabBarController'                #一行代码实现 Lottie 动画TabBar,支持中间带+号的
pod 'Pageboy'                                 #简单、信息丰富的页面视图控制器   ->  Swift 

TabBar样式,自带红点角标,支持动态刷新。
pod 'QWTabBar'                              #自定义TabBar动画
pod 'BATabBarController'                 #一个带有独特动画的 TabBarController 供选择
IrregularTabbarCustom                    #定义不规则的tabbar   ->  https://github.com/singer1026/IrregularTabbarCustom
LLRiseTabBar-iOS                           #中间凸起的TabBar        ->  https://github.com/NoCodeNoWife/LLRiseTabBar-iOS
10.异步绘制View
pod 'Texture'                   #优秀的异步绘制库(旧:AsyncDisplayKit),一个完整的UI体系库
pod 'YYText'
pod 'BSText'                    #The Swift Version of YYText
pod 'MPITextKit'                #YYText的替代库 可以实现文字展开收起,图文混排
pod 'STULabel'                  #一个比UILabel和UITextView更快,的异步绘制库
pod 'Nimbus'                    #一个全新的UIVie框架
pod 'YYLable'
11.富文本,公式,KaTeX
pod 'DTCoreText'                #支持HTML,富文本显示
pod 'DTRichTextEditor'          #富文本编辑器
pod 'ZSSRichTextEditor'         #一个漂亮的 iOS 富文本所见即所得编辑器,带有语法突出显示的源视图
YYRichEditor                    #富文本编辑器实现   ->  https://github.com/WillkYang/YYRichEditor
pod 'AttributedString'          #基于Swift插值方式优雅的构建富文本, 支持点击长按事件, 支持不同类型过滤, 支持自定义视图等
pod 'iosMath'                   #KaTeX公式展示
pod 'MathEditor'                #KaTeX公式编辑器
pod 'iosMathEditor'             #KaTeX公式编辑器
MathJax-src                     #KaTeX公式展示,一个js框架   ->  https://github.com/mathjax/MathJax-src
KaTeX                           #js框架  ->  https://github.com/KaTeX/KaTeX
SPMathKit                       #aTeX公式展示 ->  https://github.com/CodingSha/SPMathKit
MathStringExpression            #计算数学表达(算术表达式)式框架,开发iOS计算器  -> https://github.com/Meterwhite/MathStringExpression
UXReader                        #适用于 iOS 的 UXReader PDF 框架 -> https://github.com/vfr/UXReader-iOS
pod 'SIXRichEditor'             #iOS 一个简洁 易用 支持html 的富文本编辑器
LMNote                          #原生 iOS 文本编辑器。 -> https://github.com/littleMeaning/LMNote
12.相册选择,图片,视频,浏览,裁剪,编辑
pod 'TZImagePickerController'       #照片选择器
pod 'TZImagePreviewController'      #对TZImagePickerController库的增强,支持用UIImage、NSURL预览照片和用NSURL预览视频。
pod 'HXPhotoPicker'                 #图片/视频选择,编辑器,滤镜  ->   ✅✅推荐  ->  有多个版本,到主页查看
pod 'HXPHPicker'                    #图片/视频选择器  Swift iOS 12.0+  (转移到HXPhotoPicker)
pod 'GKPhotoBrowser'                #iOS仿微信、今日头条等图片浏览器
pod 'KSPhotoBrowser'                #一个小而美的图片浏览器。
pod 'YBImageBrowser'                #图片浏览器-注意依耐
pod 'ZLPhotoBrowser'                #微信样式的图片选择器,支持预览/相册内拍照及录视频、拖拽/滑动选择,编辑图片/视频,支持多语言国际化等功能
pod 'SKPhotoBrowser'                #简单的照片浏览器/查看器
pod 'PYPhotoBrowser'                #简单的图片浏览器  ->  有多个版本,到主页查看
pod 'MWPhotoBrowser'                #一个简单的iOS照片和视频浏览器
SDPhotoBrowser                      #图片浏览器  ->  https://github.com/gsdios/SDPhotoBrowser
pod 'JPImageresizerView'            #专门裁剪图片、GIF、视频      ->  推荐
pod 'JPCrop'                        #图片裁剪       ->  推荐
pod 'RSKImageCropper'               #图片裁剪       -> 也不错
pod 'TOCropViewController'          #UImage裁剪视图控制器
pod 'RSKImageCropViewController'    #相册剪裁
pod 'VisionCamera'                  #Vision Camera  ->  https://github.com/mrousavy/react-native-vision-camera
pod 'GPUImage3'                     #图形处理
pod 'Harbeth'                       #基于Metal   -> 图像、视频、相机滤镜框架
pod 'OpencvQueen'                   #基于OpenCV  -> 图像、视频、相机滤镜框架
EnjoyCamera                         #含了50多种滤镜,可以选择图片和编辑图片,可以拍照和实时滤镜等   ->  https://github.com/QinminiOS/EnjoyCamera
pod 'ZWHTMLImage'                   #iOS快速实现对WEB网页、HTML的native图片预览功能。两行代码调用、同时支持UIWebView与WKWebView。支持懒加载(滚动加载)类型HTML网页
pod 'UIImageColors'                 #从图像中获取最主要和最突出的颜色
13.播放器,视频,音乐
pod 'AliyunPlayer_iOS'          #阿里云播放器 v3.x    ->  https://github.com/aliyunvideo/AliyunPlayer_iOS
pod 'AliPlayerSDK_iOS'          #阿里云播放器 v4.x+   ->  https://help.aliyun.com/document_detail/124708.html
    如果需要支持artc、artp或rts:
    pod 'AliPlayerSDK_iOS', '5.3.0'
    pod 'AliPlayerSDK_iOS_ARTP', '5.3.0'
    pod 'AliPlayerSDK_iOS_ARTC', '5.3.0'
    pod 'RtsSDK', '1.5.0'

pod 'KDEAudioPlayer'            #AudioPlayer 是 AVPlayer 的语法和功能糖。它播放您的音频文件(本地和远程)。
QPlayer                         #QPlayer是一款你不容错过的视频播放器  ->  https://github.com/dgynfi/QPlayer
GKWYMusic                       #iOS基于FreeStreamer的仿网易云音乐播放器 -> https://github.com/QuintGao/GKWYMusic
pod 'KTVHTTPCache'              #一个强大的媒体缓存框架。
pod 'WMPlayer'                  #WMPlayer-AVPlayer的封装,继承UIView,支持pods,手势快进、快退,全面适配全面屏,同时支持网络和本地视频的播放
pod 'SJVideoPlayer'             #短视频播放器 可接入 ijkplayer aliplayer alivodplayer plplayer       ->  可以(支持弹幕)
pod 'ZFPlayer'                  #支持定制任何播放器SDK和控制层
ijkplayer                       #bilibili开源的播放器 ->  https://github.com/bilibili/ijkplayer
其它:腾讯,七牛云的播放器也可以试试
14.K线图,Chart
pod 'DGCharts'                  #一个优秀的Swift Chart库,PS:原名Charts,Apple 推出了一个名为Charts的SwiftUI Charts库
pod 'AAChartKit'                #Chart  OC
pod 'AAInfographics'            #Chart  Swift
pod 'PNChart'                   #Chart  OC
pod 'PNChartSwift'              #Chart  Swift
YYKline                         #Kline、Chart、Volume、Scroll、Scale、MACD、KDJ、K线图、分时图... -> https://github.com/WillkYang/YYKline
YYStock                         #k线图 (old)
pod 'KSChart'                   #k线图
15.Crash崩溃,日志收集
pod 'Bugly'                     #BUG收集
pod 'TKCrashNilSafe'            #iOS防奔溃处理!
pod 'AvoidCrash'                #防止APP崩溃
pod 'LSSafeProtector'           #强大的防止crash框架,不改变原代码支持KVO自释放,可以检测到dealloc时未释放的kvo,等19种crash
pod 'NSObjectSafe'
pod 'FLEX'                      #适用于 iOS 的应用内调试和探索工具
pod 'JXCaptain'                 #像美国队长一样威猛的应用调试工具箱!
EarlGrey                        #iOS UI 自动化测试框架 -> https://github.com/google/EarlGrey
MTHawkeye                       #iOS 的分析/调试辅助工具。 (内存泄漏、OOM、ANR、Hard Stalling、网络、OpenGL、时间配置文件...)
16.支付、内购、In App Purchases
pod 'AlipaySDK-iOS'             #支付宝支付
pod 'WechatOpenSDK'             #微信支付
pod 'SwiftyStoreKit'            #In App Purchases -> 可以
pod 'IAPHelper'                 #In App Purchases
pod 'XYIAPKit'                  #In App Purchases
LQThirdParty                    #三方支付,登录,分享Demo -> https://github.com/LQi2009/LQThirdParty
pod 'FGIAPService'              #iap内购实现解决方案
pod 'DYFStoreKit'               #一个用于应用内购买的轻量级易用 iOS 库。(Objective-C)
TKPayKit                        #支付SDK,支持微信,支付宝 ->  https://github.com/RANSAA/TKPayKit
DemoStore                       # -> https://github.com/RANSAA/DemoStore
ApplePayDemo                    #ApplePay苹果支付demo -> https://github.com/YasinZhou/ApplePayDemo
DLInAppPurchase                 #苹果内购最详细教程      ->  https://github.com/Liqiankun/DLInAppPurchase
RevenueCat                      #RevenueCat 是一款功能强大、可靠且免费使用的应用内购买服务器,具有跨平台支持。我们的开源框架提供了 StoreKit 和 Google Play 结算的后端和包装器,使应用内购买和订阅的实施变得容易。
                                : https://github.com/RevenueCat/purchases-ios
PurchaseX                       #一个基于Swift的内购处理方案
17.地图,定位
pod 'BMKLocationKit'            #百度地图定位
pod 'AMapLocation'              #高德地图定位 
18.推送,通知与消息显示
pod 'JPush'                              #极光推送
pod 'GTSDK'                              #个推推送
pod 'JDStatusBarNotification'            #高度可定制且功能丰富的通知显示在状态栏下方。iOS 13+ (有点像灵动岛)
pod 'SimulatorRemoteNotifications'       #用于将模拟远程通知发送到 iOS 模拟器的库
pod 'SwiftMessages'                      #一个非常灵活的 UIKit 和 SwiftUI 消息栏
19.人脸识别
FaceDemo                        #人脸识别   ->  https://github.com/RANSAA/FaceDemo
KJVisionDemo                    #人脸检测、人脸识别、目标跟踪     ->   https://github.com/yangKJ/KJVisionDemo
FaceComparison                  #人脸识别、人脸比对、人脸对齐、人脸特征,iPad项目  -> https://github.com/haidong-li/FaceComparison
20.OCR、身份证、银行卡
BankCardID                      #依据银行卡号码,判断是什么银行的卡  ->   https://github.com/zhengwenming/BankCardID
JYBDAVCapture                   #OCR扫描身份证及银行卡   -> https://github.com/tiantianios/JYBDAVCapture
pod 'Tesseract-OCR-iOS'         #Tesseract OCR iOS
PaddleOCR                       #Python -> https://github.com/PaddlePaddle/PaddleOCR
tesseract                       #C++  ->  https://github.com/tesseract-ocr/tesseract
chineseocr                      #Python -> https://github.com/chineseocr/chineseocr
21.国际化
pod 'Localize'                  #本地化
22.Font、字体
pod 'FontBlaster'               #以编程方式将自定义字体加载到您的 iOS、macOS 和 tvOS 应用程序中。
23.定时器,任务,Task
pod 'Schedule'                   #使用 Fluent API 在 Swift 中调度计时任务。(计时器的友好替代品)
pod 'SwiftyTimer'               #SwiftyTimer 允许您使用方便的闭包语法即时安排延迟和重复计时器。是时候摆脱 Objective-C 的废话了。
24.AR
pod 'ARVideoKit'                #捕捉和录制 ARKit 视频、照片、实况照片和 GIF。
25.手势解锁,密码
pod 'TOPasscodeViewController'  #iOS 的模式密码输入和验证视图控制器
pod 'TQGestureLockView'         #手势密码,类似支付宝手势解锁
pod 'DYFAuthIDAndGestureLock'   #手势密码解锁和 TouchID (指纹) / FaceID(面容) 解锁,代码简洁高效。
pod 'JXPatternLock'             # 图形解锁/手势解锁 / 手势密码 / 图案密码 / 九宫格密码
YZAuthID                        #iOS TouchID / FaceID验证类库
26.选择器,城市选择器,日历选择器
pod 'JTCalendar'                #日历选择器
pod 'FSCalendar'                #日历选择器
pod 'HorizonCalendar'           #日历控件
pod 'STPickerView'              #一个多功能的选择器,有城市选择,日期选择和单数组源自定的功能(old)
pod 'EFColorPicker'             #颜色选择器
pod 'BRPickerView'              #iOS中常用的选择器组件,主要包括:日期选择器,城市,一,二,三级联动选择 
TKSimplePicker                  #日历,单列选择器 -> https://github.com/RANSAA/TKUIKit
27.二维码
pod 'EFQRCode'                              #二维码    Swift
pod 'LBXScan'                               #二维码、扫码、扫一扫、ZXing、ZBar、iOS系统AVFoundation扫码封装,扫码界面效果封装 -> 有多个版本
pod 'swiftScan'                             #二维码生成,识别   Swift
pod 'SGQRCode'                              #二维码生成,识别
pod 'QRCodeReader.swift',                   #Simple QRCode reader in Swift
pod 'QRCode'                                #A QRCode generator written in Swift.
pod 'QRCodeReaderViewController'            #二维码
pod 'QRCodeScan'                            #二维码/条形码扫描、生成/识别、仿微信、支付宝
pod 'MMScan'                                #轻量级的二维码以及条码扫描-> 使用iOS自带API开发
ZFScan                                      #二维码/条形码 扫描和生成  -> https://github.com/Zirkfied/ZFScan
28.设备信息相关
pod "Device"                                #轻量级工具,用于检测当前设备和用 swift 编写的屏幕尺寸
29.日期与时间,DATE
pod 'SwiftDate'                #Swift  ->  用于在 Swift 中解析、验证、操作、比较和显示日期、时间和时区的工具包。
30.扩展、效率,一致性,日志
pod 'SwifterSwift'                   #包含 500 多个原生 Swift 扩展的便捷集合,可提高您的工作效率。
pod "TypographyKit"                # iOS 上一致且易于访问的视觉样式,支持动态类型。
pod 'SwiftState'                     # Elegant state machine for Swift.
pod 'Swinject'                        #一个Swift 的轻量级依赖注入框架  ->   HOOK ?
pod 'CocoaLumberjack'           #一个快速简单,但功能强大且灵活的Mac和iOS日志记录框架
pod 'SwiftTrace'                     #跟踪 Swift 和 Objective-C 方法调用
pod 'SpeedySwift'                   #这是一个app开发的加速库
31.动画,转场
#动画与转场
pod 'NVActivityIndicatorView'       #一组很棒的加载动画  -> 很不错 -> Swift
pod 'lottie-ios'                    #优秀动画库 -> 直接加载动画设计资文件
pod 'Koloda'                        #卡片动画 -> 类似陌陌首页动画 -> 很不错 -> Swift
pod 'Spring'                        #一个在 Swift 中简化 iOS 动画的库。
pod 'Hero'                          #适用于 iOS 和 tvOS 的优雅过渡库
pod 'WXSTransition'                 #界面转场动画         ->  推荐
pod 'HHTransition'                  #主流转场动画,无侵入,API简单易用。        ->  推荐
pod 'RBBAnimation'                  #基于块的动画制作简单,带有简化功能和一个 CASpringAnimation 替换。
pod 'TABAnimated'                   #-> 很不错(TableView Cell加载动画)  -> 一个由iOS原生组件映射出骨架屏的框架,包含快速植入,低耦合,兼容复杂视图等特点,提供国内主流骨架屏动画的加载方案,同时支持上拉加载更多、自定制动画。
pod 'VCTransitionsLibrary'          #vc push, tabvc 转场动画
pod 'SXWaveAnimate'                 #水波纹,圆圈进度条与动画  ->  不错
pod 'PopupDialog'                   #弹窗动画                   
pod 'PopMenu'                       #PopMenu 是受新浪微博/网易应用启发的弹出动画菜单。
pod 'pop'                           #facebook开源的一套动画,有卡片动画效果,类似陌陌首页发现动画
pod 'IBAnimatable'                  #使用 IBAnimatable 在 Interface Builder 中为 App Store 就绪的应用程序设计和原型定制 UI、交互、导航、过渡和动画。
JXTransition                        #自定义转场动画  -> 可以  -> https://github.com/pujiaxin33/JXTransition
popping                             #动画集 ->https://github.com/schneiderandre/popping
ShareOfCoreAnimation                #一些基础动画 -> https://github.com/rjinxx/ShareOfCoreAnimation

LearniOSAnimations                  #系统学习iOS动画,有很多代码示例  -> 很不错      -> https://github.com/andyRon/LearniOSAnimations 
                                    #LearniOSAnimations动画详细讲解地址 ->  ttps://blog.devtang.com/2016/03/13/iOS-transition-guide/
IOSAnimationDemo                    #IOS动画总结     ->  https://github.com/yixiangboy/IOSAnimationDemo     

YSLDraggableCardContainer           #卡片动画,类似陌陌首页发现动画 -> https://github.com/ColinEberhardt/VCTransitionsLibrary
pod 'CYLTabBarController'           #一行代码实现 Lottie 动画TabBar,支持中间带+号的TabBar样式,自带红点角标,支持动态刷新。
RippleTableViewController           #为静止的图片添加水滴涟漪效果(水滴滴落荡开效果)   ->  https://github.com/appcornerit/RippleTableViewController
pod 'DBSphereTagCloud'              #3D效果,  自动旋转效果,  惯性滚动效果
pod 'DBSphereTagCloudSwift'         #3D效果,  自动旋转效果,  惯性滚动效果
pod 'BAFluidView'                   #UIView模拟运动中流体的2D动画 -> 水波纹效果
MLMProgressCollection               #进度,刻度,水波纹,统计   ->  https://github.com/MengLiMing/MLMProgressCollection
DaiNavigationTransition             #push转场动画 -> https://github.com/DaidoujiChen/DaiNavigationTransition
pod 'QWTabBar'                      #自定义TabBar动画
pod 'BATabBarController'            #一个带有独特动画的 TabBarController 供选择
pod 'FDFullscreenPopGesture'        #UINavigationController的类别,用于启用具有iOS7+系统样式的全屏弹出手势。 有点老可以用于参考。

TableViewAnimationKit               #TableView Cell展示动画  -> 可以参考    ->  https://github.com/alanwangmodify/TableViewAnimationKit
AWPolygonView                       #能力分布图是(如人物角色属相值图)  ->  https://github.com/alanwangmodify/AWPolygonView

DMHeartFlyAnimation                 #直播点赞动画 -> https://github.com/singer1026/DMHeartFlyAnimation
FloatingHearts                      #直播点赞动画(Swift原版) -> https://github.com/saidmarouf/FloatingHearts

AxcDrawPath_Tool                    #AxcAEKit系列拆分出来的一个贝塞尔曲线绘制工具,以科技风为主,动画为辅  ->  https://github.com/axclogo/AxcDrawPath_Tool

GKDYVideo                           #iOS仿抖音短视频,点赞,加载动画, ->  https://github.com/QuintGao/GKDYVideo

pod 'SkeletonView'                  #一个骨架显示动画View,用于在数据未加载时显示的结构动画   -> 推荐   ->   Swift

pod 'DynamicButton'               #Swift 中的另一个动画扁平按钮  -> 可以

pod 'Splitflap'                         #Splitflap 是一个简单易用的组件,用于呈现可变的字母数字文本,例如经常用作机场或火车站的公共交通时刻表或一些翻转时钟。

pod 'SwiftEntryKit'                   #SwiftEntryKit 是 iOS 的演示库。它可用于在您的 iOS 应用程序中轻松显示叠加层。 -> 一些显示动画

32.SwiftUI

About-SwiftUI                        #收集 Apple 和其他公司发布的有关新框架 SwiftUI 的所有信息。  -> https://github.com/Juanpe/About-SwiftUI

SwiftUIX                               #对标准 SwiftUI 库的扩展和添加。  -> https://github.com/SwiftUIX/SwiftUIX

33.编码工具与Debug

pod 'LifetimeTracker'                #LifetimeTracker 可以在您开发应用程序时发现保留周期/内存问题,并立即向您显示这些问题,因此您可以更轻松地找到它们。
pod 'CocoaDebug'                    #iOS 调试工具

Sourcery                                 #Swift 语言的代码生成器,允许您自动生成样板代码。
SwiftLint                                 #SwiftLint 是一个用于强制检查 Swift 代码风格和规定的一个工具
pod 'KMCGeigerCounter'           #一个帧率计,当您的动画丢帧时,它会像盖革计数器一样发出咔哒声
SwiftGen                                 #适用于assets, storyboards, Localizable.strings的Swift代码生成器  ->   支持多种方式  ->   https://github.com/SwiftGen/SwiftGen
swift-coreml-diffusers                #Swift 应用程序演示 Core ML 稳定扩散   ->  https://github.com/huggingface/swift-coreml-diffusers
ShapeScript                             #适用于 macOS 的 ShapeScript 3D 建模应用程序的源代码   ->  https://github.com/nicklockwood/ShapeScript

34.Core ML

Swift Core ML Diffusers         #演示 Core ML 稳定扩散的 Swift 应用程序  -> https://github.com/huggingface/swift-coreml-diffusers

35.精选iOS 生态系统列表。技术分享与Demo

QMKKXProduct                   #技术分享示例(很多)  -> 可以  -> 比如有一直需要的内购 -> https://github.com/HansenCCC/QMKKXProduct
Awesome                        #iOS精选列表,包括Objective-C和Swift项目。->  一个优秀的三方库精选列表  ->  https://github.com/chenxing640/Awesome
Some-Many-Books                #个人收藏书籍列表
AwemeDemoTransition            #iOS抖音个人作品页转场实现demo

awesome-ios                          #精选的 iOS 生态系统列表,包括 Objective-C 和 Swift 项目  ->  https://github.com/vsouza/awesome-ios
awesome-objective-c               #精选的 Objective-C 框架、库和软件列表。 ->  https://github.com/uhub/awesome-objective-c

awesome-ios-chart                  #精选的 iOS 图表库列表,包括 Objective-C 和 Swift  ->  https://github.com/ameizi/awesome-ios-chart

[iOS Examples](https://links.jianshu.com/go?to=https%3A%2F%2Fiosexample.com)和[Cocoa Controls](https://links.jianshu.com/go?to=https%3A%2F%2Fwww.cocoacontrols.com)这两个网站收集了大量的开源组件和库,并且进行了非常细致的分类,你能够非常方便地找到适合自己的“轮子”

[Open-Source iOS Apps](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fdkhamsing%2Fopen-source-ios-apps):作者在这个项目中收录了很多优秀的、完整的开源 iOS App,并做了详细分类,还专门标出了上架了 App Store 的开源 iOS App。

[NSHipster](https://links.jianshu.com/go?to=https%3A%2F%2Fnshipster.com)AFNetworking 和 Alamofire 的作者 Mattt维护的,主要关注的是一些不常用的 iOS 知识点。如果你想扩展自己的 iOS 知识面,这个网站里的内容会非常适合你。

[Awesome iOS](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fvsouza%2Fawesome-ios)也是一个值得推荐的网站,里面包含了 iOS 开发的方方面面,而且内容都是经过人工筛选、分类的。我觉得,**你遇到任何 iOS 的问题,都应该先到这个网站看看。**  
Awesome iOS 最大的特点就是大而全,囊括了从开发、调试到发布 App Store 的各种学习资料,也包含了博客、书籍、教程、邮件订阅、播客的推荐。同时,这个网站还包括了 iOS 开发更细分的 Awesome 推荐,比如关于 ARKit 的 [Awesome ARKit](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Folucurious%2FAwesome-ARKit),关于面试问题收集的 [Awesome iOS Interview](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fdashvlas%2Fawesome-ios-interview) question list 等等。

[iOS知名开发者](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fipader%2FSwiftGuide%2Fblob%2Fmaster%2F2019%2FSwiftDevelopers.md):你可以通过关注他们的博客、Twitter、GitHub ,来了解走在 iOS 领域前沿开发者的视野和 iOS 最新的动向。  
[开源项目团队](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fipader%2FSwiftGuide%2Fblob%2Fmaster%2F2019%2FSwiftDevelopmentTeam.md):如果你正在使用他们的开源项目,通过关注他们的动向,随时了解这些开源项目的最新进展

[行业优质博客汇总](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fawesome-tips%2Fblogs)

[Book-Recommend-Github](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FiOShuyang%2FBook-Recommend-Github):推荐生活当中积累的Objective-C以及Swift优秀三方库

36.Swift学习项目

[DanTang](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fhrscy%2FDanTang):单糖——良品生活指南:家居零食、礼物美妆、海淘购物  
[TodayNews](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fhrscy%2FTodayNews):Swift 4 - 模仿今日头条  
[XMLYFM](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FEastwu5788%2FXMLYFM):这个项目模仿喜马拉雅FM,目前仅实现了发现中的推荐页面、分类页面、广播页面、榜单页面、主播页面  
[XMLYFM](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fdaomoer%2FXMLYFM):swift项目仿写喜马拉雅App,采用MVC+MVVM设计模式,Moya+SwiftyJSON+HandyJSON网络框架和数据解析。数据来源抓包及部分本地json文件  
项目分析地址:[https://juejin.im/post/5b97743df265da0af21351aa](https://links.jianshu.com/go?to=https%3A%2F%2Fjuejin.im%2Fpost%2F5b97743df265da0af21351aa)  
[YYSwiftProject](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fdaomoer%2FYYSwiftProject):swift项目,仿写有妖气漫画,旨在提高swift代码能力,熟悉使用知名三方库并自定义封装一些控件  
[dctt](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FLight413%2Fdctt):xx头条【完整项目持续迭代中】(一款本地生活信息发布APP,分享新鲜事、找人找对象等生活服务)。已App Store上架,这里仅供学习交流  
[U17](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FspicyShrimp%2FU17):精仿有妖气漫画(Swift5)  
[LBXMLYFM-Swift](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Flb2281075105%2FLBXMLYFM-Swift):Swift5项目仿写喜马拉雅App,采用MVC+MVVM设计模式,Moya+SwiftyJSON+HandyJSON网络框架和数据解析。数据来源抓包及部分本地json文件
昨天以前掘金 iOS

货拉拉基于“声明式”的埋点方案实践

2025年3月11日 10:19

1. 背景

业务埋点的开发和维护一直以来都是研发环节中的一个痛点,对于产品来说埋点数据可以帮助判断需求上线后的运行情况,对于开发而言则需要在编写业务逻辑的同时,还需要增加一些数据采集上报埋点平台的任务,我们也一直在探索一些提效的解决方案,这里先简单概述下埋点开发中的几个痛点:

主要痛点概述:

  • 无法完全自动化: 埋点逻辑主要集中在业务数据采集,无法通过类似AOP等技术来解决全部的埋点需求
  • 埋点 的属性数据繁多: 主要属性都集中在业务逻辑数据中,和业务紧密相关
  • 埋点 逻辑掺杂 业务 逻辑: 业务逻辑中掺杂了很多埋点逻辑代码,不便于维护和排查问题
  • 新增 埋点 逻辑占用太多开发时间: 遇到埋点稍多的需求,会占用太多开发时间,需要一种高效的方式来让开发人员快速的增加埋点逻辑

上述的这些显而易见的问题,是我们一直以来不断去探索埋点方案的一个重要原因,在不断的需求开发节奏中,找出一种能快速实现埋点需求,又能和业务逻辑解耦合,同时还能提高我们的编写埋点代码的效率,这也是我们本文将要讨论的埋点方案和具体的实践过程。

2. 目前面临的问题

这部分主要从货拉拉APP开发中,针对埋点遇到的一些痛点,进行详细的描述,也让大家了解后续我们的方案主要解决的核心问题是什么,以及如何在现有的情况下,来通过我们这套方案去改善现状,优化我们的架构。

2.1 埋点现状架构图

在讲述痛点之前,我先用一张目前我们业务中埋点现状的架构图,从宏观的角度来看下当前的埋点逻辑情况。

从这幅架构简图中,我们可以看到项目在基于MVP架构模式开发下,对于埋点数据的采集、聚合、以及最后的上报,都没有一个合适的设计来支撑,也可能是项目在初期就对埋点这种逻辑不敏感,忽略了它在日后不断增量所带来的问题,因此造就了现在开发所面临的问题挑战。

2.2 痛点一:组件间数据传递繁琐

在组件化的架构模式下,大到模块之间的信息传递,小到组件内部的MVP(单元)之间的数据交互,组件之间的交互是非常频繁的,拿MVP模式的Protocol为例,通常使用协议来定义一个类的功能接口,每个类的Protocol 只开放和这个类功能息息相关的接口,符合“单一原则”和“开放封闭原则”。

那接下来我来看下业务中的埋点事件:

//埋点属性通常都是 key-value 的形式Map形式
事件名:test_eventName_key
属性值(map):
{
    key_a:value_a
    key_b:value_b    } 模块x(比如:车型、地址...)
    key_c:value_c
        ...
    key_d:value_d
    key_e:value_e    } 模块x(比如:支付方式、发票...)
    key_f:value_f
        ...
    key_z:value_z    } 模块x(比如:额外服务、订单备注、货物信息...)
}

埋点数据通常情况下覆盖了当前业务Page 中的大部分的模块,这在货拉拉APP中十分场景,比如:首页、确认下单页、订单详情页、等待应答页等页面,每个页面包含的组件少则5-6,多则10+种小模块,如果埋点要求上报这些模块内的数据,那就需要将这些数据先采集组合成map,然后上报给埋点平台,那么采集过程中,大部分的逻辑都落在了各个类的 Protocol 中,因为模块之间的交互都是通过接口来传递的,所以Protocol中就会出现一些埋点相关的接口。

@protocol xxx {
     func trace(x,y) 
          ...
}

因为埋点的触发逻辑可能在任意的类中任意地方出现,因此每个类都要做好获取上述所有埋点数据的能力准备,这就造成了我们业务代码中,本来就比较复杂的业务逻辑,再混入一些埋点相关的逻辑,就会变得更加的难以维护。

2.3 痛点二:埋点属性采集方式不统一

埋点事件中具体属性值的采集,严格来说应该是声明和修改的地方保持单一,通常由所属的模块负责,比如车型的名称和国标ID,都是由车型模块进行维护,其他模块使用的时候通过统一的方式读取即可,这样如果变量值有改动,则各个引用的地方会同步修改,而现实中这些值在上述讲到的模块传递问题中,会出现不同模块,传递的形式也不同,有的可能是作为方法的参数获取的,有的则是通过其他对象持有获取的,维护起来会特别麻烦。

2.4 小结

开发中上述痛点,是主要的影响点,数据的采集和组装占据了大部分的埋点开发工作量,同时这些痛点也会对架构有所腐蚀,因为需求的迭代和周期,有时候并不能同步到埋点中,这就导致维护滞后等副作用。

3. 埋点方案设计

在正式介绍方案之前呢,我们先看了解下目前业内APP埋点的一些主流方案:

代码埋点 可视化埋点 自动埋点
优势 灵活性强,精准控制,轻量 无需开发,实时生效,不依赖版本发布 实现简单,自动化,实时数据分析
劣势 工作量大、易出错,调试困难,无法动态下发 埋点数据存在局限性 灵活性差,数据冗余,隐私问题
适用场景 需要精细控制收集数据和埋点数据结构的情况。 比较规范和简单的页面,主要分析一些点击事件场景 数据收集需求较为标准的场景,如应用的日常运营、用户行为分析等

上述三类方案,基本涵盖了业内主流的埋点方案,而还有一种是采用混合(代码埋点 + 自动埋点)埋点,这也是一种主流的方案之一,货拉拉用户端也是采用了这种模式,通用的页面浏览和控件点击都通过自动埋点实现,而其余的则是通过代码埋点实现。

所以本次方案优化改造的核心是围绕代码埋点这部分进行的,宗旨是如何高效且尽量无侵入的方式,对代码埋点进行改造和优化。

3.1 方案整体概述

方案的大致核心功能点如下:

  • 独立出埋点管理模块,各组件间直接通过埋点管理模块读取和注册数据
  • 移除各个组件的Protocol文件中显式的埋点相关逻辑
  • 各组件间不再直接和埋点数据进行通讯,组件间只需要通过特定方式暴露自己的埋点所需数据即可
  • 埋点统一管理,引入Scope 概念

改造后的埋点架构简图:

上述架构图中可以看出,我们对埋点的采集和上报做了比较大的改动,去除了各个组件内关于埋点数据需要暴露出来的接口API,另外还引入了“公共数据”以及“Scope”概念,通过各个模块独立的埋点数据“声明”,来将该模块的数据提前暴露出来,注意这个时候数据只是被“声明”出来,并不是提前保存,只有埋点用到时,采取实时获取确定的值。

下面就对这些概念进行详细说明。

3.2 独立出埋点管理模块(TrackifyKit)

如需从业务中剥离解耦埋点这部分逻辑,那么就需要独立出一个单独的模块来承接。模块作为二方库的形式引入进来,并且由一个TraceManager单例类来和业务进行交互,TraceManager会对外暴露出三种类型的API,来供业务模块去调用,这三种分别是:

    func traceUpdateCommonData(with dataDic:Dictionary<String, Any>)
    
    func traceRegisterCatcher(scope:TraceEventScope, 
                              host:AnyObject,
                              pageVC:UIViewController?,
                              catcher:@escaping Catcher)
    
    func traceTrigger(eventName:EventName,
                      withProperties:[PropertiesName:Any]?,
                      pageVC:UIViewController?)
    
    func traceTrigger(eventName:EventName,
                      withProperties:[PropertiesName:Any]?,
                      scopeKeys:[TraceEventScope:[PropertiesName]]?,
                      alias:[String:String]?,
                      pageVC:UIViewController?)

下面对这三类API做更详细的解释:

3.3 更新公共属性

考虑到项目中会有一些公共属性需要统一上报,比如:城市定位、个人信息等相关信息,这里为了方便业务使用,可以允许业务更新一些全局的数据,你可以把他理解为一个全局的map,方便后续埋点上报组合数据的的时候使用

/// 埋全局通用数据
/// - Parameter dataDic: [k,v]]
public func traceUpdateCommonData(with dataDic:Dictionary<String, Any>) {}

3.4 注册Catcher捕获器提前声明埋点字段

Catcher 概念是我们这次方案的核心,它的原理是通过业务组件,主动向 Eventbus 中注册一个 Catcher 对象,它并不会强引用业务组件类,而这个 Catcher 会在埋点触发的时候,自动去动态的获取注册类中的数据,这部分数据就会通过Catcher 对象,进入EventBus,从而和其他埋点数据进行组合、过滤、处理等操作,最终完成数据上报。

接下来看下如何在业务中注册 Catcher 捕获器:

/// 注册一个埋点Catcher ⚠️注意 weak self
    /// - Parameters:
    ///   - scope: Catcher 所属的 Scope
    ///   - host: Catcher 所在的生命周期对象
    ///   - catcher: Catcher block
    ///   - pageVC: 关联的页面
    @objc public func traceRegisterCatcher(scope:TraceEventScope,
                                           host:AnyObject,
                                           pageVC:UIViewController?,
                                           catcher: @escaping Catcher) {
    }

业务中所有的类,只要是需要提供一些埋点属性,都可以在类的初始化时机,调用上面API,来进行埋点属性注册,通过这个注册,将内部需要的属性值提前准备好,等待埋点管理Eventbus内部使用。

    func registerTraceCatcher() {
        TraceManager.shared.traceRegisterCatcher(scope: "vehicleScope",
                                                    host: self,
                                                    pageVC: nil) { [weak self] in
            guard let self = self else { return nil }
            return [
                "key":"vaule",
                ...
                ...
            ]
        }
    }

这个方法首先需要入参一个 scope 参数,这个可以根据业务模块进行不同粒度的划分,host则是当前类对象,这个参数主要是在埋点管理器内部通过 host 对已经释放的业务类进行 catcher 清理使用,pageVC 参数是应对导航栈中可重复push多个同一类,则不同示例的页面,为了做socpe 区分,相同对象的内部数据必须进行区分获取。

3.5 埋点上报

说完了catcher 其实埋点的数据准备工作已经讲完了,各个模块类的内部已经通过 traceRegisterCatcher方式将各个埋点事件所需要的数据提前准备好了,接下来就看下埋点触发的逻辑。

下面是埋点触发所需API:

    /// 埋点上报
    /// - Parameters:
    ///   - eventName: 埋点事件名
    ///   - withProperties: 埋点属性[k,v]
    ///   - scopeKeys: scope-keys 对
    ///   - alias:字段别名
    ///   - pageVC: 关联的页面
    @objc public func traceTrigger(eventName:EventName,
                                   withProperties:[PropertiesName:Any]?,
                                   scopeKeys:[TraceEventScope:[PropertiesName]]?,
                                   alias:[String:String]?,
                                   pageVC:UIViewController?) {

    }

入参分析:

eventName: 埋点的事件名,也是埋点信息中必不可少。

withProperties:埋点的属性值,这里的属性值不要求必须传入埋点所有的必要字段,只需要传入当前类Context 中能够直接获取到的字段,那其余的字段怎么获取呢?这就用到我们前面 3.4 讲的Catcher 捕获器了,通过它去其他模块试试获取数据。

scopeKeys: 这个参数需要传入的类型是 Map<scope,[PropertiesName]>,key 代表 scope 也就是每个类在调用 traceRegisterCatcher方法注册 catcher 的时候声明的 scope 参数,有了这个scope ,埋点管理器才知道需要去哪个类中获取埋点字段,因此前提是你要提前在各个类中调用 traceRegisterCatcher 方法。[PropertiesName]这个数组就是需要的字段名称 key,根据埋点文档传入即可。

alias: 这个入参则是一个修改字段名称的别名方式,可能各个埋点会有相同数据,但是字段上报的key会不相同,这里可以做下别名配置。

pageVC: 这个入也是和 traceRegisterCatcher方法的入参中 pageVC 一一对应,用来区分导航栈中相同类不同实例的页面。

接下来再看下埋点上报的一个demo实例:

        let scopeKeys = [            module_a_scope:[                "properties_name_x",                ...            ],
            module_b_scope:[                "properties_name_x",                ...            ],
            module_c_scope:[                "properties_name_x",                ...            ]
        ]
        TraceManager.shared.traceTrigger(eventName: "confirmorder_click",
                                            withProperties: ["module_name":moduleName],
                                            scopeKeys: scopeKeys)

这样上报埋点,字段的获取全部通过埋点管理器的 scope 去各个类中注册的 catcher 里面实时获取,然后在管理器内部进行过滤、组合、更名等操作后,再交给埋点平台上报,整个流程不需要再各个类的业务Interface 类中声明埋点相关的逻辑,这部分逻辑统一由管理器去管理,实现了和业务逻辑解耦的效果。

3.6 渐进式改造

新的方案并不需要项目中所有的埋点都进行同时改造,而是可以和之前的代码埋点逻辑并存,开发人员可以根据节奏逐渐替换,货拉拉用户端在接入该方案后,埋点代码逻辑更加的清晰,且和业务解耦,业务层只负责提供属于自己的那部分数据等待获取,而埋点触发的地方也只需要根据不同的key去取相应的数据即可。

某业务模块:

// MARK: -- Module_A_Presenter 模块
@class Module_A_Presenter {
    init() {
        registerTraceCatcher()
    }   
}

// MARK: -- 埋点注册
extension Module_A_Presenter {
    func registerTraceCatcher() {
        TraceManager.shared.traceRegisterCatcher(scope: "Module_A_Presenter_Scop",
                                                    host: self,
                                                    pageVC: nil) { [weak self] in
            guard let self = self else { return nil }
            return [
                "key_1":self.key_1,
                ...
            ]
        }
    }
}

这样就完成了该模块的改造,而无需像之前那种方式,把这些数据通过 Interface 暴露出去,造成埋点逻辑和业务逻辑混杂在一起。另外声明式的这种埋点方式,也大大提高了埋点逻辑的可维护性,各自模块各自维护,数据收集再分别去各个模块获取,减少了模块之间的交互频率。

4. 总结

以上内容就是本期给大家分享的一个基于“声明式”的管理复杂业务埋点的方案,该方案实现起来并不复杂,主要是改变出一种新的埋点思路,来去解决现有代码埋点的问题,俗话说“没有最好的架构,只有最适合的架构”,适合的架构都是一点点优化重构堆砌出来的,就像本文所分享的方案,优化埋点的上报形式也是对项目架构的一种优化,让我们的产品迭代和维护都更加的高效。

期待与失望的循环:苹果的 AI 困境与韧性 | 肘子的 Swift 周报 #074

作者 东坡肘子
2025年3月11日 08:02

issue74.webp

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

期待与失望的循环:苹果的 AI 困境与韧性

几天前苹果宣布将 “More Personalized Siri” 功能推迟到明年,再结合始终不见踪影的 Swift Assist,很显然,苹果没有达成其在 WWDC 2024 上构建的 AI 愿景。至少在大模型的应用场景上,苹果没有展现出一贯的快速跟随能力。

但就此断定苹果在本次 AI 浪潮中失去了反击的能力还为时尚早。无论是有意还是无意,苹果在转向 M 系列芯片时采用的统一内存机制让其 Mac 系列设备具备了相当不错的本地模型推理能力。而且,随着最新支持 512GB 内存的 Mac Studio 推出,苹果又以另外的身份在本次 AI 变革中取得了一席之地。

虽然英伟达仍是模型训练和推理的首选,但 Mac 设备凭借高集成度与高能效比,已吸引不少用户。随着更多模型优化适配 MLX,其在大模型推理中的表现有望进一步提升。

当然,Mac 设备取得的成绩仍无法掩盖苹果在大模型方向上反应不及时的窘境,但至少表明了一些多管齐下的大企业所具备的天然优势:东边不亮西方亮。这种策略多元性恰恰是科技巨头在面对快速变化的技术浪潮时的一张王牌。

大模型真的代表未来?没能紧跟这股浪潮,企业就一定会被淘汰吗?尽管我并不这样认为,但显然在当下的舆论环境中,消费者、投资者需要企业在大模型开发、应用等领域中交出一份有感的答卷。也就是即便企业有既定的 AI 发展路线,也可能因市场和舆论环境的变化而被迫调整。

于是,我们就在最近的一年以及未来的几年中会看到越来越多的 PPT 式的愿景发布会。不给出期待大家会失望,给出了期待但没有达成大家会继续失望。这种"期待-失望"的循环似乎已成为科技行业的新常态,特别是在 AI 这样充满不确定性的领域。

在 AI 领域,企业既要保持定力,又要让市场满意,这种平衡正变得越来越难,“期待-失望”的循环也许已成为行业的常态。对于开发者和企业来说,或许真正的智慧在于既能拥抱创新浪潮,又不盲目追随市场喧嚣,在踏实做事与满足期待之间找到平衡点。毕竟,技术的真正价值不在于其炫目的承诺,而在于它能为用户带来的实际改变。

前一期内容全部周报列表

原创

让 @State 实现懒加载

Observation 框架为 Swift 带来了原生的属性级观察能力,有效避免了 SwiftUI 中因无关属性变化而引发的多余视图更新,从而提升了应用性能。但由于 @State 并未提供类似 @StateObject 的懒加载构造方式,在某些场景下会因实例过早构建而引起性能损失甚至逻辑问题。本文将探讨如何为 Observable 实例定制一个支持懒加载的 @State 解决方案。

近期推荐

在 Swift 中动态构造泛型类型的方法探索 (Dynamically Constructing Generic Types in Swift)

泛型类型通常需要在编译期确定具体类型,但在部分特殊场景下,编译期无法提供足够的类型信息。Kyle Ye 在开发 OpenSwiftUI 时,发现 SwiftUI 中的 _ConditionalContent<TrueContent, FalseContent> 就属于这种情况。本文介绍了一种通过定义自定义的 MetadataProtocolDescriptor 类型来访问和解析运行时类型信息,以动态构建泛型类型的实现方案。

巧用视图复用,提升 SwiftUI 滚动列表性能 (Designing a Custom Lazy List in SwiftUI with Better Performance)

SwiftUI 提供了 ListLazyVStack 等惰性容器,但在处理大规模数据时,这些组件的性能仍然存在局限性。此外,SwiftUI 目前的 Layout 协议尚未支持惰性加载,使得开发者难以通过原生方式构建更高效的自定义滚动容器。在本文中,Matthaus Woolard 采用行复用机制,通过动态调整视图位置而非创建新视图,实现了一种适用于固定高度、重复子视图的高性能滚动容器。这种方法虽对适用场景有所限制,但其核心思路对于优化 SwiftUI 视图性能具有重要的借鉴价值。

原文中的代码较为分散,我已将其整理并汇总至 Gist,方便阅读和参考。

XCode 虚拟目录万年问题探究与我的开源工具解决方案

长期以来,Xcode 采用虚拟目录管理项目,不仅导致多人协作时合并冲突频繁,也对 CI/CD 流程和现代工具的兼容性不友好。随着 Apple 逐步优化 Xcode 目录管理,现代 Xcode 项目已更多依赖实际文件结构,但对于历史遗留项目,虚拟目录仍然是一个棘手的问题。在本文中,ZhgChgLi 详细分析了虚拟目录的影响,并介绍了他的开源工具 XCFolder。该工具可自动将 Xcode 项目中的虚拟目录转换为实际文件目录,使项目结构更加清晰,同时兼容 XcodeGenTuist,为 Xcode 项目的现代化管理提供了一种高效的解决方案。

SwiftUI 性能优化:如何有效结合 UIKit (SwiftUI Performance - How to use UIKit)

随着 SwiftUI 功能的不断丰富,越来越多的开发者选择以 SwiftUI 为核心构建应用,同时在性能关键的场景中引入 UIKit 以优化体验。在本文中,Majid Jabrayilov 探讨了如何在 SwiftUI 架构下合理结合 UIKit,以兼顾开发便捷性和运行效率。文章重点介绍了 UIHostingConfiguration——一个能够在 UITableViewCellUICollectionViewCell 内高效集成 SwiftUI 视图的机制。通过这种方式,开发者可以充分利用 UIKit 的高效视图复用,同时保持 SwiftUI 组件的灵活性,从而提升应用在处理大规模数据时的流畅度。

清理 Xcode 垃圾文件,释放 Mac 磁盘空间 (How to Clean Xcode Junk and Reclaim Valuable Disk Space on Your Mac)

对于苹果开发者来说,Xcode 占用大量磁盘空间是一个常见问题,影响系统性能和开发效率。Karin Prater 在本文中详细解析了 Xcode 生成的各种文件类型,并提供了一系列安全、高效的清理方法,帮助开发者释放磁盘空间,优化开发环境。

从零到一:构建复杂系统的探索之路 (Building Complex Things)

传统的 macOS CI 运行环境(基于 Mac 物理机)易受到外部干扰,导致环境漂移和构建不稳定。Paul Samuels 旨在通过引入虚拟化技术,使 CI 运行环境更加可控和可复用。在这篇文章中,Paul 分享了他的构建过程,从最初的问题定义、工具调研,到原型验证,最终实现了一个稳定且可重复使用的解决方案。这不仅是一次技术探索,更展示了复杂系统构建的全过程。

往期内容

THANK YOU

如果你觉得这份周报或者我的文章对你有所帮助,欢迎 点赞 并将其 转发 给更多的朋友。

weekly.fatbobman.com 订阅本周报的电子邮件版本。访问我的博客 肘子的 Swift 记事本 查看更多的文章。加入 Discord 社区,与 2000+ 中文开发者深入交流 Swift、SwiftUI 开发体验。

转眼间,已是十几年前的游戏代码了

作者 jason_yang
2025年3月10日 15:51

起因

最近在做safari的一些兼容问题,不得不安装虚拟机,来调试Safari-16版本以下的兼容问题。(关于搭建环境:具体参考juejin.cn/post/747968…

想起很久以前已经跑不起的游戏项目,想想顺便试试。(由于蛋疼的mac系统和xcode完全不向下兼容老系统)。

使用vm + mac os 10.8 + xcode 5 让它复活了。

image.png

image.png

image.png

一看创建时间已经是2013年的项目了。怀念当年使用ipad touch + mac air 调试游戏的日子,纯粹的独立游戏开发者 image.png

第一款runtwo

早期没有美工,自己硬者头皮用firework上,结果很纯色。(难怪说游戏是第九艺术,你即是导演也是演员)

代码结构

image.png

技术总结

十几年过去了,让我回顾一下当年使用的技术栈和工具

  • cocos2d-iphone 2.1
  • xcode 5.0
  • ios sdk 7
  • admob 植入广告(独立游戏开发者必备)
  • Tiled图库编辑器
  • firework(三剑客之一)

image.png

Tiled图库编辑器 image.png

技术优化:

  • 通过Tiled生成地图块,加速数据加载(类似前端雪碧图)
  • 通过单例模式,创建重复对象公用的数据对象
  • 通过预渲染所有关卡,随机选关,实现无线滚动关卡

游戏效果

Mar-10-2025 15-36-17.gif

Mar-10-2025 15-37-23.gif

项目地址

github.com/mjsong07/Ru…

第二款NutcrakerShake

第二款是2014作品,找了熟人做美工,画面没那么辣眼了。

image.png

image.png

项目结构

image.png

技术点总结:

  • 新增中美日-三种语言支持(通过ios的localizble.strings只是配置多种语言特性实现)
  • 新增sqlite3 实现本地化存储
  • 新增粒子系统 practicleTeture
  • 新增SeparatingAxisTheorem库实现精准判断不规则物品的碰撞逻辑

新增国际化配置 中美日 image.png

image.pngimage.png

本地持久化 image.png

粒子系统 practicleTeture image.png

游戏效果

Mar-10-2025 15-28-34.gif

项目地址

github.com/mjsong07/Nu…

两个项目地址已附上,喜欢折腾朋友跑vm试试,哈

结语

cocos2d后续也发生了很大的变化,不仅仅支持ios,推出跨平台的cocos2dx,再后来还出了的ide,js版本,有兴趣的可以继续研究。

www.cocos.com/

在经历了 java -> ios -> 前端 不同领域的开发,仍然觉得游戏开发具有更高的挑战和灵活性。游戏在ai的加持下变得越来越可独立开发,所有需要美工设计动效都可以交付给ai,单纯实现自己是想法。 等技术成熟后,也把这老掉牙的游戏升级个unity 3d版本 哈~

iOS开发之网络代码进化史

作者 YungFan
2025年3月10日 15:05

介绍

目前,iOS 原生网络请求数据共有 4 种方式,分别是传统 Completion 回调Completion 回调 + ResultCombine 框架Swift Concurrency (async/await)。本文以下载图片为例,详细讲解 4 种网络请求的差异。

传统Completion回调

一种最基础的异步处理方式,使用闭包作为回调函数。

代码

/// 错误枚举
enum FetchError: Error {
    case badURL
    case badRequest
    case badImage
}

/// 1. 传统completion回调
func fetchImage(from url: String, completion: @escaping (UIImage?, Error?) -> Void) {
    guard let url = URL(string: url) else {
        completion(nil, FetchError.badURL)
        return
    }
    let request = URLRequest(url: url)
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            completion(nil, error)
        } else if (response as? HTTPURLResponse)?.statusCode != 200 {
            completion(nil, FetchError.badRequest)
        } else {
            guard let data = data,
                  let image = UIImage(data: data) else {
                completion(nil, FetchError.badImage)
                return
            }
            completion(image, nil)
        }
    }
    task.resume()
}

fetchImage(from: "https://search-operate.cdn.bcebos.com/7e85570b817e17e8f3ae93134cc78451.gif") { image, error in
    if let image {
        print(image)
    } else if let error {
        print(error)
    }
}

优点

  • 简单易懂,学习曲线低。
  • 兼容性好,适用于所有 iOS 版本。

缺点

  • 可能导致"回调地狱"。
  • 错误处理比较分散,每个回调都需要单独处理错误。
  • 取消操作比较麻烦。

Completion回调+Result

对传统 Completion 回调的改进,使用 Swift 的 Result 类型统一处理成功和失败情况。

代码

enum FetchError: Error {
    case badURL
    case badRequest
    case badImage
}

/// 2. completion回调+Result
func fetchImage(from url: String, completion: @escaping (Result<UIImage, Error>) -> Void) {
    guard let url = URL(string: url) else {
        completion(.failure(FetchError.badURL))
        return
    }
    let request = URLRequest(url: url)
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            completion(.failure(error))
        } else if (response as? HTTPURLResponse)?.statusCode != 200 {
            completion(.failure(FetchError.badRequest))
        } else {
            guard let data = data,
                  let image = UIImage(data: data) else {
                completion(.failure(FetchError.badImage))
                return
            }
            completion(.success(image))
        }
    }
    task.resume()
}

fetchImage(from: "https://search-operate.cdn.bcebos.com/7e85570b817e17e8f3ae93134cc78451.gif") { result in
    switch result {
    case let .success(image):
        print(image)
    case let .failure(error):
        print(error)
    }
}

优点

  • 相比传统回调,代码更加清晰和统一。
  • 能够明确区分成功和失败状态。
  • 兼容所有 iOS 版本。

缺点

  • 仍然会导致回调嵌套问题。
  • 取消机制同样需要额外处理。

Combine框架

Combine 是 Apple 在 iOS 13 引入的响应式编程框架,专为处理异步事件流设计。

代码

enum FetchError: Error {
    case badURL
    case badRequest
    case badImage
}

/// 3. Combine
func fetchImage(from url: String) -> AnyPublisher<UIImage, Error> {
    guard let url = URL(string: url) else { return Fail<UIImage, Error>(error: FetchError.badURL).eraseToAnyPublisher() }
    let request = URLRequest(url: url)
    let session = URLSession.shared
    let dataPublisher = session.dataTaskPublisher(for: request)
        .retry(3)
        .timeout(5, scheduler: DispatchQueue.global())
        .tryMap { data, response -> Data in
            guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badRequest }
            return data
        }
        .tryMap { data -> UIImage in
            guard let image = UIImage(data: data) else {throw FetchError.badImage}
            return image
        }
        .receive(on: DispatchQueue.main)
        .subscribe(on: DispatchQueue.global())
        .eraseToAnyPublisher()
    return dataPublisher
}

var cancellables = Set<AnyCancellable>()
fetchImage(from: "https://search-operate.cdn.bcebos.com/7e85570b817e17e8f3ae93134cc78451.gif")
    .sink(
        receiveCompletion: { completion in
            switch completion {
            case .finished:
                print("Image download completed successfully")
            case let .failure(error):
                print(error)
            }
        }, receiveValue: { image in
            print(image)
        }
    )
    .store(in: &cancellables)

优点

  • 声明式编程风格,代码更简洁。
  • 强大的操作符支持。
  • 优雅的错误处理和转换机制。
  • 提供标准的取消机制。

缺点

  • 学习曲线较陡峭。
  • 仅支持 iOS 13 及以上版本。
  • 长链式调用可能影响代码可读性。

4. Swift Concurrency (async/await)

Swift 5.5 引入的现代异步编程模型,使用结构化并发的概念。

代码

enum FetchError: Error {
    case badURL
    case badRequest
    case badImage
}

/// 4. Concurrency
func fetchImage(from url: String) async throws -> UIImage {
    guard let url = URL(string: url) else { throw FetchError.badURL }
    let request = URLRequest(url: url)
    let (data, response) = try await URLSession.shared.data(for: request)
    guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badRequest }
    let image = UIImage(data: data)
    guard let image = image else { throw FetchError.badImage }
    return image
}

Task {
    do {
        let image = try await fetchImage(from: "https://search-operate.cdn.bcebos.com/7e85570b817e17e8f3ae93134cc78451.gif")
        print(image)
    } catch {
        print(error)
    }
}

优点

  • 代码简洁直观,接近同步代码的阅读体验。
  • 易于理解的错误处理。
  • 内置取消支持和结构化并发。
  • 易于调试和维护。

缺点

  • 一定的学习曲线。
  • 需要 iOS 15 及以上版本。

shell脚本,怎么查找项目中的重复图片

2025年3月10日 10:22

要在 iOS 项目中使用 Shell 脚本查找重复图片,可借助计算图片文件哈希值的方法,将哈希值相同的图片判定为重复图片

#!/bin/bash

# 定义iOS项目的根目录,需根据实际情况修改
PROJECT_DIR="/path/to/your/ios/project"

# 定义图片文件的扩展名,可按需添加或修改
IMAGE_EXTENSIONS=("jpg" "jpeg" "png" "gif")

# 创建临时文件用于存储图片的哈希值和文件名
TEMP_FILE="/tmp/image_hashes.txt"

# 遍历指定的图片扩展名
for ext in "${IMAGE_EXTENSIONS[@]}"; do
    # 查找项目目录下指定扩展名的图片文件,并计算其MD5哈希值
    find "$PROJECT_DIR" -type f -iname "*.$ext" -print0 | while IFS= read -r -d '' file; do
        md5_hash=$(md5 -q "$file")
        echo "$md5_hash $file" >> "$TEMP_FILE"
    done
done

# 对临时文件按哈希值进行排序
sort "$TEMP_FILE" -o "$TEMP_FILE"

# 用于记录上一个图片的哈希值和文件名
prev_hash=""
prev_file=""

# 遍历排序后的临时文件,查找重复图片
while read -r line; do
    current_hash=$(echo "$line" | cut -d' ' -f1)
    current_file=$(echo "$line" | cut -d' ' -f2-)
    if [ "$current_hash" = "$prev_hash" ]; then
        echo "发现重复图片: $prev_file$current_file"
    fi
    prev_hash="$current_hash"
    prev_file="$current_file"
done < "$TEMP_FILE"

# 删除临时文件
# rm "$TEMP_FILE"

Flutter - iOS编译加速

作者 LinXunFeng
2025年3月9日 20:37

欢迎关注微信公众号:FSA全栈行动 👋

一、前言

在项目完全重构成纯 Flutter 之后 ,iOS 端在 i7 Mac Mini 构建机上的打包时间差不多在 12分钟 左右,而在升级了 Xcode 16 之后,构建机的打包时间有了质的 “提升”,来到了 25分钟,换成 M1 来了也压不住,甚至更久~

这种情况在退回 Xcode 15 是可以解决的,但是这并不是长久之计,因为苹果早晚会强制要求升级的,好在申请了台 M4 Mac Mini 来打包,时间来到了 15 分钟,不过随着业务功能不断迭代,构建时间也慢慢增加,目前来到了 17、18分钟,但一旦哪天对 M4 构建机进行维护,让 i7M1 顶上时,再加上多个打包任务并行,完成打包的时间动不动就得 半小时 起步,真的很令人绝望~

这里先给出优化后的打包时间

构建机 优化前(min) 优化后(min)
i7 25+ 14+
M4 16+ 8+
  • 优化前 : Profile + 源码依赖 原生插件
  • 优化后 : Release + 二进制依赖 原生插件
  • 环境 : Xcode 16

二、编译模式对比

这里我拿了一个业务组件来做测试,分别使用 Xcode 15Xcode 16ProfileRelease 两种模式来观察编译用时

版本 Profile (s) Release (s)
Xcode 15 389 384.6
Xcode 16 952.3 477.4

可以看到升级到 Xcode 16 后,两种模式的编译时间都比使用 Xcode 15 的要久,特别是 Profile 模式下的编译时间更离谱,是 Release2倍 多~

而我们的项目为了方便,是以编译模式进行环境区分的。

  • Profile : 测试包使用,对应 kProfileMode
  • Release : 上架包使用,对应 kReleaseMode

三、调整环境判断

基于现状,只能调整项目中对环境的区分逻辑,改用 Dart Define 将环境参数传入。

这里使用 --dart-define-from-file 传递文件的方式

fvm spawn 3.24.5 build ipa --release --export-options-plist=path/to/ad_hoc.plist --dart-define-from-file=path/to/test.env

test.env 文件以键值对的方式设置环境变量

APP_ENV=test

取值方式如下,注意,一定要加上 const

/// dart define 环境变量
String get appEnv => const String.fromEnvironment('APP_ENV');

判断是否为 release

enum AppBuildMode {
  release,
  debug,
  test,
}

AppBuildMode? fetchAppEnvType() {
  switch (appEnv.toLowerCase()) {
    case "debug":
      return AppBuildMode.debug;
    case "test":
      return AppBuildMode.test;
    case "release":
      return AppBuildMode.release;
    default:
      return null;
  }
}

bool isRelease() {
  final envType = fetchAppEnvType();
  if (envType == null) {
    // 没有使用 dart define 设置环境变量
    return kReleaseMode;
  } else {
    return AppBuildMode.release == envType;
  }
}

四、浅探索耗时

当然,我们也可以尝试去探索一下,到底是哪里耗时这么久。

通过 Xcode 自身去查看编译耗时会发现最长的是 Run Script,其主要负责编译 Flutter 侧的代码。

注:这里的时间是 Xcode 16 + Release 下的

但是展开详细内容会发现一点有用的信息都没有,无法定位到具体问题。

经过对 flutter_tools 的代码进行阅读后发现,可以通过设置环境变量 VERBOSE_SCRIPT_LOGGING 来使其加上 --verbose 参数,进而将打包过程中的一些信息打印出来。

具体操作: Runner -> Build Phases -> Run Script 中补充一句 export VERBOSE_SCRIPT_LOGGING=1

# 补充这一句
export VERBOSE_SCRIPT_LOGGING=1

/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build

再次编译就可以看到详细的 flutter 命令打包信息,可以将其导出后慢慢查看。

下面是摘出的主要耗时记录和文件大小

Profile

# Xcode 15
[   +2 ms] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -c /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.o
[+165207 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.o
[ +289 ms] ...
[   +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-profile/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/app.dill
[+96580 ms] ...


# Xcode 16
[        ] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -c /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.o
[+596589 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.o
[ +290 ms] ...
[   +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-profile/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/app.dill
[+92259 ms] ...
# Xcode 15、Xcode 16 一样

ls -lh
total 853368
drwxr-xr-x@ 3 lxf  staff    96B  3  7 15:48 App.framework
drwxr-xr-x@ 3 lxf  staff    96B  3  7 15:48 App.framework.dSYM
-rw-r--r--@ 1 lxf  staff   323M  3  7 15:38 snapshot_assembly.S
-rw-r--r--@ 1 lxf  staff    93M  3  7 15:48 snapshot_assembly.o

Release

# Xcode 15
[        ] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -c /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.o
[+92077 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.o
[ +245 ms] ...
[   +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/app.dill
[+88256 ms] ...

# ========== 华丽的分割线 ========== #

# Xcode 16
[        ] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -c /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.o
[+246277 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.o
[ +237 ms] ...
[   +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/app.dill
[+88139 ms] ...
# Xcode 15、Xcode 16 一样

ls -lh
total 572808
drwxr-xr-x@ 3 lxf  staff    96B  3  7 17:08 App.framework
drwxr-xr-x@ 3 lxf  staff    96B  3  7 16:02 App.framework.dSYM
-rw-r--r--@ 1 lxf  staff   213M  3  7 17:04 snapshot_assembly.S
-rw-r--r--@ 1 lxf  staff    67M  3  7 17:08 snapshot_assembly.o

你可能会觉得最耗时的是 xcrun clang,但其实每一行前面的中括号内的时间,是上一行的命令的耗时,即 xcrun cc 最耗时,而其它命令的执行时间是差不多的。

xcrun cc 命令是用于将 Flutter 生成的汇编代码(snapshot_assembly.S)编译为目标文件(snapshot_assembly.o),不知道苹果使用的 clang 版本是有什么问题,在 Profile 下的编译时长是 Release 下的 2倍 多,它就是造成编译时间变长的主要原因,到这我就没继续往下研究了,有兴趣的小伙伴可以尝试研究看看。

除此之外,汇编文件 snapshot_assembly.S 的大小相差 100M+,我们可以在日志中找到生成汇编代码的 gen_snapshot_* 命令,如下所示

executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/0385e340094e836ea63c75553c018e82/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/0385e340094e836ea63c75553c018e82/app.dill

gen_snapshot_* 命令加上 --trace-compiler 标志并重新运行,让其提供每个函数的编译时间,并记录到 result.txt 中,精简命令如下

gen_snapshot_* --trace-compiler ... app.dill > result.txt 2>&1

result.txt 中的内容长这个样子

Precompiling optimized function: 'dart:core_StateError_StateError.' @ token 21950, size 52
--> 'dart:core_StateError_StateError.' entry: 0x108d00090 size: 56 time: 935 us
Precompiling optimized function: 'dart:core_RangeError_RangeError.' @ token 9976, size 94
--> 'dart:core_RangeError_RangeError.' entry: 0x108d000e0 size: 72 time: 133 us
...

根据 result.txt 中的耗时(time)进行从大到小排序,并输出到 sorted_result.txt

grep '^-->' result.txt | awk '{for(i=1;i<=NF;i++) if($i=="time:") print $(i+1), $0}' | sort -nr | cut -d' ' -f2- > sorted_result.txt

排序后我们就可以清晰的知道哪些方法是比较耗时的,大家自行判断是否优化即可。

经过对比两个 sorted_result.txt 后发现,一些方法在 Profile 中存在而 Release 中没有,即发生了 Tree Shaking

Flutter 中,Tree Shaking 是一种优化技术,用于删除未使用的代码,以减小应用的大小并提高性能。对于不同的构建模式,Tree Shaking 的行为有所不同:

模式 描述
Debug 不会进行 Tree Shaking
因为 Debug 模式主要用于开发和调试,保留所有代码和调试信息,以便于开发者进行调试。
Profile 会进行部分 Tree Shaking
主要用于性能分析,尽可能地优化代码,同时保留一些调试信息,以便开发者能分析性能问题。
Release 会进行全面的 Tree Shaking
会删除未使用的代码,并进行其他优化,以确保应用的体积尽可能小,并且性能最佳。

关于构建模式的详细说明,可以看官方文档 docs.flutter.dev/testing/bui…

因此,如果我们希望最大限度地减少应用的体积并提高性能,建议在 Release 模式下构建 Flutter 应用。

Profile 模式切到 Release 模式后的打包时间如下

构建机 Profile(min) Release(min)
i7 25+ 18+
M4 16+ 9+

可以看到,切换编译模式已经很大程度地优化了编译时长,不过我们还可以再进一步优化。

五、二进制依赖

二进制依赖iOS 端老生常谈的优化点了,通过直接使用编译好的库或模块,从而避免编译的时间和资源消耗。

因此,原生插件越多,编译速度就越慢,二进制依赖的优化效果越好,二进制依赖的优化效果越好,编译速度就越快,所以编译越慢,编译越快 ~

在这里我使用的是 Rugby 这个工具。

安装

curl -Ls https://swiftyfinch.github.io/rugby/install.sh | bash

安装完成后输出如下内容

🏈 Rugby has been installed ✓

/Users/lxf/.rugby/clt is not in your $PATH
Add it manually to your shell profile.
For example, if you use zsh, run this command:
$ echo '\nexport PATH=$PATH:~/.rugby/clt' >> ~/.zshrc
Than open a new window or tab in the terminal for applying changes.

根据提示,将 rugby 添加到环境变量中。

完成后新开个终端,执行如下命令验证 rugby 是否可以被正常使用

rugby --version

# 输出
2.10.2

使用

在执行完 pod install 后,再执行 rugby cache 即可将原生插件从源码依赖转成二进制依赖了

rugby cache \
  --arch arm64 \
  --sdk ios \
  --except chat_bottom_container realm dart_native \
  --config Release

这里通过 --except 将一些不做二进制依赖的包过滤掉了。

当这些参数太多之后,命令会变得很长,不好看,可以将这些参数整理到 plans.yml 文件中

profile:
- command: cache
  sdk: ios
  config: Profile
  except:
    - chat_bottom_container
    - realm
    - dart_native

release:
- command: cache
  sdk: ios
  config: Release
  except:
    - chat_bottom_container
    - realm
    - dart_native

然后改为 rugby plan 去执行,并且指定使用 plans.yml 中的 release

rugby plan release -p /User/lxf/.../plans.yml

不过需要注意的是,如果你再次执行 pod install 将会还原为源码依赖!rugby 的修改就会失效~

而我们平时执行的 flutter build ipa 命令,其内部是有可能会去执行 pod install 的,那如何避免呢?

经过 flutter_tools 的源码阅读,发现它会做如下判断

  1. 涉及的文件是否存在
  2. 对比 pod_inputs.fingerprint 中的各项 MD5
  3. 对比 Podfile.lockPods/Manifest.lock 内容

pod_inputs.fingerprint 位于 build/ios 目录,内容如下

{
    "files": {
        "/Users/lxf/.../ios/Runner.xcodeproj/project.pbxproj": "21b527dc18081de6eabe26c6a4e851b2",
        "/Users/lxf/.../ios/Podfile": "25baa69590b287fd88a578ae5fa2f964",
        ".../flutter/packages/flutter_tools/bin/podhelper.rb": "29abcfc3297c225fc1d1ae2380787cd6"
    }
}

所以现在很明确,我们需要调整打包步骤

  1. flutter pub get/upgrade
  2. cd ios && pod install
  3. 切成二进制依赖
  4. 自己生成 pod_inputs.fingerprint
  5. 拷贝 Podfile.lock 至 Pods/Manifest.lock
  6. flutter build ipa

其中第 3 ~ 第 5 步我已经做了封装在我的 github.com/LinXunFeng/… 项目中,使用如下

Condor

安装 condor

brew tap LinXunFeng/tap && brew install condor

指定编译模式

设置环境变量 CONDOR_BUILD_MODE,对应 plans.yml 里的 profilerelease

export CONDOR_BUILD_MODE=release

也可以使用 --mode 参数来指定模式

condor optimize-build --mode release

二进制依赖与同步文件

进入到 Flutter 项目的根目录,执行如下命令

cd path/to/your/flutter_project

condor optimize-build --config path/to/rugby/plans.yml

如果你想指定 fvm 安装的且非全局默认的 flutter,则可以加上 --flutter 参数

condor optimize-build --config path/to/rugby/plans.yml --flutter "fvm spawn 3.24.5"

最后执行打包命令即可。

六、最后

希望苹果下一个版本的 Xcode 可以解决这个问题吧,不然的话,emmm,我也不会升级电脑的~

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 iOS 技术,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~

xcode集成deepseek插件实现AI编程

作者 邓小乐
2025年3月8日 21:07

背景

Apple在Xcode16下推出了人工智能产品Swift Assist以及预测性代码补齐(Predictive Code Completion)。 但是Swift Assist一直没上线。而预测性代码补齐(Predictive Code Completion)又锁国区。在vscode各种AI插件各种浪的当下,Xcode苦AI久矣!

这次介绍一下一个非常牛逼的xcode插件,给我们带来了AI辅助。他就是CopilotForXcode。CopilotForXcode的readme写的是Copilot for Xcode is an Xcode Source Editor Extension that provides GitHub Copilot, Codeium and ChatGPT support for Xcode.

而今天我们的核心目的是基于CopilotForXcode插件的能力让Xcode集成deepseek。

优缺点

  • Xcode直接集成deepseek的优点在于可以直接传递代码的上下文,省却了从网页来回搬运代码的问题。并且可以直接在代码中体现deepseek的修改建议,一键accept。最后他的回答的质量还是可以的。

  • 缺点就是费API token,要充值,但这个不是最大的问题。最败体验的是撸完码,抬起手要等3秒才能回来一个建议,速度太慢了。

准备工作

  1. Xcode,不用多说。
  2. 能上网,能打开deepseek开放平台网站platform.deepseek.com
  3. 需要能访问Github。

详细步骤

配置deepseek

  1. 打开deepseek开放平台。接着手机验证码一键登录或者微信扫一扫

  2. 页面的左侧有一个最醒目的黄色的按钮叫做充值,想用就充值吧,不充不让用,充了才能变强。最小金额好像是10块钱。

image.png

  1. 充值按钮的上方有一个叫做API Keys的入口。接下来重中之重,让我们创建一个API Key吧

image.png

  1. 注意!!!请一定要保存好你这个key,接下来要配置在Xcode插件中。然后这个key千万别泄漏了,否则充值的钱就没了。能不能退不退款,我也就不知道了。

下载CopilotForXcode

CopilotForXcode插件的链接是github.com/intitni/Cop… 下载安装方式有多种,项目的readme里面都有介绍。我平时有使用homebrew,我就直接用方式二brew命令安装了。 下载完后就长这个样子

image.png

第一种方式,直接下载

直接打开网站下载它最新的release版本Copilot.for.Xcode.app.zip,然后解压丢到Mac的Applications文件夹内,一气呵成。

第二种方式,用brew安装

brew install --cask copilot-for-xcode

开启Copilot插件权限

下载完毕后,丢到应用程序文件夹(Applications)内直接打开程序。就会提示获取权限,一路允许就对了。 需要注意的点:

  1. 系统设置-通用-登录项与扩展。有一个允许在后台的Toggle开关记得打开。 image.png

  2. 还是系统设置-通用-登录项与扩展。在允许在后台设置区域的下方还有一个设置扩展区域。注意点击箭头所示区域,有一个弹层,再把Copilot for Xcode的开关打开。

image.png

如果是MacOS14,设置扩展的位置有点不太一样。在系统设置-隐私与安全下面。下面是人家官方的文档,写的比我的详细

#### macOS 15
From the Apple menu located in the top-left corner of your screen click `System Settings`. Navigate to `General` then `Login Items & Extensions`. Click `Xcode Source Editor` and tick `Copilot for Xcode`.

#### MacOS 14
From the Apple menu located in the top-left corner of your screen click `System Settings`. Navigate to `Privacy & Security` then toward the bottom click `Extensions`. Click `Xcode Source Editor` and tick `Copilot`.

#### Older Versions
If you are using macOS Monterey, enter the `Extensions` menu in `System Preferences.app` with its dedicated icon.

### Granting Permissions to the App

The first time the app is open and command run, the extension will ask for the necessary permissions.

Alternatively, you may manually grant the required permissions by navigating to the `Privacy & Security` tab in the `System Settings.app`.

- To grant permissions for the Accessibility API, click `Accessibility`, and drag `CopilotForXcodeExtensionService.app` to the list. You can locate the extension app by clicking `Reveal Extension App in Finder` in the host app.

配置Copilot插件

  • 打开CopilotForXcode插件后界面如下,我们主要关心Service的配置

image.png

  • 打开Service后,我们找到Chat Models这一列,点击add model。准备添加deepseek。

image.png

点击后弹层长这个样子

image.png

  • 我们先取名字,就叫做deepseek吧。

image.png

  • 然后我们选择格式,选完后的结果就如上面这张图一样了。

image.png

  • 接下来我们处理API Key。对,就是之前从deepseek开放平台申请的那个字符串。如下图所示,在点击了加号后,会有一个弹层,让你取个名字然后输入deepseek的APIKey。之后就添加好了

image.png

  • 选择使用API Key

image.png

  • 选择模型 直接填deepseek-chat就可以了,这里千万别乱填。这个是网络请求的参数,告诉deepseek那边使用哪个模型处理数据,不是随便取名字的。之后直接点击右下角的save就OK了。

image.png

  • by the way,有哪些模型可以使用呢?

当前的deepseek只有2个模型可以选择

image.png

可以看文档 api-docs.deepseek.com/zh-cn/quick…

  • 再检查一下feature配置

image.png

到此,我们的chat差不多就配置好了

Hello deepseek

插件配置好了后,顶部menubar可以看到一个小触手。那就是插件的快捷入口,点击后有一个open chat的按钮。我们接下来就可以开始聊天了

image.png

image.png

Suggestion

现在我们还需要处理代码建议,也就是自动补全。也是这位作者名下的另外一个库。叫做CustomSuggestionServiceForCopilotForXcode,我们接下来基于这个库来实现deepseek的FIM补全功能。api-docs.deepseek.com/zh-cn/guide…

CustomSuggestionServiceForCopilotForXcode

CustomSuggestionServiceForCopilotForXcode的下载地址是github.com/intitni/Cus… 打开后直接去release页面下载它的最新的Custom.Suggestion.Service.app.zip 解压后我直接丢应用文件夹(Applications)了。

image.png

安装CustomSuggestionServiceForCopilotForXcode
  1. 打开CustomSuggestionServiceForCopilotForXcode App
  2. 再打开我们的小章鱼jio,也就是CopilotForXcode(别搞错了和上面的是2个不同的app),点击open Copilot for Xcode。会打开Copilot的控制面板,然后我们这次点击Extension-SelectExtensions

image.png

image.png

image.png

  1. enable它

image.png

  1. feature设置

image.png

配置CustomSuggestionServiceForCopilotForXcode
  • 让我们再回到CustomSuggestionServiceForCopilotForXcode,她的样子和下面一样,按照箭头点击。选择Custom Model(FIM API)

image.png

  • 接着点击旁边的Edit Model按钮

image.png

如图选择

  1. Mistral
  2. Full URL,https://api.deepseek.com/beta/completions url的出处参考的是deepseek官方文档api-docs.deepseek.com/zh-cn/api/c…
  3. API Key选择如之前的一样,我们创建的deepseek
  4. model name还是deepseek-chat
  5. 点击右下角的save

最后配置完成。可以安安静静地享受AI编程的愉快了。附上提示代码图如下:

image.png

flutter webview crash 问题

作者 AntG
2025年3月8日 11:46

现象

Flutter界面H5界面 之间来回切换(切换10多次),导致 iOS App Crash, flutter 版本 3.22.0;

Crash 原因

WebViewPageState 一直没有释放, 通过 DevToolMemory 工具发现, webviewController 一直在持有 WebViewPage, github 上找到相应的 issue 连接 webviewController 不能主动释放

最后的代码链接,里面附上了释放的过程

image.pngimage.png

DevTool Memory 简单使用

  1. 点击白色的点,可以记录当前 App内存, 即 内存快照 Snapshots;
  2. 通过diff 可以比对内存变化, 后面有内存变化的Delta(减少或增加)的个数;
  3. 点击之后class, 下方可以看到持有的联调;

image.png

image.png

Demo 连接

github.com/demoYang/us…

Shell:统计文件夹下面所有仓库指定时间内代码提交量

2025年3月7日 14:32

很多公司都要每周或者每个月统计代码提交量,我们一般都是进入指定仓库,然后执行git log命令,这对于小公司,仓库很少的时候非常好用。但是有一些公司,组件化做的非常细,代码仓库几十个,这个时候我们在挨个仓库执行git log命令就会很繁琐。这个时候我们就可以使用shell脚本帮我们快速执行

git log 查询代码量

git log --author="作者名" --after="2025-02-01 00:00:00" --before="2025-03-01 00:00:00" --pretty=tformat: --numstat | grep -v 'static'  | awk '{ add += $1 ; subs += $2 ; loc += $1 - $2 ;sum += $1 + $2} END { printf "增加行数:%s 删除行数:%s  变化总行数:%s 增删总数:%s\n",add,subs,loc,sum}'

这段代码的主要功能是统计指定作者在特定时间范围内提交的代码行数变化情况,并且会排除与 static 相关的文件变更记录。具体统计的指标包括增加行数、删除行数、变化总行数(增加行数与删除行数之和)以及增删总数(增加行数减去删除行数)。

git log 部分

git log --author="作者名" --after="2025-02-01 00:00:00" --before="2025-03-01 00:00:00" --pretty=tformat: --numstat
  • --author="作者名":指定只统计作者为 作者名 的提交记录。
  • --after="2025-02-01 00:00:00"--before="2025-03-01 00:00:00":限定提交记录的时间范围,只统计 2025 年 2 月 1 日 0 点到 2025 年 3 月 1 日 0 点之间的提交。
  • --pretty=tformat::不输出提交的描述信息,只保留 --numstat 输出的统计信息。
  • --numstat:为每个提交输出文件级别的统计信息,每行包含三个字段,分别是增加的行数、删除的行数和文件路径。

grep -v 'static' 部分

grep -v 'static'
  • -v 选项表示反向匹配,即过滤掉包含 static 字符串的行。这一步的作用是排除与 static 相关的文件变更记录,只统计其他文件的代码行数变化。

awk 部分

awk '{ add += $1 ; subs += $2 ; loc += $1 - $2 ;sum += $1 + $2} END { printf "增加行数:%s 删除行数:%s  变化总行数:%s 增删总数:%s\n",add,subs,loc,sum}'
  • { add += $1 ; subs += $2 ; loc += $1 - $2 ;sum += $1 + $2}:对于每一行输入,将第一个字段(增加的行数)累加到变量 add 中,将第二个字段(删除的行数)累加到变量 subs 中,计算增加行数与删除行数的差值并累加到变量 loc 中,计算增加行数与删除行数的和并累加到变量 sum 中。
  • END { printf "增加行数:%s 删除行数:%s 变化总行数:%s 增删总数:%s\n",add,subs,loc,sum}:在处理完所有输入行后,使用 printf 函数输出统计结果,包括增加行数、删除行数、变化总行数和增删总数。

shell脚本统计文件夹下面的代码提交量

#!/bin/bash

# 定义时间范围
start_date="2025-01-01"
end_date="2025-03-01"

# 目标文件夹路径
target_folder="/path/to/your/folder"

# 指定作者
author="指定作者"

# 初始化总统计数据
total_added=0
total_deleted=0
total_changed=0
total_net=0

# 打印时间范围信息
echo "统计时间范围:从 $start_date$end_date"
echo "指定作者:$author"
echo "---------------------"

# 进入目标文件夹
cd "$target_folder"

# 遍历每个子文件夹
for repo in */; do
    # 检查是否为 Git 仓库
    if [ -d "$repo/.git" ]; then
        # 统计指定时间范围内、指定作者的代码行数变化,排除合并提交
        stats=$(cd "$repo" && git log --author="$author" --numstat --since="$start_date" --until="$end_date" --no-merges | awk 'NF==3 {plus+=$1; minus+=$2} END {print plus, minus, plus + minus, plus - minus}')
        # 分割统计结果
        read -r added_lines deleted_lines changed_lines net_lines <<< "$stats"

        # 检查变量是否为空,如果为空则赋值为 0
        if [ -z "$changed_lines" ]; then
            changed_lines=0
        fi
        if [ -z "$added_lines" ]; then
            added_lines=0
        fi
        if [ -z "$deleted_lines" ]; then
            deleted_lines=0
        fi
        if [ -z "$net_lines" ]; then
            net_lines=0
        fi

        # 过滤掉变化总行数为 0 的仓库
        if [ "$changed_lines" -ne 0 ]; then
            # 输出仓库名称和统计信息
            echo "仓库:$(basename "$repo")"
            echo "  增加行数:$added_lines"
            echo "  删除行数:$deleted_lines"
            echo "  变化总行数:$changed_lines"
            echo "  增删总数:$net_lines"
            # 累加总统计数据
            total_added=$((total_added + added_lines))
            total_deleted=$((total_deleted + deleted_lines))
            total_changed=$((total_changed + changed_lines))
            total_net=$((total_net + net_lines))
        fi
    fi
done

# 输出总统计数据
echo "---------------------"
echo "总统计信息:"
echo "  总增加行数:$total_added"
echo "  总删除行数:$total_deleted"
echo "  总变化总行数:$total_changed"
echo "  总增删总数:$total_net"

代码详细解析

1. 定义变量

#!/bin/bash

# 定义时间范围
start_date="2025-01-01"
end_date="2025-03-01"

# 目标文件夹路径
target_folder="/path/to/your/folder"

# 指定作者
author="指定作者"

# 初始化总统计数据
total_added=0
total_deleted=0
total_changed=0
total_net=0
  • #!/bin/bash:指定脚本使用 Bash 解释器执行。
  • start_dateend_date:定义统计的时间范围。
  • target_folder:指定包含多个 Git 仓库的目标文件夹路径。
  • author:指定要统计的代码作者。
  • total_addedtotal_deletedtotal_changedtotal_net:分别用于存储所有仓库的总增加行数、总删除行数、总变化总行数和总增删总数,初始值都为 0。

2、进入目标文件夹

# 进入目标文件夹
cd "$target_folder"

3、遍历子文件夹并统计代码变化

# 遍历每个子文件夹
for repo in */; do
    # 检查是否为 Git 仓库
    if [ -d "$repo/.git" ]; then
        # 统计指定时间范围内、指定作者的代码行数变化,排除合并提交
        stats=$(cd "$repo" && git log --author="$author" --numstat --since="$start_date" --until="$end_date" --no-merges | awk 'NF==3 {plus+=$1; minus+=$2} END {print plus, minus, plus + minus, plus - minus}')
        # 分割统计结果
        read -r added_lines deleted_lines changed_lines net_lines <<< "$stats"

        # 检查变量是否为空,如果为空则赋值为 0
        if [ -z "$changed_lines" ]; then
            changed_lines=0
        fi
        if [ -z "$added_lines" ]; then
            added_lines=0
        fi
        if [ -z "$deleted_lines" ]; then
            deleted_lines=0
        fi
        if [ -z "$net_lines" ]; then
            net_lines=0
        fi

        # 过滤掉变化总行数为 0 的仓库
        if [ "$changed_lines" -ne 0 ]; then
            # 输出仓库名称和统计信息
            echo "仓库:$(basename "$repo")"
            echo "  增加行数:$added_lines"
            echo "  删除行数:$deleted_lines"
            echo "  变化总行数:$changed_lines"
            echo "  增删总数:$net_lines"
            # 累加总统计数据
            total_added=$((total_added + added_lines))
            total_deleted=$((total_deleted + deleted_lines))
            total_changed=$((total_changed + changed_lines))
            total_net=$((total_net + net_lines))
        fi
    fi
done
  • for repo in */; do ... done:遍历目标文件夹下的所有子文件夹。

  • if [ -d "$repo/.git" ]; then ... fi:检查当前子文件夹是否为 Git 仓库,通过判断是否存在 .git 文件夹来确定。

  • stats=$(cd "$repo" && git log --author="$author" --numstat --since="$start_date" --until="$end_date" --no-merges | awk 'NF==3 {plus+=$1; minus+=$2} END {print plus, minus, plus + minus, plus - minus}')
    

    • cd "$repo":进入当前 Git 仓库。
    • git log --author="$author" --numstat --since="$start_date" --until="$end_date" --no-merges:获取指定作者在指定时间范围内(排除合并提交)的提交记录,并以 --numstat 格式输出,该格式会显示每个文件的增加行数和删除行数。
    • awk 'NF==3 {plus+=$1; minus+=$2} END {print plus, minus, plus + minus, plus - minus}':使用 awk 脚本对 git log 的输出进行处理,统计增加行数、删除行数、变化总行数和增删总数。
  • read -r added_lines deleted_lines changed_lines net_lines <<< "$stats":将 stats 变量的值分割为四个变量,分别存储增加行数、删除行数、变化总行数和增删总数。

  • if [ -z "$changed_lines" ]; then ... fi:检查变量是否为空,如果为空则将其赋值为 0。

  • if [ "$changed_lines" -ne 0 ]; then ... fi:过滤掉变化总行数为 0 的仓库,只处理有代码变化的仓库。

  • echo "仓库:$(basename "$repo")":输出当前仓库的名称。

  • total_added=$((total_added + added_lines)) 等:累加当前仓库的统计数据到总统计数据中

Shell脚本初识

2025年3月7日 14:16

shell初识

Shell(Unix Shell)是一种命令行解释器,是Unix操作系统下最传统的人机接口。Shell脚本是解释执行的,不需要编译,和大部分的编程语言很相似,也有基本的变量和流程控制语句。我们平时使用Shell有两种方式:

  • 1、输入命令,执行,这种方式称为交互式
  • 2、批处理方式,用户事先写好shell脚本,然后顺序批次执行

第一个Shell环境是ThompsonShell,在贝尔实验室开发并于1971年发布。 现代Shell最突出的祖先是被称为sh的BourneShell,这是以在AT&T工作的创始人stephenBourne命名的

shell`一直基于这个概念,不断添加各种新功能,演变出很多`shell

例如,很早版本的OS X中使用的是tcsh作为默认的shell,这是由csh(c shell),一种C语言演变而来

os x 10.3版本之后,默认的shellbash

除了默认的bash,现在macos中,默认的Shell变成了zsh。这是一种由Paul Falstad于1990年开发的。它是一个BourneShell,它使用bash和previous shell的特性,并添加了更多的特性:

  • 拼写检查功能
  • 内置的编程特性
  • 友好的交互

bash

确认当前终端tty使用的shell类型

tty

于此同时,macOS还提供了很多种其他类型的shell

lsshell

运行shell脚本

  • 1、进入所在目录,然后输入命令./test.sh

  • 2、直接将.sh拖到终端

    #!/bin/bash
    echo "Hello World !"
    

    #! 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种 Shell

    echo 命令用于向窗口输出文本。

.zshrc .bashrc .bash_profile 的区别

在使用命令行工具时,我们可能会遇到一些教程,可能需要你把一些配置写入到.bashrc.bash_profile或者.zshrc等。那么这几个文件到底有什么作用和区别?

首先,从文件名称判断.bashrc、.bash_profile是给Bash来使用的。而.zshrc是给zsh来使用的。

交互式登录和非登录Shell

当调用Shell时,Shell从一组启动文件中读取信息并执行命令。读取什么文件取决于Shell是作为交互式登录还是非登录调用

换而言之,Shell分为交互式的或者非交互式的

  • 交互式Shell是读取和写入到用户终端的Shell程序,用户在终端输入命令,并在回车后立即执行
  • 非交互式Shell是与终端不相关的Shell程序,如脚本执行

当作为交互式登录Shell调用时,Bash会先查找/etc/profile文件,如果该文件存在,它将运行文件中列出的命令。然后,搜索~/.bashprofile ~/.bash_login以及~/.profile文件,顺序读取。

Bash作为交互式非登录shell调用时,会读取~/.bashrc

所以说,.bashrc和.bash_profile之间的区别是,.bash_profileBash作为交互式登录shell调用时被读取并执行,而.bashrc对于交互式非登录shell被执行。

确认当前是登录还是非登录shelltty中执行echo $0,输出的Shell如果前面带-,说明是登录Shell。

shell1

建议配置

  •     1、bash
    
    • 将配置选项放到~/.bashrc中,然后在~/.bash_profile中通过source调用。
  •     2、zsh
    
    • 建议仍然将配置选项放到~/.bashrc~/.bash_profile中通过source调用,最后在~/.zshrcsource调用~/.bash_profile.

shell基础语法

1、变量

定义变量时,变量名不加美元符号

name="xiaoming"

使用一个定义过的变量,只要在变量名前面加美元符号即可

echo $name
echo ${name}

变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界

2、Shell 字符串

字符串是shell编程中最常用最有用的数据类型,字符串可以用单引号,也可以用双引号,也可以不用引号。

str='this is a string'
str1="this is a string"

拼接字符串

echo "hello ${name}"

获取字符串长度

str='this is a string'
echo ${#str}

提取子字符串

string="runoob is a great site"
echo ${string:1:4} # 输出 unoo

3、Shell 数组

bash支持一维数组(不支持多维数组),并且没有限定数组的大小。

array_name=(value0 value1 value2 value3)
# 读取数组元素
echo $array_name[0]
# 取得数组元素的个数
length1=${#array_name[@]}
echo $length1
# 或者
length2=${#array_name[*]}
echo $length2

4、Shell 注释

# 开头的行就是注释,会被解释器忽略。

##### 用户配置区 开始 #####
#
#
# 这里可以添加脚本描述信息
# 
#
##### 用户配置区 结束  #####

多行注释

多行注释还可以使用以下格式:

:<<EOF
注释内容...
注释内容...
注释内容...
EOF

5、Shell 传递参数

我们可以在执行 Shell 脚本时,向脚本传递参数,脚本内获取参数的格式为: $nn 代表一个数字,1 为执行脚本的第一个参数,2 为执行脚本的第二个参数,以此类推……

echo "执行的文件名:$0";
echo "第一个参数为:$1";
echo "第二个参数为:$2";
echo "第三个参数为:$3";
echo "传递到脚本的参数个数:$#";
echo "传递的参数作为一个字符串显示:$*";
echo "-- $* 演示 ---"
for i in "$*"; do
    echo $i
done

echo "-- $@ 演示 ---"
for i in "$@"; do
    echo $i
done

Shell运算符

Shell 和其他编程语言一样,支持多种运算符,包括:

  • 算数运算符
  • 关系运算符
  • 布尔运算符
  • 字符串运算符
  • 文件测试运算符

原生bash不支持简单的数学运算,但是可以通过其他命令来实现,例如 awk 和 expr,expr 最常用。

expr 是一款表达式计算工具,使用它能完成表达式的求值操作。

算数运算符

#!/bin/bash

a=10
b=20

# +
val1=`expr $a + $b`
echo "a + b : $val1"

# -
val=`expr $a - $b`
echo "a - b : $val"

# *
val3=`expr $a * $b`
echo "a * b : $val3"

# /
val4=`expr $b / $a`
echo "b / a : $val4"

# %
val5=`expr $b % $a`
echo "b % a : $val5"

# 判等
if [ $a == $b ]
then
   echo "a 等于 b"
fi
if [ $a != $b ]
then
   echo "a 不等于 b"
fi

关系运算符

运算符 说明 举例
-eq 检测两个数是否相等,相等返回 true。 [ aeqa -eq b ] 返回 false。
-ne 检测两个数是否不相等,不相等返回 true。 [ anea -ne b ] 返回 true。
-gt 检测左边的数是否大于右边的,如果是,则返回 true。 [ agta -gt b ] 返回 false。
-lt 检测左边的数是否小于右边的,如果是,则返回 true。 [ alta -lt b ] 返回 true。
-ge 检测左边的数是否大于等于右边的,如果是,则返回 true。 [ agea -ge b ] 返回 false。
-le 检测左边的数是否小于等于右边的,如果是,则返回 true。 [ alea -le b ] 返回 true。
if [ $a -eq $b ]
then
   echo "$a -eq $b : a 等于 b"
else
   echo "$a -eq $b: a 不等于 b"
fi

if [ $a -ne $b ]
then
   echo "$a -ne $b: a 不等于 b"
else
   echo "$a -ne $b : a 等于 b"
fi

if [ $a -gt $b ]
then
   echo "$a -gt $b: a 大于 b"
else
   echo "$a -gt $b: a 不大于 b"
fi

if [ $a -lt $b ]
then
   echo "$a -lt $b: a 小于 b"
else
   echo "$a -lt $b: a 不小于 b"
fi

if [ $a -ge $b ]
then
   echo "$a -ge $b: a 大于或等于 b"
else
   echo "$a -ge $b: a 小于 b"
fi

if [ $a -le $b ]
then
   echo "$a -le $b: a 小于或等于 b"
else
   echo "$a -le $b: a 大于 b"
fi

布尔运算符

运算符 说明 举例
! 非运算,表达式为 true 则返回 false,否则返回 true。 [ ! false ] 返回 true。
-o 或运算,有一个表达式为 true 则返回 true。 [ alt20oa -lt 20 -o b -gt 100 ] 返回 true。
-a 与运算,两个表达式都为 true 才返回 true。 [ alt20aa -lt 20 -a b -gt 100 ] 返回 false。
if [ $a != $b ]
then
   echo "$a != $b : a 不等于 b"
else
   echo "$a == $b: a 等于 b"
fi
if [ $a -lt 100 -a $b -gt 15 ]
then
   echo "$a 小于 100 且 $b 大于 15 : 返回 true"
else
   echo "$a 小于 100 且 $b 大于 15 : 返回 false"
fi
if [ $a -lt 100 -o $b -gt 100 ]
then
   echo "$a 小于 100 或 $b 大于 100 : 返回 true"
else
   echo "$a 小于 100 或 $b 大于 100 : 返回 false"
fi
if [ $a -lt 5 -o $b -gt 100 ]
then
   echo "$a 小于 5 或 $b 大于 100 : 返回 true"
else
   echo "$a 小于 5 或 $b 大于 100 : 返回 false"
fi

字符串运算符

下表列出了常用的字符串运算符,假定变量 a 为 "abc",变量 b 为 "efg":

运算符 说明 举例
= 检测两个字符串是否相等,相等返回 true。 [ a=a = b ] 返回 false。
!= 检测两个字符串是否不相等,不相等返回 true。 [ a!=a != b ] 返回 true。
-z 检测字符串长度是否为0,为0返回 true。 [ -z $a ] 返回 false。
-n 检测字符串长度是否不为 0,不为 0 返回 true。 [ -n "$a" ] 返回 true。
$ 检测字符串是否为空,不为空返回 true。 [ $a ] 返回 true。
a="abc"
b="efg"

if [ $a = $b ]
then
   echo "$a = $b : a 等于 b"
else
   echo "$a = $b: a 不等于 b"
fi

if [ $a != $b ]
then
   echo "$a != $b : a 不等于 b"
else
   echo "$a != $b: a 等于 b"
fi

if [ -z $a ]
then
   echo "-z $a : 字符串长度为 0"
else
   echo "-z $a : 字符串长度不为 0"
fi

if [ -n "$a" ]
then
   echo "-n $a : 字符串长度不为 0"
else
   echo "-n $a : 字符串长度为 0"
fi

if [ $a ]
then
   echo "$a : 字符串不为空"
else
   echo "$a : 字符串为空"
fi

文件测试运算符

操作符 说明 举例
-b file 检测文件是否是块设备文件,如果是,则返回 true。 [ -b $file ] 返回 false。
-c file 检测文件是否是字符设备文件,如果是,则返回 true。 [ -c $file ] 返回 false。
-d file 检测文件是否是目录,如果是,则返回 true。 [ -d $file ] 返回 false。
-f file 检测文件是否是普通文件(既不是目录,也不是设备文件),如果是,则返回 true。 [ -f $file ] 返回 true。
-g file 检测文件是否设置了 SGID 位,如果是,则返回 true。 [ -g $file ] 返回 false。
-k file 检测文件是否设置了粘着位(Sticky Bit),如果是,则返回 true。 [ -k $file ] 返回 false。
-p file 检测文件是否是有名管道,如果是,则返回 true。 [ -p $file ] 返回 false。
-u file 检测文件是否设置了 SUID 位,如果是,则返回 true。 [ -u $file ] 返回 false。
-r file 检测文件是否可读,如果是,则返回 true。 [ -r $file ] 返回 true。
-w file 检测文件是否可写,如果是,则返回 true。 [ -w $file ] 返回 true。
-x file 检测文件是否可执行,如果是,则返回 true。 [ -x $file ] 返回 true。
-s file 检测文件是否为空(文件大小是否大于0),不为空返回 true。 [ -s $file ] 返回 true。
-e file 检测文件(包括目录)是否存在,如果是,则返回 true。 [ -e $file ] 返回 true。
file="/Users/jiangjunhui/Desktop/test.sh"
if [ -r $file ]
then
   echo "文件可读"
else
   echo "文件不可读"
fi

if [ -w $file ]
then
   echo "文件可写"
else
   echo "文件不可写"
fi

if [ -x $file ]
then
   echo "文件可执行"
else
   echo "文件不可执行"
fi

if [ -f $file ]
then
   echo "文件为普通文件"
else
   echo "文件为特殊文件"
fi

if [ -d $file ]
then
   echo "文件是个目录"
else
   echo "文件不是个目录"
fi

if [ -s $file ]
then
   echo "文件不为空"
else
   echo "文件为空"
fi

if [ -e $file ]
then
   echo "文件存在"
else
   echo "文件不存在"
fi

一个Android开发学习开发ios桌面小组件

2025年3月7日 11:21

1 创建widget extension

image.png

image.png

2 创建完成之后 理论上就可以运行了

关键代码包括 TimelineProvider TimelineEntry

正常代码是下面这样的

//
//  home_widget_study.swift
//  home_widget_study
//
//  Created by gy on 2025/3/7.
//

import WidgetKit
import SwiftUI

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), emoji: "😀")
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), emoji: "😀")
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []

        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate, emoji: "😀")
            entries.append(entry)
        }

         //entries 定义了刷新的时间 这里数组长度为5 每个元素的时间相差一小时 意思就是从小组件添加之后一小时刷新一次 好像这个时间不能设置的太短 最短好像是5分钟
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }

//    func relevances() async -> WidgetRelevances<Void> {
//        // Generate a list containing the contexts this widget is relevant in.
//    }
}


//里面的属性可以根据自己的需求 自定义
//但是这个date 好像是必须的 意思是下次刷新的时间 因为小组件不会自己刷新
struct SimpleEntry: TimelineEntry {
    let date: Date
    let emoji: String
}

struct home_widget_studyEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        VStack {
            Text("Time:")
            Text(entry.date, style: .time)

            Text("Emoji:")
            Text(entry.emoji)
        }
    }
}

struct home_widget_study: Widget {
    let kind: String = "home_widget_study"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            if #available(iOS 17.0, *) {
                home_widget_studyEntryView(entry: entry)
                    .containerBackground(.fill.tertiary, for: .widget)
            } else {
                home_widget_studyEntryView(entry: entry)
                    .padding()
                    .background()
            }
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

#Preview(as: .systemSmall) {
    home_widget_study()
} timeline: {
    SimpleEntry(date: .now, emoji: "😀")
    SimpleEntry(date: .now, emoji: "🤩")
}

如果你的TimerLineProvider初始代码如下,运行会报错。你可以参考上面的代码修改调整一下。下面的代码好像是高版本的写法。

func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(),model: 0)
    }

    func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
        SimpleEntry(date: Date(), configuration: configuration,model: "456")
    }
    
    func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
        var entries: [SimpleEntry] = []

        print("timelinetimelinetimelinetimelinetimeline===================")
        // Generate a timeline consisting of five entries an hour apart, starting from the current date.
        let currentDate = Date()
        let userdefaults = UserDefaults(suiteName: "group.com.aiintelligent.iossoftware.homewidget")
        let model = userdefaults?.string(forKey: "model") ?? "model"
        let a = print("==========model===",model)
        for hourOffset in 0 ..< 5 {
            let entryDate = Calendar.current.date(byAdding: .minute, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(date: entryDate,model: model)
            entries.append(entry)
        }

        return Timeline(entries: entries, policy: .atEnd)
    }

如果你的widget body 是如下代码 也参照上面的修改 这个好像也是高版本代码

var body: some WidgetConfiguration {
        AppIntentConfiguration(kind: kind, intent: ConfigurationAppIntent.self, provider: Provider()) { entry in
            1EntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
    }

3 主动刷新小组件

WidgetCenter.shared.reloadTimelines(ofKind: "home_widget_study")

ofKind 在这里

image.png

4 主程序与小组件之间通信 传递数据

group_name 需要在开发者后台配置 在小组件 跟 主程序使用的group_name一定要一样,切必须是开发者后台配置的值。

 var userdefaults = UserDefaults(suiteName: "group_name")

swiftui 设置十六进制的颜色

2025年3月7日 11:25
extension Color {
    init(hex: String) {
        let hex = hex.trimmingCharacters(in: .whitespacesAndNewlines).replacingOccurrences(of: "#", with: "")
        var rgbValue: UInt64 = 0
        Scanner(string: hex).scanHexInt64(&rgbValue)
        
        let a, r, g, b: UInt64
        switch hex.count {
        case 8: // 包含 Alpha (如 #FFFF9CD4)
            a = (rgbValue >> 24) & 0xFF
            r = (rgbValue >> 16) & 0xFF
            g = (rgbValue >> 8) & 0xFF
            b = rgbValue & 0xFF
        case 6: // 不包含 Alpha (如 #FF9CD4)
            a = 255
            r = (rgbValue >> 16) & 0xFF
            g = (rgbValue >> 8) & 0xFF
            b = rgbValue & 0xFF
        default:
            a = 255
            r = 0
            g = 0
            b = 0
        }
        
        self.init(
            .sRGB,
            red: Double(r) / 255,
            green: Double(g) / 255,
            blue: Double(b) / 255,
            opacity: Double(a) / 255
        )
    }
}

使用 Color(hex: "FFFF9CD4")

❌
❌