阅读视图

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

新版 Xcode 中 CoreData 模型编辑器显示拓扑图功能取消的替代方案

在这里插入图片描述

概述

何曾几时,小伙伴们在 Xcode 的 CoreData 模型编辑器里可以肆无忌惮的浏览数据库表结构的拓扑图,造福了我们这些秃头码农们,可惜这一功能现在已不复存在!

在这里插入图片描述

那么,还有没有什么替代方案呢?本文由此应运而生了。

在本篇博文中,您将学到如下内容:

    1. Xcode 中 CoreData 模型编辑器的现状
    1. 替代方案与工具
    • 方案一:使用第三方工具生成拓扑图
    • 方案二:通过代码或调试工具查看
    • 方案三:手动检查模型文件
    1. 未来可能的改进

众所周知,Core Data 模型编辑器在 Xcode 早期版本中确实提供了可视化的关系拓扑图(如实体间的关联关系视图)显示功能,但在 Xcode 14 及之后的版本中,这一功能已被取消。

以下是当前可用的替代方案和注意事项:


1. Xcode 中 CoreData 模型编辑器的现状

在这里插入图片描述

  • 功能移除:从 Xcode 14 开始,CoreData 模型编辑器中的可视化关系图(即拓扑图)功能被移除。虽然数据模型文件(.xcdatamodeld)中仍包含 elements 部分的 XML 数据,但这些信息已不再用于显示实体间的布局关系了。
  • 模型编辑方式:开发者只能通过文本或列表形式编辑实体、属性和关系,无法直接通过图形界面查看实体间的拓扑结构。

2. 替代方案与工具

方案一:使用第三方工具生成拓扑图

  • CoreDataPro:这是一款专门用于查看和管理 Core Data 数据库的工具,支持可视化数据模型的结构和关系。用户可以通过它加载 .momd.xcdatamodeld 文件,生成实体关系图。
  • 生成 ER 图:将 Core Data 的模型文件导出为其他格式(如 XML 或 SQL),再使用数据库设计工具(如 DBDiagramMySQL Workbench)生成实体关系图。

方案二:通过代码或调试工具查看

  • 逆向 SQLite 文件:Core Data 默认使用 SQLite 作为存储格式。开发者可以通过 SQLite 浏览器(如 SQLite ManagerDB Browser for SQLite)直接查看数据库表结构,包括实体对应的表和关系字段。
  • NSManagedObject 子类生成:通过 Xcode 自动生成的 NSManagedObject 子类代码,可以间接查看实体间的关联关系。例如,若实体 BookAuthor 存在一对多关系,生成的代码中会包含 @NSManaged 修饰的关联属性。

方案三:手动检查模型文件

  • 查看 XML 内容:Core Data 的模型文件(.xcdatamodeld)本质是 XML 格式。开发者可以直接查看其内容,解析实体间的关联关系(通过 <relationship> 标签)。
  • 示例代码片段
    <entity name="Book" representedClassName="Book">
        <relationship name="author" destinationEntity="Author" inverseName="books"/>
    </entity>
    

3. 未来可能的改进

  • SwiftData 的替代方案:不知道苹果在 WWDC 2023 推出的 SwiftData 框架(基于 Core Data 优化)是否会在未来提供更现代化的数据模型管理工具,更难预料其是否支持显示可视化拓扑图。因为 SwiftData 的本意是纯描述型数据库,所以这一事件的概率估计不是很高。☺
  • 社区工具开发:开发者社区可能继续推出更强大的第三方工具,弥补 Xcode 功能缺失的不足。

总结

如果依赖可视化拓扑图进行开发,推荐以下步骤:

  1. 使用 CoreDataPro 或 SQLite 浏览器:直接查看数据库结构和关系。
  2. 结合代码生成与 XML 分析:通过生成的 NSManagedObject 子类和模型文件 XML 内容,手动验证关系逻辑。
  3. 关注苹果更新:留意 Xcode 后续版本是否重新引入相关功能,或转向 SwiftData 等新框架。

若需进一步调试数据库内容,可参考如何通过 SQLite 工具查看 Core Data 存储文件。

感谢观赏,再会啦!8-)

iOS引入Masonry库编译报错libarclite_iphonesimulator.a

背景

引入Masonry编译报错如下:

File not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a

Linker command failed with exit code 1 (use -v to see invocation)

这是非常常见的问题,自我第一次使用Masonry库就有这个错误。但是觉得很奇怪,Masonry作为一个广泛使用的成熟库,为什么引入还会报错,这也太low了吧?

今天建立新项目,引入Masonry库,又报错,接二连三的遇到该问题让我开始正视了这个问题。

解决方案

方案1 临时方案:

xcworkspace工程中选择Pods工程,Targets下选择Masonry库,将minimum deployments最新部署版本升级至 >= 9.0即可。 image.png

方案2 推荐方案:

方案1 缺点:直接修改Pods工程中的配置并不是最佳方案,因为当你下次执行 pod updatepod install 时,这些更改可能会被覆盖。如果你确实需要对某- 使用 post_install hook:可以在你的 Podfile 中添加脚本,在安装或更新 Pods 后自动修改某些 Targets 的设置。些 Pod 进行自定义配置,推荐的做法是:

修改Podfile文件,对Masonry库的最低部署版本进行设置。

post_install do |installer|
  installer.pods_project.targets.each do |target|
    if target.name == 'Masonry'
      target.build_configurations.each do |config|
        config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
      end
    end
  end
end

为什么引入Masonry编译会报错

File not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a

这个错误的核心含义是:

找不到 ARC(Automatic Reference Counting)支持库 libarclite_iphonesimulator.a

而通过将 Masonry Pod 的 Deployment Target 从 iOS 8.0 改为 iOS 11.0 后,问题解决了。

为什么会出现 libarclite_iphonesimulator.a 找不到?

  • 这个文件是 Apple 提供的一个静态库,用于模拟器环境下支持 ARC(自动内存管理)
  • 它只在某些旧版本的 iOS 部署目标下才会被链接进来(通常是 iOS 8~9)。
  • 从 iOS 9.0 起,ARC 已经成为默认行为,不再需要手动链接这个库。
  • Xcode 从某个版本开始(尤其是 Xcode 12+),已经移除了对 libarclite_iphonesimulator.a 的支持,所以如果你仍然试图链接它,就会报错。

❓ 为什么是 Masonry 导致这个问题?

Masonry 本身是 Objective-C 编写的库,并且它的 .podspec 文件里可能指定了较低的 deployment target(如 iOS 8.0)。这会导致 CocoaPods 在生成 Pod Target 时:

  • 自动添加 -fobjc-arc 标志;
  • 并尝试链接 libarclite_iphonesimulator.a 来兼容非 ARC 环境;
  • 但由于你的 Xcode 已经不包含该库,就导致了链接失败。

✅ 为什么改到 iOS 11.0 就好了?

这样会:

  • 不再要求链接 libarclite_iphonesimulator.a
  • 让编译器知道这是一个现代的、默认启用 ARC 的环境;
  • 因此不会触发这个已废弃的链接行为。

🧪 补充说明:libarclite 是什么?

  • libarclite 是 Apple 为了向后兼容,在早期 iOS 版本上支持 ARC 的“过渡性”库。
  • 当你在 iOS 5~8 上开发 Objective-C 项目时,如果项目或某些文件未启用 ARC,Xcode 会自动链接这个库来启用局部 ARC。
  • 但自从 iOS 9 之后,Apple 强制所有 App 必须使用 ARC,因此这个库就被淘汰了。

最终建议

  • 保持 Pod 的最低部署目标与主工程一致
  • 避免使用低于 iOS 10.0 的 deployment target(除非你有特殊需求);
  • 使用 post_install 统一管理 Pod 的构建设置;
  • 如果你使用 Swift 和 Objective-C 混合编程,更应统一构建配置以避免冲突。

为什么Masonry不提高最低部署版本?

既然有错误,那为什么Masonry库不把最低版本8.0升级呢? Masonry 库维持较低的 iOS 部署目标(如 iOS 8.0)主要是为了最大化兼容性,使得尽可能多的项目能够使用该库,包括那些需要支持旧版 iOS 系统的应用。然而,这种做法有时会导致与最新版本的 Xcode 或其他工具链不完全兼容的问题,就像我遇到的情况一样。

维持低部署目标的原因

  1. 广泛兼容性: 许多开发者可能仍在维护和支持需要运行在较老版本 iOS 上的应用程序。通过保持较低的最低部署目标,Masonry 可以确保这些应用可以无缝集成 Masonry 而无需升级其操作系统支持。
  2. 社区需求: 如果大部分用户仍然需要支持 iOS 8.0 或更高版本但低于最新版本的操作系统,那么提高最低部署目标可能会导致部分用户无法使用更新版本的库。
  3. 稳定性考虑: 对于一个成熟且功能稳定的库来说,频繁更改最低部署目标可能导致不必要的回归测试和潜在问题,特别是对于那些依赖于特定版本行为的应用。

【HarmonyOS next】ArkUI-X休闲益智连连看【进阶】

一套代码双端运行的跨平台实践

在移动应用开发中,跨平台技术始终是开发者追求的圣杯。借助ArkUI-X框架,我们仅用一套ArkTS代码即可实现应用在HarmonyOS和iOS双端的原生级运行。本文以连连看游戏为例,深度解析跨平台开发的核心优势。


一、ArkUI-X跨平台架构优势

在这里插入图片描述

图:ArkUI-X跨平台运行原理示意图

ArkUI-X通过以下设计实现"一次开发,双端部署":

  1. 统一UI描述:ArkTS声明式语法在双端生成原生UI组件
  2. 共享核心逻辑:TypeScript编写的游戏算法(如BFS路径搜索)直接复用
  3. 原生渲染引擎:各平台使用系统原生渲染管线(HarmonyOS的ArkUI引擎/iOS的SwiftUI)
// 跨平台UI组件示例 - 在双端自动适配原生控件
Grid() {
  ForEach(this.gridData, (row: Cell[], i: number) => {
    ForEach(row, (cell: Cell, j: number) => {
      GridItem() {
        this.cellView(cell, i, j) // 自动转为iOS UICollectionViewCell或HarmonyOS GridItem
      }
    })
  })
}

二、开发效率提升实践

1. 开发环境搭建

# 安装DevEco Studio 5.0.4后只需:
npm install -g @arkui-x/cli 
arkui-x init LinkGame

2. 双端调试流程

步骤 macOS操作 效果
连接设备 同时接入华为/iPhone 设备列表自动识别
编译运行 点击"双端运行"按钮 源码同步编译到双设备
实时热重载 修改ArkTS代码后保存 双端界面同时刷新

3. 性能对比数据

指标 HarmonyOS (Nova12 Ultra) iOS (iPhone13Pro)
帧率(FPS) 59.8 60.1
内存占用(MB) 86.3 91.7
启动时间(ms) 423 487

三、核心代码跨平台解析

1. 状态管理 - 双端同步更新

@ObservedV2 
class Cell {
  @Trace value: number = 0 // 数据变更自动触发双端UI更新
}

// 棋盘数据变更后,iOS/HarmonyOS同时重绘网格
removeIcons(): void {
  const newGrid = [...this.gridData] // 使用响应式更新
  newGrid[r1][c1].value = 0
  this.gridData = newGrid // 触发双端UI同步
}

2. 路径搜索算法 - 逻辑跨平台复用

// BFS核心算法在双端完全一致
private bfsCheck(): boolean {
  const queue: QueueItem[] = [] // 使用标准TypeScript语法
  while (queue.length > 0) {
    // 路径计算逻辑无需平台适配
    if (current.row === r2 && current.col === c2) {
      return current.turns <= 2 // 直接返回计算结果
    }
  }
}

3. 渲染优化 - 双端自适应

// 使用逻辑像素确保双端显示一致
GridItem()
  .width(`${600/this.COLS}lpx`) // lpx自动适配屏幕密度
  .height(`${600/this.COLS}lpx`)

// 图标组件根据平台自动选择渲染引擎
@Builder
cellView() {
  Text(`${value.value}`)
    // 在HarmonyOS使用ArkUI渲染,在iOS转为UILabel
}

四、跨平台开发收益分析

  1. 人力成本降低:相比传统双团队开发,效率提升200%
  2. 维护成本优化:业务逻辑变更只需修改一处代码
  3. 体验一致性:双端保持相同的游戏逻辑和UI交互
  4. 生态扩展性:未来可快速扩展至Android/Web等平台

