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

Core Data关联实体+异步请求:文章存储运行时报错求助

问题描述

我正在开发一款Feed应用,Core Data数据库中包含WebFeed与Article两个实体,二者为一对多关联。目前WebFeed的创建功能已实现,但编写高效的文章获取存储函数时遇到问题:该函数需在用户创建WebFeed后、ScrollView触发刷新时执行,从WebFeed保存的URL拉取文章并存入数据库,但执行let article = Article(context: taskcontext)时出现运行时错误,推测是未在正确队列执行,尝试多种方法仍未解决,寻求相关建议。

代码片段

WebFeedProvider 类

class WebFeedProvider {
    //MARK: - Logging
    let logger = Logger(subsystem: "com.danielw.WebFeeder", category: "persistence")
    //MARK: - CoreData
    static let shared = WebFeedProvider()
    
    /// A webFeeds provider for use with canvas previews.
    static let preview: WebFeedProvider = {
        let provider = WebFeedProvider(inMemory: true)
        WebFeed.makePreviews(context: provider.container.viewContext, count: 10)
        return provider
    }()
    
    private let inMemory: Bool
    
    private init(inMemory: Bool = false) {
        self.inMemory = inMemory
    }
    
    /// A persistent container to set up the Core Data stack.
    lazy var container: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "WebFeeds")
        
        guard let description = container.persistentStoreDescriptions.first else {
            fatalError("Failed to retrieve a persistent store description.")
        }
        
        if inMemory {
            description.url = URL(fileURLWithPath: "/dev/null")
        }
    
        container.loadPersistentStores { storeDescription, error in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        }
        
        container.viewContext.automaticallyMergesChangesFromParent = true
        container.viewContext.name = "viewContext"
        container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        return container
    }()
    
    /// Creates and configures a private queue context.
    private func newTaskContext() -> NSManagedObjectContext {
        /// Create a private queue context.
        let taskContext = container.newBackgroundContext()
        
        taskContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        /// Set unused undoManager to nil for macOS (it is nil by default on iOS)
        /// to reduce resource requirements.
        taskContext.undoManager = nil
        return taskContext
    }
    func fetchWebFeeds() async {
        await fetchArticles()
    }
    func importWebFeeds(from feedProperties: Set<FeedProperties>) {
        container.viewContext.perform {

        }
    }
    func createWebFeed(from urlString: String) async {
        logger.debug("Starting, creating a new WebFeed.")
        var parsedFeed = FeedProperties()
        do {
            try await WebFeedParser.shared.getFeed(from: urlString) { result in
                switch result {
                case .success(let feedProperties):
                    parsedFeed = feedProperties
                case .failure(let error):
                    print(error.rawValue)
                }
            }
        } catch {
            logger.error("\(error.localizedDescription)")
        }
        do {
            let taskContext = newTaskContext()
            try await taskContext.perform {
                let webFeed = WebFeed(context: taskContext)
                webFeed.id = UUID()
                webFeed.feedURL = parsedFeed.feedURL
                webFeed.homepage = parsedFeed.homePage
                webFeed.title = parsedFeed.title
                webFeed.summary = parsedFeed.summary
                
                try taskContext.save()
            }
        } catch {
            logger.error("\(error.localizedDescription)")
        }
        
    }
 
    
    private func fetchArticles() async {
        let taskContext = newTaskContext()
        await taskContext.perform {
            do {
                let feeds = try taskContext.fetch(WebFeed.fetchRequest())
                
                for feed in feeds {
                    guard let feedURL = feed.feedURL else { continue }
                    
                        let parser = WebFeedParser.shared
                        Task {
                            try await parser.fetchArticles(from: feedURL) { result in
                                switch result {
                                case .success(let articleProperties):
                                    for properties in articleProperties {
                                        let article = Article(context: taskContext)
                                    }
                                    
                                    
                                case .failure(let parserError):
                                    self.logger.error("Failed to parse \(feedURL): \(parserError)")
                                }
                            }
                        }
                        
                    
                }
            } catch {
                self.logger.error("Failed to fetch WebFeeds for Article Parsing: \(error.localizedDescription)")
            }
        }
    }
    /// Synchronously deletes given records in the Core Data store with the specified object IDs.
    func deleteWebFeeds(identifiedBy objectIDs: [NSManagedObjectID]) {
        let viewContext = container.viewContext
        logger.debug("Start deleting data from the store...")
        
        viewContext.performAndWait {
            objectIDs.forEach { objectID in
                let webFeed = viewContext.object(with: objectID) as! WebFeed
                
                viewContext.delete(webFeed)
            }
        }
        save()
        logger.debug("Successfully deleted data.")
    }
    /// Asynchronously deletes records in the Core Data store with the specified `WebFeed` managed objects.
    func deleteWebFeeds(_ webFeeds: [WebFeed]) async throws {
        let objectIDs = webFeeds.map { $0.objectID }
        let taskContext = newTaskContext()
        /// Add name and author to identify source of persistent history changes.
        taskContext.name = "deleteContext"
        taskContext.transactionAuthor = "deleteWebFeeds"
        logger.debug("Start deleting data from the store...")
        
        try await taskContext.perform {
            /// Execute the batch delete.
            let batchDeleteRequest = NSBatchDeleteRequest(objectIDs: objectIDs)
            guard let fetchResult = try? taskContext.execute(batchDeleteRequest),
                  let batchDeleteResult = fetchResult as? NSBatchDeleteResult,
                  let success = batchDeleteResult.result as? Bool, success
            else {
                self.logger.debug("Failed to execute batch delete request.")
                throw WebFeedError.batchDeleteError
            }
        }
        
        logger.debug("Successfully deleted data.")
    }
    private func save() {
        container.viewContext.perform {
            do {
                try self.container.viewContext.save()
                self.logger.debug("Successfully saved context.")
            } catch {
                self.logger.error("Failed to save context: \(error)")
            }
        }
    }
}

