这里每天分享一个 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 的新知识,快来关注我吧