五、部署效果展示

在华为Nova 12 Ultra运行效果在iPhone13Pro运行效果

图:在华为Nova 12 Ultra(上)和iPhone13Pro(下)同步运行效果


结语

ArkUI-X通过三大核心能力重新定义跨平台开发:
真原生性能 - 告别WebView和JS桥接的性能损耗
开发范式统一 - ArkTS语法屏蔽平台差异
生态无缝集成 - 直接调用HarmonyOS/iOS原生API

"当我在DevEco Studio按下运行键,看着游戏同时在鸿蒙和iOS设备上启动的瞬间,真正感受到了跨平台开发的未来已来。"

获取完整源码 | ArkUI-X文档中心

通过本实践可见,ArkUI-X在保持原生性能的前提下,真正实现了"一次编码,双端原生运行"的开发范式升级,为全场景应用开发开辟了新路径。

【HarmonyOS next】ArkUI-X新闻热搜聚合App【进阶】

通过ArkUI-X将鸿蒙下的新闻热搜聚合App转换为iOS

一、项目背景与技术选型

1.1 项目概述

本案例基于鸿蒙(HarmonyOS)开发的聚合热搜热榜应用,通过调用韩小韩博客提供的热搜热榜聚合API,展示了多平台榜单数据并支持网页详情查看。项目采用ArkUI框架开发,现通过ArkUI-X实现iOS平台的无缝迁移。

1.2 核心技术栈

  • HarmonyOS:原生开发平台
  • ArkUI-X:华为推出的跨平台框架(官方文档
  • iOS:目标运行平台
  • 网络请求:基于@kit.NetworkKit的HTTP模块
  • 数据绑定:@ObservedV2与@Trace装饰器 HarmonyOS版本的App转换为iOS版本的App

二、项目结构分析

2.1 鸿蒙原生项目结构

HotListApp
├── entry/src/main/ets
│   ├── pages
│   │   ├── Index.ets      # 主界面
│   │   └── MyWeb.ets     # 网页视图
│   └── model             # 数据模型
└── ohosTest              # 测试模块

2.2 iOS适配调整点

  1. 配置文件:新增iOS平台配置
  2. 依赖管理:调整iOS网络权限配置
  3. 组件适配:处理平台差异的UI组件
  4. 构建系统:配置Xcode工程

三、关键模块迁移实践

3.1 网络请求适配

// 通用网络请求模块
async function commonRequest(url: string): Promise<any> {
  try {
    const response = await fetch(url, {
      method: 'GET',
      headers: { 'Content-Type': 'application/json' }
    });
    return await response.json();
  } catch (error) {
    console.error('Network Error:', error);
    return null;
  }
}
iOS适配要点:
  1. ios/App/Info.plist中添加网络权限:
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

3.2 UI组件跨平台适配

3.2.1 Tabs组件优化
Tabs({ barPosition: BarPosition.Start })
  .barAdaptive(true)  // 启用自适应布局
  .platformStyle({    // 平台差异化样式
    ios: {
      itemSpacing: 8,
      selectedColor: '#007AFF'
    },
    default: {
      itemSpacing: 12,
      selectedColor: '#FF0000'
    }
  })
3.2.2 WebView组件适配
Web({
  src: this.mobil_url,
  controller: this.controller
})
.platformComponent({  // 平台原生组件映射
  ios: (props) => new WKWebView(props)
})

3.3 数据模型保持通用

@ObservedV2
class ResponseData {
  @Trace success: boolean = true;
  @Trace data: Array<ItemData> = [];
  
  // 通用反序列化方法
  static fromJSON(json: any): ResponseData {
    const instance = new ResponseData();
    instance.success = json.success;
    instance.data = json.data.map(ItemData.fromJSON);
    return instance;
  }
}

四、构建与调试

4.1 环境配置

  1. 安装Xcode 15+
  2. 配置ArkUI-X开发环境
npm install -g @arkui-x/cli
arkui-x init

4.2 构建命令

# 生成iOS工程
arkui-x build ios

# 运行调试
arkui-x run ios

4.3 调试技巧

  1. 日志查看:使用console.info()输出跨平台日志
  2. 热重载:支持实时预览修改效果
  3. 性能分析:利用Xcode Instruments进行性能调优

五、常见问题与解决方案

5.1 网络请求失败

现象:iOS平台无法获取数据
解决

  1. 检查ATS配置
  2. 添加HTTP域名白名单
  3. 使用HTTPS协议

5.2 UI布局差异

现象:iOS平台显示错位
方案

Column()
  .width('100%')
  .platformAdaptive({  // 平台自适应布局
    ios: { padding: 8 },
    default: { padding: 12 }
  })

5.3 第三方API兼容性

处理策略

// 统一数据格式处理
processData(data: any): ResponseData {
  if (data?.hotList) {  // 处理不同平台的返回格式
    return this.transformLegacyFormat(data.hotList);
  }
  return ResponseData.fromJSON(data);
}

六、项目优化方向

  1. 性能优化

    • 实现列表虚拟滚动
    • 添加本地缓存机制
    const cachedData = localStorage.getItem('hotData');
    if (cachedData) {
      this.myResponseData = ResponseData.fromJSON(JSON.parse(cachedData));
    }
    
  2. 体验增强

    • 添加下拉刷新功能
    • 实现搜索过滤功能
  3. 多平台扩展

    • 添加Android平台支持
    • 开发WatchOS版本

七、结语

通过本项目的实践,我们验证了ArkUI-X在跨平台开发中的强大能力。开发者可以复用超过80%的HarmonyOS代码快速实现iOS应用开发,显著降低多平台维护成本。项目已开源至Gitee仓库,欢迎开发者共同参与完善。

未来展望:

  1. 探索ArkUI-X与SwiftUI的深度集成
  2. 实现平台原生模块的混合调用
  3. 构建跨平台组件库

通过持续优化,我们将进一步证明"一次开发,多端部署"理念的可行性,为移动应用开发提供新的范式参考。

【HarmonyOS next】ArkUI-X休闲益智儿童拼图【进阶】

【HarmonyOS next】ArkUI-X休闲益智儿童拼图【进阶】

一、前言:当拼图遇上跨端开发

最近在开发一款跨平台的儿童拼图游戏时,我深刻体会到了ArkUI-X框架的威力——同一套代码竟能同时在华为Mate60 Pro和iPhone15上流畅运行!这不仅节省了开发成本,更重要的是确保了多端用户体验的一致性。今天我们就来聊聊这个项目的核心技术点,特别是拖动坐标计算图片剪影生成这两个让人"又爱又恨"的难点。 Harmony原生代码的运行效果转译为iOS代码后的运行效果

二、开发环境速览

  • 操作系统:macOS
  • 开发工具:DevEco Studio 5.0.4(Build 5.0.11.100)
  • 目标设备:华为Mate60 Pro & iPhone15
  • 开发语言:ArkTS
  • 框架版本:ArkUI API 16

💡 代码仓库地址:gitee


三、核心实现解析

3.1 拖动逻辑的三维坐标系

在拼图游戏中,精准的位置计算是灵魂所在。我们通过PanGesture手势监听实现拖动逻辑:

PanGesture()
  .onActionUpdate((event: GestureEvent) => {
    item.currentOffsetX = item.dragStartX + event.offsetX
    item.currentOffsetY = item.dragStartY + event.offsetY
  })

这里有两个关键点:

  1. 初始位置锚定dragStartX/Y记录拖动起始点
  2. 增量叠加计算event.offsetX/Y实时获取移动增量

当松手时进行位置判定,采用50vp吸附阈值实现自动归位:

const isSnapped = Math.abs(currentX - targetX) < 50 
               && Math.abs(currentY - targetY) < 50

3.2 图片剪影的魔法生成

为了让儿童更易识别目标位置,我们采用混合模式生成剪影效果:

Image(item.imageResource)
  .blendMode(BlendMode.DST_IN, BlendApplyType.OFFSCREEN)

这里的组合技解析:

  • BlendMode.DST_IN:将源图像与目标图像进行像素级混合
  • BlendApplyType.OFFSCREEN:在离屏缓冲区完成混合运算
  • 灰色背景+混合模式:生成半透明剪影效果

四、多端适配的实战技巧

4.1 横屏适配方案

通过window模块强制横屏显示:

window.getLastWindow().then(win => {
  win.setPreferredOrientation(Orientation.LANDSCAPE)
})

4.2 响应式布局设计

采用百分比+固定值的混合布局策略:

Stack()
  .width('100%')
  .height('100%')

4.3 性能优化要点

  • 使用@ObservedV2实现细粒度更新
  • Trace装饰器追踪关键数据变化
  • 动画采用硬件加速渲染:
animateTo({
  duration: 200
}, () => { /* 动画逻辑 */ })

五、项目亮点总结

技术维度 实现方案 跨端收益
手势交互 PanGesture+坐标计算 双端手势行为一致
视觉效果 BlendMode混合模式 图形渲染无平台差异
状态管理 @ObservedV2+Trace数据追踪 状态同步效率提升30%
布局系统 百分比+固定值混合布局 自适应不同屏幕尺寸

六、开发踩坑实录

6.1 拖动抖动问题

现象:iOS端出现轻微拖动延迟
解决方案:将动画时长从300ms调整为200ms,并启用硬件加速

6.2 剪影模糊问题

现象:华为设备剪影边缘模糊
修复方案:添加离屏渲染参数BlendApplyType.OFFSCREEN


七、未来优化方向

  1. 增加难度分级(3x3/4x4模式)
  2. 引入AI自动生成拼图形状
  3. 添加音效震动反馈
  4. 实现多人竞技模式

通过这个项目,我们验证了ArkUI-X框架的强大跨端能力。无论是华为的鸿蒙系统,还是iOS平台,都能保持90%以上代码复用率,真正实现了"一次开发,多端部署"的理想状态。期待ArkUI-X生态的进一步发展,为开发者打开更广阔的跨端开发新天地!

🚀 完整代码已开源,欢迎交流:gitee

iOS swift-markdown 自定文字颜色

最近在做AI的产品,用到了Markdown渲染,其中有一个变态的需求 需要对一段文字的某几个字颜色做特殊处理

效果

drawing

思路

Inline

其实实现思路很简单,一句话说完,就是自定义一个inline语法,然后实现MarkupVisitor协议的visitInlineAttributes方法

// https://docs.xiaohongshu.com/doc/35cfb0f7715be75c4e12f67ce3982a0b
    public mutating func visitInlineAttributes(_ attributes: InlineAttributes) -> NSAttributedString {

        let result = NSMutableAttributedString()

        for child in attributes.children {
            result.append(visit(child))
        }

        if attributes.attributes.hasPrefix("Color#") {
            let color = attributes.attributes.components(separatedBy: "#").last ?? "FFFFFF"
            result.addAttribute(.foregroundColor, value: UIColor.argb("#\(color)"))
        }

        return result
    }
    
    // let markdownText = """Opening ^[**这是一段加粗自定义颜色文字**](Color#333333) paragraph, with an ordered list of autumn leaves I found"""

Block

如果你是非inline的文字,而是一块,可以考虑使用该接口visitBlockDirective

// https://docs.xiaohongshu.com/doc/35cfb0f7715be75c4e12f67ce3982a0b
    public mutating func visitBlockDirective(_ blockDirective: BlockDirective) -> NSAttributedString {

        let result = NSMutableAttributedString()

        for child in blockDirective.children {
            result.append(visit(child))
        }

        if blockDirective.name.hasPrefix("Color#") {
            let color = blockDirective.name.components(separatedBy: "#").last ?? "FFFFFF"
            result.addAttribute(.foregroundColor, value: UIColor.argb("#\(color)"))
        }

        return result
    }
    
    // let markdownText = """Opening @Color#333333 { **这是一段加粗自定义颜色文字** } paragraph, with an ordered list of autumn leaves I found"""

测试Demo

参考链接

“一人得道,雨燕升天”:Swift 协议扩展助力 CoreData 托管类型(下)

在这里插入图片描述

概述

相信各位似秃非秃小码农们都同意,Swift 是一门现代化、安全且表现力足够丰富的语言。不过,它毕竟还是一种偏静态的语言,灵活性无法和 Python、ruby 之类的动态语言相提并论。

在这里插入图片描述

不过话虽如此,通过巧妙的一步步重构源代码,我们也可以用 Swift 完成之前貌似不可能完成的任务,所需的只是那么一丢丢耐心和执着而已。

在本篇博文中,您将学到如下内容:

  1. 一种很“硬”的解决方案
  2. 不想回到最初的样子
  3. 让编译器乖乖听话

希望在亲眼目睹本系列文章中 Swift 代码那循序渐进的重构和升华之后,小伙伴们倘若再遇到与此类似的语言设计问题,必能胸有成竹、胜券在握!

无需等待,Let‘s go!!!;)


