阅读视图

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

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

前言

作为iOS的老人基本上都听说过加急审核通道对于加急审核通道最初是苹果审核给开发者用来维护重大Bug,提供的特殊通道。

如果是说遇到重大节假日或者有严重闪退问题,比如:新用户注册失败,搜索闪退,启动闪退等等..

有一位粉丝最近遇到了加急审核的时候触发了账号等待终止的通知,具体如下:

We're unable to accommodate your expedite request.
Your developer account is currently pending termination due to violations of the Apple Developer Program License Agreement. Refer to the message you received in App Store Connect for more information.
If you believe your account shouldn't be terminated, you may submit an appeal to the App Review Board if you have not done so already.

加急审核能不能用?

首先刚刚已经提过了关于苹果提供加急审核背景,常言道:存在即合理! 如果说产品确实出现了紧急Bug,真的需要修复没有问题。但是建议新账号新产品不要使用。

这就好比一个新手村的小白,突然跟PNC说我想获得新手通关秘籍。对于苹果审核人员来说只会是满头问号。内心OS:谁给你的勇气?勇气大帝梁静茹么?

正常迭代的产品申请加急审核最快在30~50分钟就可以进入【这里是指从等待审核状态到正在审核状态】。

那么老账户可以使用但是别作! 这里说的别作是指别动不动就加急审核!尤其是小修小补的bug,想快速开奖。虽然加急审核没有明确的次数说明,但是归根结底加急审核是为开发者提供“绿色通道”,而不是试探苹果底线的通道。

这就好星爷“九品芝麻官”中的场景。

wechat_2025-05-22_185630_813.png

偶尔使用还好,频繁使用对于审核人员的内心。

wechat_2025-05-22_185850_582.png

为什么会触发封号?

说句拗口的话,封号不是因为加速审核而封号,而是本身就有问题而封号。说的直白一点,加速审核不过是导火索罢了。 其实苹果早就盯上了,只是问题不太严重睁一只眼闭一只,本来大家可以相安无事,非得站出来打破这种平衡。

举个栗子,这就好比追一个女神本来处于比较暧昧的关系半推半就,你突然心急了。让女神一下子看到了你的另一面,瞬间关系结束回到冰点。

那么到底有哪些行为开发者不适合加急审核呢?

1.套用的代码,重复使用的矩阵型App

2.新产品使用被下架品牌词做App名称

3.购买的账号,身份信息存在可疑

4.存在关联的测试设备或收款账户

5.过渡堆砌关键词在元数据中

作为开发者要拎的清楚大小王,不要疯狂去试探苹果的规则,除非UI原创、玩法新颖、代码古法手打。

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

ByAI:Swift6.2新特性学习

1. 控制isolation的默认Actor推断(SE-0466)

核心功能:允许模块通过编译器标志-default-isolation MainActor,将isolation类型和函数的actor指定为MainActor,简化并发编程。

class DataController {
    func load() { } // 默认@MainActor隔离
}

struct App {
    let controller = DataController() // 自动在主Actor运行
}

意义

  • 适用于UI密集型模块,减少并发复杂性
  • 背景模块仍可使用原有并发模型
  • 解决开发者学习曲线问题,尤其适合不需要高并发的应用

2. 原始标识符(Raw Identifiers, SE-0451)

核心功能:允许使用反引号定义包含空格、数字等非常规字符的标识符。

enum HTTPError: String {
    case `401` = "Unauthorized" // 使用数字作为枚举case
}

@Test func `Strip HTML tags from string`() { // 可读的测试名称
    // 测试代码
}

意义

  • 提升枚举、测试方法命名的直观性
  • 支持与外部系统(如HTTP状态码)直接映射

3. 字符串插值默认值(SE-0477)

核心功能:在字符串插值中为可选值提供默认值,无需??运算符。

let age: Int? = nil
print("Age: (age, default: "Unknown")") // 输出"Age: Unknown"

