本篇文章作为《Swift UI利用@Query动态切换排序》扩展篇,主要涉及@Query动态排序列表时,传递并绑定变量,以实现外部视图与排序视图之间的变量绑定。
排序示例
struct ProspectsView: View {
@Environment(\.modelContext) var modelContext
@State private var isShowingScanner = false
@State private var selectedProspects = Set<Prospect>()
@Query(sort: $sortDescriptor) var prospects: [Prospect]
let filter: FilterType
var body: some View {
NavigationStack {
List(prospects, selection: $selectedProspects) { prospect in
NavigationLink(destination: EditView(prosect:prospect)) {
VStack(alignment: .leading) {
Text(prospect.name)
.font(.headline)
Text(prospect.emailAddress)
.foregroundStyle(.secondary)
}
}
}
.navigationTitle(title)
}
.sheet(isPresented: $isShowingScanner) {
CodeScannerView(codeTypes: [.qr], simulatedData: "Paul Hudson\npaul@hackingwithswift.com", completion: handleScan)
}
}
}
在ProspectsView视图中,尝试对List进行动态排序。

因为@Query不支持动态绑定到@State属性,因此需要将列表放到一个排序视图当中,利用构造方法传递排序规则,相关知识可以阅读《Swift UI利用@Query动态切换排序》。
因此,创建了一个排序视图UserView:
import SwiftUI
struct UserView: View {
var body: some View {
}
}
将SwiftData导入到排序视图中,然后将需要动态排序的列表剪切进来:
import SwiftUI
import SwiftData
struct UserView: View {
@Environment(\.modelContext) var modelContext
@Query var prospects: [Prospect]
var body: some View {
List(prospects, selection: $selectedProspects) { prospect in
NavigationLink(destination: EditView(prosect:prospect)) {
VStack(alignment: .leading) {
Text(prospect.name)
.font(.headline)
Text(prospect.emailAddress)
.foregroundStyle(.secondary)
}
}
}
}
}
现在通过构造方法调整@Query的排序方式,以完成动态排序,在外部的ProspectsView视图中定义一个排序变量:
@State private var nameSore = false
设置两个排序按钮,放在toolbar中,根据按钮调整nameSore的布尔值:
ToolbarItem(placement: .topBarTrailing) {
Menu("排序") {
Button("姓名排序") {
nameSore = true
}
Button("最近排序") {
nameSore = false
}
}
}

将nameSore传递到列表中,以初始化排序方式:
init(nameSort:Bool) {
_prospects = Query(sort: \Prospect.name, order: nameSort == true ? .forward : .reverse)
}
现在存在一个问题,那就是原本列表在ProspectsView视图中使用时,绑定的是selectedPropects,现在迁移到排序视图后,绑定的selectedProspects并没有带过来,导致缺失变量的报错。
简单的做法是将selectedProspects变量也剪切到UserView视图中。

但是外部视图ProspectsView中的delete()方法,toolbar都需要使用selectedProspects,因此剪切到UserView视图的方法也不现实。

因此引出本文的核心内容,在@Query动态排序扩展中,传递绑定变量。
传递绑定变量
因为排序视图的List列表需要绑定selectedProspects数组,因此可以将selectedProspects数组传递给UsersView视图。
ProspectsView的selectedProspects数组是一个Set类型:
@State private var selectedProspects = Set<Prospect>()
因此需要在UsersView视图中创建一个@Binding变量:
@Binding var selectedProspects:Set<Prospect>
在UsersView视图中,设置selectedProspects为Binding<Set<Prospect>>,这表示接收的是一个Binding类型,然后赋值给selectedProspects。
init(nameSort:Bool,selectedProspects:Binding<Set<Prospect>>) {
_prospects = Query(sort: \Prospect.name, order: nameSort == true ? .forward : .reverse)
self._selectedProspects = selectedProspects
}

通过Binding<>完成传递绑定变量,当nameSort为true时,对列表进行正序排序,反之则倒序排序。
同时,UsersView的List列表绑定的是UsersView中的selectedProspects数组,selectedProspects数组本身由@Binding属性包装器包装,实际绑定的还是PropectsView父视图中的数组。
实现效果
最终的实现效果为,既可以传递绑定变量,完成对SwiftData对象的修改,也可以完成对列表的动态排序。