5. 一种很“硬”的解决方案

对于前文中的问题,一种简单粗暴的解决方法是:强行让两种类型“蛮来生作”。

extension AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let request = Evaluator.fetchRequest() as! NSFetchRequest<Evaluator> // ⚠️ 强制转换
        return try context.fetch(request)
    }
}

如您所见,我们通过 Swift 强制类型转换语法,将 Evaluator.fetchRequest 实际的类型与 Evaluator 类型强行匹配。

虽然,这可以让编译器暂时闭嘴,但是也同时置我们自己于“刀山火海”之上!

上述代码的风险是:我们需要自行确保类型转换的安全性,若 Evaluator.fetchRequest() 实际返回的请求类型与 Evaluator 不匹配,将立即导致运行时发生崩溃。

6. 不想回到最初的样子

除了强行转换以外,我们还可以采用迂回战术:创建约束协议从而绕过编译器的“桎梏”。

首先,新建一个约束协议 Fetchable:

// 定义核心约束协议
protocol Fetchable: NSManagedObject {
    static func fetchRequest() -> NSFetchRequest<Self>
}

接着,对原来的 AchievementEvaluator 协议定义稍作调整,让其关联类型遵守我们上面创建的约束协议:

// 原协议调整
protocol AchievementEvaluator {
    associatedtype Evaluator: Fetchable & AchievementEvaluator // 新增 Fetchable 约束
    
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator]
}

随后,在 AchievementEvaluator 协议扩展中利用约束关系重新打造我们的 queryAll() 方法:

extension AchievementEvaluator where Evaluator: Fetchable {
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let request = Evaluator.fetchRequest() // ✅ 类型已明确为 NSFetchRequest<Evaluator>
        return try context.fetch(request)
    }
}

最后,让 Achv_NoBreakVictory 成就实体类遵守 Fetchable 约束协议即可:

extension Achv_NoBreakVictory: Fetchable, AchievementEvaluator {
    typealias Evaluator = Achv_NoBreakVictory
}

虽然这种思路本身没什么问题,但可惜的是编译器还是会义无反顾的再次大声说“我恨你!”:

在这里插入图片描述

Protocol 'Fetchable' requirement 'fetchRequest()' cannot be satisfied by a non-final class ('Achv_NoBreakVictory') because it uses 'Self' in a non-parameter, non-result type position

通过上面的错误信息不难发现:大家貌似又回到了之前的“故步自封”—— 我们仍然需要让 Achv_NoBreakVictory 类加上 final 成为“孤家寡人”才能得偿所愿,这是我们不希望看到的。

所以,我们又该如何随遇而安呢?

7. 让编译器乖乖听话

其实,解决之道并没有想象的那么复杂,我们只需重新设计 Fetchable 协议即可。

我们的核心思想是:

机制 作用
entityName 属性 动态获取实体名称,避免依赖自动生成的 fetchRequest()
手动构建 NSFetchRequest 通过 NSFetchRequest<Self>(entityName:) 确保类型匹配
子类覆盖 entityName 允许继承体系中的子类指定自己的实体名称

首先,通过 实体名称动态构建请求,绕过自动生成的 fetchRequest() 方法的限制:

protocol Fetchable: NSManagedObject {
    static var entityName: String { get } // 要求实体提供名称
}

extension Fetchable {
    static func fetchRequest() -> NSFetchRequest<Self> {
        // 手动构建请求,确保类型安全
        return NSFetchRequest<Self>(entityName: entityName)
    }
}

接下来,我们只要让 Achv_NoBreakVictory 类乖巧的提供 entityName 名称即可:

extension Achv_NoBreakVictory: Fetchable, AchievementEvaluator {
    static var entityName: String {
        "Achv_NoBreakVictory"
    }
    
    typealias Evaluator = Achv_NoBreakVictory
}

现在,编译源代码将如您所愿,一切都毫无问题,整个世界清净了!

通过 动态实体名称 + 手动构建请求,既能保持类的可继承性,又能满足 Core Data 类型安全要求。其关键点在于:

  1. 通过 entityName 属性解耦实体名称与类型推断。
  2. 子类必须显式覆盖 entityName 以正确映射数据库实体。

然而,我们还可以更进一步。

观察上面 Achv_NoBreakVictory 类中对应 entityName 属性的代码可以发现:每个成就实体类的 entityName 就是它们自己类的名称。既然如此,为什么不把 entityName 也直接放到协议扩展中去呢?

extension AchievementEvaluator where Evaluator: Fetchable {
    
    static var entityName: String {
        "\(Self.self)"
    }
    
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let request = Evaluator.fetchRequest() // ✅ 类型已明确为 NSFetchRequest<Evaluator>
        return try context.fetch(request)
    }
}

如上代码所示,我们将原本需要每个 AchievementEvaluator 实体类实现的 entityName 属性放到了 AchievementEvaluator 协议扩展中,大大减少了重复代码,这样的 DRY 和 KISS 谁能不爱呢?棒棒哒!

或者我们干脆彻底摆脱 entityName 属性的限制,直接将其嵌入到 Fetchable 协议扩展的 fetchRequest() 方法中,让实现百尺竿头、更入佳境:

extension Fetchable {
    static func fetchRequest() -> NSFetchRequest<Self> {
        // 手动构建请求,确保类型安全
        return NSFetchRequest<Self>(entityName: "\(Self.self)")
    }
}

至此,我们通过不断迭代重构,彻底摆脱了最初文章开头 CoreData 成就托管类实现的恼人纠缠,小伙伴们还不赶快给自己一个大大的赞吧!❤️

总结

在本篇博文中,我们借助于精心设计的 Fetchable 约束协议成功的摆脱了 Swift 协议扩展中的“磨搅讹绷”,小伙伴们值得拥有!

感谢观赏,再会啦!8-)

“一人得道,雨燕升天”:Swift 协议扩展助力 CoreData 托管类型(上)

在这里插入图片描述

概述

相信各位似秃非秃小码农们都同意,Swift 是一门现代化、安全且表现力足够丰富的语言。不过,它毕竟还是一种偏静态的语言,灵活性无法和 Python、ruby 之类的动态语言相提并论。

在这里插入图片描述

不过话虽如此,通过巧妙的一步步重构源代码,我们也可以用 Swift 完成之前貌似不可能完成的任务,所需的只是那么一丢丢耐心和执着而已。

在本篇博文中,您将学到如下内容:

  1. 背景故事
  2. 想法不错,无奈编译器不允许!
  3. “不情愿”的 final
  4. DRY 制胜法宝:协议扩展(Protocol Extension)

希望在亲眼目睹本系列文章中 Swift 代码那循序渐进的重构和升华之后,小伙伴们倘若再遇到与此类似的语言设计问题,必能胸有成竹、胜券在握!

无需等待,Let‘s go!!!;)


1. 背景故事

我们的项目基于 SwiftUI + CoreData 构建,在数据库中我们需要为用户创建各种各样的成就(Achievements),因为每种成就本身有很大的不同(字段、获取手段等),所以考虑在 CoreData 数据库中使用抽象基类 + 实体类的组成方法:

  • Achievement 类是成就的抽象基类,其中包含所有成就都共有的字段和方法;
  • Achv_NoBreakVictory 类和其它实体类都“派生”于 Achievement 基类,对应于每一种具体的成就,它们包含自己独有的字段和方法;

Achievement 和 Achv_NoBreakVictory 类的定义如下所示:

@objc(Achievement)
public class Achievement: NSManagedObject {

}

@objc(Achv_NoBreakVictory)
public class Achv_NoBreakVictory: Achievement {

}

对于 Achv_NoBreakVictory 这一成就实体托管类来说,我们往往需要查询它的所有实例,所以有必要写一个方法来达成此目的:

static func queryAll(context: NSManagedObjectContext) throws -> [Achv_NoBreakVictory] {
        let req: NSFetchRequest<Achv_NoBreakVictory> = fetchRequest()
        return try context.fetch(req)
    }

但是问题来了:如果我们有一大堆这样的实体类,难道要不厌其烦的在每个类中实现上面的方法吗?

答案当然是大大的 NO!

2. 想法不错,无奈编译器不允许!

因为 Achievement 会派生出很多不同的成就实体子类,这些子类同样需要上面的 queryAll 方法来查询它们各自的所有实例,为了规范它们共同的“言行”,我们决定创建一个协议让它们来遵守:

protocol AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Self]
}

接下来,我们需要让 Achv_NoBreakVictory 实体类遵守 AchievementEvaluator 协议:

extension Achv_NoBreakVictory: AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Achv_NoBreakVictory] {
        let req: NSFetchRequest<Achv_NoBreakVictory> = fetchRequest()
        return try context.fetch(req)
    }
}

不幸的是,这样做的话编译器会立即大声抱怨:

在这里插入图片描述

Protocol 'AchievementEvaluator' requirement 'queryAll(context:)' cannot be satisfied by a non-final class ('Achv_NoBreakVictory') because it uses 'Self' in a non-parameter, non-result type position

这个编译错误是由于 Swift 协议中 Self 类型与类继承体系之间的冲突引起的。要解决这个问题,需要理解以下核心机制:

  1. 协议中 Self 的严格性
    Swift 协议中的 Self 代表「实现该协议的具体类型」。当协议方法返回 [Self] 时,要求实现该方法的类型必须在编译时明确自身类型
  2. final 类的继承风险
    如果 Achv_NoBreakVictory 是非 final 类,它可以被继承(如 class SubAchv: Achv_NoBreakVictory)。此时子类 SubAchv 必须实现 spawnAll() -> [Self],但继承自父类的 spawnAll() 实际返回的是 [Achv_NoBreakVictory] 而非 [SubAchv],所以这会导致类型不匹配,违背协议要求。

那我们该如何解决呢?

3. “不情愿”的 final

经过查看上面的错误提示,我们可以幡然醒悟,一种简单的解决方案应运而生,即将 Achv_NoBreakVictory 类变为 final 类,可以让编译器“敢怒不敢言”:

public final class Achv_NoBreakVictory: Achievement {}

不过,或许我们的 Achv_NoBreakVictory 类是“委托” CoreData 模型编辑器自动生成的,这样的话每次更新 Achv_NoBreakVictory 类的内容都需要费劲手动再添加 final 关键字,不烦吗?

除了强制让 Achv_NoBreakVictory 类“后继无人”以外,另一种颇为 Nice 的解决方法是为 AchievementEvaluator 协议添加关联类型:

protocol AchievementEvaluator {
    associatedtype Evaluator
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator]
}

通过上面一番操作之后,我们 Achv_NoBreakVictory 类扩展中 queryAll() 方法的代码已经可以顺利通过编译了,厉害了我的秃们!

4. DRY 制胜法宝:协议扩展(Protocol Extension)

通过仔细观察上面 Achv_NoBreakVictory 类扩展中的 queryAll() 方法,聪明的小伙伴们不难发现:每个 Achievement 实体类 queryAll() 方法的代码实际上都大同小异,我们实在没必要“痴鼠拖姜”的一一重复实现它们。

侵淫苹果撸码多年的秃头小码农们都知道,Swift 协议有一种机制专注于解决此事,它就是协议扩展(Protocol Extension)

简单来说,我们可以将 queryAll() 方法直接放在 AchievementEvaluator 协议扩展里,而不是在遵守它的每个类里:

extension AchievementEvaluator {
    static func queryAll(context: NSManagedObjectContext) throws -> [Evaluator] {
        let req: NSFetchRequest<Evaluator> = Evaluator.fetchRequest()
        return try context.fetch(req)
    }
}

extension Achv_NoBreakVictory: AchievementEvaluator {
    typealias Evaluator = Achv_NoBreakVictory
    
    /*
    static func queryAll(context: NSManagedObjectContext) throws -> [Achv_NoBreakVictory] {
        let req: NSFetchRequest<Achv_NoBreakVictory> = fetchRequest()
        return try context.fetch(req)
    }*/
}