优势

  • 支持不同类型默认值(如Int?String
  • 替代??,避免类型不匹配问题

4. enumerated()集合协议支持(SE-0459)

核心功能enumerated()返回类型现在符合Collection协议,提升性能。

List(names.enumerated(), id: .offset) { 
    Text("User ($0.offset): ($0.element)")
}

优势

  • 支持SwiftUI直接绑定
  • 链式操作(如dropFirst(500))时间复杂度优化

5. 方法与构造器键路径(SE-0479)

核心功能:键路径支持直接引用方法。

let functions = strings.map(.uppercased) // 获取方法引用
functions[0]() // 执行方法

let uppercased = strings.map(.uppercased()) // 直接调用

限制

  • 不支持asyncthrows方法
  • 需通过参数标签区分重载方法

6. 严格内存安全检查(SE-0458)

核心功能:通过@safe@unsafe标记代码,强制使用unsafe关键字调用不安全操作。

let name: String?
unsafe { print(name.unsafelyUnwrapped) } // 显式声明不安全操作

目的

  • 减少未定义行为风险
  • 类似try/await的显式错误处理机制

7. 回溯API(SE-0419)

核心功能:捕获调用堆栈信息,支持符号化(Symbolication)。

func functionC() {
    let frames = Backtrace.capture().symbolicated()?.frames
    print(frames) // 输出调用链函数名和行号
}

用途

  • 调试复杂调用流程
  • 快速定位崩溃上下文

8. 弱引用常量(weak let, SE-0481)

核心功能:允许weak let声明不可变弱引用。

@MainActor class Session {
    weak let user: User? // 不可变弱引用
}

优势

  • 支持Sendable协议
  • 避免循环引用,同时保持线程安全

9. 事务性观察值(SE-0475)

核心功能:通过Observations结构监听@Observable数据变化。

@Observable class Player { var score = 0 }

let playerScores = Observations { player.score }
for await score in playerScores { // 异步流监听变化
    print(score) 
}

特性

  • 发送初始值和后续变化
  • 自动合并同时发生的多次更新

10. 任务优先级提升API(SE-0462)

核心功能:检测和处理任务优先级动态调整。

withTaskPriorityEscalationHandler {
    // 执行任务
} onPriorityEscalated: { old, new in
    print("优先级从(old)升至(new)")
}

用途

  • 手动调整子任务优先级(escalatePriority(to:)
  • 响应系统自动优先级提升事件

11. 任务命名(SE-0469)

核心功能:为任务和子任务添加名称,便于调试。

Task(name: "NetworkRequest") { 
    print(Task.name) // 输出"NetworkRequest"
}

group.addTask(name: "Stories (i)") { ... }

优势

  • 在Xcode调试器中标识任务
  • 结合日志快速定位问题任务

12. Swift测试框架增强

关键改进

  • 退出测试(ST-0008) :捕获precondition等致命错误
#expect(processExitsWith: .failure) {
    dice.roll(sides: 0) // 预期触发崩溃
}
  • 附件支持(ST-0009) :附加调试数据到测试
Attachment.record(result, named: "Character") // 失败时附加数据
  • 条件评估API(ST-0010) :动态检查测试条件
if try await ConditionTrait.disabled(...).evaluate() { ... }

其他重要改进

  • 固定大小数组(InlineArray, SE-0453)

    var names: InlineArray<4, String> = ["A", "B", "C", "D"]
    
  • 正则表达式后向断言(SE-0448)

    let regex = /(?<=$)\d+/ // 匹配"$"后的数字
    
  • 非逃逸类型(SE-0446) :禁止值逃逸创建上下文

  • Objective-C闭包Sendable(SE-0463) :默认标记为可发送

原文地址:www.hackingwithswift.com/articles/27…

Swift6.2中的default isolation

背景

Swift6.2的新特性中有一项,允许开发者控制默认的隔离上下文(Isolation) ,支持将 @MainActor 设为模块或文件级别的默认隔离环境。

核心概念

1. 静态隔离(Static Isolation)

隔离(Isolation)是 Swift 并发模型的核心机制,用于标识代码运行的上下文(如主线程或特定 Actor)。

默认情况下,未明确标注的代码被视为 nonisolated(非隔离),即不绑定任何 Actor,需自身保证线程安全。

// 默认情况下,此函数是 nonisolated 的
func howIsThisIsolated() {
    // 需自行保证线程安全
}

2. nonisolated 的问题

  • 线程安全负担nonisolated 默认假设代码是线程安全的,但全局可变状态可能导致数据竞争。
  • 不适用于主线程场景:大量 UI 代码本应运行在主线程(@MainActor),但开发者常忘记标注,导致潜在问题。
var global = 42 // 编译报错:非隔离环境下的可变全局变量不安全

解决方案:控制默认隔离

1. 模块级默认隔离(SE-0466)

在 Swift Package 中通过 defaultIsolation 设置默认隔离:

// Package.swift
let package = Package(
    targets: [
        .target(
            name: "Test",
            swiftSettings: [
                .defaultIsolation(MainActor.self) // 将模块默认隔离设为 @MainActor
            ]
        )
    ]
)
  • 效果:模块内未标注的声明默认绑定 @MainActor,无需手动添加。
  • 兼容性:仍可通过 nonisolated 显式标记不需要隔离的代码。

2. 文件级覆盖(SE-0478)

通过 typealias 在单个文件中覆盖模块默认设置:

// File.swift
private typealias DefaultIsolation = MainActor // 文件默认隔离设为 @MainActor
// 或
private typealias DefaultIsolation = nonisolated // 文件默认隔离设为 nonisolated

权衡与争议

1. 性能考量

  • 潜在风险:默认 @MainActor 可能导致主线程负载过高,影响性能。

主线程性能问题可通过优化同步代码解决,而数据竞争的风险更值得警惕。

2. 语言方言(Dialect)问题

  • 代码可读性:默认隔离设置不在源码中显式体现,不同模块可能有不同默认值,增加理解成本。

  • 适用场景

    • UI 项目:适合默认 @MainActor,减少手动标注。
    • 服务端/嵌入式:可能更倾向保留 nonisolated 默认。

代码示例与解释

示例 1:模块级默认隔离

// 模块设置为 .defaultIsolation(MainActor.self)
var global = 42 // 合法:默认隔离为 @MainActor,全局变量受主线程保护

func updateUI() {
    global += 1 // 安全:在主线程执行
}

示例 2:显式覆盖默认

// 文件内设置 typealias DefaultIsolation = nonisolated
var global = 42 // 非法:nonisolated 默认下可变全局变量不安全

nonisolated func safeUpdate() {
    // 需自行实现线程安全逻辑
}

总结

1. 核心价值

  • 简化代码:减少 @MainActor 和 nonisolated 的手动标注。
  • 渐进式披露:新手可忽略并发细节,专注业务逻辑;进阶开发者仍能精细控制。

2. 使用建议

  • UI 密集型项目:积极尝试默认 @MainActor,减少潜在的主线程错误。
  • 底层库/服务端:评估性能需求,谨慎选择默认隔离。
  • 过渡期策略:逐步迁移现有代码,优先标记关键路径。

Mac 配置Flutter环境

这边提供两种安装方式

1.采用Homebrew安装

  • 安装Dart sdk
brew tap dart-lang/dart
brew install dart
  • 安装Flutter sdk
brew tap flutter/flutter
brew install flutter
  • 配置 PATH 环境变量
export PATH="$PATH:/Applications/flutter/bin"
  • 初始化Flutter
flutter precache

2.官网下载安装

  • 镜像环境,由于在国内访问Flutter有时可能会受到限制,Flutter官方为中国开发者搭建了临时镜像
export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
  • 下载对应Flutter SDK
  • 如何查看该下载的版本,x64还是arm64
uname -a

结果如下:
Darwin CondydeMac-mini.local 24.5.0 Darwin Kernel Version 24.5.0: Tue Apr 22 19:53:26 PDT 2025; root:xnu-11417.121.6~2/RELEASE_X86_64 x86_64

这边得到的最后结果是x86_64,说明该下载x64版本,反之展示arm64则下载对应arm64版本
  • 解压到指定位置,然后解压即可
unzip ~/Downloads/flutter_macos_3.32.0-stable.zip
  • 配置 Flutter 的 PATH 环境变量
export PUB_HOSTED_URL=https://pub.flutter-io.cn //国内用户需要设置
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn //国内用户需要设置
export PATH=`存放目录`/flutter/bin:$PATH  # 这边需要你解压的位置
  • 刷新当前终端窗口
source ~/.bash_profile
  • 验证目录是否在已经在PATH中
echo $PATH

成功如下:
/Users/condy/flutter/bin:/Users/condy/flutter/bin:/Users/condy/Desktop/App/auth-flutter/flutter/bin:/usr/local/bin:/usr/local/sbin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Library/Apple/usr/bin
  • 验证是否成功
which flutter

成功如下:
/Users/condy/flutter/bin/flutter
  • 运行 flutter doctor 命令
flutter doctor -v

这个命令会检查你当前的配置环境,并在命令行窗口中生成一份报告。安装 Flutter 会附带安装Dart SDK,所以不需要再对Dart进行单独安装。

成功如下:

**Flutter assets will be downloaded from https://storage.flutter-io.cn. Make sure you trust this******
**source!******
[✓] Flutter (Channel stable, 3.32.0, on macOS 15.5 24F74 darwin-x64, locale zh-Hans-CN) [631ms]
******•** **Flutter version 3.32.0 on channel stable at /Users/condy/flutter******
******•** **Upstream repository https://github.com/flutter/flutter.git******
******•** **Framework revision be698c48a6 (2 days ago), 2025-05-19 12:59:14 -0700******
******•** **Engine revision 1881800949******
******•** **Dart version 3.8.0******
******•** **DevTools version 2.45.1******
******•** **Pub download mirror https://pub.flutter-io.cn******
******•** **Flutter download mirror https://storage.flutter-io.cn******
  
[✗] Android toolchain - develop for Android devices [120ms]
******✗** **Unable to locate Android SDK.******
**Install Android Studio from: https://developer.android.com/studio/index.html******
**On first launch it will assist you in installing the Android SDK components.******
**(or visit https://flutter.dev/to/macos-android-setup for detailed instructions).******
**If the Android SDK has been installed to a custom location, please use******
**`flutter config --android-sdk` to update to that location.******
  
  
[!] Xcode - develop for iOS and macOS [8.0s]
******✗** **Xcode installation is incomplete; a full installation is necessary for iOS and macOS******
**development.******
**Download at: https://developer.apple.com/xcode/******
**Or install Xcode via the App Store.******
**Once installed, run:******
**sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer******
**sudo xcodebuild -runFirstLaunch******
******•** **CocoaPods version 1.16.2******
  
[✓] Chrome - develop for the web [45ms]
******•** **Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome******
  
[!] Android Studio (not installed) [44ms]
******•** **Android Studio not found; download from https://developer.android.com/studio/index.html******
**(or visit https://flutter.dev/to/macos-android-setup for detailed instructions).******
  
[✓] Connected device (2 available) [6.4s]
******•** **macOS (desktop) • macos  • darwin-x64     • macOS 15.5 24F74 darwin-x64******
******•** **Chrome (web)    • chrome • web-javascript • Google Chrome 136.0.7103.114******
  
[✓] Network resources [6.7s]
******•** **All expected network resources are available.******
  
! Doctor found issues in 3 categories.
The Flutter CLI developer tool uses Google Analytics to report usage and diagnostic
data along with package dependencies, and crash reporting to send basic crash
reports. This data is used to help improve the Dart platform, Flutter framework,
and related tools.
  
Telemetry is not sent on the very first run. To disable reporting of telemetry,
run this terminal command:
  
    flutter --disable-analytics
  
If you opt out of telemetry, an opt-out event will be sent, and then no further
information will be sent. This data is collected in accordance with the Google
Privacy Policy (https://policies.google.com/privacy).
  • 初始化
flutter precache

到此就安装完毕,即可食用。

掌握 Dart 模式匹配: 2. 核心概念与基础模式

在上一章中,我们介绍了 Dart 模式匹配的宏观概念及其在简化代码中的重要性。本章将深入探讨模式匹配的基石——模式(Patterns) 。通过理解这些基础模式,你将能够灵活运用模式匹配进行数据解构和逻辑处理。本章提供简洁、实用的代码示例,帮助你快速上手。

2.1 模式(Patterns)的基本构成

在 Dart 中,模式是描述值“形状”的语法结构,用于匹配和解构数据。它可以是简单的字面量,也可以是复杂的嵌套结构。例如:

  • 42:一个常量模式
  • var x:一个变量模式
  • [a, b]:一个列表模式,包含两个变量模式。
  • {'name': n, 'age': a}:一个映射模式,包含两个变量模式。
  • Point(x: x, y: y):一个对象模式,解构对象的属性。

模式适用于以下场景:

  • 变量声明var (x, y) = point;
  • 赋值表达式(x, y) = (y, x);
  • switch 语句或表达式switch (value) { case int i: ... }
  • if-case 语句if (data case [int a, int b]) { ... }
  • for-in 循环for (var (key, value) in map.entries) { ... }

匹配与解构

  • 匹配:检查值是否符合模式的结构和内容。
  • 解构:匹配成功后,提取值并绑定到变量。

示例

Dart

void main() {
  // 匹配和解构坐标记录
  var point = (x: 10, y: 20);
  // 在 if-case 中引入新变量,需要使用 `var` 或明确类型
  if (point case (x: var x, y: var y)) {
    // x 和 y 会自动作为局部变量引入并推断类型(int)
    print('Point: ($x, $y)'); // 输出: Point: (10, 20)
  }

  // 尝试匹配类型不符的列表
  var data = [1, 'two'];
  // 使用 var 解构,变量 a, b 会推断为 dynamic,模式匹配成功
  if (data case [var a, var b]) {
    print('Two items: $a, $b');
  } else {
    print('Not two items.');
  }

  // 如果想严格匹配类型,则明确指定类型
  if (data case [int a, int b]) { // 明确指定 int 类型,此模式匹配失败
    print('Two integers: $a, $b');
  } else {
    print('Not two integers.'); // 输出: Not two integers.
  }
}

2.2 基础模式类型

Dart 提供四种基础模式,用于构建复杂模式匹配逻辑。

2.2.1 常量模式 (Constant Pattern)

作用:匹配值是否等于字面量或 const 常量。

语法:字面量(如 1, 'hello', true, null)或 const 变量。

示例

Dart

void checkStatusCode(int code) {
  switch (code) {
    case 200: // 常量模式
      print('Success');
    case 404: // 常量模式
      print('Not found');
    default:
      print('Unknown status: $code');
  }
}

void main() {
  checkStatusCode(200); // 输出: Success
  checkStatusCode(500); // 输出: Unknown status: 500
}

2.2.2 变量模式 (Variable Pattern)

作用:绑定匹配到的值到新变量,支持解构。

语法

  • var <variableName>
  • final <variableName>
  • <Type> <variableName>(类型可推断时通常省略,或利用简写模式在声明/赋值上下文)

示例

Dart

void main() {
  // 解构用户记录(使用简写模式,仅限于变量声明上下文)
  var user = (name: 'Alice', age: 25);
  // 当解构变量名与记录字段名一致时,使用简写模式 `(:name, :age)`
  // name 会被推断为 String,age 会被推断为 int
  var (:name, :age) = user;
  print('User: $name, Age: $age'); // 输出: User: Alice, Age: 25

  // 解构 API 响应列表
  var response = [200, 'OK'];
  // 列表解构时,变量名自动推断类型
  if (response case [var code, var message]) {
    print('Response: $code - $message'); // 输出: Response: 200 - OK
  }

  // switch 中绑定值(变量模式与类型测试模式的结合)
  void processInput(Object input) {
    switch (input) {
      case int value: // 类型测试模式:同时检查类型并绑定到变量 value (value 推断为 int)
        print('Number: $value');
      case String text: // 类型测试模式:同时检查类型并绑定到变量 text (text 推断为 String)
        print('Text: $text');
      default:
        print('Other: $input');
    }
  }
  processInput(42); // 输出: Number: 42
  processInput('Hello'); // 输出: Text: Hello
}

2.2.3 通配符模式 (Wildcard Pattern _)

作用:忽略不关心的值,保持模式结构完整。

语法_

示例

Dart

void main() {
  // 忽略记录中的字段(使用简写模式和通配符,仅限于变量声明上下文)
  var user = (name: 'Bob', id: 123, role: 'user');
  // 解构 name 字段,并忽略 id 和 role 字段
  var (:name, id: _, role: _) = user;
  print('User: $name'); // 输出: User: Bob

  // switch 中忽略子模式(列表模式结合通配符)
  void processRequest(List<String> request) {
    switch (request) {
      case ['GET', var path]: // 解构第一个元素为 'GET',第二个元素绑定到 path
        print('Fetching: $path');
      case ['POST', var path, _]: // 解构第一个为 'POST',第二个绑定到 path,忽略第三个
        print('Posting to: $path');
      case _: // 匹配任何其他情况
        print('Invalid request.');
    }
  }
  processRequest(['GET', '/api/users']); // 输出: Fetching: /api/users
  processRequest(['POST', '/api/data', 'payload']); // 输出: Posting to: /api/data
}

2.2.4 类型测试模式 (Type-Test Pattern)

作用:检查值类型并进行智能类型提升。

语法<Type> <variableName>

示例

Dart

void main() {
  void processShape(Object shape) {
    switch (shape) {
      case int radius: // 类型测试模式:检查 shape 是否为 int,并将智能提升后的值绑定到 radius
        print('Circle, radius: $radius');
      case List<double> rect: // 类型测试模式:检查 shape 是否为 List<double>,并绑定到 rect
        print('Rectangle, dimensions: $rect');
      default:
        print('Unknown shape.');
    }
  }
  processShape(5); // 输出: Circle, radius: 5
  processShape([3.0, 4.0]); // 输出: Rectangle, dimensions: [3.0, 4.0]
}

2.2.5 空检查模式 (?)

作用:匹配非空值,确保空安全处理。

语法<pattern>?

示例

Dart

void main() {
  // 处理可空用户数据(if-case 中需要明确声明变量)
  (String?, int?)? user = ('Alice', null);

  // (name: var name, age: var age):仅当 user 非空,且其 `name` 字段非空时才匹配。
  // `name` 会被推断为 String (非空),`age` 会被推断为 `int?`。
  if (user case (String name, int? age)) {
    print('Name: $name, Age: ${age ?? "unknown"}'); // 输出: Name: Alice, Age: unknown
  }

  // 处理可空坐标(在 switch 表达式中使用空检查模式)
  String processNullableCoord((int?, int?)? coords) {
    return switch (coords) {
      // 记录本身非空且两个字段都非空时匹配 (x, y 会被推断为 int)
      (int x, int y)? => 'Point: ($x, $y)',
      // 记录非空,第一个字段非空,第二个字段为 null
      (int x, null)? => 'Missing y coordinate ($x).',
      // 记录非空,第一个字段为 null,第二个字段非空
      (null, int y)? => 'Missing x coordinate (y=$y).',
      // 记录非空,两个字段都为 null
      (null, null)? => 'Both coordinates missing.',
      // 记录本身为 null
      null => 'No coordinates provided.',
      // 对于 (int?, int?)? 类型,如果所有上述具象模式都被覆盖,Dart 编译器可能认为它是穷尽的,
      // 不需要 _ 默认分支。如果未来添加更多复杂类型,可能需要 _。
    };
  }

  print(processNullableCoord((10, 20))); // 输出: Point: (10, 20)
  print(processNullableCoord(null)); // 输出: No coordinates provided.
  print(processNullableCoord((10, null))); // 输出: Missing y coordinate (10).
  print(processNullableCoord((null, 20))); // 输出: Missing x coordinate (y=20).
  print(processNullableCoord((null, null))); // 输出: Both coordinates missing.
}

章节总结

本章深入讲解了 Dart 模式匹配的四种基础模式:

  • 常量模式:用于匹配字面量或 const 常量。
  • 变量模式:用于绑定值以便解构,并强调了利用 varfinal 进行类型推断。同时明确了简写模式 (:name) 主要用于变量声明赋值上下文。
  • 通配符模式:使用 _ 明确表示忽略不关心的值,提升代码可读性。
  • 类型测试模式:直接在 case 后指定类型即可进行类型检查和智能类型提升,这是 Dart 3 中推荐的简洁用法。
  • 空检查模式:通过 ? 后缀确保模式只匹配非空值,提升空安全处理的优雅性。

这些优化后的示例展示了如何在实际场景(如 API 响应、几何形状处理)中使用模式匹配,代码简洁且空安全。

展望下一章

理解了基础模式之后,是时候解锁模式匹配更强大的功能了。下一章将深入探讨解构模式,包括列表模式映射模式记录模式对象模式,帮助你优雅地处理各种复杂数据结构,让你的 Dart 代码更加简洁和富有表现力。

学习Swift,这些资料可供参考

Swift作为苹果后起的主力语言在其生态中扮演者越来越重要的角色。OC从诞生到现在已经四十多年的历史了,虽然它依然是很多现有工程的主力开发语言,但毫无疑问,后续新起的App或新的系统功能特性将会以Swift为主,甚至是纯Swift为主。所以掌握Swift相比掌握OC有更高的价值

这里列举一些学习Swift的资料,帮助开发者学习和掌握Swift

Swift官网

1. Swift语言官网

在这里可以学习Swift的基础知识、枚举、泛型、并发等全类型知识

docs.swift.org/swift-book/…

2. Swift Github源码

从这里可以了解Swift是怎么从源码一步步编译成机器码的过程

github.com/swiftlang/s…

3. SwiftUI官方教程

developer.apple.com/tutorials/s…

4. Swift标准库API

作为参考手册,查看标准库里有哪些API可用

developer.apple.com/documentati…

5. Swift官网论坛

forums.swift.org/

6. The Swift Programming Language中文版

doc.swiftgg.team/documentati…

7. Swift跨平台框架

github.com/TokamakUI/T…

8. Swift服务端框架

github.com/vapor/vapor

github.com/PerfectlySo…

本文使用 文章同步助手 同步

掌握 Dart 模式匹配: 1.模式匹配导论

1. 模式匹配导论

1.1 什么是模式匹配?

在 Dart 语言中,模式匹配(Pattern Matching)是一项强大而灵活的语言特性,它允许你检查一个值是否符合特定的结构或“形状”,并在此过程中,有条件地从中提取(解构)出内部数据。你可以将模式匹配想象成一个智能的“数据侦探”,它不仅能够识别数据的外在形态,还能深入其内部,优雅地解构并提取出你所需的信息。

核心概念剖析:

  • 模式(Pattern) :这是模式匹配的“蓝图”,它定义了你期望匹配的值的结构。模式可以是简单的字面量、变量,也可以是复杂的组合,比如列表、映射、记录或自定义对象。

    // 简单模式:字面量模式
    const number = 10;
    if (number case 10) { // 这里的 '10' 就是一个字面量模式
      print('数字是10');
    }
    
    // 复杂模式:列表模式 (用于类型测试和解构)
    final coordinates = [10, 20];
    if (coordinates case [int x, int y]) { // 这里的 '[int x, int y]' 是一个列表模式
      print('X坐标: $x, Y坐标: $y');
    }
    
  • 匹配(Matching) :这是模式匹配的核心操作。当一个值与某个模式所描述的结构和内容完全相符时,我们称之为“匹配成功”。反之,则匹配失败。

    final status = 'success';
    // 匹配成功
    if (status case 'success') {
      print('操作成功!');
    }
    
    final result = ['error', '文件未找到'];
    // 匹配失败
    if (result case ['success', String message]) {
      print('不会打印,因为第一个元素不是 "success"');
    }
    
  • 解构(Destructuring) :这是模式匹配最实用的功能之一。一旦模式匹配成功(或在变量声明等场景下天然满足模式),你可以同时将匹配到的值的一部分或全部“拆解”出来,并将这些解构出的数据绑定到新的局部变量。这极大地简化了从复杂数据结构中提取数据的过程,让代码更简洁、更直观。

    // 变量声明解构:从 Record (记录) 中提取数据
    final point = (100, 200);
    final (x, y) = point; // 解构 point 到 x 和 y
    print('点坐标: ($x, $y)'); // 输出:点坐标: (100, 200)
    
    // 变量声明解构:从 List 中提取数据
    final colors = ['red', 'green', 'blue'];
    final [firstColor, secondColor, _] = colors; // 解构前两个元素,_ 表示忽略第三个
    print('第一个颜色: $firstColor, 第二个颜色: $secondColor'); // 输出:第一个颜色: red, 第二个颜色: green
    
    // 变量声明解构:从 Map 中提取数据
    final user = {'name': 'Alice', 'age': 30, 'city': 'New York'};
    final {'name': userName, 'age': userAge} = user; // 解构 name 和 age 字段
    print('用户名: $userName, 年龄: $userAge'); // 输出:用户名: Alice, 年龄: 30
    
    // for-in 循环解构:遍历 List 中的 Record
    final listOfPoints = [(1, 2), (3, 4), (5, 6)];
    for (final (px, py) in listOfPoints) { // 每次迭代都解构一个 Record
      print('处理点: ($px, $py)');
    }
    

为什么 Dart 需要模式匹配?(解决痛点)

在 Dart 3 之前,处理复杂或多变的数据结构常常伴随着一些痛点:

  • 冗余的代码:你可能需要大量的 if (obj is Type) 条件判断,然后进行强制类型转换 (as Type) 才能访问到具体的内部数据。这导致代码臃肿、重复且不够优雅。
  • 可读性差:多层嵌套的 if-else 语句或繁琐的类型转换会让代码逻辑变得模糊,一眼难以看清其真实意图,降低了代码的可读性和维护性。
  • 安全性挑战:如果类型转换前的检查不够严谨,强制类型转换(as Type)可能会在运行时抛出 CastError 异常,导致程序意外崩溃。
  • switch 语句的局限性:传统的 switch 语句只能基于常量或枚举值进行简单的等值匹配,无法处理更复杂的条件判断和结构化数据的匹配。

模式匹配的引入,正是为了系统性地解决这些问题。它让你的 Dart 代码变得:

  • 简洁 (Concise) :一行代码即可完成类型检查、解构和变量绑定,大幅减少样板代码。
  • 可读 (Readable) :模式直观地表达了数据的结构,让代码意图一目了然,更容易理解。
  • 安全 (Safe) :结合编译时检查(尤其是在与密封类结合时),模式匹配能有效减少运行时错误,提高代码的健壮性。
  • 富有表达力 (Expressive) :你可以用更自然、更贴近数据结构本身的方式来编写处理逻辑,增强了语言的表现力。

1.2 Dart 模式匹配简史

Dart 3.0 的引入及其重要性:

模式匹配是 Dart 3.0 中最具里程碑意义的新特性之一。它与 记录(Records)密封类(Sealed Classes) 共同构成了 Dart 语言在数据建模和处理方面的一套强大而统一的工具集。Dart 团队在设计这些特性时,充分吸取了其他现代编程语言(如 Rust、Kotlin、Scala)的成功经验,旨在在保持 Dart 既有简洁性的同时,显著增强其表达能力。

在 Dart 3.0 发布之前,开发者主要依赖以下方式来处理类似模式匹配的场景:

  • is 运算符和 as 运算符:这是最常见的类型检查和转换方式。

    Object value = [1, 'hello'];
    // 传统方式:冗长且易错
    if (value is List<Object?>) {
      if (value.length == 2 && value[0] is int && value[1] is String) {
        int number = value[0] as int; // 可能抛出 CastError
        String text = value[1] as String; // 可能抛出 CastError
        print('Found: $number, $text');
      }
    }
    
    // 对比模式匹配:
    if (value case [int number, String text]) {
      print('Found: $number, $text'); // 更简洁、安全
    }
    

    从上面的对比可以看出,模式匹配极大地简化了代码,并提升了类型安全性。

  • 级联运算符 (..) :虽然常用于对同一个对象执行一系列操作,但它并不能用于解构数据。

  • 自定义 getter 或辅助方法:为了从复杂对象中提取数据,有时需要编写额外的 getter 方法或封装在辅助函数中。

模式匹配的引入,标志着 Dart 语言在表达力和安全性方面的重大飞跃。它使得 Dart 在处理复杂数据流、实现更优雅的条件逻辑以及支持更高级的编程范式(如代数数据类型)方面,都迈出了坚实的一步,进一步巩固了其作为现代、高效编程语言的地位。

1.3 模式匹配与 Dart 语言哲学

模式匹配的加入并非偶然,它与 Dart 语言的设计哲学高度契合:

  • 渐进式增强:Dart 始终致力于在不牺牲易用性的前提下,逐步引入强大的新特性。模式匹配提供了一种更高级、更声明式的处理数据的方式,但它并没有取代现有的 isas 运算符,而是作为一种更优、更简洁的替代方案。
  • 静态类型安全:Dart 是一种强类型语言,强调在编译时捕获错误。模式匹配通过其类型测试和穷尽性检查等机制,进一步强化了这种静态类型安全,减少了运行时错误的可能性。
  • 生产力:Flutter 框架的流行使得 Dart 在 UI 开发领域扮演着重要角色。模式匹配能够极大地提高开发人员在处理复杂 UI 状态、解析 API 响应等场景下的开发效率和代码质量。
  • 可读性和维护性:干净、易读的代码是 Dart 社区一直追求的目标。模式匹配通过减少样板代码和明确数据意图,使得代码更易于理解和维护。

章节总结

本章我们深入探讨了 Dart 模式匹配的核心概念。我们了解到,模式匹配不仅仅是简单的值比较,更是一种集匹配解构类型安全于一体的强大工具。它旨在解决传统 Dart 代码中处理复杂数据结构时的冗余、可读性差和潜在运行时安全问题。通过与 Dart 3.0 中的记录密封类协同工作,模式匹配极大地提升了 Dart 语言的表现力、简洁性和健壮性,使我们能够编写更优雅、更可靠的代码。

展望下一章

理解了模式匹配的价值和背景后,是时候深入到它的具体实现细节了。在下一章,我们将聚焦于模式匹配的基石——核心概念与基础模式。我们会详细讲解各种基础模式类型,包括常量模式变量模式通配符模式类型测试模式以及处理可空性的空检查。通过丰富的代码示例和清晰的解释,你将学会如何在不同场景下运用这些基础模式,为后续掌握更复杂的解构和应用打下坚实的基础。

SwiftUI 让视图自适应高度的 6 种方法(四)

在这里插入图片描述

概览

在 SwiftUI 的世界里,我们无数次都梦想着视图可以自动根据布局上下文“因势而变”‌。大多数情况下,SwiftUI 会将每个视图尺寸处理的井井有条,不过在某些时候我们还是得亲力亲为。

在这里插入图片描述

如上图所示,无论顶部 TabView 容器里子视图高度如何变化,TabView 本身的高度都能“随遇而安”。如何用最简单、最现代化、最有趣且最切中要害的方法让容器尺寸与子视图的高度“如影随形”呢?

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

    1. 最“相得益彰”的实现:自定义布局 Layout
    • 9.1 重装上阵 Layout
    • 9.2 “奇怪的” TabView
    • 9.3 MaxHeightLayout 的实现

相信学完本课后,小伙伴们必能脑洞大开、格局打开,用“千姿百态”的方法让问题的解决一发入魂、九转功成!

那还等什么呢?Let‘s go!!!;)


9. 最“相得益彰”的实现:自定义布局 Layout

在一口气介绍完上面 5 种“五花八门”的实现之后,我们完全可以“鸣金收兵”。但是为了面面俱到,我们最后还是决定用自定义布局 Layout 来为整个系列博文画一个圆满的句号。

9.1 重装上阵 Layout

所谓自定义布局 Layout,其实就是创建一款遵守 Layout 协议的“容器”(严格说应该是视图集合 Collection of views),然后“恣意”为内部的子视图“排兵布阵”:

在这里插入图片描述

为什么说用 Layout 这种方法更加“鞭辟入里”呢?因为这是处理多个同一层级子视图布局最自然的方式。

大家回忆一下:我们是将所有喜爱的成语用 ForEach 挨个放在 TabView 容器里的,在父容器中对它们的布局“运筹帷幄”是理所当然的事。

9.2 “奇怪的” TabView

我们的目标是创建一个通用自定义布局 MaxHeightLayout,然后实时计算出所有子视图中最高的 Height。由于 MaxHeightLayout 是作为一个“容器”放在 TabView 中的,我们必须显式设置 TabView 的高度,而不能通过设置 MaxHeightLayout 的高度来间接影响 Tabview。

为什么会这样呢?这是由于 TabView 自身的特殊性质造成的。

比如在下面的代码中,我们在 TabView 里放置了一个高度为 200 的圆形:

TabView {
    Circle()
        .foregroundStyle(.green.gradient)
        .frame(height: 200)
}
.tabViewStyle(.page)

尽管我们将内部圆形的高度设置为 200,明确“暗示” TabView 把自己的高度也做出相应调整 ,但 TabView 还是会无动于衷:

在这里插入图片描述

要想 TabView 能够充分容纳高度为 200 的圆形,我们必须将 TabView 的高度显式设置为 200:

TabView {
    Circle()
        .foregroundStyle(.green.gradient)
}
.tabViewStyle(.page)
.frame(height: 200)

在这里插入图片描述

换句话说,TabView 不会站在子视图的角度考虑问题,它会完全忽略子视图尺寸的提议,“一意孤行”。

9.3 MaxHeightLayout 的实现

上面讨论的结果迫使我们必须让自定义布局 MaxHeightLayout 想办法将计算产生的最大高度传递向外给 TabView 才行。

有很多种方法可以达到目的,这里我们采用最简单的一种:绑定(Binding)。

struct MaxHeightLayout: Layout {
    
    var spacing: CGFloat?
    
    @Binding var maxHeight: CGFloat
    
    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
                
        let proposalWidth = proposal.width!
        let idealViewSizes = subviews.map { $0.sizeThatFits(.init(width: proposalWidth / CGFloat(subviews.count), height: nil)) }
        let totalHeight = idealViewSizes.map {$0.height}.max() ?? 0.0
                
        // 防止反复赋值造成渲染循环
        if totalHeight > maxHeight {
            maxHeight = totalHeight
        }
        
        return CGSize(width: proposalWidth, height: totalHeight)
    }
    
    private func calcSpaces(subviews: Subviews) -> [CGFloat] {
        if let spacing {
            [CGFloat](repeating: spacing, count: subviews.count - 1)
        } else {
            subviews.indices.map { idx in
                guard idx < subviews.count - 1 else { return 0 }
                                
                return subviews[idx].spacing.distance(to: subviews[idx+1].spacing, along: .horizontal)
            }
        }
    }
    
    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        
        let spaces = calcSpaces(subviews: subviews)
        var point = CGPoint(x: bounds.minX, y: bounds.minY)
        
        let subviewWidth = bounds.width / CGFloat(subviews.count)
        
        for idx in subviews.indices {
            subviews[idx].place(at: point, proposal: .init(width: subviewWidth - spaces[idx], height: maxHeight))
            
            if idx < subviews.count - 1 {
                point.x += subviewWidth - spaces[idx]
            }
        }
    }    
}

在上面的代码中,我们主要做了这么几件事:

  • 让 MaxHeightLayout “容器”中每个子视图的宽都平分容器的宽度;
  • 用 calcSpaces 方法计算子视图间的空隙,并确保 placeSubviews 方法在布局子视图时应用它们;
  • 只在必要时更新 maxHeight 绑定的值(totalHeight > maxHeight 时),这是避免“递归渲染”的重要手段;

最后,只要将 TabView 中原来内层的 ForEach 循环以及相关逻辑放在 MaxHeightLayout 里就可以啦:

Section("喜爱的成语") {
    TabView {
        ForEach(likeIdioms.chunked(into: 2), id: \.self) { idiomChunk in
            
            VStack {
                MaxHeightLayout(maxHeight: $maxHeight) {                    
                    ForEach(idiomChunk) { idiom in
                        likeIdiomCard(idiom)
                    }
                    
                    if idiomChunk.count < 2 {
                        Rectangle()
                            .foregroundStyle(.clear)
                    }
                }
                
                Spacer()
            }
        }
    }
    .tabViewStyle(.page)
    .frame(height: maxHeight)
    .padding(.bottom, 8)
}

运行代码可以发现结果和其它的实现毫无二致!

在这里插入图片描述

借助自定义布局 Layout 的灵活性,我们可以非常轻松的改变 TabView 中成语显示的数量,比如改为 3 列也不在话下:

在这里插入图片描述

至此,我们圆满完成了本系列博文中的所有任务。秃头小伙伴们还不赶紧给自己一个大大的赞吧!爱你们哦!❤

总结

在本篇博文中,我们介绍了如何使用自定义布局 Layout 来实现 SwiftUI 视图高度的“遥相呼应”,精彩的大结局小伙伴们不容错过哦!

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

Combine-常见使用场景

介绍

在现代 iOS 开发中,响应式编程日益重要。Apple 推出的 Combine 框架为开发者提供了强大的声明式 API,用于处理异步事件流。本文将结合常见场景,逐一展示 Combine 的实际用法,包括网络请求、输入控制、定时器、通知监听、异步任务处理以及视图控制器之间的逆向传值。

网络请求

通过 Combine 可以优雅地封装网络请求流程。

let url = URL(string: "https://api.example.com/data")!
// Publisher
let publisher = URLSession.shared.dataTaskPublisher(for: url)
    .map { $0.data }
    .decode(type: Response.self, decoder: JSONDecoder())
    .eraseToAnyPublisher()
// 订阅
let cancellable = publisher
    .sink(receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            print("Error: \(error)")
        case .finished:
            break
        }
    }, receiveValue: { response in
        print("Response: \(response)")
    })

