diff --git a/WordPress/Classes/Models/Blog+History.swift b/WordPress/Classes/Models/Blog+History.swift new file mode 100644 index 000000000000..4b3123fdd10f --- /dev/null +++ b/WordPress/Classes/Models/Blog+History.swift @@ -0,0 +1,42 @@ +import Foundation + +extension Blog { + + /// Returns the blog currently flagged as the one last used, or the primary blog, + /// or the first blog in an alphanumerically sorted list, whichever is found first. + @objc(lastUsedOrFirstInContext:) + static func lastUsedOrFirst(in context: NSManagedObjectContext) -> Blog? { + lastUsed(in: context) + ?? (try? WPAccount.lookupDefaultWordPressComAccount(in: context))?.defaultBlog + ?? firstBlog(in: context) + } + + /// Returns the blog currently flaged as the one last used. + static func lastUsed(in context: NSManagedObjectContext) -> Blog? { + guard let url = RecentSitesService().recentSites.first else { + return nil + } + + return blog(with: NSPredicate(format: "visible = YES AND url = %@", url), in: context) + } + + private static func firstBlog(in context: NSManagedObjectContext) -> Blog? { + blog(with: NSPredicate(format: "visible = YES"), in: context) + } + + private static func blog(with predicate: NSPredicate, in context: NSManagedObjectContext) -> Blog? { + let request = NSFetchRequest(entityName: NSStringFromClass(Blog.self)) + request.includesSubentities = false + request.predicate = predicate + request.fetchLimit = 1 + request.sortDescriptors = [NSSortDescriptor(key: "settings.name", ascending: true)] + + do { + return try context.fetch(request).first + } catch { + DDLogError("Couldn't fetch blogs with predicate \(predicate): \(error)") + return nil + } + } + +} diff --git a/WordPress/Classes/Services/BlogService.h b/WordPress/Classes/Services/BlogService.h index a3a417ad1a29..93662767e027 100644 --- a/WordPress/Classes/Services/BlogService.h +++ b/WordPress/Classes/Services/BlogService.h @@ -26,35 +26,6 @@ extern NSString *const WPBlogUpdatedNotification; */ - (nullable Blog *)blogByHostname:(NSString *)hostname; -/** - Returns the blog currently flagged as the one last used, or the primary blog, - or the first blog in an alphanumerically sorted list, whichever is found first. - */ -- (nullable Blog *)lastUsedOrFirstBlog; - -/** - Returns the blog currently flagged as the one last used, or the primary blog, - or the first blog in an alphanumerically sorted list that supports the given - feature, whichever is found first. - */ -- (nullable Blog *)lastUsedOrFirstBlogThatSupports:(BlogFeature)feature; - -/** - Returns the blog currently flaged as the one last used. - */ -- (nullable Blog *)lastUsedBlog; - -/** - Returns the first blog in an alphanumerically sorted list. - */ -- (nullable Blog *)firstBlog; - -/** - Returns the default WPCom blog. - */ -- (nullable Blog *)primaryBlog; - - /** * Sync all available blogs for an acccount * diff --git a/WordPress/Classes/Services/BlogService.m b/WordPress/Classes/Services/BlogService.m index 60631cd9baf2..76575e5cdf10 100644 --- a/WordPress/Classes/Services/BlogService.m +++ b/WordPress/Classes/Services/BlogService.m @@ -44,78 +44,6 @@ - (Blog *)blogByHostname:(NSString *)hostname return [blogs firstObject]; } -- (Blog *)lastUsedOrFirstBlog -{ - Blog *blog = [self lastUsedOrPrimaryBlog]; - - if (!blog) { - blog = [self firstBlog]; - } - - return blog; -} - -- (Blog *)lastUsedOrFirstBlogThatSupports:(BlogFeature)feature -{ - Blog *blog = [self lastUsedOrPrimaryBlog]; - - if (![blog supports:feature]) { - blog = [self firstBlogThatSupports:feature]; - } - - return blog; -} - -- (Blog *)lastUsedOrPrimaryBlog -{ - Blog *blog = [self lastUsedBlog]; - - if (!blog) { - blog = [self primaryBlog]; - } - - return blog; -} - -- (Blog *)lastUsedBlog -{ - // Try to get the last used blog, if there is one. - RecentSitesService *recentSitesService = [RecentSitesService new]; - NSString *url = [[recentSitesService recentSites] firstObject]; - if (!url) { - return nil; - } - - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"visible = YES AND url = %@", url]; - Blog *blog = [self blogWithPredicate:predicate]; - - return blog; -} - -- (Blog *)primaryBlog -{ - return [[WPAccount lookupDefaultWordPressComAccountInContext:self.managedObjectContext] defaultBlog]; -} - -- (Blog *)firstBlogThatSupports:(BlogFeature)feature -{ - NSPredicate *predicate = [self predicateForVisibleBlogs]; - NSArray *results = [self blogsWithPredicate:predicate]; - - for (Blog *blog in results) { - if ([blog supports:feature]) { - return blog; - } - } - return nil; -} - -- (Blog *)firstBlog -{ - NSPredicate *predicate = [self predicateForVisibleBlogs]; - return [self blogWithPredicate:predicate]; -} - - (void)syncBlogsForAccount:(WPAccount *)account success:(void (^)(void))success failure:(void (^)(NSError *error))failure diff --git a/WordPress/Classes/System/3DTouch/WP3DTouchShortcutCreator.swift b/WordPress/Classes/System/3DTouch/WP3DTouchShortcutCreator.swift index 9a3254a51da5..63049a6252a4 100644 --- a/WordPress/Classes/System/3DTouch/WP3DTouchShortcutCreator.swift +++ b/WordPress/Classes/System/3DTouch/WP3DTouchShortcutCreator.swift @@ -79,7 +79,7 @@ open class WP3DTouchShortcutCreator: NSObject { fileprivate func loggedInShortcutArray() -> [UIApplicationShortcutItem] { var defaultBlogName: String? if blogService.blogCountForAllAccounts() > 1 { - defaultBlogName = blogService.lastUsedOrFirstBlog()?.settings?.name + defaultBlogName = Blog.lastUsedOrFirst(in: mainContext)?.settings?.name } let notificationsShortcut = UIMutableApplicationShortcutItem(type: WP3DTouchShortcutHandler.ShortcutIdentifier.Notifications.type, @@ -154,7 +154,7 @@ open class WP3DTouchShortcutCreator: NSObject { } fileprivate func doesCurrentBlogSupportStats() -> Bool { - guard let currentBlog = blogService.lastUsedOrFirstBlog() else { + guard let currentBlog = Blog.lastUsedOrFirst(in: mainContext) else { return false } diff --git a/WordPress/Classes/System/3DTouch/WP3DTouchShortcutHandler.swift b/WordPress/Classes/System/3DTouch/WP3DTouchShortcutHandler.swift index 1b1a947aa6a8..805aa52973d6 100644 --- a/WordPress/Classes/System/3DTouch/WP3DTouchShortcutHandler.swift +++ b/WordPress/Classes/System/3DTouch/WP3DTouchShortcutHandler.swift @@ -42,8 +42,7 @@ open class WP3DTouchShortcutHandler: NSObject { case ShortcutIdentifier.Stats.type: WPAnalytics.track(.shortcutStats) clearCurrentViewController() - let blogService: BlogService = BlogService(managedObjectContext: ContextManager.sharedInstance().mainContext) - if let mainBlog = blogService.lastUsedOrFirstBlog() { + if let mainBlog = Blog.lastUsedOrFirst(in: ContextManager.sharedInstance().mainContext) { tabBarController.mySitesCoordinator.showStats(for: mainBlog) } return true diff --git a/WordPress/Classes/System/WordPressAppDelegate+openURL.swift b/WordPress/Classes/System/WordPressAppDelegate+openURL.swift index 0ffc6b889196..36b0c7dd25a4 100644 --- a/WordPress/Classes/System/WordPressAppDelegate+openURL.swift +++ b/WordPress/Classes/System/WordPressAppDelegate+openURL.swift @@ -143,8 +143,7 @@ import AutomatticTracks let tags = params.value(of: NewPostKey.tags) let context = ContextManager.sharedInstance().mainContext - let blogService = BlogService(managedObjectContext: context) - guard let blog = blogService.lastUsedOrFirstBlog() else { + guard let blog = Blog.lastUsedOrFirst(in: context) else { return false } @@ -181,8 +180,7 @@ import AutomatticTracks let title = params.value(of: NewPostKey.title) let context = ContextManager.sharedInstance().mainContext - let blogService = BlogService(managedObjectContext: context) - guard let blog = blogService.lastUsedOrFirstBlog() else { + guard let blog = Blog.lastUsedOrFirst(in: context) else { return false } diff --git a/WordPress/Classes/Utility/Universal Links/NavigationActionHelpers.swift b/WordPress/Classes/Utility/Universal Links/NavigationActionHelpers.swift index 21beed3fa4cc..d6387314a0fa 100644 --- a/WordPress/Classes/Utility/Universal Links/NavigationActionHelpers.swift +++ b/WordPress/Classes/Utility/Universal Links/NavigationActionHelpers.swift @@ -4,9 +4,7 @@ import WordPressFlux extension NavigationAction { func defaultBlog() -> Blog? { let context = ContextManager.sharedInstance().mainContext - let service = BlogService(managedObjectContext: context) - - return service.lastUsedOrFirstBlog() + return Blog.lastUsedOrFirst(in: context) } func blog(from values: [String: String]?) -> Blog? { diff --git a/WordPress/Classes/Utility/ZendeskUtils.swift b/WordPress/Classes/Utility/ZendeskUtils.swift index 7839711110f9..e5e1d704643f 100644 --- a/WordPress/Classes/Utility/ZendeskUtils.swift +++ b/WordPress/Classes/Utility/ZendeskUtils.swift @@ -412,9 +412,8 @@ private extension ZendeskUtils { } // 2. Use information from selected site. - let blogService = BlogService(managedObjectContext: context) - guard let blog = blogService.lastUsedBlog() else { + guard let blog = Blog.lastUsed(in: context) else { // We have no user information. completion() return @@ -630,9 +629,7 @@ private extension ZendeskUtils { } static func getCurrentSiteDescription() -> String { - let blogService = BlogService(managedObjectContext: ContextManager.sharedInstance().mainContext) - - guard let blog = blogService.lastUsedBlog() else { + guard let blog = Blog.lastUsed(in: ContextManager.sharedInstance().mainContext) else { return Constants.noValue } @@ -694,13 +691,13 @@ private extension ZendeskUtils { // Add gutenbergIsDefault tag - if let blog = blogService.lastUsedBlog() { + if let blog = Blog.lastUsed(in: context) { if blog.isGutenbergEnabled { tags.append(Constants.gutenbergIsDefault) } } - if let currentSite = blogService.lastUsedOrFirstBlog(), !currentSite.isHostedAtWPcom, !currentSite.isAtomic() { + if let currentSite = Blog.lastUsedOrFirst(in: context), !currentSite.isHostedAtWPcom, !currentSite.isAtomic() { tags.append(Constants.mobileSelfHosted) } @@ -1045,9 +1042,7 @@ private extension ZendeskUtils { /// Provides the current site id to `getZendeskMetadata`, if it exists private static var currentSiteID: Int? { - guard let siteID = BlogService(managedObjectContext: ContextManager.shared.mainContext) - .lastUsedOrFirstBlog()? - .dotComID else { + guard let siteID = Blog.lastUsedOrFirst(in: ContextManager.shared.mainContext)?.dotComID else { return nil } return Int(truncating: siteID) diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift index a66af27aacb4..07dd872f4881 100644 --- a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift @@ -396,7 +396,7 @@ class MySiteViewController: UIViewController, NoResultsViewHost { /// - Returns:the main blog for an account (last selected, or first blog in list). /// private func mainBlog() -> Blog? { - return blogService.lastUsedOrFirstBlog() + return Blog.lastUsedOrFirst(in: ContextManager.sharedInstance().mainContext) } /// This VC is prepared to either show the details for a blog, or show a no-results VC configured to let the user know they have no blogs. @@ -911,7 +911,7 @@ class MySiteViewController: UIViewController, NoResultsViewHost { return } - guard let blog = blogService.lastUsedOrFirstBlog() else { + guard let blog = Blog.lastUsedOrFirst(in: ContextManager.sharedInstance().mainContext) else { return } diff --git a/WordPress/Classes/ViewRelated/Me/Account Settings/AccountSettingsViewController.swift b/WordPress/Classes/ViewRelated/Me/Account Settings/AccountSettingsViewController.swift index fa5340441402..6618ae78f8ff 100644 --- a/WordPress/Classes/ViewRelated/Me/Account Settings/AccountSettingsViewController.swift +++ b/WordPress/Classes/ViewRelated/Me/Account Settings/AccountSettingsViewController.swift @@ -111,8 +111,8 @@ private class AccountSettingsController: SettingsController { // If the primary site has no Site Title, then show the displayURL. if primarySiteName.isEmpty { - let blogService = BlogService(managedObjectContext: ContextManager.sharedInstance().mainContext) - primarySiteName = blogService.primaryBlog()?.displayURL as String? ?? "" + let account = try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.sharedInstance().mainContext) + primarySiteName = account?.defaultBlog.displayURL as String? ?? "" } let primarySite = EditableTextRow( diff --git a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController+JetpackPrompt.swift b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController+JetpackPrompt.swift index 53b32b1287d7..3cad12cf9922 100644 --- a/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController+JetpackPrompt.swift +++ b/WordPress/Classes/ViewRelated/Notifications/Controllers/NotificationsViewController+JetpackPrompt.swift @@ -1,7 +1,7 @@ extension NotificationsViewController { func promptForJetpackCredentials() { - guard let blog = blogService.lastUsedBlog() else { + guard let blog = Blog.lastUsed(in: managedObjectContext()) else { return } @@ -38,11 +38,4 @@ extension NotificationsViewController { } } - - // MARK: - Private Computed Properties - - fileprivate var blogService: BlogService { - return BlogService(managedObjectContext: managedObjectContext()) - } - } diff --git a/WordPress/Classes/ViewRelated/Reader/ReaderReblogPresenter.swift b/WordPress/Classes/ViewRelated/Reader/ReaderReblogPresenter.swift index 1df746ac4aea..7141a2b77c50 100644 --- a/WordPress/Classes/ViewRelated/Reader/ReaderReblogPresenter.swift +++ b/WordPress/Classes/ViewRelated/Reader/ReaderReblogPresenter.swift @@ -39,11 +39,7 @@ class ReaderReblogPresenter { } presentEditor(with: readerPost, blog: blog, origin: origin) default: - guard let blog = blogService.lastUsedOrFirstBlog() else { - return - } presentBlogPicker(from: origin, - blog: blog, blogService: blogService, readerPost: readerPost) } @@ -55,7 +51,6 @@ class ReaderReblogPresenter { private extension ReaderReblogPresenter { /// presents the blog picker before the editor, for users with multiple sites func presentBlogPicker(from origin: UIViewController, - blog: Blog, blogService: BlogService, readerPost: ReaderPost) { diff --git a/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift b/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift index 7848ed5ddc81..2bdb9c24d7ff 100644 --- a/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift +++ b/WordPress/Classes/ViewRelated/System/Coordinators/MySitesCoordinator.swift @@ -158,8 +158,7 @@ class MySitesCoordinator: NSObject { func showCreateSheet(for blog: Blog?) { let context = ContextManager.shared.mainContext - let service = BlogService(managedObjectContext: context) - guard let targetBlog = blog ?? service.lastUsedOrFirstBlog() else { + guard let targetBlog = blog ?? Blog.lastUsedOrFirst(in: context) else { return } diff --git a/WordPress/Classes/ViewRelated/System/WPTabBarController.m b/WordPress/Classes/ViewRelated/System/WPTabBarController.m index 98e7dcfa42a5..16fae3758af9 100644 --- a/WordPress/Classes/ViewRelated/System/WPTabBarController.m +++ b/WordPress/Classes/ViewRelated/System/WPTabBarController.m @@ -446,8 +446,7 @@ - (Blog *)currentOrLastBlog if (blog == nil) { NSManagedObjectContext *context = [[ContextManager sharedInstance] mainContext]; - BlogService *blogService = [[BlogService alloc] initWithManagedObjectContext:context]; - blog = [blogService lastUsedOrFirstBlog]; + blog = [Blog lastUsedOrFirstInContext: context]; } return blog; diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 52e72109837f..b96a894a4fd8 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -998,6 +998,8 @@ 4A50668128B364CA00DD09F4 /* ContainerContextFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A50667F28B364CA00DD09F4 /* ContainerContextFactory.swift */; }; 4A82C43128D321A300486CFF /* Blog+Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A82C43028D321A300486CFF /* Blog+Post.swift */; }; 4A82C43228D321A300486CFF /* Blog+Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A82C43028D321A300486CFF /* Blog+Post.swift */; }; + 4AD5656F28E413160054C676 /* Blog+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5656E28E413160054C676 /* Blog+History.swift */; }; + 4AD5657028E413160054C676 /* Blog+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5656E28E413160054C676 /* Blog+History.swift */; }; 4AFB8FBF2824999500A2F4B2 /* ContextManagerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFB8FBE2824999400A2F4B2 /* ContextManagerMock.swift */; }; 4B2DD0F29CD6AC353C056D41 /* Pods_WordPressUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8DCE7542239FBC709B90EA85 /* Pods_WordPressUITests.framework */; }; 4C8A715EBCE7E73AEE216293 /* Pods_WordPressShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F47DB4A8EC2E6844E213A3FA /* Pods_WordPressShareExtension.framework */; }; @@ -5966,6 +5968,7 @@ 4A50667E28B3218800DD09F4 /* ManagedObjectContextFactory.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ManagedObjectContextFactory.h; sourceTree = ""; }; 4A50667F28B364CA00DD09F4 /* ContainerContextFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContainerContextFactory.swift; sourceTree = ""; }; 4A82C43028D321A300486CFF /* Blog+Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Blog+Post.swift"; sourceTree = ""; }; + 4AD5656E28E413160054C676 /* Blog+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Blog+History.swift"; sourceTree = ""; }; 4AFB8FBE2824999400A2F4B2 /* ContextManagerMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextManagerMock.swift; sourceTree = ""; }; 4D520D4E22972BC9002F5924 /* acknowledgements.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = acknowledgements.html; path = "../Pods/Target Support Files/Pods-Apps-WordPress/acknowledgements.html"; sourceTree = ""; }; 51A5F017948878F7E26979A0 /* Pods-Apps-WordPress.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Apps-WordPress.release.xcconfig"; path = "../Pods/Target Support Files/Pods-Apps-WordPress/Pods-Apps-WordPress.release.xcconfig"; sourceTree = ""; }; @@ -13432,6 +13435,7 @@ 2420BEF025D8DAB300966129 /* Blog+Lookup.swift */, F10D634E26F0B78E00E46CC7 /* Blog+Organization.swift */, 8B4EDADC27DF9D5E004073B6 /* Blog+MySite.swift */, + 4AD5656E28E413160054C676 /* Blog+History.swift */, ); name = Blog; sourceTree = ""; @@ -18929,6 +18933,7 @@ FA681F8A25CA946B00DAA544 /* BaseRestoreStatusFailedViewController.swift in Sources */, 081E4B4C281C019A0085E89C /* TooltipAnchor.swift in Sources */, 3F851415260D0A3300A4B938 /* UnifiedPrologueEditorContentView.swift in Sources */, + 4AD5656F28E413160054C676 /* Blog+History.swift in Sources */, 179A70F02729834B006DAC0A /* Binding+OnChange.swift in Sources */, D8D7DF5A20AD18A400B40A2D /* ImgUploadProcessor.swift in Sources */, 436D56222117312700CEAA33 /* RegisterDomainDetailsViewModel.swift in Sources */, @@ -22120,6 +22125,7 @@ 3F46EEC928BC493E004F02B2 /* JetpackPromptsViewModel.swift in Sources */, FABB24D22602FC2C00C8785C /* ReaderHelpers.swift in Sources */, FABB24D32602FC2C00C8785C /* SubjectContentStyles.swift in Sources */, + 4AD5657028E413160054C676 /* Blog+History.swift in Sources */, FABB24D42602FC2C00C8785C /* AbstractPost.m in Sources */, FABB24D52602FC2C00C8785C /* PostStatsTableViewController.swift in Sources */, FABB24D62602FC2C00C8785C /* BlogService+Deduplicate.swift in Sources */, diff --git a/WordPress/WordPressTest/ReaderReblogActionTests.swift b/WordPress/WordPressTest/ReaderReblogActionTests.swift index 42adf97c6039..398ff3a1e5cc 100644 --- a/WordPress/WordPressTest/ReaderReblogActionTests.swift +++ b/WordPress/WordPressTest/ReaderReblogActionTests.swift @@ -13,7 +13,6 @@ class MockReblogPresenter: ReaderReblogPresenter { class MockBlogService: BlogService { var blogsForAllAccountsExpectation: XCTestExpectation? - var lastUsedOrFirstBlogExpectation: XCTestExpectation? var blogCount = 1 @@ -25,10 +24,6 @@ class MockBlogService: BlogService { blogsForAllAccountsExpectation?.fulfill() return [Blog(context: self.managedObjectContext), Blog(context: self.managedObjectContext)] } - override func lastUsedOrFirstBlog() -> Blog? { - lastUsedOrFirstBlogExpectation?.fulfill() - return Blog(context: self.managedObjectContext) - } } class MockPostService: PostService { @@ -97,11 +92,12 @@ class ReblogPresenterTests: ReblogTestCase { func testPresentEditorForMultipleSites() { // Given - blogService!.lastUsedOrFirstBlogExpectation = expectation(description: "lastUsedOrFirstBlog was called") blogService!.blogCount = 2 let presenter = ReaderReblogPresenter(postService: postService!) + let origin = MockViewController() + origin.presentExpectation = expectation(description: "blog selector is presented") // When - presenter.presentReblog(blogService: blogService!, readerPost: readerPost!, origin: UIViewController()) + presenter.presentReblog(blogService: blogService!, readerPost: readerPost!, origin: origin) // Then waitForExpectations(timeout: 4) { error in if let error = error { @@ -145,3 +141,14 @@ class ReblogFormatterTests: XCTestCase { wpImage) } } + +private class MockViewController: UIViewController { + + var presentExpectation: XCTestExpectation? + + override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { + presentExpectation?.fulfill() + super.present(viewControllerToPresent, animated: flag, completion: completion) + } + +}