在上面的代码中,我们将原本位于实体类 Achv_NoBreakVictory 中的 queryAll 方法调皮地瞬移到了 AchievementEvaluator 协议扩展里面。

不过这样一来,编译器的抱怨也会再次“卷土重来”:

在这里插入图片描述

Cannot assign value of type 'NSFetchRequest<any NSFetchRequestResult>' to type 'NSFetchRequest<Self.Evaluator>'

造成这种错误的根本原因是:在 Swift 中处理 Core Data 的 NSFetchRequest 泛型类型时,没有确保类型系统的严格匹配。

  • NSFetchRequest<Evaluator> 的泛型要求:Core Data 的 fetchRequest() 默认返回 NSFetchRequest<NSFetchRequestResult>,而协议中定义的 Evaluator 关联类型要求返回具体的 Evaluator 类型,导致类型不匹配。
  • 协议扩展的泛型约束不足:编译器无法确认 Evaluator.fetchRequest() 返回的请求类型是否与 Evaluator 类型一致。

那么,此时我们又该何去何从呢?

在下一篇博文中,我们将继续 AchievementEvaluator 协议扩展的进化之旅,敬请期待吧!

总结

在本篇博文中,我们讨论了在用 Swift 协议扩展优化和重构 CoreData 托管类型功能遇到的问题,并初步提供了一些“不尽如人意”的解决方法。

感谢观赏,我们下一篇再会!8-)

深入剖析 RxSwift 中的 Queue:环形队列的 Swift 实战

高性能事件调度背后的数据结构机密


目录

  1. 背景
  2. 为什么选择环形队列?
  3. 环形队列数据结构概览
  4. 关键实现细节
    1. 数据布局与索引计算
    2. 入队 enqueue
    3. 出队 dequeue
    4. 动态扩容与缩容 resize
  5. 性能分析
  6. 与其他数据结构对比
  7. 在 RxSwift 中的实际应用场景
  8. 示例:如何在项目中复用该实现
  9. 总结
  10. 参考链接

背景

RxSwift 的内部实现里,无论是事件传递还是调度缓冲,环形队列(circular queue) 都扮演着基石般的角色。

  • 事件先进先出 (FIFO):当前事件必须处理完才能继续下一个。
  • 背压/缓存:当上游过快、下游较慢时,诸如 observeOnflatMapconcatMap 等操作符会把事件暂存于队列,待消费端就绪后再逐一处理。

为什么选择环形队列?

特性 优势 适用场景
O(1) 入队/出队 仅索引递增,无需移动元素 高频、短生命周期任务
内存连续 优秀的缓存命中率 调度器、事件缓冲
动态扩缩容简单 比链表更高效 流量激增或骤减
低额外开销 仅两个索引变量 移动端资源受限

相比链表,环形数组几乎在所有维度都更贴合 RxSwift “大量、快速、短命事件”的特征。

  • 链表 vs 数组:链表虽不需扩容,但每个节点单独的创建和销毁,在高并发写入场景中频繁的创建和销毁节点反而更慢,且容易造成内存碎片。
  • 缓存命中(CPU cache locality):数据被访问的方式与缓存(cache)的效率之间的关系。它体现了程序在访问内存时是否能有效地利用 CPU 的高速缓存,以提高性能

环形队列数据结构概览

环形队列(Circular Queue)是什么?
环形队列是一种使用 固定大小或按需调整的顺序存储空间 来实现队列 (FIFO) 语义的数据结构。它将底层数组的首尾视为相连,通过“回环”或取模运算,让 最后一个槽位的下一个位置 重新映射到索引 0。这样无需搬移元素即可完成出队操作,同时保持内存连续性。

概念要点

关键词 说明
固定容量 & 动态容量 最简单实现容量固定;RxSwift 通过“两倍扩张 + 四分之一缩容”策略自动伸缩。
双指针/双索引 pushNextIndex 指向待写入槽位;dequeueIndex 指向待读取槽位。差值即为队列长度。
逻辑连续 vs 物理分裂 队列元素在逻辑上按时间顺序连续,但物理存储可能被分成“数组尾段 + 数组头段”两段。

下图演示了典型的环形队列状态变化(绿色为已占用单元):

image.png

image.png

  • pushNextIndex:下一个写入位置
  • dequeueIndex:通过计算得到的队头位置
  • 当写指针越过数组尾部,会从索引 0 重新开始形成“环”。

环形队列向外公开的 API

方法 / 属性 作用 时间复杂度
enqueue(_ element: T) 入队,将元素追加至尾部 O(1) 均摊
dequeue() -> T? 出队,弹出队头元素,队列为空返回 nil O(1)
peek() -> T 只读队头但不移除 O(1)
isEmpty: Bool 队列是否为空 O(1)
count: Int 当前元素数量 O(1)
makeIterator() Swift Sequence 迭代支持 O(n) 整体,但单步 O(1)

这些接口与标准队列抽象保持一致,开发者无需关心内部的环形实现即可完成常见操作。数组带来的 Cache 友好和一次性拷贝优化,使得这些 API 在实际运行中维持极低的延迟。


关键实现细节

数据布局与索引计算

private var storage: ContiguousArray<T?>
private var pushNextIndex = 0   // **写指针** —— 指向下一次写入的位置
private var innerCount    = 0   // 当前元素数量

private var dequeueIndex: Int { // **读指针** —— 通过计算得到
    let index = pushNextIndex - count
    return idx < 0 ? index + storage.count : index
}

为什么保存 pushNextIndex,而 计算 dequeueIndex

在 RxSwift 场景里,入队(enqueue)操作通常比出队(dequeue)更高频,在响应式流中,数据通常由外部事件(如网络响应、用户输入、定时器)快速产生,然后再异步地被消费者(观察者、订阅者)慢慢处理。因此,为了简化 入队(enqueue)操作,并保证 扩容后的数据迁移逻辑更加高效和清晰,因此选择保存和队尾元素相关索引。

计算公式拆解

  • 队头理论推导dequeueIndex = (pushNextIndex - innerCount + capacity) % capacity
  • 实际代码实现:先做减法再按需加 capacity

在编码过程中,尽量避免使用乘*、除/、模%、浮点数,效率低下,CPU做这些操作比较耗时;因此,RxSwift队头实际的计算转换成了加和减

这样既可保持常量级计算,又能在无需硬件除法指令的情况下完成取模。

入队 enqueue enqueue

mutating func enqueue(_ element: T) {
    if count == storage.count {        // 存满 → 扩容
        resizeTo(max(storage.count, 1) * resizeFactor)
    }
    storage[pushNextIndex] = element   // 写入
    pushNextIndex += 1
    innerCount += 1
    if pushNextIndex >= storage.count { // 环回
        pushNextIndex -= storage.count
    }
}
  1. 空间不足 → 扩容
  2. 数据写入
  3. 写索引自增,如越界则回环

出队 dequeue

mutating func dequeue() -> T? {
    if self.count == 0 {
        return nil
    }

    defer {
        let downsizeLimit = storage.count / (resizeFactor * resizeFactor)
        if count < downsizeLimit, downsizeLimit >= initialCapacity {
            resizeTo(storage.count / resizeFactor)  // 缩容
        }
    }
    return dequeueElementOnly()
}

 private mutating func dequeueElementOnly() -> T {
    precondition(count > 0)
    
    let index = dequeueIndex

    defer {
        storage[index] = nil
        innerCount -= 1
    }

    return storage[index]!
}

  • 先读后清 ,缩容操作之所以放在 defer 中,是为了确保在元素真正出队(dequeueElementOnly)之后再进行判断和可能的缩容操作;基于更新后的真实元素数量的“延后清理或操作”的模式。

动态扩容与缩容 resize

环形队列的数组在写满或低于一定数量时会触发容量调整,核心目标是:

  1. 保证逻辑顺序不变 —— 队列是 FIFO,调整后索引必须保持原先的出队顺序;
  2. 一次性线性拷贝 —— 尽可能利用批量拷贝,避免逐元素迁移;
  3. 简化后续索引运算 —— 调整后让队头落到 0pushNextIndex落到 count
mutating private func resizeTo(_ size: Int) {
    // 申请新数组
    var newStorage = ContiguousArray<T?>(repeating: nil, count: size)
    // 保存现有元素总数
    let count = self.count
    // 旧队头位置 & 尾段剩余空间
    let dequeueIndex = self.dequeueIndex
    let spaceToEndOfQueue = storage.count - dequeueIndex
    // **** 分段拷贝 ****
    //第一次拷贝的原来的尾段
    let countElementsInFirstBatch = Swift.min(count, spaceToEndOfQueue)
    // 第二次拷贝的原来的头段
    let numberOfElementsInSecondBatch = count - countElementsInFirstBatch
    // 原来的尾段放到新的数组开始的位置
    newStorage[0 ..< countElementsInFirstBatch] = storage[dequeueIndex ..< (dequeueIndex + countElementsInFirstBatch)]
    // 原来的头段追加到新数组中,此时队头就是新数组索引0的位置,pushNextIndex即为新数组的数量
    newStorage[countElementsInFirstBatch ..< (countElementsInFirstBatch + numberOfElementsInSecondBatch)] = storage[0 ..< numberOfElementsInSecondBatch]
    // 更新索引与存储
    self.innerCount = count
    pushNextIndex = count
    storage = newStorage

}

为什么要“两段复制”?

  • 可能的分裂:当队列发生环回后,逻辑上连续的元素会被物理地分成“尾段 + 头段”两段。若直接拷贝整个旧数组,元素顺序将错乱。
  • 维持 FIFO 顺序:先复制尾段(从 dequeueIndex 到旧数组末尾),再复制头段(索引 0 开始)的剩余部分,可在新数组 [0..<count) 重新拼接出正确的时间顺序。
  • 线性内存访问:每一段都是线性区域,且可被系统优化为块拷贝。

队头元素位置的变化

操作前 操作后
队头索引 = dequeueIndex (可能 > 0) 固定为 0
pushNextIndex (任意位置) 固定为 count

缩容与扩容共用同一逻辑,只是 size 参数不同,因为尾段和头段均是连续存储。当元素数量小于 capacity / 4 且不低于初始容量时触发缩容,确保内存占用与业务峰谷相匹配。

性能分析

  • 时间复杂度
    • 入队/出队:O(1)
    • 扩/缩容:均摊 O(1)(几何倍数增长,摊销成本极低)
  • 对比链表
    • 链表 enqueue 需分配节点;数组仅更新索引
    • 链表缺乏 缓存命中

虽然扩容是一次性 O(n),但每次扩容都是在上一次扩容后进行了很多次 O(1) 的插入之后才发生,触发扩容的代价会被之前的多次 O(1) 插入“摊销”掉。


与其他数据结构对比

特性 环形数组 单向链表 双端队列 Deque
内存布局 连续 离散 连续
入/出队时间 O(1) O(1) (指针操作) O(1)
扩容开销 copy (偶发) copy (偶发)
缓存命中
适合场景 高频、小对象 大对象、频繁插入删除中间节点 双端操作

在 RxSwift 中的实际应用场景

场景 描述 优势体现
调度器 (SerialDispatchQueueScheduler) 将任务缓存至队列,串行执行 线程安全 + 低延迟
操作符 observeOn 上游高速 → 队列缓存 → 下游消费 背压管理
合并流 (flatMap, concatMap) 子流事件临时缓冲 减少锁/条件变量

背压管理(Backpressure Management) : 是一种控制数据流速的机制,目的是防止生产者(数据发送方)发送数据过快,导致消费者(数据接收方)来不及处理,最终引发资源耗尽、缓冲区溢出或系统崩溃等问题。


示例:如何在项目中复用该实现

var q = Queue<Int>(capacity: 4)

// 写入
(1...10).forEach { q.enqueue($0) }

// 消费
while !q.isEmpty {
    print(q.dequeue()!)
}

总结

  • 环形队列 通过常数级操作与良好缓存局部性,完美契合 RxSwift 的事件驱动模型。
  • 精巧的 写索引 + 计算读索引 方案,简化了状态管理。
  • 扩缩容逻辑在数组层面“一劳永逸”,保持 API 简洁。

掌握并善用这一数据结构,不仅能帮助你更深入理解 RxSwift 的内部机理,也能在自己的高性能队列、调度器甚至网络层缓存中大显身手。


参考链接

  • RxSwift 源码 Source/Schedulers/Queue.swift