控制输入

结合 @Published 和 debounce,可以高效地处理用户输入,避免频繁触发操作。

class ViewController: UIViewController {
    @Published var text = ""
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        $text
            .debounce(for: 0.5, scheduler: DispatchQueue.main)
            .sink { [weak self] value in
                guard let self = self else { return }
                self.processInput(value)
            }
            .store(in: &cancellables)
    }

    func processInput(_ input: String) {
        // 处理输入内容
        print("Input: \(input)")
    }
}

定时器

使用 Combine 的 Timer.publish 可以轻松创建定时器。

private var subscription: AnyCancellable?

subscription = Timer
            .publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .scan(0) { count, _ in // 累加,count为闭包最后一次返回的值
                count + 1
            }
            .sink(receiveCompletion: { _ in
                print("finish")
            }, receiveValue: { [weak self] count in // 操作UI时,[weak self]不可少
                guard let self = self else { return }
                self.countLbl.text = count.format
            })

通知

借助 NotificationCenter 和 Combine,可以优雅地响应通知事件。

private var subscriptions = Set<AnyCancellable>()

NotificationCenter
            .default
            .publisher(for: UITextField.textDidChangeNotification, object: inputTxtField) // 监听输入
            .compactMap { ($0.object as? UITextField)?.text } // 此时的publisher是通知
            .map { "The user entered: \($0)" }
            .assign(to: \.text, on: textLbl)
            .store(in: &subscriptions)

