diff --git a/WordPress/Classes/Models/Role.swift b/WordPress/Classes/Models/Role.swift index cdb328882dad..9bd0615bea19 100644 --- a/WordPress/Classes/Models/Role.swift +++ b/WordPress/Classes/Models/Role.swift @@ -12,6 +12,14 @@ extension Role { func toUnmanaged() -> RemoteRole { return RemoteRole(slug: slug, name: name) } + + static func lookup(withBlogID blogID: NSManagedObjectID, slug: String, in context: NSManagedObjectContext) throws -> Role? { + guard let blog = try context.existingObject(with: blogID) as? Blog else { + return nil + } + let predicate = NSPredicate(format: "slug = %@ AND blog = %@", slug, blog) + return context.firstObject(ofType: Role.self, matching: predicate) + } } extension Role { diff --git a/WordPress/Classes/Services/ReaderSearchSuggestionService.swift b/WordPress/Classes/Services/ReaderSearchSuggestionService.swift index aaa21f5fab2f..9c476d6de7ac 100644 --- a/WordPress/Classes/Services/ReaderSearchSuggestionService.swift +++ b/WordPress/Classes/Services/ReaderSearchSuggestionService.swift @@ -4,22 +4,33 @@ import CocoaLumberjack /// Provides functionality for fetching, saving, and deleting search phrases /// used to search for content in the reader. /// -@objc class ReaderSearchSuggestionService: LocalCoreDataService { +@objc class ReaderSearchSuggestionService: NSObject { + + private let coreDataStack: CoreDataStack + + @objc init(coreDataStack: CoreDataStack) { + self.coreDataStack = coreDataStack + super.init() + } /// Creates or updates an existing record for the specified search phrase. /// /// - Parameters: /// - phrase: The search phrase in question. /// - @objc func createOrUpdateSuggestionForPhrase(_ phrase: String) { - var suggestion = findSuggestionForPhrase(phrase) - if suggestion == nil { - suggestion = NSEntityDescription.insertNewObject(forEntityName: ReaderSearchSuggestion.classNameWithoutNamespaces(), - into: managedObjectContext) as? ReaderSearchSuggestion - suggestion?.searchPhrase = phrase + @objc(createOrUpdateSuggestionForPhrase:) + func createOrUpdateSuggestion(forPhrase phrase: String) { + self.coreDataStack.performAndSave { context in + var suggestion = self.findSuggestion(forPhrase: phrase, in: context) + if suggestion == nil { + suggestion = NSEntityDescription.insertNewObject( + forEntityName: ReaderSearchSuggestion.classNameWithoutNamespaces(), + into: context + ) as? ReaderSearchSuggestion + suggestion?.searchPhrase = phrase + } + suggestion?.date = Date() } - suggestion?.date = Date() - ContextManager.sharedInstance().save(managedObjectContext) } @@ -30,13 +41,13 @@ import CocoaLumberjack /// /// - Returns: A matching search phrase or nil. /// - @objc func findSuggestionForPhrase(_ phrase: String) -> ReaderSearchSuggestion? { + private func findSuggestion(forPhrase phrase: String, in context: NSManagedObjectContext) -> ReaderSearchSuggestion? { let fetchRequest = NSFetchRequest(entityName: "ReaderSearchSuggestion") fetchRequest.predicate = NSPredicate(format: "searchPhrase MATCHES[cd] %@", phrase) var suggestions = [ReaderSearchSuggestion]() do { - suggestions = try managedObjectContext.fetch(fetchRequest) as! [ReaderSearchSuggestion] + suggestions = try context.fetch(fetchRequest) as! [ReaderSearchSuggestion] } catch let error as NSError { DDLogError("Error fetching search suggestion for phrase \(phrase) : \(error.localizedDescription)") } @@ -44,57 +55,18 @@ import CocoaLumberjack return suggestions.first } - - /// Finds and returns all ReaderSearchSuggestion starting with the specified search phrase. - /// - /// - Parameters: - /// - phrase: The search phrase in question. - /// - /// - Returns: An array of matching `ReaderSearchSuggestion`s. - /// - @objc func fetchSuggestionsLikePhrase(_ phrase: String) -> [ReaderSearchSuggestion] { - let fetchRequest = NSFetchRequest(entityName: "ReaderSearchSuggestion") - fetchRequest.predicate = NSPredicate(format: "searchPhrase BEGINSWITH[cd] %@", phrase) - - let sort = NSSortDescriptor(key: "date", ascending: false) - fetchRequest.sortDescriptors = [sort] - - var suggestions = [ReaderSearchSuggestion]() - do { - suggestions = try managedObjectContext.fetch(fetchRequest) as! [ReaderSearchSuggestion] - } catch let error as NSError { - DDLogError("Error fetching search suggestions for phrase \(phrase) : \(error.localizedDescription)") - } - - return suggestions - } - - - /// Deletes the specified search suggestion. - /// - /// - Parameters: - /// - suggestion: The `ReaderSearchSuggestion` to delete. - /// - @objc func deleteSuggestion(_ suggestion: ReaderSearchSuggestion) { - managedObjectContext.delete(suggestion) - ContextManager.sharedInstance().saveContextAndWait(managedObjectContext) - } - - /// Deletes all saved search suggestions. /// @objc func deleteAllSuggestions() { - let fetchRequest = NSFetchRequest(entityName: "ReaderSearchSuggestion") - var suggestions = [ReaderSearchSuggestion]() - do { - suggestions = try managedObjectContext.fetch(fetchRequest) as! [ReaderSearchSuggestion] - } catch let error as NSError { - DDLogError("Error fetching search suggestion : \(error.localizedDescription)") - } - for suggestion in suggestions { - managedObjectContext.delete(suggestion) + self.coreDataStack.performAndSave { context in + let fetchRequest = NSFetchRequest(entityName: "ReaderSearchSuggestion") + do { + let suggestions = try context.fetch(fetchRequest) as! [ReaderSearchSuggestion] + suggestions.forEach(context.delete(_:)) + } catch let error as NSError { + DDLogError("Error fetching search suggestion : \(error.localizedDescription)") + } } - ContextManager.sharedInstance().save(managedObjectContext) } } diff --git a/WordPress/Classes/Services/ReaderTopicService.m b/WordPress/Classes/Services/ReaderTopicService.m index f93a24d94cff..c77c647599c4 100644 --- a/WordPress/Classes/Services/ReaderTopicService.m +++ b/WordPress/Classes/Services/ReaderTopicService.m @@ -267,7 +267,7 @@ - (ReaderSearchTopic *)searchTopicForSearchPhrase:(NSString *)phrase topic.following = NO; // Save / update the search phrase to use it as a suggestion later. - ReaderSearchSuggestionService *suggestionService = [[ReaderSearchSuggestionService alloc] initWithManagedObjectContext:self.managedObjectContext]; + ReaderSearchSuggestionService *suggestionService = [[ReaderSearchSuggestionService alloc] initWithCoreDataStack:[ContextManager sharedInstance]]; [suggestionService createOrUpdateSuggestionForPhrase:phrase]; [[ContextManager sharedInstance] saveContextAndWait:self.managedObjectContext]; diff --git a/WordPress/Classes/Services/RoleService.swift b/WordPress/Classes/Services/RoleService.swift index 8c0ad2e26a7f..feb74c173f3b 100644 --- a/WordPress/Classes/Services/RoleService.swift +++ b/WordPress/Classes/Services/RoleService.swift @@ -6,11 +6,11 @@ import WordPressKit struct RoleService { let blog: Blog - fileprivate let context: NSManagedObjectContext + fileprivate let coreDataStack: CoreDataStack fileprivate let remote: PeopleServiceRemote fileprivate let siteID: Int - init?(blog: Blog, context: NSManagedObjectContext) { + init?(blog: Blog, coreDataStack: CoreDataStack) { guard let api = blog.wordPressComRestApi(), let dotComID = blog.dotComID as? Int else { return nil } @@ -18,28 +18,22 @@ struct RoleService { self.remote = PeopleServiceRemote(wordPressComRestApi: api) self.siteID = dotComID self.blog = blog - self.context = context - } - - /// Returns a role from Core Data with the given slug. - /// - func getRole(slug: String) -> Role? { - let predicate = NSPredicate(format: "slug = %@ AND blog = %@", slug, blog) - return context.firstObject(ofType: Role.self, matching: predicate) + self.coreDataStack = coreDataStack } /// Forces a refresh of roles from the api and stores them in Core Data. /// - func fetchRoles(success: @escaping ([Role]) -> Void, failure: @escaping (Error) -> Void) { + func fetchRoles(success: @escaping () -> Void, failure: @escaping (Error) -> Void) { remote.getUserRoles(siteID, success: { (remoteRoles) in - let roles = self.mergeRoles(remoteRoles) - success(roles) + self.coreDataStack.performAndSave({ context in + self.mergeRoles(remoteRoles, in: context) + }, completion: success, on: .main) }, failure: failure) } } private extension RoleService { - func mergeRoles(_ remoteRoles: [RemoteRole]) -> [Role] { + func mergeRoles(_ remoteRoles: [RemoteRole], in context: NSManagedObjectContext) { let existingRoles = blog.roles ?? [] var rolesToKeep = [Role]() for (order, remoteRole) in remoteRoles.enumerated() { @@ -57,7 +51,5 @@ private extension RoleService { } let rolesToDelete = existingRoles.subtracting(rolesToKeep) rolesToDelete.forEach(context.delete(_:)) - ContextManager.sharedInstance().save(context) - return rolesToKeep } } diff --git a/WordPress/Classes/Services/SiteManagementService.swift b/WordPress/Classes/Services/SiteManagementService.swift index 335b9cf1626c..b8eceb19f119 100644 --- a/WordPress/Classes/Services/SiteManagementService.swift +++ b/WordPress/Classes/Services/SiteManagementService.swift @@ -19,7 +19,14 @@ extension NSNotification.Name { /// SiteManagementService handles operations for managing a WordPress.com site. /// -open class SiteManagementService: LocalCoreDataService { +open class SiteManagementService: NSObject { + + private let coreDataStack: CoreDataStack + + init(coreDataStack: CoreDataStack) { + self.coreDataStack = coreDataStack + } + /// Deletes the specified WordPress.com site. /// /// - Parameters: @@ -33,15 +40,16 @@ open class SiteManagementService: LocalCoreDataService { } remote.deleteSite(blog.dotComID!, success: { - self.managedObjectContext.perform { - let blogService = BlogService(managedObjectContext: self.managedObjectContext) - blogService.remove(blog) - - ContextManager.sharedInstance().save(self.managedObjectContext, completion: { - NotificationCenter.default.post(name: .WPSiteDeleted, object: nil) - success?() - }, on: .main) - } + self.coreDataStack.performAndSave({ context in + guard let blogInContext = try? context.existingObject(with: blog.objectID) as? Blog else { + return + } + let blogService = BlogService(managedObjectContext: context) + blogService.remove(blogInContext) + }, completion: { + NotificationCenter.default.post(name: .WPSiteDeleted, object: nil) + success?() + }, on: .main) }, failure: { error in failure?(error) diff --git a/WordPress/Classes/Services/SiteSegmentsService.swift b/WordPress/Classes/Services/SiteSegmentsService.swift index 5b55d662a32a..ce4dc1b3447f 100644 --- a/WordPress/Classes/Services/SiteSegmentsService.swift +++ b/WordPress/Classes/Services/SiteSegmentsService.swift @@ -11,28 +11,27 @@ protocol SiteSegmentsService { } // MARK: - SiteSegmentsService -final class SiteCreationSegmentsService: LocalCoreDataService, SiteSegmentsService { +final class SiteCreationSegmentsService: SiteSegmentsService { // MARK: Properties /// A facade for WPCOM services. private let remoteService: WordPressComServiceRemote - // MARK: LocalCoreDataService - - override init(managedObjectContext context: NSManagedObjectContext) { + init(coreDataStack: CoreDataStack) { + let account = coreDataStack.performQuery { context in + try? WPAccount.lookupDefaultWordPressComAccount(in: context) + } let api: WordPressComRestApi - if let account = try? WPAccount.lookupDefaultWordPressComAccount(in: context) { + if let account { api = account.wordPressComRestV2Api } else { api = WordPressComRestApi.anonymousApi(userAgent: WPUserAgent.wordPress(), localeKey: WordPressComRestApi.LocaleKeyV2) } self.remoteService = WordPressComServiceRemote(wordPressComRestApi: api) - - super.init(managedObjectContext: context) } // MARK: SiteSegmentsService diff --git a/WordPress/Classes/Services/SiteVerticalsService.swift b/WordPress/Classes/Services/SiteVerticalsService.swift index 005cbce3fb5a..dfd47bba07df 100644 --- a/WordPress/Classes/Services/SiteVerticalsService.swift +++ b/WordPress/Classes/Services/SiteVerticalsService.swift @@ -47,26 +47,25 @@ final class MockSiteVerticalsService: SiteVerticalsService { /// Retrieves candidate Site Verticals used to create a new site. /// -final class SiteCreationVerticalsService: LocalCoreDataService, SiteVerticalsService { +final class SiteCreationVerticalsService: SiteVerticalsService { // MARK: Properties /// A facade for WPCOM services. private let remoteService: WordPressComServiceRemote - // MARK: LocalCoreDataService - - override init(managedObjectContext context: NSManagedObjectContext) { + init(coreDataStack: CoreDataStack) { + let account = coreDataStack.performQuery { context in + try? WPAccount.lookupDefaultWordPressComAccount(in: context) + } let api: WordPressComRestApi - if let account = try? WPAccount.lookupDefaultWordPressComAccount(in: context) { + if let account { api = account.wordPressComRestV2Api } else { api = WordPressComRestApi.anonymousApi(userAgent: WPUserAgent.wordPress(), localeKey: WordPressComRestApi.LocaleKeyV2) } self.remoteService = WordPressComServiceRemote(wordPressComRestApi: api) - - super.init(managedObjectContext: context) } // MARK: SiteVerticalsService diff --git a/WordPress/Classes/ViewRelated/Blog/QuickStartTourGuide.swift b/WordPress/Classes/ViewRelated/Blog/QuickStartTourGuide.swift index 104cdd886979..879ce968ad71 100644 --- a/WordPress/Classes/ViewRelated/Blog/QuickStartTourGuide.swift +++ b/WordPress/Classes/ViewRelated/Blog/QuickStartTourGuide.swift @@ -510,7 +510,7 @@ private extension QuickStartTourGuide { } private func grantCongratulationsAward(for blog: Blog) { - let service = SiteManagementService(managedObjectContext: ContextManager.sharedInstance().mainContext) + let service = SiteManagementService(coreDataStack: ContextManager.sharedInstance()) service.markQuickStartChecklistAsComplete(for: blog) } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Management/DeleteSiteViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Management/DeleteSiteViewController.swift index 4453a65aa4fb..1decbb801544 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Management/DeleteSiteViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Management/DeleteSiteViewController.swift @@ -221,7 +221,7 @@ open class DeleteSiteViewController: UITableViewController { let trackedBlog = blog WPAppAnalytics.track(.siteSettingsDeleteSiteRequested, with: trackedBlog) - let service = SiteManagementService(managedObjectContext: ContextManager.sharedInstance().mainContext) + let service = SiteManagementService(coreDataStack: ContextManager.sharedInstance()) service.deleteSiteForBlog(blog, success: { [weak self] in WPAppAnalytics.track(.siteSettingsDeleteSiteResponseOK, with: trackedBlog) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Management/SiteSettingsViewController+SiteManagement.swift b/WordPress/Classes/ViewRelated/Blog/Site Management/SiteSettingsViewController+SiteManagement.swift index a31c2f6ac696..86bc60fbec46 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Management/SiteSettingsViewController+SiteManagement.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Management/SiteSettingsViewController+SiteManagement.swift @@ -48,7 +48,7 @@ public extension SiteSettingsViewController { let trackedBlog = blog WPAppAnalytics.track(.siteSettingsExportSiteRequested, with: trackedBlog) - let service = SiteManagementService(managedObjectContext: ContextManager.sharedInstance().mainContext) + let service = SiteManagementService(coreDataStack: ContextManager.sharedInstance()) service.exportContentForBlog(blog, success: { WPAppAnalytics.track(.siteSettingsExportSiteResponseOK, with: trackedBlog) @@ -79,7 +79,7 @@ public extension SiteSettingsViewController { SVProgressHUD.show(withStatus: status) WPAppAnalytics.track(.siteSettingsDeleteSitePurchasesRequested, with: blog) - let service = SiteManagementService(managedObjectContext: ContextManager.sharedInstance().mainContext) + let service = SiteManagementService(coreDataStack: ContextManager.sharedInstance()) service.getActivePurchasesForBlog(blog, success: { [weak self] purchases in SVProgressHUD.dismiss() diff --git a/WordPress/Classes/ViewRelated/People/PeopleViewController.swift b/WordPress/Classes/ViewRelated/People/PeopleViewController.swift index 85f7dc9e4d02..b76f595d77d0 100644 --- a/WordPress/Classes/ViewRelated/People/PeopleViewController.swift +++ b/WordPress/Classes/ViewRelated/People/PeopleViewController.swift @@ -173,7 +173,7 @@ class PeopleViewController: UITableViewController, UIViewControllerRestoration { tableView.deselectSelectedRowWithAnimation(true) refreshNoResultsView() - guard let blog = blog else { + guard let blog else { return } @@ -420,7 +420,7 @@ private extension PeopleViewController { func loadUsersPage(_ offset: Int = 0, success: @escaping ((_ retrieved: Int, _ shouldLoadMore: Bool) -> Void)) { guard let blog = blogInContext, let peopleService = PeopleService(blog: blog, coreDataStack: ContextManager.shared), - let roleService = RoleService(blog: blog, context: viewContext) else { + let roleService = RoleService(blog: blog, coreDataStack: ContextManager.shared) else { return } @@ -438,7 +438,7 @@ private extension PeopleViewController { }) group.enter() - roleService.fetchRoles(success: {_ in + roleService.fetchRoles(success: { group.leave() }, failure: { error in loadError = error @@ -521,11 +521,10 @@ private extension PeopleViewController { } func role(person: Person) -> Role? { - guard let blog = blog, - let service = RoleService(blog: blog, context: viewContext) else { - return nil + guard let blog = blog else { + return nil } - return service.getRole(slug: person.role) + return try? Role.lookup(withBlogID: blog.objectID, slug: person.role, in: viewContext) } func setupFilterBar() { diff --git a/WordPress/Classes/ViewRelated/People/PersonViewController.swift b/WordPress/Classes/ViewRelated/People/PersonViewController.swift index ea782f7a0e62..9aca7e9600ef 100644 --- a/WordPress/Classes/ViewRelated/People/PersonViewController.swift +++ b/WordPress/Classes/ViewRelated/People/PersonViewController.swift @@ -598,10 +598,7 @@ private extension PersonViewController { case .Viewer: return .viewer case .User: - guard let service = RoleService(blog: blog, context: context) else { - return nil - } - return service.getRole(slug: person.role)?.toUnmanaged() + return try? Role.lookup(withBlogID: blog.objectID, slug: person.role, in: context)?.toUnmanaged() case .Email: return .follower } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderSearchSuggestionsViewController.swift b/WordPress/Classes/ViewRelated/Reader/ReaderSearchSuggestionsViewController.swift index e4b9317739e1..2e5b2c4c9a94 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderSearchSuggestionsViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderSearchSuggestionsViewController.swift @@ -143,7 +143,7 @@ class ReaderSearchSuggestionsViewController: UIViewController { @objc func clearSearchHistory() { - let service = ReaderSearchSuggestionService(managedObjectContext: ContextManager.sharedInstance().mainContext) + let service = ReaderSearchSuggestionService(coreDataStack: ContextManager.sharedInstance()) service.deleteAllSuggestions() tableView.reloadData() updateHeightConstraint() diff --git a/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabViewModel.swift b/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabViewModel.swift index 3f9a826863b7..33438774b035 100644 --- a/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabViewModel.swift +++ b/WordPress/Classes/ViewRelated/Reader/Tab Navigation/ReaderTabViewModel.swift @@ -252,7 +252,6 @@ extension ReaderTabViewModel { } private func clearSearchSuggestions() { - let context = ContextManager.sharedInstance().mainContext - ReaderSearchSuggestionService(managedObjectContext: context).deleteAllSuggestions() + ReaderSearchSuggestionService(coreDataStack: ContextManager.sharedInstance()).deleteAllSuggestions() } } diff --git a/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreationWizardLauncher.swift b/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreationWizardLauncher.swift index 108a9769971d..a5f79104be97 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreationWizardLauncher.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreationWizardLauncher.swift @@ -82,7 +82,7 @@ final class SiteCreationWizardLauncher { case .name: return SiteNameStep(creator: self.creator) case .segments: - let segmentsService = SiteCreationSegmentsService(managedObjectContext: ContextManager.sharedInstance().mainContext) + let segmentsService = SiteCreationSegmentsService(coreDataStack: ContextManager.sharedInstance()) return SiteSegmentsStep(creator: self.creator, service: segmentsService) case .siteAssembly: let siteAssemblyService = EnhancedSiteCreationService(managedObjectContext: ContextManager.sharedInstance().mainContext) diff --git a/WordPress/WordPressTest/SiteManagementServiceTests.swift b/WordPress/WordPressTest/SiteManagementServiceTests.swift index 72c82895f624..5117950ae891 100644 --- a/WordPress/WordPressTest/SiteManagementServiceTests.swift +++ b/WordPress/WordPressTest/SiteManagementServiceTests.swift @@ -56,7 +56,7 @@ class SiteManagementServiceTests: CoreDataTestCase { override func setUp() { super.setUp() - siteManagementService = SiteManagementServiceTester(managedObjectContext: contextManager.mainContext) + siteManagementService = SiteManagementServiceTester(coreDataStack: contextManager) mockRemoteService = siteManagementService.mockRemoteService } @@ -95,18 +95,14 @@ class SiteManagementServiceTests: CoreDataTestCase { waitForExpectations(timeout: 2, handler: nil) } - func testDeleteSiteRemovesExistingBlogOnSuccess() { + func testDeleteSiteRemovesExistingBlogOnSuccess() throws { let context = contextManager.mainContext - let blog = - + let blog = insertBlog(context) - insertBlog(context) let blogObjectID = blog.objectID - XCTAssertFalse(blogObjectID.isTemporaryID, "Should be a permanent object") - let expect = expectation( - description: "Remove Blog success expectation") + let expect = expectation(description: "Remove Blog success expectation") mockRemoteService.reset() siteManagementService.deleteSiteForBlog(blog, success: { @@ -115,8 +111,7 @@ class SiteManagementServiceTests: CoreDataTestCase { mockRemoteService.successBlockPassedIn?() waitForExpectations(timeout: 2, handler: nil) - let shouldBeRemoved = try? context.existingObject(with: blogObjectID) - XCTAssertFalse(shouldBeRemoved != nil, "Blog was not removed") + try XCTAssertEqual(context.count(for: Blog.fetchRequest()), 0) } func testDeleteSiteCallsFailureBlock() {