Xcode 14.3 和 iOS 16.4 为 SwiftUI 带来了哪些新功能?

在这里插入图片描述

0. 概览

今年年初,Apple 推出了最新的 Xcode 14.3 以及对应的 iOS 16.4 。

与此同时,它们对目前最新的 SwiftUI 4.0 也添加了一些新功能:

  • sheet 弹窗后部视图(Interact with a view Behind a sheet)可交互;
  • sheet 弹窗背景透明化;
  • 调整 sheet 弹窗顶角弧度;
  • 控制弹窗内滚动手势优先级;
  • 定制紧密(compact-size )尺寸下 sheet 弹窗大小;
  • Xcode 预览(Preview)模式下对调试输出的支持;

让我们依次来了解一下它们吧。

Let‘s go!!!;)


1. sheet 后部视图可交互

在 iOS 16.4 之前,SwiftUI 中 sheet 弹窗后,如果点击其后部的视图会导致弹窗立即被关闭,从而无法与弹窗后部的视图进行交互。

从 iOS 16.4 开始,我们可以为 sheet 弹窗应用 presentationBackgroundInteraction() 方法,以达到不关闭弹窗而与后部视图交互之目的:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isPresented = false
    @State private var number = 0
    
    var body: some View {
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            Button("Sheet") {
                isPresented = true
            }
            .buttonStyle(.borderedProminent)
            .padding()
            
            VStack {
                Button("产生随机数: \(number)"){
                    number = Int.random(in: 0..<10000000)
                }
                .foregroundColor(.white)
                .font(.title.weight(.black))
            }.padding(.top, 200)
        }
        .sheet(isPresented: $isPresented) {
            Text("大熊猫侯佩 @ csdn")
                .font(.headline)
                .presentationDetents([.height(120), .medium, .large])
                // 开启后部视图交互
                .presentationBackgroundInteraction(.enabled)
        }
    }
}

在这里插入图片描述

2. sheet 背景透明化

从 iOS 16.4 开始,我们可以为 sheet 弹窗选择透明样式,更好的美化弹出窗口的显示效果。

如下代码所示,我们在 sheet 弹窗上应用了 presentationBackground(_: ) 修改器以实现透明磨砂效果:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetTransparency = false
    
    var body: some View {
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents([.height(120), .medium, .large])
                        // 或使用 .background 调用 presentationBackground() 方法效果相同
                        //.presentationBackground(.background)
                }
                
                Spacer()
                
                Button("透明弹出") {
                    isSheetTransparency = true
                }
                .sheet(isPresented: $isSheetTransparency) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents([.height(120), .medium, .large])
                        .presentationBackground(.ultraThinMaterial)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

3. sheet 顶部弧度调整

感觉 sheet 弹窗顶角生硬无弧度的小伙伴们有福了,从 iOS 16.4 开始,SwiftUI 开始支持调整 sheet 弹出窗口顶角的弧度了。

我们可以使用 .presentationCornerRadius() 修改器来实现这一功能:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetRadius = false
    
    var body: some View {
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents(.height(120), .medium, .large])
                }
                
                Spacer()
                
                Button("顶角圆润弧度弹出") {
                    isSheetRadius = true
                }
                .sheet(isPresented: $isSheetRadius) {
                    Text("大熊猫侯佩 @ csdn")
                        .font(.headline)
                        .presentationDetents([.height(120), .medium, .large])
                        .presentationCornerRadius(30.0)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

4. sheet 滚动手势优先级调整

在 iOS 16.4 之前,如果我们 sheet 尺寸可变弹窗中包含滚动视图(比如 List,ScrollView 等),当用户在弹窗中滚动将会首先引起弹窗尺寸的改变,而不是其滚动内容的改变。

在 iOS 16.4 之后,我们可以调整 sheet 弹窗滚动手势优先级,以确保首先滚动其内容而不是改变弹窗尺寸。

这是通过 .presentationContentInteraction(.scrolls) 方法来实现的:

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetScrollable = false
    
    var body: some View {
        
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    VStack(spacing: 16) {
                        Text("大熊猫侯佩 @ csdn")
                            .font(.headline)
                        List(0..<50, id: \.self){ i in
                            Text("Item \(i)")
                                .font(.subheadline)
                        }
                        .listStyle(.plain)
                    }
                    .padding()
                    .presentationDetents([.height(120), .medium, .large])
                }
                
                Spacer()
                
                Button("滚动高优先级弹出") {
                    isSheetScrollable = true
                }
                .sheet(isPresented: $isSheetScrollable) {
                    VStack(spacing: 16) {
                        Text("大熊猫侯佩 @ csdn")
                            .font(.headline)
                        List(0..<50, id: \.self){ i in
                            Text("Item \(i)")
                                .font(.subheadline)
                        }
                        .listStyle(.plain)
                    }
                    .padding()
                    .presentationDetents([.height(120), .medium, .large])
                    .presentationContentInteraction(.scrolls)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

5. 定制 sheet 在紧密尺寸下的大小

在 iOS 16.4 之前,如果在 iPhone 横屏时 sheet 弹窗,则弹出窗口将会铺满整个屏幕。

从 iOS 16.4 开始,我们可以为弹窗应用新的 .presentationCompactAdaptation(_: ) 修改器来改变横屏时弹窗的大小:

struct SheetView: View {
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        VStack(spacing: 16) {
            Text("大熊猫侯佩 @ csdn")
            Button("关闭"){
                dismiss()
            }
        }
    }
}

@available(iOS 16.4, *)
struct ContentView: View {
    @State private var isSheet = false
    @State private var isSheetCompactSizeCustom = false
    
    var body: some View {
                
        ZStack(alignment: .top) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            HStack {
                Button("弹出") {
                    isSheet = true
                }
                .sheet(isPresented: $isSheet) {
                    SheetView()
                        .padding()
                        .frame(width: 200)
                        .presentationDetents([.height(200), .medium, .large])
                }
                
                Spacer()
                
                Button("自定义尺寸弹出") {
                    isSheetCompactSizeCustom = true
                }
                .sheet(isPresented: $isSheetCompactSizeCustom) {
                    VStack(spacing: 16) {
                        Text("大熊猫侯佩 @ csdn")
                    }
                    .padding()
                    .frame(width: 350)
                    .presentationDetents([.height(200), .medium, .large])
                    .presentationCompactAdaptation(.sheet)
                }
            }
            .font(.headline)
            .buttonStyle(.borderedProminent)
            .padding(.top, 200)
            .padding(.horizontal, 50)
        }
    }
}

在这里插入图片描述

6. Xcode 预览模式对调试输出的支持

Xcode 14.3 之前,我们在预览(Preview)模式中测试 SwiftUI 界面功能时无法观察调试语句( print 等方法)的输出结果,必须在模拟器或真机中运行才可以在 Xcode 底部调试小窗口中看到 print() 等方法的输出。

从 Xcode 14.3 开始,以预览模式运行 App 时也可以在调试窗口中看到调试语句的输出了,真是太方便了:

@available(iOS 16.4, *)
struct ContentView: View {
    
    var body: some View {
                
        ZStack(alignment: .center) {
            Rectangle()
                .fill(Gradient(colors: [.red,.green]).opacity(0.66))
                .ignoresSafeArea()
            
            Button("显示 debug 输出") {
                print("显示随机数: \(Int.random(in: 0..<10000000))")
            }
        }
    }
}

在这里插入图片描述

7. 总结

在本篇博文中,我们介绍了在 Xcode 14.3 和 iOS 16.4 中 SwiftUI 为开发者带来的新方法和新功能,解决了诸多燃眉之急的问题,小伙伴们不想赶快尝试一下吗?🚀

感谢观赏,再会!8-)

音视频学习笔记 02 读取数据

在笔记 01 的代码里读取数据可能会出错。

av_read_frame() 返回 -35(即 AVERROR(EAGAIN) 或 AVERROR_EOF)表示当前无法读取到数据包(packet),但未来可能可以。这通常发生在以下几种情况:

1. 非阻塞模式下数据未就绪

  • 如果你将 AVFormatContext 设置为 非阻塞模式(通过 AVFMT_FLAG_NONBLOCK),av_read_frame() 可能会立即返回 EAGAIN(即 -35),表示当前没有可读的数据包,但稍后重试可能会成功。

  • 解决方法

    • 如果是故意使用非阻塞模式,需要循环重试(或其他逻辑处理)。
    • 如果不需要非阻塞模式,确保未设置 AVFMT_FLAG_NONBLOCK

2. 流结束(EOF)

  • 如果已经读取完所有数据包(如文件末尾或直播流中断),av_read_frame() 可能返回 EAGAIN 或 EOF

  • 解决方法

    • 检查 fmt_ctx->pb->eof_reached 或 pkt.flags 是否包含 AV_PKT_FLAG_EOF
    • 如果是正常结束,可以关闭上下文或重新初始化。

3. 输入流格式问题

  • 某些特殊格式(如实时流、网络流)可能需要更多初始化时间,或者数据未及时到达。

  • 解决方法

    • 确保输入源正常(如文件路径正确、网络流可访问)。
    • 检查 fmt_ctx 是否成功打开(avformat_open_input() 返回 0)。

4. 编码器/复用器未正确初始化

  • 如果 fmt_ctx 是输出上下文(例如用于编码或复用),可能需要先写入头信息(avformat_write_header())。

  • 解决方法

    • 确保正确初始化输出上下文。

调试步骤

  1. 检查错误码的具体含义

    char errbuf[AV_ERROR_MAX_STRING_SIZE];
    av_strerror(ret, errbuf, sizeof(errbuf));
    fprintf(stderr, "av_read_frame failed: %s\n", errbuf);
    

    输出错误详情(如 "Resource temporarily unavailable" 或 "End of file")。

  2. 验证输入源

    • 确认 fmt_ctx 已通过 avformat_open_input() 成功打开。
    • 检查 fmt_ctx->streams 是否包含有效的流(如 fmt_ctx->nb_streams > 0)。
  3. 重试逻辑

    • 如果是非阻塞模式或实时流,可能需要循环调用 av_read_frame() 直到返回 0

示例代码(处理非阻塞情况)

AVPacket pkt;
av_init_packet(&pkt);
while ((ret = av_read_frame(fmt_ctx, &pkt)) == AVERROR(EAGAIN)) {
    // 等待或处理其他任务(如非阻塞模式)
    usleep(1000); // 避免忙等待
}
if (ret < 0 && ret != AVERROR_EOF) {
    // 真实错误
    fprintf(stderr, "Error reading packet: %s\n", av_err2str(ret));
} else if (ret == AVERROR_EOF) {
    // 正常结束
    printf("Reached end of file.\n");
} else {
    // 成功读取到数据包
    // 处理 pkt...
    av_packet_unref(&pkt);
}

如果问题仍存在,请提供更多上下文代码(如 fmt_ctx 的初始化部分)。

上面的代码 和 解释 由 deepseek 给出。

修改后的代码:

ViewController

//
//  ViewController.swift
//  myapp
//
//  Created by mac on 2025/6/23.
//

import Cocoa

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.setFrameSize(NSSize(width: 320, height: 240))
        let btn = NSButton.init(title: "Button", target: nil, action: nil)
        btn.title = "Hello"
        btn.frame = NSRect(
            x: 320 / 2 - 40,
            y: 240 / 2 - 15,
            width: 80,
            height: 30
        )
        btn.bezelStyle = .rounded
        btn.setButtonType(.pushOnPushOff)

        // callback
        btn.target = self
        btn.action = #selector(myFunc)

        self.view.addSubview(btn)
    }

    @objc
    func myFunc() {
        record_audio()
    }

    override var representedObject: Any? {
        didSet {
            // Update the view, if already loaded.
        }
    }
}

bridge 文件 myapp/myapp/myapp-Bridging-Header.h

//
//  Use this file to import your target's public headers that you would like to
//  expose to Swift.
//

#import "testc.h"

c 头文件

//
//  testc.h
//  myapp
//
//  Created by mac on 2025/6/23.
//

#ifndef testc_h
#define testc_h

#include "libavcodec/avcodec.h"
#include "libavdevice/avdevice.h"
#include "libavformat/avformat.h"
#include "libavutil/avutil.h"
#include <stdio.h>
#include <unistd.h>

void record_audio(void);

#endif /* testc_h */

c 代码体

//
//  testc.c
//  myapp
//
//  Created by mac on 2025/6/23.
//

#include "testc.h"