异步

通过 Future 可以将传统的回调封装为 Combine 的 Publisher。

// Publisher
func authorize() -> AnyPublisher<Bool, Error> {
        // 延期Publisher等待订阅
        Deferred {
            // 任何异步操作都可以包进Future
            Future { promise in
                UNUserNotificationCenter
                    .current()
                    .requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
                        // Promise处理
                        if let error = error {
                            // AnyPublisher的第2个参数
                            promise(.failure(error))
                        } else {
                            // AnyPublisher的第1个参数
                            promise(.success(granted))
                        }
                    }
            }
        }
        .eraseToAnyPublisher()
    }
    
private var subscriptions = Set<AnyCancellable>()
// 订阅
authorize() // AnyPublisher
            .replaceError(with: false) // 异常处理
            .receive(on: DispatchQueue.main)
            .sink { [weak self] val in
                guard let self = self else { return }
                self.permissionLbl.text = "Status: \(val ? "Granted" : "Denied")"
            }
            .store(in: &subscriptions)

UIViewController逆向传值

当需要从下一个页面传值回当前页面时,Combine 提供了比 delegate 更优雅的方式。

// 接收值的UIViewController
let nextViewController = NextViewController()
let publisher = PassthroughSubject<String, Never>()
nextViewController.publisher = publisher