注意:UserView排序视图的selectedProspects使用.constant进行绑定:
#Preview {
UserView(nameSort: true, selectedProspects: .constant(Set<Prospect>()))
}
完整代码
ProspectsView代码(父视图)
import SwiftUI
import SwiftData
import CodeScanner
struct ProspectsView: View {
@Query var prospects: [Prospect]
@Environment(\.modelContext) var modelContext
@State private var isShowingScanner = false
@State private var selectedProspects = Set<Prospect>()
@State private var hideTips = false
@State private var nameSore = false
let filter: FilterType
init(filter: FilterType) {
self.filter = filter
if filter != .none {
let showContactedOnly = filter == .contacted
_prospects = Query(filter: #Predicate {
$0.isContacted == showContactedOnly
}, sort: [SortDescriptor(\Prospect.name)])
}
}
func delete() {
for prospect in selectedProspects {
modelContext.delete(prospect)
}
selectedProspects=[]
}
func handleScan(result: Result<ScanResult, ScanError>) {
isShowingScanner = false
switch result {
case .success(let result):
let details = result.string.components(separatedBy: "\n")
guard details.count == 2 else { return }
let person = Prospect(name: details[0], emailAddress: details[1], isContacted: false)
modelContext.insert(person)
case .failure(let error):
print("Scanning failed: \(error.localizedDescription)")
}
}
var title: String {
switch filter {
case .none:
"Everyone"
case .contacted:
"Contacted people"
case .uncontacted:
"Uncontacted people"
}
}
var body: some View {
NavigationStack {
if filter == .none,hideTips == false {
VStack {
HStack {
Spacer()
Image(systemName: "minus.square.fill")
.onTapGesture {
hideTips = true
}
}
Image(systemName: "lasso.badge.sparkles")
.font(.title)
Spacer().frame(height: 10)
Text("是否联系了潜在客户")
}
.foregroundColor(Color.white)
.frame(width: 200,height: 100)
.background(Color.blue)
.cornerRadius(10)
}
UserView(nameSort:nameSore,selectedProspects:$selectedProspects)
.navigationTitle(title)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Scan", systemImage: "qrcode.viewfinder") {
isShowingScanner = true
}
}
ToolbarItem(placement: .topBarLeading) {
EditButton()
}
ToolbarItem(placement: .topBarTrailing) {
Menu("排序") {
Button("姓名排序") {
nameSore = true
}
Button("最近排序") {
nameSore = false
}
}
}
if selectedProspects.isEmpty == false {
ToolbarItem(placement: .bottomBar) {
Button("Delete Selected", action: delete)
}
}
}
}
.sheet(isPresented: $isShowingScanner) {
CodeScannerView(codeTypes: [.qr], simulatedData: "Paul Hudson\npaul@hackingwithswift.com", completion: handleScan)
}
}
}
#Preview {
ProspectsView(filter: .none)
.modelContainer(for: Prospect.self)
}
UserView代码(排序视图)
import SwiftUI
import SwiftData
struct UserView: View {
@Environment(\.modelContext) var modelContext
@Query var prospects: [Prospect]
@Binding var selectedProspects:Set<Prospect>
init(nameSort:Bool,selectedProspects:Binding<Set<Prospect>>) {
_prospects = Query(sort: \Prospect.name, order: nameSort == true ? .forward : .reverse)
self._selectedProspects = selectedProspects
}
var body: some View {
List(prospects, selection: $selectedProspects) { prospect in
NavigationLink(destination: EditView(prosect:prospect)) {
VStack(alignment: .leading) {
Text(prospect.name)
.font(.headline)
Text(prospect.emailAddress)
.foregroundStyle(.secondary)
}
}
.tag(prospect)
.swipeActions {
if prospect.isContacted {
Button("Mark Uncontacted", systemImage: "person.crop.circle.badge.xmark") {
prospect.isContacted.toggle()
}
.tint(.blue)
} else {
Button("Mark Contacted", systemImage: "person.crop.circle.fill.badge.checkmark") {
prospect.isContacted.toggle()
}
.tint(.green)
}
Button("Delete", systemImage: "trash", role: .destructive) {
modelContext.delete(prospect)
}
Button("Remind Me", systemImage: "bell") {
addNotification(for: prospect)
}
.tint(.orange)
}
}
}
}
#Preview {
UserView(nameSort: true, selectedProspects: .constant(Set<Prospect>()))
}
