当前位置:首页>iOSAPP>一个 iOS 17 新特性,把 Core Data 时代最丑的代码扫掉了

一个 iOS 17 新特性,把 Core Data 时代最丑的代码扫掉了

  • 2026-08-07 08:53:19
一个 iOS 17 新特性,把 Core Data 时代最丑的代码扫掉了

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

前言

前段时间在项目里改一个过滤逻辑,翻到一段 NSPredicate 的代码:

NSPredicate(format: "author == %@ AND title CONTAINS[c] %@", authorName, keyword)

格式字符串里拼了两个占位符,大小写不敏感用了 [c],看起来没问题,跑起来直接崩了——authorName 传了个 nil,运行时才报错。

这种事 NSPredicate 干了很多年了。字符串谓词,没有类型检查,错拼一个字段名编译完全不报错,到运行时才炸。

Swift 5.9 引入的 #Predicate 宏彻底解决了这个事。

NSPredicate 的痛点

先说清楚旧方案到底哪里烦:

  • 字符串驱动:谓词写在字符串里,字段名拼错了编译器看不出来
  • 无类型安全:传错类型的值要到运行时才崩溃
  • 可读性差:CONTAINS[c]、MATCHES、IN 这些 Objective-C 遗产语法,每次都要查文档

NSPredicate 是 Objective-C 时代的产物,Core Data 用了几十年,凑合能用,但写起来总有点提心吊胆。

#Predicate Macro 是什么

#Predicate 是 Swift 5.9(Xcode 15)引入的宏,把谓词从字符串时代带进了类型安全的时代。

先看个最简单的例子。有一组文章数据:

struct Article {
    let title: String
    let author: String
    let readCount: Int
}

let articles = [
    Article(title: "Swift Macros 实战", author: "Antoine", readCount: 2400),
    Article(title: "SwiftUI 性能优化", author: "Paul", readCount: 1800),
    Article(title: "Xcode 调试技巧", author: "Antoine", readCount: 3100),
]

想过滤出阅读量超过 2000 的 Antoine 的文章:

let predicate = #Predicate<Article> { article in
    article.author == "Antoine" && article.readCount > 2000
}

let result = try articles.filter(predicate).map(\.title)
// ["Swift Macros 实战", "Xcode 调试技巧"]

就是个普通的 Swift 闭包写法,字段名拼错了编译器直接报错,类型不匹配也是编译期就发现,不用等到运行时炸。

支持哪些操作

常见的逻辑操作都能用:

逻辑组合

// AND
#Predicate<Article> { $0.author == "Antoine" && $0.readCount > 1000 }

// OR
#Predicate<Article> { $0.author == "Paul" || $0.author == "Antoine" }

// NOT
#Predicate<Article> { !$0.title.contains("Swift") }

字符串操作

// 包含(大小写敏感)
#Predicate<Article> { $0.title.contains("Swift") }

// 前缀/后缀
#Predicate<Article> { $0.title.hasPrefix("Xcode") }

数值比较

#Predicate<Article> { $0.readCount >= 2000 }

可选值

struct Post {
    let title: String
    let tag: String?
}

#Predicate<Post> { post in
    post.tag != nil && post.tag!.contains("iOS")
}

不过要注意,#Predicate 支持的操作是 NSPredicate 的子集——复杂的正则匹配、IN 集合查询这些目前还不支持,遇到了需要绕路。

和 SwiftData 一起用

#Predicate 最重要的应用场景是 SwiftData。

用 FetchDescriptor 过滤数据库记录:

@Model
class Movie {
    var title: String
    var rating: Double
    var genre: String
    
    init(title: String, rating: Double, genre: String) {
        self.title = title
        self.rating = rating
        self.genre = genre
    }
}

// 查出评分高于 4 分的动作片
let predicate = #Predicate<Movie> { movie in
    movie.rating > 4.0 && movie.genre == "Action"
}
let descriptor = FetchDescriptor<Movie>(predicate: predicate)
let movies = try context.fetch(descriptor)

或者直接在 @Query 里内联写:

@Query(filter: #Predicate<Movie> { $0.rating > 4.0 })
var topMovies: [Movie]

这里的 #Predicate 不只是 Swift 层面的过滤——SwiftData 会把它转换成 SQL,在数据库层面执行,效率比把全部数据取出来再 filter 高多了。这是它和普通 .filter{} 本质的区别。

底层生成了什么

Swift 宏的本质是编译器插件,在构建阶段生成代码。展开 #Predicate 之后,实际上是一堆 PredicateExpressions 的嵌套结构:

// 你写的
let pred = #Predicate<Article> { $0.author == "Antoine" }

// 展开后大概长这样
let pred = Predicate<Article>({ input in
    PredicateExpressions.build_Equal(
        lhs: PredicateExpressions.build_KeyPath(
            root: PredicateExpressions.build_Arg(input),
            keyPath: \.author
        ),
        rhs: PredicateExpressions.build_Arg("Antoine")
    )
})

这些 PredicateExpressions 对象是结构化数据,SwiftData 可以把它们翻译成 SQL,filter() 可以在内存里直接执行——同一个 Predicate 对象服务于两个完全不同的底层系统。

你自己不需要写这些,宏帮你生成,你只用写普通的 Swift 闭包语法。

可用平台

#Predicate 从 iOS 17、macOS 14 起才有:

@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)
public struct Predicate<each Input>: Sendable

老版本系统还要继续用 NSPredicate,这是目前最大的限制。如果你的 App 最低支持版本还没到 iOS 17,可以把 #Predicate 留着新功能用,旧功能先别动。

最后

#Predicate 本质上就是把谓词从字符串时代搬到了 Swift 类型系统里,编译期检查、自动补全、重构安全,这些是 NSPredicate 永远给不了的。

有在用 SwiftData 或者 #Predicate 的,评论区聊聊踩了什么坑。

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

最新文章

随机文章