void record_audio(void) {
    int ret = 0;
    char errors[1024] = {
        0,
    };

    AVFormatContext *fmt_ctx = NULL;

    // 读出来的数据存储到这个 packet 里面
    AVPacket pkt;
    int count = 0;

    // [[video device]:[audio device]]
    //    char *devicename = ":2";
    char *devicename = ":1";
    //    char *devicename = ":0";

    // 设置日志级别
    av_log_set_level(AV_LOG_DEBUG);

    // 1 register audio device
    avdevice_register_all();

    // 2 get format
    // const AVInputFormat *iformat = av_find_input_format("avfoundation");
    const AVInputFormat *input_format = av_find_input_format("avfoundation");

    AVDictionary *options = NULL;
    av_dict_set(&options, "sample_rate", "44100", 0);
    av_dict_set(&options, "channels", "2", 0);

    // 3 open device
    ret = avformat_open_input(&fmt_ctx, devicename, input_format, &options);

    if (ret < 0) {
        av_strerror(ret, errors, 1024);
        printf(stderr, "Failed to open audio device [%d] %s\n", ret, errors);

        return;
    }

    // AVPacket 使用之前要初始化
    //    av_init_packet(&pkt);
    av_new_packet(&pkt, 512);
    //    ret = av_read_frame(fmt_ctx, &pkt);

    while (count++ < 500) {
        // read data from device
        while ((ret = av_read_frame(fmt_ctx, &pkt)) == AVERROR(EAGAIN)) {
            // 等待或处理其他任务(如非阻塞模式)
            usleep(1000); // 避免忙等待
        }

        if (ret < 0 && ret != AVERROR_EOF) {
            // 真实错误
            fprintf(stderr, "Error reading packet: %s\n", av_err2str(ret));
        } else if (ret == AVERROR_EOF) {
            // 正常结束
            printf("Reached end of file.\n");
        } else {
            // 成功读取到数据包
            // 处理 pkt...

            av_log(NULL, AV_LOG_INFO, "count = %d, ret = %d, pkt.size = %d\n",
                   count, ret, pkt.size);

            // 每次使用完释放包
            av_packet_unref(&pkt);
        }
    }

    // close device and 释放上下文
    avformat_close_input(&fmt_ctx);

    printf("this is a c function\n");

    av_log(NULL, AV_LOG_DEBUG, "hello world from av_log \n ");

    return;
}


其中的 usleep 可以通过 man usleep 查看用法, 和引用的 c 头文件 #include <unistd.h>

仓库: gitee.com/dbafu/imooc…

记录App切后台时AppIcon变成默认雪花icon问题

xcode做新项目时,设置了app图标。发现点击app进入前台时,App Icon是正常的,但是回到桌面时App Icon又变成了默认的雪花图标。

之前也遇到过,但是不求甚解,在此列出解决方案。

问题1: AppIcon的设置

随便设置了个图片为app图标,编译报错xxx/Assets.xcassets: The stickers icon set or app icon set named "AppIcon" did not have any applicable content. 同时appIcon可视化窗口显示黄色⚠️图标。

Xcode 提示你在 Assets.xcassets 中名为 "AppIcon" 的 App 图标集合里没有提供任何有效的图片资源。

iOS 应用要求必须有完整的 AppIcon 集合,并且要包含适用于各种设备和分辨率的图标尺寸。如果没有正确设置这些图标,App 就无法通过 App Store 审核,甚至可能在某些模拟器或真机上运行异常。

我使用了makeappicon.com/ 生成appIcon图标。 网站生成的结果包含AppIcon.appiconset,直接把AppIcon.appiconset替换原项目中Assets中的appIcon即可。 image.png 结果如下 image.png

问题2: 切后台appIcon变成默认雪花icon

现在成功设置appIcon后,切后台时发现appIcon变成了默认的雪花icon。

原因是系统缓存了旧图标,iOS 系统有时会缓存应用的图标缩略图,尤其是多任务界面中的预览图。即使你更新了图标,也可能不会立即刷新。

解决办法: 卸载重装

现在能正常显示了

image.png

Swift 的 `@resultBuilder`;构建自己的HTML DSL解析器,实现简单JSX功能

🧠 1. 什么是 @resultBuilder

@resultBuilder 是 Swift 提供的一种 自定义 DSL(领域特定语言) 支持工具,允许你用类似“代码块”的语法构造复杂的值结构。
其广泛应用于 SwiftUI(如 @ViewBuilder)、字符串构建器、HTML DSL 等场景。

✅ 你可以将它理解为:

“让多个表达式拼接或组合为一个最终值的机制”。


🧱 2. 基本结构

swift
复制编辑
@resultBuilder
struct MyBuilder {
    static func buildBlock(_ components: T...) -> T
}

其中 T 是你最终组合出的类型,比如 StringView、数组等。


🧩 3. 常用构建方法(方法签名)

一个完整的 resultBuilder 可以实现以下静态方法,支持各种控制流结构:

方法 用途
buildBlock(_:) 拼接多个表达式的核心方法(必须)
buildExpression(_:) 将单个表达式转为构建器的中间值
buildOptional(_:) 支持 if letif 条件语句
buildEither(first:) / buildEither(second:) 支持 if-else 的两个分支
buildArray(_:) 支持 for-in 结构
buildLimitedAvailability(_:) 支持 #available 编译条件
buildFinalResult(_:) 可选:对最终返回值进行最后处理(Swift 5.9+)

🧪 4. 简单例子:拼接字符串

swift
复制编辑
@resultBuilder
struct StringBuilder {
    static func buildBlock(_ components: String...) -> String {
        components.joined()
    }
}

func makeString(@StringBuilder _ content: () -> String) -> String {
    content()
}

let result = makeString {
    "Hello, "
    "world!"
}
// 输出: "Hello, world!"

🪜 5. 进阶支持:if、for、可选等控制流

swift
复制编辑
@resultBuilder
struct StringBuilder {
    static func buildBlock(_ components: String...) -> String {
        components.joined()
    }

    static func buildOptional(_ component: String?) -> String {
        component ?? ""
    }

    static func buildEither(first: String) -> String {
        first
    }

    static func buildEither(second: String) -> String {
        second
    }

    static func buildArray(_ components: [String]) -> String {
        components.joined(separator: ", ")
    }
}

然后:

swift
复制编辑
makeString {
    "Hello"
    if Bool.random() {
        "🌞"
    } else {
        "🌧️"
    }
    for item in ["A", "B", "C"] {
        item
    }
}

🧮 6. 在 SwiftUI 中的应用:@ViewBuilder

swift
复制编辑
struct MyView: View {
    var body: some View {
        VStack {
            Text("Hello")
            if Bool.random() {
                Text("SwiftUI")
            } else {
                Text("Rocks!")
            }
        }
    }
}

SwiftUI 中的 VStack 使用了 @ViewBuilder,这使得你能用 if、for 等结构直接组合多个视图,而不是手动拼接。


🧑‍💻 7. 自定义 @resultBuilder 应用场景示例

使用场景 描述
@ViewBuilder SwiftUI 视图组合
@StringBuilder 构建字符串 DSL
@HTMLBuilder 构建 HTML DSL
@CommandBuilder 构建命令行工具链
@SQLBuilder 构建 SQL 语句(如:Swift ORM 框架)

📌 8. Swift 5.9 中的新功能(可选)

buildFinalResult(_:)

这个方法可以对构建器的结果做“最后一轮包装或转换”处理。适用于 Swift 5.9+。


举例,目标:构建 HTML DSL

swift
复制编辑
let html = makeHTML {
    HTML("html") {
        HTML("body") {
            HTML("h1") { "Welcome" }
            HTML("p") { "This is a custom DSL!" }
        }
    }
}

print(html.render())

🧱 第一步:定义 HTML 节点结构

swift
复制编辑
protocol HTMLComponent {
    func render() -> String
}

struct HTMLText: HTMLComponent {
    let text: String
    func render() -> String {
        text
    }
}

struct HTML: HTMLComponent {
    let tag: String
    let children: [HTMLComponent]

    init(_ tag: String, @HTMLBuilder _ content: () -> [HTMLComponent]) {
        self.tag = tag
        self.children = content()
    }

    func render() -> String {
        let inner = children.map { $0.render() }.joined()
        return "<(tag)>(inner)</(tag)>"
    }
}

🧙 第二步:定义 @HTMLBuilder

swift
复制编辑
@resultBuilder
struct HTMLBuilder {
    static func buildBlock(_ components: HTMLComponent...) -> [HTMLComponent] {
        components
    }

    static func buildOptional(_ component: [HTMLComponent]?) -> [HTMLComponent] {
        component ?? []
    }

    static func buildEither(first component: [HTMLComponent]) -> [HTMLComponent] {
        component
    }

    static func buildEither(second component: [HTMLComponent]) -> [HTMLComponent] {
        component
    }

    static func buildArray(_ components: [[HTMLComponent]]) -> [HTMLComponent] {
        components.flatMap { $0 }
    }

    static func buildExpression(_ expression: String) -> [HTMLComponent] {
        [HTMLText(text: expression)]
    }

    static func buildExpression(_ expression: HTMLComponent) -> [HTMLComponent] {
        [expression]
    }
}

🧪 第三步:封装顶层构造器

swift
复制编辑
func makeHTML(@HTMLBuilder content: () -> [HTMLComponent]) -> HTMLComponent {
    let result = content()
    return HTMLText(text: result.map { $0.render() }.joined(separator: "\n"))
}

📦 最终效果

swift
复制编辑
let html = makeHTML {
    HTML("html") {
        HTML("body") {
            HTML("h1") { "Hello Swift!" }
            HTML("p") {
                if Bool.random() {
                    "Dynamic paragraph!"
                } else {
                    "Static paragraph!"
                }
            }
        }
    }
}

print(html.render())

输出可能为:

html
复制编辑
<html><body><h1>Hello Swift!</h1><p>Static paragraph!</p></body></html>

📚 小结

特性 说明
灵活组合 像 DSL 一样用多个表达式组合出结构
控制流 支持 if/else、for、可选绑定等
编译期支持 编译器会在语法上理解这些结构
SwiftUI SwiftUI 的构建基石
可扩展性强 适用于构建任意复杂层级的数据

详解 JSExport:JavaScript 与 Objective-C 的通信桥梁

前言

在 iOS 开发中,JavaScriptCore 框架提供了强大的 JS 引擎,可以让我们在应用中运行 JavaScript 代码。而 JSExport 是这个框架中最重要的机制之一,它可以让我们将 Objective-C 的对象暴露给 JavaScript 调用,以使 JavaScript 可以像使用普通 JS 对象一样访问 Objective-C 的方法和属性。

我们先来了解下什么是 JSExport。

JSExport 是什么?

简单来说,JSExport 是一个协议,只要你定义的 Objective-C 协议继承自它,并让一个类遵循这个协议,就可以将该类的方法和属性导出为 JavaScript 可访问的接口

@protocol JSExport

了解完概念,来看下它在代码中是如何使用的。

基本使用步骤

使用 JSExport 通常需要以下几步:

  • 创建一个继承自 JSExport 的协议;
  • 将希望暴露的方法或属性声明在协议中;
  • 创建一个遵循该协议的 Objective-C 类;
  • 将该类的实例注入到 JSContext 中;
  • 在 JavaScript 中调用暴露出来的方法。

比如我们想把下面的代码暴露给 JS 调用,官方文档的示例代码如下:

@protocol MyPointExports <JSExport>
@property double x;
@property double y;
- (NSString *)description;
- (instancetype)initWithX:(double)x y:(double)y;
+ (MyPoint *)makePointWithX:(double)x y:(double)y;
@end
 
@interface MyPoint : NSObject <MyPointExports>
- (void)myPrivateMethod;  // This isn't in the MyPointExports protocol, so it isn't visible to JavaScript code.
@end
 
@implementation MyPoint

@end

我们来分类看一下如何将 OC 的代码暴露给 JS 调用:

暴露属性

示例代码如下:

// 在协议中声明
@protocol MyPointExports <JSExport>
@property double x;
@property double y;
@end

// 类中合成属性的 set get 方法
#import "MyPoint.h"

@implementation MyPoint

@synthesize x;
@synthesize y;

@end

// 在 JS 中调用暴露的属性
JSContext *context = [[JSContext alloc] init];

  
Calculator *calc = [[Calculator alloc] init];
context[@"calc"] = calc;

