You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在自定义View初始化器中为@FetchRequest绑定@SceneStorage并动态设置排序描述符?

问题:在自定义View初始化器中动态绑定@SceneStorage并设置@FetchRequest的sortDescriptors

假设某应用使用Core Data,包含一个带有name和date属性的Item实体。尝试在自定义View初始化器中动态设置@FetchRequest的sortDescriptors参数并绑定@SceneStorage时,触发了以下错误提示:

在View未完成加载时访问SceneStorage值,这将始终返回默认值。

原代码如下:

enum ItemSort: String {
    case date
    case name
    
    static let `default`: Self = .date
    
    var descriptor: SortDescriptor<Item> {
        switch self {
        case .date:
            return .init(\.date, order: .forward)
        case .name:
            return .init(\.name, order: .forward)
        }
    }
}

struct ItemList: View {
    @SceneStorage private var sort: ItemSort
    @FetchRequest private var items: FetchedResults<Item>
    
    init() {
        _sort = .init(wrappedValue: .default, "itemSort")
        _items = .init(sortDescriptors: [_sort.wrappedValue.descriptor], animation: .default)  // 触发错误
    }
    
    var body: some View {
        List(items) { item in
            /* ... */
        }
    }
}

解决方案

问题根源

@SceneStorage是依赖SwiftUI环境的属性包装器,在View初始化阶段,它还未完成与当前场景存储的绑定,此时直接访问wrappedValue只能拿到默认值,同时触发警告。要实现动态排序并绑定场景存储,需要避开初始化阶段的直接访问,改用状态驱动的方式更新查询规则。


方案一:用ObservableObject管理查询逻辑

将Core Data的查询逻辑封装到ViewModel中,通过监听排序状态变化来动态更新FetchRequest,同时同步@SceneStorage的值:

import CoreData
import SwiftUI
import Combine

class ItemListViewModel: ObservableObject {
    @Published var sort: ItemSort = .default
    @Published var items: FetchedResults<Item> = []
    
    private let context: NSManagedObjectContext
    private var fetchController: NSFetchedResultsController<Item>
    private var cancellables = Set<AnyCancellable>()
    
    init(context: NSManagedObjectContext) {
        self.context = context
        
        // 初始化查询请求
        let fetchRequest = Item.fetchRequest()
        fetchRequest.sortDescriptors = [sort.descriptor]
        
        // 创建结果控制器
        fetchController = NSFetchedResultsController(
            fetchRequest: fetchRequest,
            managedObjectContext: context,
            sectionNameKeyPath: nil,
            cacheName: nil
        )
        
        // 执行初始查询
        do {
            try fetchController.performFetch()
            items = fetchController.fetchedObjects ?? []
        } catch {
            fatalError("初始查询失败:\(error.localizedDescription)")
        }
        
        // 监听排序变化,更新查询
        $sort
            .sink { [weak self] newSort in
                guard let self = self else { return }
                self.fetchController.fetchRequest.sortDescriptors = [newSort.descriptor]
                do {
                    try self.fetchController.performFetch()
                    self.items = self.fetchController.fetchedObjects ?? []
                } catch {
                    print("重新查询失败:\(error.localizedDescription)")
                }
            }
            .store(in: &cancellables)
    }
}

// 修改后的ItemList View
struct ItemList: View {
    @SceneStorage("itemSort") private var rawSort: String = ItemSort.default.rawValue
    @StateObject private var viewModel: ItemListViewModel
    
    init(context: NSManagedObjectContext) {
        _viewModel = StateObject(wrappedValue: ItemListViewModel(context: context))
    }
    
    var body: some View {
        List(viewModel.items) { item in
            VStack(alignment: .leading) {
                Text(item.name ?? "未命名")
                Text(item.date?.formatted() ?? "无日期")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
        .onAppear {
            // 从SceneStorage恢复排序设置
            if let savedSort = ItemSort(rawValue: rawSort) {
                viewModel.sort = savedSort
            }
        }
        .onChange(of: viewModel.sort) { newSort in
            // 将排序设置保存到SceneStorage
            rawSort = newSort.rawValue
        }
        .toolbar {
            // 示例:添加切换排序的按钮
            Button(viewModel.sort == .date ? "按名称排序" : "按日期排序") {
                viewModel.sort = viewModel.sort == .date ? .name : .date
            }
        }
    }
}

方案二:利用View的id强制刷新(简化版)

如果不想引入ViewModel,可以通过给子视图设置id的方式,当排序规则变化时强制重建子视图,从而用新的sortDescriptors初始化@FetchRequest:

enum ItemSort: String {
    case date
    case name
    
    static let `default`: Self = .date
    
    var descriptor: SortDescriptor<Item> {
        switch self {
        case .date:
            return .init(\.date, order: .forward)
        case .name:
            return .init(\.name, order: .forward)
        }
    }
}

struct ItemList: View {
    @SceneStorage("itemSort") private var sort: ItemSort = .default
    private let context: NSManagedObjectContext
    
    init(context: NSManagedObjectContext) {
        self.context = context
    }
    
    var body: some View {
        ItemListView(sort: sort, context: context)
            .id(sort) // 排序变化时,强制重建子视图
    }
}

// 封装FetchRequest的子视图
private struct ItemListView: View {
    let sort: ItemSort
    let context: NSManagedObjectContext
    @FetchRequest private var items: FetchedResults<Item>
    
    init(sort: ItemSort, context: NSManagedObjectContext) {
        self.sort = sort
        self.context = context
        // 用当前排序规则初始化FetchRequest
        _items = FetchRequest(
            sortDescriptors: [sort.descriptor],
            animation: .default
        )
    }
    
    var body: some View {
        List(items) { item in
            VStack(alignment: .leading) {
                Text(item.name ?? "未命名")
                Text(item.date?.formatted() ?? "无日期")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
    }
}

两种方案对比

  • 方案一:适合复杂场景,支持更灵活的查询逻辑(比如添加过滤条件),状态管理更清晰。
  • 方案二:代码简洁,无需额外的ViewModel,适合简单的动态排序需求。

内容的提问来源于stack exchange,提问作者parapote

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 07:50:18