subscription = publisher.sink { [weak self] info in
    guard let self = self else { return }
    // 处理info
}

present(nextViewController, animated: true)



// 传递值的UIViewController
var publisher: PassthroughSubject<String, Never>?

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    publisher?.send("传值")
    dismiss(animated: true) {
        self.publisher?.send(completion: .finished)
    }
}

SwiftUI 让视图自适应高度的 6 种方法(三)

在这里插入图片描述

概览

在 SwiftUI 的世界里,我们无数次都梦想着视图可以自动根据布局上下文“因势而变”‌。大多数情况下,SwiftUI 会将每个视图尺寸处理的井井有条,不过在某些时候我们还是得亲力亲为。

在这里插入图片描述

如上图所示,无论顶部 TabView 容器里子视图高度如何变化,TabView 本身的高度都能“随遇而安”。如何用最简单、最现代化、最有趣且最切中要害的方法让容器尺寸与子视图的高度“如影随形”呢?

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

  • 7. 最难满足编译器的方法:visualEffect
    • 7.1 第一个问题
    • 7.2 第二个问题
    1. 避免递归渲染(Recursive rendering)的一点考虑

相信学完本课后,小伙伴们必能脑洞大开、格局打开,用“千姿百态”的方法让问题的解决一发入魂、九转功成!

那还等什么呢?Let‘s go!!!;)


7. 最难满足编译器的方法:visualEffect

其实,自从一开始在博文开头抛出这个问题,很多秃头小伙伴们可能就已经想到用 visualEffect 方法了:

在这里插入图片描述

实际上,visualEffect 修改器方法的本职工作是在 SwiftUI 视图上更顺畅的应用可视特效(Effects),提供几何数据只是它的“副业”而已。

利用 visualEffect 方法来获取 SwiftUI 视图高度原本很简单:

likeIdiomCard(idiom)
    .visualEffect { content, proxy in
        let height = proxy.size.height
        if height > maxHeight {
            maxHeight = height
        }
        
        return content
    }

不过,上述代码会有两个问题。

7.1 第一个问题

首先,如果我们编译运行则会发现 visualEffect 方法的闭包并不会得到调用。这是因为直接照原样返回 content 貌似并不会触发闭包的回调,仔细想想也可以理解:将心比心,如果新视图的特效和原来如出一辙,为毛还要浪费渲染算力呢?

这个问题很好解决,只需“瞒天过海”让 SwiftUI 渲染引擎以为我们应用了不同的特效即可:

return effect.offset(.zero)

7.2 第二个问题

第二个问题是如果将编译器切换到 Swift 6 或启用 Swift 5 的严格并发模式,那么立马就会触发 Compiler 的“牢骚满腹”:

在这里插入图片描述

之前所有的实现都没有类似的问题,visualEffect 为毛那么“难伺候”呢?虽说代码也可以达到目的,但患有强迫症的秃头码农们又怎能善罢甘休!?

其实,编译器如此这般抱怨也不完全是“空穴来风”,因为 SwiftUI 视图的任何状态默认都必须隐式在 MainActor 上访问和修改,但 visualEffect 方法的闭包显然无法做此保证。于是乎,一种简单的方法就是我们自己撸码来确保这一点:

likeIdiomCard(idiom)
    .visualEffect { effect, proxy in
        Task {@MainActor in
            let height = proxy.size.height
            if height > maxHeight {
                maxHeight = height
            }
        }
        
        return effect.offset(.zero)
    }

在上面的代码中,我们在 visualEffect 闭包中创建了一个运行在 MainActor 上的任务(Task),这是通过用 @MainActor 修饰任务闭包来实现的。这样一来,我们对于 maxHeight 状态的读写操作会以“原子”的方式在主线程上执行,不会再有任何同步问题,这自然让编译器乖乖闭嘴!

在其它情况下,可能 proxy 本身也不是可发送(Sendable )的对象,这时我们还可以使用 局部只读临时变量 来如愿以偿:

likeIdiomCard(idiom)
    .visualEffect { effect, proxy in
    // 假设 proxy 不是可发送的对象
        Task {@MainActor [height = proxy.size.height] in
            if height > maxHeight {
                maxHeight = height
            }
        }
        
        return effect.offset(.zero)
    }

8. 避免递归渲染(Recursive rendering)的一点考虑

现在,经过小伙伴们的不懈努力,上面所有 5 种方法都能圆满的完成任务。

不过,如果“吹毛求疵”的我们希望 TabView 自适应的高度能够与底部有一些空隙,我们可能会这么写:

TabView {    
    //...
}
.tabViewStyle(.page)
.frame(height: maxHeight + 20)

在上面的实现中,我们“贴心”的让 TabView 的高度在 maxHeight 基础上增加 20 以获得一些底部的间隙。

但是,倘若我们胆敢运行上述代码,TabView 自身的高度就会立即进入“突飞猛涨”的节奏,让小伙伴们目瞪口呆:

在这里插入图片描述

仔细观察 Xcode 预览中的调试日志就会发现,我们可怜的 TabView 实际在以每次 20 的速率疯狂的长高ing。我们称这种现象称为典型的递归渲染(Recursive rendering 或渲染反噬)。

造成这种情况的原因是:每次好不容易用 maxHeight 设置了 TabView 的高度之后,我们又“贪得无厌”的增加了 TabView 的高度,这样会再次迫使 maxHeight 以新的高度重新求值,从而周而复始没完没了。

解决这种问题的办法有很多,一种就是直接打破“死循环”,让桎梏烟消云散:

TabView {    
    //...
}
.tabViewStyle(.page)
.frame(height: maxHeight)
.padding(.bottom, 20)