WebFeedParser 类

class WebFeedParser {
    static let shared = WebFeedParser()
    
    private init() { }
    
    //MARK: - FeedCreation
    func getFeed(from urlString: String, completion: @escaping (Result<FeedProperties, ParserError>) -> Void) async throws {
        
        guard let url = URL(string: urlString) else {
            completion(.failure(.invalidURL))
            return
        }
        
        var feedProperties = FeedProperties()
        let session = URLSession.shared
        var feedData: Data?
        
        do {
            let (data, _) = try await session.data(from: url)
            feedData = data
        } catch {
            completion(.failure(.connectionError))
        }
        do {
            let decoder = SynDecoder()
            if let feedData {
                let decodedFeed = try decoder.decode(feedData)
                feedProperties.feedURL = url
                feedProperties.homePage = decodedFeed.siteURL
                feedProperties.title = decodedFeed.title
                feedProperties.summary = decodedFeed.summary
                
                completion(.success(feedProperties))
            }
        } catch {
            completion(.failure(.decoderError))
        }
    }
    func fetchArticles(from url: URL, completion: @escaping (Result<[ArticleProperties], ParserError>) -> Void) async throws {
        var articleProperties: [ArticleProperties] = []
        let session = URLSession.shared
        var feedData: Data?
        
        do {
            let (data, _) = try await session.data(from: url)
            feedData = data
        } catch {
            completion(.failure(.connectionError))
        }
        do {
            let decoder = SynDecoder()
            if let feedData {
                let decodedFeed = try decoder.decode(feedData)
                decodedFeed.children.forEach { feedProperties in
                    var newArticle = ArticleProperties()
                    
                    newArticle.title = feedProperties.title
                    newArticle.imageURL = feedProperties.imageURL
                    newArticle.summary = feedProperties.summary
                    newArticle.published = feedProperties.published
                    newArticle.url = feedProperties.url
                    
                    articleProperties.append(newArticle)
                }
                completion(.success(articleProperties))
            }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 01:32:01