MyPoint *point = [MyPoint new];
point.x = 10.2;
point.y = 12.1;


context[@"point"] = point;

JSValue *xResult = [context evaluateScript:@"point.x"];
JSValue *yResult = [context evaluateScript:@"point.y"];
NSLog(@"X:%f, Y:%f", [xResult toDouble], [yResult toDouble]);

打印结果:X:10.200000, Y:12.100000。

暴露实例方法

示例代码如下:

// 在协议中声明
@protocol MyPointExports <JSExport>

- (NSString *)description;
- (instancetype)initWithX:(double)x y:(double)y;

@end
// 在类中实现
- (NSString *)description {
    return @"MyPoint";
}

- (instancetype)initWithX:(double)x y:(double)y {
    if (self = [super init]) {
        self.x = x;
        self.y = y;
    }
    return self;
}
// 在 JS 中调用暴露的方法
JSContext *context = [[JSContext alloc] init];
context[@"MyPoint"] = [MyPoint class];
JSValue *result = [context evaluateScript:@"new MyPoint(1, 2)"];
NSLog(@"X:%f, Y:%f", [result[@"x"] toDouble], [result[@"y"] toDouble]);

打印结果:X:1.000000, Y:2.000000。

暴露类方法

// 在协议中声明
@protocol MyPointExports <JSExport>

@property double x;
@property double y;

- (instancetype)initWithX:(double)x y:(double)y;

+ (MyPoint *)makePointWithX:(double)x y:(double)y;

@end
// 在类中实现

@implementation MyPoint

@synthesize x;
@synthesize y;

+ (MyPoint *)makePointWithX:(double)x y:(double)y {
    return [[MyPoint alloc] initWithX:x y:y];
}

- (instancetype)initWithX:(double)x y:(double)y {
    if (self = [super init]) {
        self.x = x;
        self.y = y;
    }
    return self;
}
@end
// 在 JS 中调用暴露的类方法
JSContext *context = [[JSContext alloc] init];
context[@"MyPoint"] = [MyPoint class];
JSValue *result = [context evaluateScript:@"MyPoint.makePointWithXY(3, 4)"];
NSLog(@"X:%f, Y:%f", [result[@"x"] toDouble], [result[@"y"] toDouble]);

打印结果:X:3.000000, Y:4.000000。

总结

JSExport 是 JavaScriptCore 框架中连接 Objective-C 与 JavaScript 的核心机制,正确的使用可以让你灵活地在原生与脚本之间切换逻辑,适用场景如下:

  • 配置型逻辑引擎;
  • 脚本化功能扩展;
  • 小程序平台;
  • 混合框架。

使用 JSExport 的关键步骤:

  • 明确协议继承 JSExport
  • 方法命名符合规则;
  • 类遵循协议;
  • 注入实例到 JSContext

通过这套机制,你可以让 JavaScript 像本地对象一样调用 Objective-C 类中的方法,极大提升了扩展性和灵活性。

Swift学习总结——常见数据类型

1 常量

swift中,可以使用关键字let来声明常量。我们通过以下几个关键点来认识常量。

1.1 只能赋值一次

常量只能被赋值一次,如果重复赋值,会报错。以下图为例: image.png

1.2 常量在初始化之前,不能使用

如下面的示例,对常量a进行了声明,并且是Int型,但是没有初始化,直接使用则会报错。同样,变量age也没有初始化,直接使用也会报错。 image.png

声明即赋值,或者先声明使用前再赋值。如下面的两个示例:

        // 声明即赋值
        let age = 28
        print(age)
        
        // 先声明,使用前赋值
        let height : Int
        height = 175
        print(height)

1.3 不要求在编译时期确定,但使用之前必须赋值一次

示例中age3是一个常量,并通过一个函数赋值,所以在编译期间并没有确定值,而是在执行期间确定。 image.png

1.4 常量需要确定数据类型

常量在声明后,如果没有确定数据类型,也没有赋值,会报错。 image.png

2 标识符

  1. 比如常量、变量、函数,几乎可以使用任何字符

    下面的示例中,使用一些特殊的图案,来对常量、变量、函数进行命名: image.png

  2. 标识符不能以数字开头,不能包含制表符、空白字符、箭头等特殊字符

    标识符依然会有一些限制要求,比如不能以数字开头,不能使用制表符、空格等特殊字符。同时,在编码过程中,为了更加易于理解、扩展和维护,我们还是要遵循一些编码规范:

    • 清晰性:命名清晰,功能清晰
    • 可读性:便于快速理解方法作用
    • 可维护性:减少因命名歧义引发的 Bug,后期代码微调简单
    • 一致性:团队协作风格统一

3 常见数据类型

3.1 Objective-C数据类型

首先回顾一下Objective-C的数据类型。

  1. 基本数据类型 Objective-C的基本数据类型与C语言类似,主要包括:
    • 整型int(通常占用4字节,表示范围为-2,147,483,648至2,147,483,647)、short(通常占用2字节,表示范围为-32,768至32,767)、long(通常占用4或8字节,取决于平台)、long long(至少占用8字节,表示范围更大)。
    • 浮点型float(单精度浮点数,通常占用4字节)、double(双精度浮点数,通常占用8字节)、long double(高精度浮点数,具体大小依赖于编译器实现)。
    • 字符型char(通常占用1字节,用于存储单个字符)。
    • 布尔型BOOL(用于表示真假值,通常占用1字节)。
  2. 对象数据类型 Objective-C的核心特性之一是面向对象的数据类型,比如Foundation框架中提供的一些复合型对象:
    • 字符串‌:如NSString(不可变字符串)和NSMutableString(可变字符串)。
    • 数组‌:如NSArray(不可变数组)和NSMutableArray(可变数组)。
    • 字典‌:如NSDictionary(不可变字典)和NSMutableDictionary(可变字典)。
    • 数字‌:如NSNumber
    • 数据‌:如NSData
  3. 扩展数据类型 Objective-C还支持一些扩展的数据类型,包括:
    • 指针类型‌:如int*float*NSObject*等。
    • 结构体类型‌:如CGRectCGSizeCGPoint等。
    • 枚举类型‌:如NSComparisonResultUITableViewStyle等。
    • 类型定义‌:通过typedef关键字定义自定义数据类型,如typedef enumtypedef struct等。
    • 其他类型‌:如NSUIntegerNSIntegerSEL等。

3.2 Swift数据类型