如上代码所示,我们放弃了对 maxHeight “指手画脚”的企图,而是转而使用 padding 修改器方法达到了相同的目的。这时,maxHeight 设置的高度和用 paddding 增加的间隙会彼此独立,从而不会有任何渲染死循环,棒棒哒!💯

在这里插入图片描述

在下一篇博文中,我们最终将用 Layout 自定义布局来精心打造一款可以自动计算子视图最大高度的容器,敬请期待吧!

总结

在本篇博文中,我们先是搞定了最让编译器头疼的 visualEffect 实现,随后介绍了什么是递归渲染以及如何让其“烟消云散”。

感谢观赏,下一篇再见!8-)

SwiftUI 让视图自适应高度的 6 种方法(二)

在这里插入图片描述

概览

在 SwiftUI 的世界里,我们无数次都梦想着视图可以自动根据布局上下文“因势而变”‌。大多数情况下,SwiftUI 会将每个视图尺寸处理的井井有条,不过在某些时候我们还是得亲力亲为。

在这里插入图片描述

如上图所示,无论顶部 TabView 容器里子视图高度如何变化,TabView 本身的高度都能“随遇而安”。如何用最简单、最现代化、最有趣且最切中要害的方法让容器尺寸与子视图的高度“如影随形”呢?

在本篇博文中,您将学到如下内容: 4. 最复杂的方法:anchorPreference

  1. 最简单的方法:onGeometryChange
  2. 最有“创意”的方法:Canvas

相信学完本课后,小伙伴们必能脑洞大开、格局打开,用“千姿百态”的方法让问题的解决一发入魂、九转功成!

那还等什么呢?Let‘s go!!!;)


4. 最复杂的方法:anchorPreference

除了 GeometryReader 以外,在 SwiftUI 中同样还有一个“六朝元老” anchorPreference 修改器方法:

在这里插入图片描述

anchorPreference 方法可以将任何视图的几何属性(geometry value)反向传递给父视图。这些几何属性里不多不少刚好包含了一个 bounds 属性可以被用来获取视图的尺寸。不过,anchorPreference 方法不能“独自为战”,它必须与 GeometryReader 携手才能完成任务。

首先,我们需要创建一个 HeightKey 将 anchorPreference 修改器方法的“侦测”结果与特定键对应起来,可以看到它必须遵循 PreferenceKey 协议:

struct : PreferenceKey {
    static var defaultValue = [Anchor<CGRect>]()
    
    static func reduce(value: inout [Anchor<CGRect>], nextValue: () -> [Anchor<CGRect>]) {
        value.append(contentsOf: nextValue())
    }
}

接着,我们可以 happy 地获取每个 likeIdiomCard 视图的高度了:

TabView {    
    ForEach(likeIdioms.chunked(into: 2), id: \.self) { idiomChunk in
        HStack {
            ForEach(idiomChunk) { idiom in
                likeIdiomCard(idiom)
                    .anchorPreference(key: HeightKey.self, value: .bounds) {
                        [$0]
                    }
            } 
            
            if idiomChunk.count == 1 {
                Rectangle()
                    .foregroundStyle(.clear)
            }
        }
        
    }
}
.backgroundPreferenceValue(HeightKey.self) { datas in
    GeometryReader { proxy in
        if let max = datas.map({ proxy[$0].size.height }).max(), max > maxHeight {
            maxHeight = max
        }
        
        return Text("Stub").opacity(0)
    }
}
.tabViewStyle(.page)
.frame(height: maxHeight)

如您所见,我们先是调用 anchorPreference 方法获取了每个 likeIdiomCard 视图的 bounds 几何属性,随后我们在 GeometryReader 闭包中将高度抽取出来,并将最大值赋予 maxHeight,最后我们在百忙之中还不忘返回一个 “Stub” 占位视图,真乃殚思极虑也。

大家可以看到,使用 anchorPreference 方法貌似并不是那么“惬意”:

  • 需要 PreferenceKey;
  • 需要 GeometryReader 配合;

最大的问题是:anchorPreference 方法只是获取到了所需尺寸的数据,我们还需使用 backgroundPreferenceValue 或 onPreferenceChange 等方法来“接收”这些数据,这显得有些横生枝节。

所幸的是,苹果可能对此也有些卑陬失色,所以在 SwiftUI 的后续版本中做出了改善和简化。

5. 最简单的方法:onGeometryChange

正如上面所说的那样,苹果在 SwiftUI 4.0(iOS 16)中专门新增了一个 onGeometryChange 修改器方法来让视图几何信息的获取轻车简从:

在这里插入图片描述

现在,利用 onGeometryChange 方法,我们获取和接收视图尺寸的逻辑都能“如胶似漆”般的融合在一起了:

likeIdiomCard(idiom)
    .onGeometryChange(for: CGFloat.self) { proxy in
        proxy.size.height
    } action: { value in
        if maxHeight < value {
            maxHeight = value
        }
    }

从上面的代码可以看到:我们在 onGeometryChange 第一个闭包中获取到了视图的高度,随后立即在第二个闭包中完成了 maxHeight 的设置任务。这不妥妥的就是之前 anchorPreference 方法的加强和简化版吗?:)

6. 最有“创意”的方法:Canvas

除了使用修改器方法来获取指定视图的尺寸信息以外,我们还可以“灵机一动”使用 SwiftUI 3.0 推出的 Canvas 原生视图来“抽丁拔楔”:

在这里插入图片描述

从 Canvas 视图的定义可以看到,它的特长其实是高性能的实现 SwiftUI 界面的绘制工作。不过,这并不影响我们“乖巧的”利用其绘制回调闭包中的尺寸信息来“旁敲侧击”得到其对应视图的高度:

likeIdiomCard(idiom)
    .background {
        Canvas { context, size in
            if size.height > maxHeight {
                maxHeight = size.height
            }
            
            let rect = CGRect(origin: .zero, size: size)
            context.fill(Path(rect), with: .color(.clear))
        }
    }

正如大家在上面代码中看到的那样,我们利用 Canvas 视图回调闭包中的 size 传入实参得到了 likeIdiomCard 视图的高度,并将其最大值赋给了 maxHeight 状态。注意,我们同样需要在最后绘制一个“占位”路径(Path)以“避免尴尬”。

在下一篇博文中,我们将会介绍另一种最让编译器“头疼”的方法并谈谈如何避免渲染反噬(Recursive rendering)。一言为定,等你们哦!

总结

在本篇博文中,我们分别介绍了另外 3 种“最复杂”、“最简单”以及最有“创意”的方法来让 SwiftUI 视图自适应尺寸这一问题“冰解的破”。

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

Re: 0x00. 从零开始的光线追踪实现-画布

定个小目标

最近刚看完一本书Ray Tracing in One Weekend,书里是通过软件渲染的形式(纯 CPU 计算)实现光线追踪,打算在 macOS 上用 Metal API 调用 GPU 实现一下。

看看本节最终效果

image.png

画三角形

老早之前我就写过一篇我当时觉得好玩的东西,用 Metal 画一个三角形(Swift 函数式风格),其实当时的写法相当 naive,不过里面相对有价值的内容就是 自定义操作符自定义操作符 之前的实现。现在首先来新建一个项目,顺便把之前 普通的方式画一个三角形普通的方式画一个三角形 抄过来,接下来在此基础上重构。

ViewController 实现 MTKViewDelegate

抄完之后第一步就是实现 draw(in view: MTKView)

import Cocoa
import MetalKit

class ViewController: NSViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    (view as? MetalView)?.delegate = self
  }

  override var representedObject: Any? {
    didSet {}
  }
}

extension ViewController: MTKViewDelegate {
  func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {}
  
  func draw(in view: MTKView) {
    if let v = view as? MetalView {
      v.render()
    }
  }
}

protocol 开道

我们定义一下 PathTracer 协议

import Metal

protocol PathTracer {
  var device: (any MTLDevice)? { get }
  var queue: (any MTLCommandQueue)? { get }
  var pipeline: (any MTLRenderPipelineState)? { get }
  
  func render()
}

MetalView 实现 PathTracer 协议

class MetalView: MTKView {
  required init(coder: NSCoder) {
    super.init(coder: coder)
    device = MTLCreateSystemDefaultDevice()
    _queue = device?.makeCommandQueue()
    createPipeline()
  }
  private var _queue: (any MTLCommandQueue)?
  private var _pipeline: (any MTLRenderPipelineState)?
}

extension MetalView: PathTracer {
  var queue: (any MTLCommandQueue)? { _queue }
  var pipeline: (any MTLRenderPipelineState)? { _pipeline }
}

接着来改造一下原先版本的 render 函数

extension MetalView {
  private func createPipeline() {
    if let device, let library = device.makeDefaultLibrary() {
      let renderPipelineDesc = MTLRenderPipelineDescriptor()
      renderPipelineDesc.vertexFunction = library.makeFunction(name: "vertexFn")
      renderPipelineDesc.fragmentFunction = library.makeFunction(name: "fragmentFn")
      renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
      _pipeline = try? device.makeRenderPipelineState(descriptor: renderPipelineDesc)
    }
  }
  
  func render() {
    guard let device = device else { fatalError("Failed to find default device.") }
    let vertexData: [Float] = [
        0.0,  0.5,
       -0.5, -0.5,
        0.5, -0.5,
    ]
    
    let dataSize = vertexData.count * MemoryLayout<Float>.size
    let vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
    let renderPassDesc = MTLRenderPassDescriptor()
    if let currentDrawable {
      renderPassDesc.colorAttachments[0].texture = currentDrawable.texture
      renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0, alpha: 1.0)
      renderPassDesc.colorAttachments[0].loadAction = .clear
      guard let queue else { fatalError("Failed to make command queue.") }
      let commandBuffer = queue.makeCommandBuffer()
      guard let commandBuffer else { fatalError("Failed to make command buffer.") }
      let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc)
      guard let encoder = encoder else { fatalError("Failed to make render command encoder.") }
      if let pipeline {
        encoder.setRenderPipelineState(pipeline)
        encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
        encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
        encoder.endEncoding()
        commandBuffer.present(currentDrawable)
        commandBuffer.commit()
      }
    }
  }
}

由于我们把 vertexData 调整了一下,所以别忘了把 shader 也调整一下,主要是调整顶点