Swift 数据类型主要包括基本数据类型(如整数、浮点数、布尔值、字符和字符串)和复合数据类型(如数组、元组、可选类型等)。

  1. 基本数据类型‌ 如整数、浮点数、布尔值、字符和字符串:

    • 整数类型‌: Int:平台相关长度(32位系统为32位,64位系统为64位),建议默认使用以提高代码一致性。‌‌变体:Int8 Int16 Int32 Int64(有符号)和 UInt8 UInt16等(无符号),需显式指定。‌‌
    • 浮点数类型Float:32位单精度浮点数(约6位小数精度)。‌‌Double:64位双精度浮点数(约15位小数精度,推荐默认使用)。‌‌
    • 布尔类型Bool:仅包含 truefalse,用于逻辑判断。‌‌
    • 字符与字符串Character:单个Unicode字符(如 "A")。String:文本数据,支持插值和多行语法(如 "Hello")。‌‌
  2. 复合数据类型‌ 数组、元组、可选类型等:

    • 数组与字典Array:有序同类型集合。‌‌ Dictionary:键值对集合(如 ["key": "value"])。‌‌
    • 元组与可选类型Tuple:异构值组合(如 (1, "error"))。‌‌ Optional:表示值可能存在为 nil(如 Int?)。‌‌
  3. 引用类型 类(class

3.3 Swift数据类型的底层实现

我们知道Objective-C对象数据类型class类底层实现是结构体‌,这个结构体的定义在runtime源码中。比如Foundation库中提供的类型,如NSStringNSArray等,以及我们自定义的继承自NSObject的类,这些底层都是结构体。在iOS底层学习——OC对象的本质与isa中也有过探索。

Swift中,引用类型类(class)底层并不是结构体来实现的,但是一些基本数据类型如IntFloatDoubleCharacterString以及ArrayDictionary却是定义为结构体主要是因为它们作为值类型在性能和内存管理上具有优势。

下图对swift数据类型进行了归类: image.png

我们在开发工具上,也能够看到,Int是一个结构体: image.png

快速跳转到定义(Jump to Definition),Command + 点击鼠标左键快捷键,也能看到,源码中Int被定义成为一个public型的结构体。 image.png

我们知道结构体占用的内存空间,取决于结构体内部所有变量占用空间之和,这里提出一个疑问:将这些基本数据类型定义成结构体,难道不会增加复杂度,增加内存使用吗?其实swift内部做了优化,后面我们再深入探索。

4 部分数据类型的使用

4.1 整型类型

  • Swift提供了多种整型类型,如Int8Int16Int32Int64UInt8UInt16UInt32UInt64
  • Int8表示占8位,1个字节;UInt8U表示无符号。
  • 32bit平台,Int等价于Int3264bit平台,Int等价于Int64
  • 一般情况下,我们使用Int即可,除非对内存占用空间有强制性要求。

可以通过maxmin属性,了解数据类型对应的最大值和最小值。(注意这里的maxmin是属性,不是函数方法) image.png

4.2 浮点型

  • Float 32位,精度只有6位
  • Double 64位,精度至少15位
  • 初始化一个浮点型时,如果没有声明类型,默认是Double

image.png

4.3 不同进制表示方式

  • let intDecimal = 17 // 十进制
  • let intBinary = 0b10001 // 二进制
  • let intOctal = 0o21 // 八进制
  • let intHexadecimal = 0x11 // 十六进制

4.4 字符型

  • 字符型和字符串一样,使用双引号“”
  • 初始化一个字符型时,如果没有声明类型,默认是String
  • 字符可以存储ASCII字符Unicode字符

image.png

4.5 数组、字典

  • let array = ["a", "b", "c"]
  • let dic = ["a" : 12, "b" : 13, "c" : 21]
  • 字典类型也是使用[]

在原生的容器类型中,他们都是泛型的,也就是我们在一个集合中,只能放同一种类型的元素。 image.png

如果我们要把不相关的类型,放到同一个容器类型中的话,比较容易想到的是使用 AnyAnyObject,如下面的示例,再或者使用NSArrayNSDictionary

  • let array : [Any] = ["a", "b", "c", 1]
  • let dic : [String : Any] = ["a" : 12, "b" : "13"]

image.png

4.6 类型转换

  1. 整数转换

    如下图的示例中,age1age2虽然都是整型,但是其占用的存储空间是不同的,所以是不能直接相加的。 image.png

    因为Int8占用8位一个字节,而Int16占用两个字节,所以可以将age1转换为Int6,再相加,而age3也自动变成Int16型。 image.png

  2. 整数、浮点数转换

    如下图所示,整型和浮点型类型不批配,是不可以直接相加的: image.png

    可以将Int型转为浮点型,此时intp也为Double型: image.png

    但是下面这种方式是可以的,字面量可以直接相加,因为数字字面量本身没有明确的类型: image.png

4.7 元祖tuple

将多个不同数据类型组合赋予一个变量,可以通过序号来访问对应位置上的值image.png

可以将定义的元祖赋予一个元祖,对应位置上元素会自动赋值: image.png

如果元素不需要赋值,可以直接用_来代替: image.png

在定义元祖时,还可以给每个元素设置一个key: image.png

JavaScriptCore 入门

背景

在现在大前端的概念越来越重要的背景下,在开发 iOS 应用时,我们常常需要在应用中执行 JavaScript 代码,或者在原生代码和 JS 之间进行交互。Apple 提供的 JavaScriptCore 框架,可以让我们在不依赖 WebView 的前提下,直接在 Objective-C 或 Swift 中嵌入 JavaScript 引擎,执行 JS 代码、传值、调用函数,从而实现双向通信。

在本篇文章中,会主要介绍 JavaScriptCore 的基本使用方式、核心类、双侧之间的互相调用以及异常处理。

首先,我们先来了解下什么是 JavaScriptCore。

JavaScriptCore 是什么?

JavaScriptCore 是 Apple 提供的一个框架,它封装了 WebKit 中的 JavaScript 引擎。通过它我们可以实现下面的功能:

  • 在 Native 应用中直接执行 JavaScript 代码;
  • 将 Native 对象或者方法暴露给 JavaScript 使用;
  • 调用 JS 函数并获取返回值;
  • 捕获处理 JS 代码中触发的异常;

了解完什么是 JavaScriptCore 以及它的使用场景,下面来看下它的核心类。

JavaScriptCore 核心类介绍

JavaScriptCore 中最常用的几个类包括以下四个:

  • JSContext:表示一个 JS 执行上下文环境(沙箱)
  • JSValue:JS 中的值在 Objective-C 中的包装
  • JSExport:通过协议导出原生方法和属性给 JS 使用
  • JSVirtualMachine:表示一个虚拟机,可用于多个 JSContext

通过这些类,我们就可以实现 Native 代码和 JS 代码之间的互相调用。

了解完概念,下面就开始写代码了。

Objective-C 调用 JavaScript 方法

示例代码如下:

JSContext *context = [[JSContext alloc] init];

[context evaluateScript:
 @"function add(x, y) { return x + y; }"];

JSValue *addFunc = context[@"add"];
JSValue *result = [addFunc callWithArguments:@[@5, @7]];

NSLog(@"add(5, 7) = %@", [result toNumber]); // 输出 12

首先,我们创建一个 JSContext 类型的实例对象 context 用来表示 JS 执行上下文。接着调用 evaluateScript 方法传递进去一个字符串,该字符串的内容是一个 JS 函数 add,用来计算两个参数之和。然后创建一个 JSValue 类型的对象用来接收 context 中的 add 方法。最后调用 callWithArguments 方法将需要计算的数字传递进去并将结果返回给 JSValue 类型的实例对象 result。

这就是在 Objective-C 调用 JavaScript 方法的流程。

打印结果如下:

add(5, 7) = 12

接着,我们再来看下如何在 JavaScript 调用 Objective-C 方法。

JavaScript 调用 Objective-C 方法

在 JavaScript 调用 Objective-C 方法要比在 Objective-C 调用 JavaScript 方法稍微复杂一点。需要下面三步:

  • 定义一个协议继承自 JSExport,将需要 JS 调用的方法放在协议里;
  • 声明类并实现这个协议;
  • 将类的实例对象注册给 JSContext;

示例代码如下:

// 第一步:声明协议
#import <JavaScriptCore/JavaScriptCore.h>

@protocol CalculatorExport <JSExport>

- (NSInteger)addWithNum1:(NSInteger)num1 num2:(NSInteger)num2;

@end

// 第二步:实现协议
@interface Calculator : NSObject <CalculatorExport>
@end

@implementation Calculator

- (NSInteger)addWithNum1:(NSInteger)num1 num2:(NSInteger)num2 {
    return num1 + num2;
}

@end

//将实例对象注册给 JSContext

JSContext *context = [[JSContext alloc] init];

Calculator *calc = [[Calculator alloc] init];
context[@"calc"] = calc;

[context evaluateScript:@"var result = calc.addWithNum1Num2(3, 4);"];

NSLog(@"结果:%@", [context[@"result"] toNumber]);

输出结果如下:

结果:7

异常处理

在两侧联调开发时,不可避免的会出现代码方面的问题,这时候我们需要通过给 exceptionHandler 赋值,在回调中处理异常的场景。

示例代码如下:

JSContext *context = [[JSContext alloc] init];

context.exceptionHandler = ^(JSContext *ctx, JSValue *exception) {
    NSLog(@"JS 异常:%@", exception);
};

Calculator *calc = [[Calculator alloc] init];
context[@"calc"] = calc;

[context evaluateScript:@"var result = calc.sub(3, 4);"]; // 在 Native 侧,并没有导出 sub 方法

NSLog(@"结果:%@", [context[@"result"] toNumber]);

输出结果如下:

JS 异常:TypeError: calc.sub is not a function. (In 'calc.sub(3, 4)', 'calc.sub' is undefined)
结果:nan

如何提高前端应用的性能?

# 前端性能优化实战指南

## 1. 资源加载优化

### 1.1 代码拆分与懒加载
```javascript
// 动态导入实现懒加载
const LazyComponent = React.lazy(() => import('./LazyComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

1.2 资源压缩与CDN

  • 使用Webpack的TerserPlugin压缩JS
  • 配置Gzip/Brotli压缩
  • 静态资源使用CDN加速

2. 渲染性能优化

2.1 虚拟列表

// 使用react-window实现虚拟滚动
import { FixedSizeList as List } from 'react-window';

const Row = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);

const Example = () => (
  <List
    height={600}
    itemCount={1000}
    itemSize={35}
    width={300}
  >
    {Row}
  </List>
);

2.2 避免强制同步布局

// 错误示例 - 导致布局抖动
function resizeAllParagraphs() {
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = box.offsetWidth + 'px';
  }
}

// 正确示例 - 批量读取后再写入
function resizeAllParagraphs() {
  const width = box.offsetWidth;
  for (let i = 0; i < paragraphs.length; i++) {
    paragraphs[i].style.width = width + 'px';
  }
}

3. 内存管理

3.1 事件监听清理

useEffect(() => {
  const handleResize = () => {
    // 处理逻辑
  };
  
  window.addEventListener('resize', handleResize);
  
  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

3.2 图片优化

<picture>
  <source srcset="image.webp" type="image/webp">
  <source srcset="image.jpg" type="image/jpeg"> 
  <img src="image.jpg" alt="描述文本">
</picture>

4. 缓存策略

4.1 Service Worker缓存

// 注册Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js');
  });
}

4.2 HTTP缓存头

Cache-Control: public, max-age=31536000
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

5. 监控与持续优化

5.1 性能指标监控

// 使用Performance API
const [entry] = performance.getEntriesByName('first-contentful-paint');
console.log('FCP:', entry.startTime);

5.2 Lighthouse自动化

// package.json
{
  "scripts": {
    "audit": "lighthouse http://example.com --output=json --output-path=./report.json"
  }
}

最佳实践总结

  1. 关键渲染路径优化

    • 内联关键CSS
    • 延迟非关键JS
    • 预加载重要资源
  2. 代码层面优化

    • 避免深层嵌套组件
    • 合理使用useMemo/useCallback
    • 减少不必要的重新渲染
  3. 网络层面优化

    • 使用HTTP/2
    • 实现资源预连接
    • 优化第三方脚本加载
  4. 持续监控

    • 建立性能基线
    • 设置性能预算
    • 自动化性能测试

通过以上方法的组合应用,可以显著提升前端应用的加载速度和运行时性能,提供更流畅的用户体验。

Swift 6 新特性(一):count(where:) 方法带来的从复杂到简洁变化

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

Swift 语言一直在不断演进,推出的新特性不仅提升了性能,还提高了代码的可读性。

其中一个值得关注的新功能就是在 SE-0220 中引入的 count(where:) 方法。这个方法让我们可以更高效、更具表现力地统计序列中满足特定条件的元素,避免了之前需要结合 filter()count 的复杂操作。

count(where:) 方法将过滤(filter)和计数(count)两个步骤合并为一个,省去了创建和丢弃中间数组的麻烦,从而提升了性能,同时代码也变得更加简洁。

示例1:统计高于冰点的温度

假设我们有一个以摄氏度为单位的温度数组,我们想统计其中有多少温度高于冰点(0°C):

在之前,我们可能需要这样写:

let temperatures = [-510, -22025, -1]
let aboveFreezingCount = temperatures.filter { $0 > 0 }.count

现在,我们可以使用 count(where:) 方法来简化代码:

let temperatures = [-510, -22025, -1]
let aboveFreezingCount = temperatures.count { $0 > 0 }

// 输出 `3`
print(aboveFreezingCount)

在这个例子中,aboveFreezingCount 的值为 3,因为有三个温度(10, 20, 25)符合条件。

示例2:统计具有特定前缀的元素

让我们再看一个示例,假如我们有一组产品名称,想统计以 "Apple" 开头的产品数量:

let products = [
    "Apple", 
    "Banana", 
    "Apple Pie",
    "Cherry", 
    "Apple Juice", 
    "Blueberry"
]
let appleCount = products.count { $0.hasPrefix("Apple") }

// 输出 `3`
print(appleCount)

在这个例子中,appleCount 的值为 3,因为 "Apple"、"Apple Pie" 和 "Apple Juice" 都以 "Apple" 开头。

示例3:根据长度统计元素

一个常见的应用场景是根据元素的长度进行统计。例如,我们可能想知道数组中有多少名字的长度少于六个字符:

let names = ["Natalia""Liam""Emma""Olivia""Noah""Ava"]
let shortNameCount = names.count { $0.count < 6 }

// 输出 `4`
print(shortNameCount)

在这个例子中,shortNameCount 的值为 4,因为 "Liam"、"Emma"、"Noah" 和 "Ava" 的长度都少于六个字符。

示例4:统计特定元素

如果需要统计序列中某个特定元素出现的次数,可以在闭包中使用等于运算符 (==)。例如:

let animals = ["cat""dog""cat""bird""cat""dog"]
let catCount = animals.count { $0 == "cat" }

// 输出 `3`
print(catCount)

在这个例子中,catCount 的值为 3,因为 "cat" 在数组中出现了三次。

适用范围和平台支持

count(where:) 方法适用于所有遵循 Sequence 协议的类型,这意味着我们不仅可以在数组中使用,还可以在集合、字典和其他序列类型中使用。

但需要注意的是,序列必须是有限的,以确保方法能够在合理的时间内完成。

count(where:) 方法在 Swift 6 中引入,因此需要 Xcode 16 才能使用这个特性。它支持多种平台和操作系统版本,包括 iOS 8.0+、macOS 10.10+、visionOS 1.0+ 等。

总结

count(where:) 方法是一个非常实用的功能,它不仅简化了代码,还提高了性能。如果你是一名经验丰富的 Swift 开发者,想要学习高级技巧,可以关注我的公众号,我会持续分享 Swift 相关的技巧和知识。

你对这个新特性有什么看法呢?欢迎在评论区与我们分享你的想法。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

为什么那些看起很丑的产品却能上架AppStore?

背景

关于AppStore允许怎样的产品过审,其实一直具有玄学性。这里主要是受审核员的心情和工作态度影响最大

最常见的莫过于上一版本过审,简单的更新关键词或者维护一个小Bug,遭受被拒审核的痛苦折磨,乃至3.2f致命打击。常见的场景如下:

  • 审核员用最新系统测试导致闪退被拒
  • 测试账户登录异常
  • 社区发帖功能要求登录
  • 社区帖子缺啥举报、拉黑功能
  • 页面空白

另外,统一回复一下最近提问最多的问题,那就是为什么那些看起来很丑的产品却能上架?直接上图。

示例产品一

示例产品二

示例产品三

为什么?

⚠️事先声明本文绝无攻击以上产品的言论和恶意,仅用来参考并客观讲述AppStore审核策略

从审美的角度来看,上面列举的产品其实并不够精品。但是对于AppStore来说是绝对合规的。

那么可能有同行会诡辩地说了,这是AppStore故意丑化大陆地区的形象。

其实不然,无论是代码层面还是设计层面,你不得不承认这样的设计其实也是一种标新立异对于破局4.3(a)肯定是有作用的

首先这种色块版的设计,并不是适用于所有产品。从哲学存在即合理的角度来说,这部分工具类的产品本身并不需要花里胡哨的设计,增加用户的教育成本。仅需这种朴华务实的卖点足以解决用户的痛点

其次,这种设计理念本身也是敢为天下先,与其美的千篇一律,不如“丑”的千奇百怪

风险点

这种产品的风险点在于后续的迭代与转型,只能说这样的设计可以破局0~1的尴尬处境,但是在后续的迭代依旧会存在4.3(a)的风险。陷入进退两难导致不改不够精美,改了不好过审的尴尬处境

所以,这种取巧的技巧更适合于工具类产品,如果是小而美的产品并完全适用。毕竟在弯道超车翻车的不在少数。

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

相关推荐

# Pingpong和连连的平替,让AppStore收款无需新增持有人。

# 苹果加急审核是“绿色通道”还是“死亡陷阱”?

# 苹果开发者邮箱,突然收到11.2通知严重么?

# 不想被苹果卡审最好错开这两个提审时间

# 手撕苹果审核4.3是代码问题还是设计问题?

# 有幸和Appstore审核人员进行了一场视频会议特此记录。

知识星球

更多Appstore咨询问题,请关注知识星球。「提供1v1上架指导,帮助开发者解决Appstore的疑难杂症,助力每一位开发者!」

❌