#include <metal_stdlib>

using namespace metal;

struct VertexIn {
  float2 position;
};

struct Vertex {
  float4 position [[position]];
};

vertex Vertex vertexFn(constant VertexIn *vertices [[buffer(0)]], uint vid [[vertex_id]]) {
  return Vertex { float4(vertices[vid].position, 0, 1) };
}

fragment float4 fragmentFn(Vertex vert [[stage_in]]) {
  return float4(1, 0, 0, 1);
}

来运行一下看看当前的结果

image.png

其实这个坐标是这样的

fig-02-ndc.svg

画直角三角形

根据前面的坐标图,我们把这个三角形改成直角三角形,其实就是把顶点坐标改成左上、右上、左下三个点,所以顶点很明显就是

let vertexData: [Float] = [
  -1,  1,
  -1, -1,
   1,  1
]

于是就会显示出这样子的结果

image.png

拼一个矩形

现在如果放两个直角三角形拼起来,就能画出我们这节最终想要的一个“战场”

extension MetalView: PathTracer {
  // ...
  func render() {
    // ...
    let vertexData: [Float] = [
      -1, 1,
      -1, -1,
      1, 1,
      1, 1,
      -1, -1,
      1, -1,
    ]

    // ...
    if let currentDrawable {
      // ...
      if let pipeline {
        // ...
        encoder.drawPrimitives(
          type: .triangle,
          vertexStart: 0,
          vertexCount: 6,
          instanceCount: 2
        )
        // ...
      }
    }
  }
}

最终我们就完成了开头提到的本节最终效果

image.png


暂时先到这,我们说白了现在就是搞了一张画布,后续的渲染工作基本上只需要调整 Fragment Shader 的代码就可以了。

PureLayout Learn

设置大小

方式1
[self.redView autoSetDimensionsToSize:CGSizeMake(100, 100)];

方式2
[redView autoSetDimension:ALDimensionWidth toSize:100];
[redView autoSetDimension:ALDimensionHeight toSize:100];

控件之间间距设置

//蓝色view的左边,距离红色view的右边为30
[self.blueView autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.redView withOffset:30];

控件之间等高 或者 等宽

 [@[self.blueView,self.redView] autoMatchViewsDimension:ALDimensionWidth];
 [@[self.blueView,self.redView] autoMatchViewsDimension:ALDimensionHeight];

控件之间水平对齐

[self.blueView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.redView];

//红色view在父view的中心点
[self.redView autoCenterInSuperview];

父子控件间距

[self.greenView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsMake(15, 15, 15, 15)];

[self.greenView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];

单个间距设置  如left
[self.purpleView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:20.f];

父子控件一样大小

[self.greenView autoPinEdgesToSuperviewEdges];

控件之间高度一半

//蓝色View的高度是红色View高度的一半
[self.blueView autoMatchDimension:ALDimensionHeight toDimension:ALDimensionHeight ofView:self.redView withMultiplier:0.5];

控件如何创建

UIImageView *contentView = [UIImageView newAutoLayoutView];

多个控件之间水平布局

@property (nonatomic, strong) UIView *redView;
@property (nonatomic, strong) UIView *blueView;
@property (nonatomic, strong) UIView *purpleView;
@property (nonatomic, strong) UIView *greenView;

@end

@implementation ThirdDemoController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.title = @"ThirdDemo";
    self.view.backgroundColor = [UIColor whiteColor];
    
    [self.view addSubview:self.redView];
    [self.view addSubview:self.blueView];
    [self.view addSubview:self.purpleView];
    [self.view addSubview:self.greenView];
    
    [self initThirdPureLayout];
}
#pragma mark - 设置约束
- (void)initThirdPureLayout{
    NSArray *colorViews = @[self.redView, self.blueView, self.purpleView, self.greenView,];
    [colorViews autoSetViewsDimension:ALDimensionHeight toSize:40.f];
    //间距为10,水平对齐,依次排列
    [colorViews autoDistributeViewsAlongAxis:ALAxisHorizontal alignedTo:ALAttributeHorizontal withFixedSpacing:10.0 insetSpacing:YES matchedSizes:YES];
    //红色view相对于其父view水平对齐
    [self.redView autoAlignAxisToSuperviewAxis:ALAxisHorizontal];
}

需要后面搞清楚

 [_purpleView autoPinEdge:ALEdgeBottom toEdge:ALEdgeBottom ofView:self.contentView withOffset:0 relation:NSLayoutRelationLessThanOrEqual];

Swift高阶函数大全:让你的代码更优雅高效

大家好!今天我们来深入探讨Swift中那些让集合操作变得轻松愉快的高阶函数。这些函数就像是数据处理流水线上的各种工具,每个都有其独特的用途和魅力。

基础三剑客

1. map:变形大师

let prices = [100, 200, 300]
let discountedPrices = prices.map { $0 * 0.8 }
// [80.0, 160.0, 240.0]

map就像一位魔术师,能把数组中的每个元素变成你想要的样子。它总是返回一个与原数组等长的新数组。

2. filter:挑剔的门卫

let inventory = ["iPhone": 10, "MacBook": 5, "AirPods": 20]
let lowStock = inventory.filter { $0.value < 15 }
// ["iPhone": 10, "MacBook": 5]

filter只放行符合条件的元素,是数据筛选的利器。

3. reduce:数据聚合器

let sales = [450, 320, 680]
let totalSales = sales.reduce(1000) { $0 + $1 } 
// 2450(初始值1000加上所有销售额)

reduce可以把集合"浓缩"成一个值,特别适合做求和、求积等聚合操作。

进阶高手

4. forEach:简洁的遍历

["苹果", "香蕉", "橙子"].forEach { fruit in
    print("今天特价:(fruit)")
}

forEach比传统for循环更简洁,但要注意:

  • 不能使用break/continue
  • 用return只能跳出当前闭包

5. sorted:排序专家

let students = [
    (name: "张三", score: 88),
    (name: "李四", score: 92),
    (name: "王五", score: 85)
]

let ranked = students.sorted { $0.score > $1.score }
// 按分数降序排列

6. contains(where:):存在性检查

let emails = ["a@test.com", "b@gmail.com", "c@qq.com"]
let hasGmail = emails.contains { $0.contains("@gmail.com") }
// true

比先filter再判断isEmpty更高效!

查找高手

7. first(where:) / last(where:)

let transactions = [100, -50, 200, -30, 150]
let firstLargeDeposit = transactions.first { $0 > 150 } // 200
let lastWithdrawal = transactions.last { $0 < 0 }       // -30

找到第一个/最后一个符合条件的元素,比filter(...).first更高效。

集合切片

8. prefix( :) 和 dropFirst( :)

let historyData = [23, 45, 67, 89, 12, 34, 56]

let recent5 = historyData.prefix(5)    // 前5个元素
let withoutFirst2 = historyData.dropFirst(2) // 去掉前2个后的元素

这些方法在处理大数据分页或滑动窗口时特别有用。

逻辑判断

9. allSatisfy(_:):全员检测

let examScores = [85, 90, 88, 92]
let allPassed = examScores.allSatisfy { $0 >= 60 } // true

检查集合中所有元素是否都满足条件,比手动遍历更优雅。

性能优化

10. lazy:延迟加载

let hugeRange = 1...1000000
let result = hugeRange.lazy
    .filter { $0 % 3 == 0 }
    .map { $0 * 2 }
    .prefix(10)

lazy会延迟计算,直到真正需要结果时才执行操作,避免创建大量中间数组。

实战组合技

let salesData = [
    (region: "North", amount: 3500),
    (region: "South", amount: 4200),
    (region: "East", amount: 3800),
    (region: "West", amount: 5100)
]

// 找出金额超过4000的地区,按金额降序排列,取前2名
let topPerformers = salesData
    .filter { $0.amount > 4000 }
    .sorted { $0.amount > $1.amount }
    .prefix(2)
    .map { $0.region }

// ["West", "South"]

使用建议

  1. 优先考虑可读性:在业务代码中大胆使用高阶函数
  2. 性能敏感处权衡:对于超大数据集,考虑使用lazy或传统循环
  3. 避免过度嵌套:如果闭包太复杂,考虑提取为独立函数
  4. 善用类型推断:简明闭包可以用0、0、1等缩写

总结对比

函数 特点 典型用途
map 一对一转换 数据类型转换、计算衍生值
filter 条件筛选 数据过滤、搜索
reduce 聚合计算 求和、求积、拼接字符串
forEach 简洁遍历 替代简单for循环
sorted 排序 各种排序需求
contains 存在性检查 快速判断条件是否满足
first/last 查找元素 替代filter(...).first
prefix/drop 切片操作 分页、窗口计算
allSatisfy 全员检测 数据验证
lazy 延迟计算 大数据处理优化

记住:没有最好的函数,只有最适合场景的选择。希望这些高阶函数能成为你Swift开发中的得力助手!如果有任何使用心得,欢迎在评论区分享~

Swift 中“===” 和 “==”区别是什么?

在 Swift 中,===== 是完全不同的操作符,它们的核心区别在于比较的目标适用场景。以下是详细解释和代码案例:


一、核心区别总结

操作符 比较目标 适用类型 是否需要自定义实现
== 值是否相等 值类型、引用类型 需实现 Equatable
=== 内存地址是否相同 仅引用类型(类) 自动处理

二、==(值相等)

特点

  1. 比较值:检查两个实例的内容是否相等。
  2. 协议依赖:类型必须遵守 Equatable 协议,并实现 static func ==
  3. 适用范围:结构体、枚举、类(需手动实现逻辑)。

示例 1:类的值比较(手动实现 Equatable

class Student: Equatable {
    var id: Int
    var name: String
    
    init(id: Int, name: String) {
        self.id = id
        self.name = name
    }
    
    // 实现 Equatable 协议:只要 id 相同则认为相等
    static func == (lhs: Student, rhs: Student) -> Bool {
        return lhs.id == rhs.id
    }
}

let studentA = Student(id: 1, name: "Alice")
let studentB = Student(id: 1, name: "Bob") // id 相同,name 不同
let studentC = studentA

print(studentA == studentB) // true(id 相同)
print(studentA == studentC) // true(同一实例,值自然相同)

示例 2:结构体的值比较(自动合成 Equatable

struct Point: Equatable {
    var x: Int
    var y: Int
    // 结构体自动合成 Equatable 实现(所有属性都遵守 Equatable)
}

let p1 = Point(x: 3, y: 5)
let p2 = Point(x: 3, y: 5)
print(p1 == p2) // true(值完全相同)

三、===(恒等性:内存地址比较)

特点

  1. 比较内存地址:判断两个引用是否指向同一个实例
  2. 仅限引用类型:只能用于类实例(结构体、枚举无法使用)。
  3. 不可自定义:由 Swift 自动处理,开发者无法修改逻辑。

示例 1:类的实例比较

class Dog {
    var name: String
    init(name: String) { self.name = name }
}

let dog1 = Dog(name: "Buddy")
let dog2 = Dog(name: "Buddy") // 内容相同,但新实例
let dog3 = dog1

print(dog1 === dog2) // false(不同内存地址)
print(dog1 === dog3) // true(同一内存地址)

示例 2:可选类型的恒等比较

let optionalDog1: Dog? = Dog(name: "Max")
let optionalDog2: Dog? = optionalDog1 // 指向同一个实例
let optionalDog3: Dog? = Dog(name: "Max")

print(optionalDog1 === optionalDog2) // true(同一实例)
print(optionalDog1 === optionalDog3) // false(不同实例)

四、特殊案例与注意事项

案例 1:未实现 Equatable 的类无法使用 ==

class Book { // 未遵守 Equatable 协议
    var title: String
    init(title: String) { self.title = title }
}

let book1 = Book(title: "Swift Guide")
let book2 = Book(title: "Swift Guide")
// print(book1 == book2) // ❌ 编译错误:未实现 Equatable

案例 2:枚举的值比较(值类型)

enum NetworkStatus: Equatable {
    case connected
    case disconnected(reason: String)
}

let status1 = NetworkStatus.disconnected(reason: "No signal")
let status2 = NetworkStatus.disconnected(reason: "No signal")
print(status1 == status2) // true(关联值相同)

五、总结

  • ==:关注“内容”是否相等,需要类型遵守 Equatable
  • ===:关注“内存地址”是否相同,仅用于类的实例比较。

Swift 链式调用的理解

一、链式调用的核心原理:返回 self

1. 为什么返回 self 能实现链式调用?

  • 方法调用的连续性:在 Swift 中,方法调用后返回的对象类型决定了后续调用的可能性。如果一个方法返回的是当前对象(self),那么连续调用下一个方法时,Swift 会将返回的 self 作为调用主体。
  • 语法结构:Swift 允许连续的点操作(.),例如 a().b().c()。每个方法的返回值必须兼容后续方法的调用主体(通常是同一类型)。

2. 示例代码详解

class Builder {
    var result: String = ""
    
    // 方法返回 self,允许链式调用
    @discardableResult
    func addText(_ text: String) -> Builder {
        result += text + " "
        return self
    }
    
    func build() -> String {
        return result.trimmingCharacters(in: .whitespaces)
    }
}

let builtString = Builder()
    .addText("Hello")   // 返回 Builder 实例
    .addText("World")   // 接收上一个 addText 返回的 Builder 实例
    .build()            // 最终调用 build 方法
  • 关键点
    • addText 返回 Builder 类型的 self,因此 .addText("World") 的调用主体是同一个 Builder 实例。
    • build() 返回 String,结束链式调用。

二、闭包实现链式调用的原理与细节

1. 闭包返回对象自身

  • 闭包作为方法参数:可以通过闭包返回对象自身,实现链式调用。
  • 示例代码详解
class Adder {
    var currentValue: Double = 0
    
    // 闭包返回 self
    func add(value: Double, then action: (Adder) -> Void) -> Adder {
        currentValue += value
        action(self) // 将 self 传入闭包
        return self
    }
    
    func getResult() -> String {
        return String(format: "%.2f", currentValue)
    }
}

// 链式调用
let result = Adder()
    .add(value: 1.111) { _ in }  // 闭包接收 self
    .add(value: 2.222) { _ in }  // 闭包接收 self
    .getResult()
  • 关键点
    • add 方法的闭包参数接收 self,允许在闭包内操作对象状态。
    • 返回 self 后,可以继续调用 add 方法。

三、可选链式调用(Optional Chaining)详解

1. 可选链式调用的语法

  • 语法格式optional?.propertyOrMethod()
  • 作用:如果 optionalnil,整个链式调用返回 nil,而不会崩溃。

2. 示例代码详解

class Person {
    var residence: Residence?
}

class Residence {
    var address: Address?
}

class Address {
    var street: String?
}

let person = Person()
person.residence = Residence()
person.residence?.address = Address()
person.residence?.address?.street = "Main St"

// 可选链式调用
if let street = person.residence?.address?.street {
    print(street) // 输出 "Main St"
} else {
    print("Address not available")
}
  • 关键点
    • person.residence?:如果 residencenil,后续调用直接返回 nil
    • ?.address?:继续检查 address 是否为 nil
    • ?.street:最终访问 street 属性。

四、扩展方法支持链式调用的实现

1. 扩展方法的语法

  • 语法格式extension TypeName { ... }
  • 作用:为已有类型添加方法或属性。

2. 示例代码详解

extension String {
    @discardableResult
    func appendIfNotEmpty(_ text: String) -> String {
        if !text.isEmpty {
            return self + text
        }
        return self
    }
}

let message = "Hello".appendIfNotEmpty(" World").appendIfNotEmpty("")
print(message) // 输出 "Hello World"
  • 关键点
    • @discardableResult:告诉编译器忽略返回值,避免未使用的返回值警告。
    • appendIfNotEmpty 返回 String 类型,允许连续调用。

五、@discardableResult 的作用与必要性

1. 什么是 @discardableResult

  • 作用:标注方法的返回值可以被忽略,即使没有显式使用返回值。
  • 必要性:链式调用中,开发者通常只关心最终结果,而中间方法的返回值(self)无需显式赋值。

2. 示例对比

// 不使用 @discardableResult
func addText(_ text: String) -> Builder {
    // ...
}

// 调用时必须显式使用返回值(否则报错)
_ = Builder().addText("Hello")

// 使用 @discardableResult
@discardableResult
func addText(_ text: String) -> Builder {
    // ...
}

// 调用时可忽略返回值
Builder().addText("Hello")

六、实际应用场景详解

1. UI 构建中的链式调用

let button = UIButton()
    .setTitle("Login", for: .normal)     // 设置标题
    .setTitleColor(.blue, for: .normal)  // 设置颜色
    .setBackgroundColor(.lightGray)      // 设置背景色
    .addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) // 添加事件
  • 关键点
    • 每个方法返回 UIButton 实例,允许连续调用。
    • addTarget 方法需要返回 UIButton 才能继续链式调用(需自定义扩展或使用库)。

2. 数据处理中的链式调用

let result = [1, 2, 3, 4, 5]
    .filter { $0 % 2 == 0 }    // 过滤偶数
    .map { $0 * 2 }           // 乘以 2
    .reduce(0, +)             // 求和
  • 关键点
    • 每个高阶函数返回新的数组或值,形成链式调用。
    • filter 返回 [Int]map 返回 [Int]reduce 返回 Int

七、链式调用的性能与注意事项

1. 性能影响

  • 内存开销:链式调用中的中间结果可能生成临时对象(如数组、字符串),需注意内存占用。
  • 闭包捕获:闭包中捕获 self 时,需避免强引用循环(使用 [weak self][unowned self])。

2. 注意事项

  • 过度链式调用:长链式调用可能导致代码可读性下降,建议拆分复杂逻辑。
  • 错误处理:链式调用中若某个方法抛出错误,需使用 do-catch 处理。

八、总结:链式调用的关键要素与技巧

要素 解释
返回 self 方法返回当前对象实例,允许连续调用。
@discardableResult 忽略中间方法的返回值,避免编译器警告。
可选链式调用 ? 安全访问可选值的属性或方法,避免崩溃。
扩展方法 为已有类型添加链式调用方法,无需修改原始类。
闭包链式调用 通过闭包传递对象自身,实现更灵活的链式逻辑。
性能与可读性 平衡链式调用的简洁性与代码的可维护性,避免过度链式调用。

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

前言

最近有位粉丝遭遇了Section 11.2(Termination) states的通知,具体原文如图:

53dcb964069b45127fbca8f846569e05.jpg

其核心的表达在于:注意,操纵App Store排名、用户评论或搜索索引可能会导致你失去开发者计划会员资格。

收到此提醒的时候,要慎重处理。可以理解为是苹果非常友好的口头警告!但是,如果没有积极整改或者及时回应,那么3.2f封号邮件就在正路上了

Section 11.2内因

踩到此问题的关键在于操控评论或者关键词排名。说人话也就是刷好评和积分墙。(这里点到为止,懂的都懂不做过度延展。)

如果是自己操作的,那么需要逐级递减评分和机刷数量,切记不要收到邮件就收手!

苹果刚说完你有问题,立马就收手正中苹果下怀,多少有点此地无银三百两的意思。本来苹果也是就是走流程吓吓你,别弄巧成拙

不管是积分墙还是好评数,需要控制一个合理数量。比如说有100个下载,那么对应5~10个好评不过分,但是100个下载,100个好评。这显然不合理!

Section 11.2外因

刚刚提及的内部因素,其实还有一部分人是外部因素。这里说直白一点就是竞品搞事情,想通过这种违规操作的行为把竞品搞嘎了。毕竟人性是最复杂的。商场如战场,在商言商。(这里没有任何冒犯的意思,只是就事论事!)

如果自己什么都没有做,明显被同行搞了。这种情况下需要从苹果开发者后台底部菜单栏-》选择联系我们->点击报告错误,如图所示。

f4e09313ad330e34cbab2e2f4527a2e2.png

说明自己的情况,必要的时候可以附加App销售最近的一个安装量。真诚地向苹果解释问题,只需要客观陈述事实即可。可别胡乱的添油加醋。

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

❌