diff --git a/Modules/Account/Sources/Account/Account.swift b/Modules/Account/Sources/Account/Account.swift index 966f85679..fefc9a582 100644 --- a/Modules/Account/Sources/Account/Account.swift +++ b/Modules/Account/Sources/Account/Account.swift @@ -44,11 +44,11 @@ public enum AccountType: Int, Codable { case inoreader = 21 case bazQux = 22 case theOldReader = 23 - + public var isDeveloperRestricted: Bool { return self == .cloudKit || self == .feedbin || self == .feedly || self == .inoreader } - + } public enum FetchType { @@ -88,18 +88,18 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, defaultName = NSLocalizedString("On My iPhone", comment: "Account name") } #endif - + return defaultName }() - + var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "account") public var isDeleted = false - + public var containerID: ContainerIdentifier? { return ContainerIdentifier.account(accountID) } - + public var account: Account? { return self } @@ -127,7 +127,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } public let defaultName: String - + public var isActive: Bool { get { return metadata.isActive @@ -144,7 +144,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public var topLevelFeeds = Set() public var folders: Set? = Set() - + public var externalID: String? { get { return metadata.externalID @@ -153,14 +153,14 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, metadata.externalID = newValue } } - + public var sortedFolders: [Folder]? { if let folders = folders { return Array(folders).sorted(by: { $0.nameForDisplay.caseInsensitiveCompare($1.nameForDisplay) == .orderedAscending }) } return nil } - + private var feedDictionariesNeedUpdate = true private var _idToFeedDictionary = [String: Feed]() var idToFeedDictionary: [String: Feed] { @@ -176,7 +176,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } return _externalIDToFeedDictionary } - + var flattenedFeedURLs: Set { return Set(flattenedFeeds().map({ $0.url })) } @@ -191,7 +191,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } } - + public var endpointURL: URL? { get { return metadata.endpointURL @@ -202,7 +202,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } } - + private var fetchingAllUnreadCounts = false var isUnreadCountsInitialized = false @@ -235,18 +235,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } } - + public var behaviors: AccountBehaviors { return delegate.behaviors } - + var refreshInProgress = false { didSet { if refreshInProgress != oldValue { if refreshInProgress { NotificationCenter.default.post(name: .AccountRefreshDidBegin, object: self) - } - else { + } else { NotificationCenter.default.post(name: .AccountRefreshDidFinish, object: self) opmlFile.markAsDirty() } @@ -281,7 +280,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } self.delegate.accountMetadata = metadata - + self.accountID = accountID self.type = type self.dataFolder = dataFolder @@ -328,9 +327,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, self.delegate.accountDidInitialize(self) } - + // MARK: - API - + public func storeCredentials(_ credentials: Credentials) throws { username = credentials.username guard let server = delegate.server else { @@ -340,21 +339,21 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, try CredentialsManager.storeCredentials(credentials, server: server) delegate.credentials = credentials } - + public func retrieveCredentials(type: CredentialsType) throws -> Credentials? { guard let username = self.username, let server = delegate.server else { return nil } return try CredentialsManager.retrieveCredentials(type: type, server: server, username: username) } - + public func removeCredentials(type: CredentialsType) throws { guard let username = self.username, let server = delegate.server else { return } try CredentialsManager.removeCredentials(type: type, server: server, username: username) } - + public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result) -> Void) { switch type { case .feedbin: @@ -367,7 +366,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, break } } - + internal static func oauthAuthorizationClient(for type: AccountType) -> OAuthAuthorizationClient { switch type { case .feedly: @@ -376,7 +375,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, fatalError("\(type) is not a client for OAuth authorization code granting.") } } - + public static func oauthAuthorizationCodeGrantRequest(for type: AccountType) -> URLRequest { let grantingType: OAuthAuthorizationGranting.Type switch type { @@ -385,31 +384,31 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, default: fatalError("\(type) does not support OAuth authorization code granting.") } - + return grantingType.oauthAuthorizationCodeGrantRequest() } - + public static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, client: OAuthAuthorizationClient, accountType: AccountType, transport: Transport = URLSession.webserviceTransport(), - completion: @escaping (Result) -> ()) { + completion: @escaping (Result) -> Void) { let grantingType: OAuthAuthorizationGranting.Type - + switch accountType { case .feedly: grantingType = FeedlyAccountDelegate.self default: fatalError("\(accountType) does not support OAuth authorization code granting.") } - + grantingType.requestOAuthAccessToken(with: response, transport: transport, completion: completion) } - public func receiveRemoteNotification(userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + public func receiveRemoteNotification(userInfo: [AnyHashable: Any], completion: @escaping () -> Void) { delegate.receiveRemoteNotification(for: self, userInfo: userInfo, completion: completion) } - + public func refreshAll(completion: @escaping (Result) -> Void) { delegate.refreshAll(for: self, completion: completion) } @@ -424,17 +423,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } } - + public func syncArticleStatus(completion: ((Result) -> Void)? = nil) { delegate.syncArticleStatus(for: self, completion: completion) } - + public func importOPML(_ opmlFile: URL, completion: @escaping (Result) -> Void) { guard !delegate.isOPMLImportInProgress else { completion(.failure(AccountError.opmlImportInProgress)) return } - + delegate.importOPML(for: self, opmlFile: opmlFile) { [weak self] result in switch result { case .success: @@ -446,13 +445,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, completion(.failure(error)) } } - + } - + public func suspendNetwork() { delegate.suspendNetwork() } - + public func suspendDatabase() { #if os(iOS) database.cancelAndSuspend() @@ -479,7 +478,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, feedMetadataFile.save() opmlFile.save() } - + public func prepareForDeletion() { delegate.accountWillBeDeleted(self) } @@ -502,11 +501,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } } - + func loadOPMLItems(_ items: [OPMLItem]) { - addOPMLItems(OPMLNormalizer.normalize(items)) + addOPMLItems(OPMLNormalizer.normalize(items)) } - + public func markArticles(_ articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result) -> Void) { delegate.markArticles(for: self, articles: articles, statusKey: statusKey, flag: flag, completion: completion) } @@ -517,7 +516,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } return existingFolder(withExternalID: externalID) } - + func existingContainers(withFeed feed: Feed) -> [Container] { var containers = [Container]() if topLevelFeeds.contains(feed) { @@ -532,7 +531,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } return containers } - + @discardableResult func ensureFolder(with name: String) -> Folder? { // TODO: support subfolders, maybe, some day @@ -566,11 +565,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func existingFolder(withDisplayName displayName: String) -> Folder? { return folders?.first(where: { $0.nameForDisplay == displayName }) } - + public func existingFolder(withExternalID externalID: String) -> Folder? { return folders?.first(where: { $0.externalID == externalID }) } - + func newFeed(with opmlFeedSpecifier: OPMLFeedSpecifier) -> Feed { let feedURL = opmlFeedSpecifier.feedURL let metadata = feedMetadata(feedURL: feedURL, feedID: feedURL) @@ -590,7 +589,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func createFeed(url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { delegate.createFeed(for: self, url: url, name: name, container: container, validateFeed: validateFeed, completion: completion) } - + func createFeed(with name: String?, url: String, feedID: String, homePageURL: String?) -> Feed { let metadata = feedMetadata(feedURL: url, feedID: feedID) let feed = Feed(account: self, url: url, metadata: metadata) @@ -598,31 +597,31 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, feed.homePageURL = homePageURL return feed } - + public func removeFeed(_ feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { delegate.removeFeed(for: self, with: feed, from: container, completion: completion) } - + public func moveFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result) -> Void) { delegate.moveFeed(for: self, with: feed, from: from, to: to, completion: completion) } - + public func renameFeed(_ feed: Feed, to name: String, completion: @escaping (Result) -> Void) { delegate.renameFeed(for: self, with: feed, to: name, completion: completion) } - + public func restoreFeed(_ feed: Feed, container: Container, completion: @escaping (Result) -> Void) { delegate.restoreFeed(for: self, feed: feed, container: container, completion: completion) } - + public func addFolder(_ name: String, completion: @escaping (Result) -> Void) { delegate.createFolder(for: self, name: name, completion: completion) } - + public func removeFolder(_ folder: Folder, completion: @escaping (Result) -> Void) { delegate.removeFolder(for: self, with: folder, completion: completion) } - + public func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result) -> Void) { delegate.renameFolder(for: self, with: folder, to: name, completion: completion) } @@ -630,17 +629,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func restoreFolder(_ folder: Folder, completion: @escaping (Result) -> Void) { delegate.restoreFolder(for: self, folder: folder, completion: completion) } - + func clearFeedMetadata(_ feed: Feed) { feedMetadata[feed.url] = nil } - + func addFolder(_ folder: Folder) { folders!.insert(folder) postChildrenDidChangeNotification() structureDidChange() } - + public func updateUnreadCounts(for feeds: Set, completion: VoidCompletionBlock? = nil) { fetchUnreadCounts(for: feeds, completion: completion) } @@ -706,7 +705,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func fetchCountForStarredArticles() throws -> Int { return try database.fetchStarredArticlesCount(flattenedFeeds().feedIDs()) } - + public func fetchUnreadArticleIDs(_ completion: @escaping ArticleIDsCompletionBlock) { database.fetchUnreadArticleIDsAsync(completion: completion) } @@ -719,7 +718,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, public func fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(_ completion: @escaping ArticleIDsCompletionBlock) { database.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion) } - + public func unreadCount(for feed: Feed) -> Int { return unreadCounts[feed.feedID] ?? 0 } @@ -747,15 +746,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, completion(.success(ArticleChanges())) return } - + update(feed.feedID, with: parsedItems, completion: completion) } - + func update(_ feedID: String, with parsedItems: Set, deleteOlder: Bool = true, completion: @escaping UpdateArticlesCompletionBlock) { // Used only by an On My Mac or iCloud account. precondition(Thread.isMainThread) precondition(type == .onMyMac || type == .cloudKit) - + database.update(with: parsedItems, feedID: feedID, deleteOlder: deleteOlder) { updateArticlesResult in switch updateArticlesResult { case .success(let articleChanges): @@ -793,12 +792,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, completion(.success(Set
())) return } - + database.mark(articles, statusKey: statusKey, flag: flag) { result in switch result { case .success(let updatedStatuses): let updatedArticleIDs = updatedStatuses.articleIDs() - let updatedArticles = Set(articles.filter{ updatedArticleIDs.contains($0.articleID) }) + let updatedArticles = Set(articles.filter { updatedArticleIDs.contains($0.articleID) }) self.noteStatusesForArticlesDidChange(updatedArticles) completion(.success(updatedArticles)) case .failure(let error): @@ -876,7 +875,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } database.delete(articleIDs: articleIDs, completion: completion) } - + /// Empty caches that can reasonably be emptied. Call when the app goes in the background, for instance. func emptyCaches() { database.emptyCaches() @@ -897,7 +896,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, structureDidChange() postChildrenDidChangeNotification() } - + public func removeFeeds(_ feeds: Set) { guard !feeds.isEmpty else { return @@ -906,7 +905,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, structureDidChange() postChildrenDidChangeNotification() } - + public func addFeed(_ feed: Feed) { topLevelFeeds.insert(feed) structureDidChange() @@ -918,13 +917,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, addFeed(feed) } } - + func removeFolder(_ folder: Folder) { folders?.remove(folder) structureDidChange() postChildrenDidChangeNotification() } - + // MARK: - Debug public func debugDropConditionalGetInfo() { @@ -955,13 +954,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, refreshInProgress = refreshProgress.numberRemaining > 0 NotificationCenter.default.post(name: .AccountRefreshProgressDidChange, object: self) } - + @objc func unreadCountDidChange(_ note: Notification) { if let feed = note.object as? Feed, feed.account === self { updateUnreadCount() } } - + @objc func batchUpdateDidPerform(_ note: Notification) { flattenedFeedsNeedUpdate = true rebuildFeedDictionaries() @@ -1090,7 +1089,7 @@ private extension Account { func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set) throws -> Set
{ return try database.fetchArticlesMatchingWithArticleIDs(searchString, articleIDs) } - + func fetchArticlesMatchingAsync(_ searchString: String, _ completion: @escaping ArticleSetResultBlock) { database.fetchArticlesMatchingAsync(searchString, flattenedFeeds().feedIDs(), completion) } @@ -1142,7 +1141,7 @@ private extension Account { if limit == nil { validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) } - + return articles } @@ -1151,13 +1150,13 @@ private extension Account { database.fetchUnreadArticlesAsync(feeds.feedIDs(), limit) { [weak self] (articleSetResult) in switch articleSetResult { case .success(let articles): - + // We don't validate limit queries because they, by definition, won't correctly match the // complete unread state for the given container. if limit == nil { self?.validateUnreadCountsAfterFetchingUnreadArticles(feeds, articles) } - + completion(.success(articles)) case .failure(let databaseError): completion(.failure(databaseError)) @@ -1225,7 +1224,7 @@ private extension Account { func rebuildFeedDictionaries() { var idDictionary = [String: Feed]() var externalIDDictionary = [String: Feed]() - + for feed in flattenedFeeds() { idDictionary[feed.feedID] = feed if let externalID = feed.externalID { @@ -1237,7 +1236,7 @@ private extension Account { _externalIDToFeedDictionary = externalIDDictionary feedDictionariesNeedUpdate = false } - + func updateUnreadCount() { if fetchingAllUnreadCounts { return @@ -1248,7 +1247,7 @@ private extension Account { } unreadCount = updatedUnreadCount } - + func noteStatusesForArticlesDidChange(_ articles: Set
) { let feeds = Set(articles.compactMap { $0.feed }) let statuses = Set(articles.map { $0.status }) @@ -1257,7 +1256,7 @@ private extension Account { // .UnreadCountDidChange notification will get sent to Folder and Account objects, // which will update their own unread counts. updateUnreadCounts(for: feeds) - + NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.statuses: statuses, UserInfoKey.articles: articles, UserInfoKey.articleIDs: articleIDs, UserInfoKey.feeds: feeds]) } @@ -1281,11 +1280,9 @@ private extension Account { } if feeds.count == 1, let feed = feeds.first { fetchUnreadCount(feed, completion) - } - else if feeds.count < 10 { + } else if feeds.count < 10 { fetchUnreadCounts(feeds, completion) - } - else { + } else { fetchAllUnreadCounts(completion) } } @@ -1388,7 +1385,7 @@ extension Account { public func existingFeed(withExternalID externalID: String) -> Feed? { return externalIDToFeedDictionary[externalID] } - + } // MARK: - OPMLRepresentable diff --git a/Modules/Account/Sources/Account/AccountMetadataFile.swift b/Modules/Account/Sources/Account/AccountMetadataFile.swift index d3bc67d65..2c60cd1f6 100644 --- a/Modules/Account/Sources/Account/AccountMetadataFile.swift +++ b/Modules/Account/Sources/Account/AccountMetadataFile.swift @@ -11,12 +11,12 @@ import os.log import RSCore final class AccountMetadataFile { - + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "accountMetadataFile") private let fileURL: URL private let account: Account - + private var isDirty = false { didSet { queueSaveToDiskIfNeeded() @@ -28,11 +28,11 @@ final class AccountMetadataFile { self.fileURL = URL(fileURLWithPath: filename) self.account = account } - + func markAsDirty() { isDirty = true } - + func load() { if let fileData = try? Data(contentsOf: fileURL) { let decoder = PropertyListDecoder() @@ -40,10 +40,10 @@ final class AccountMetadataFile { } account.metadata.delegate = account } - + func save() { guard !account.isDeleted else { return } - + let encoder = PropertyListEncoder() encoder.outputFormat = .binary @@ -54,7 +54,7 @@ final class AccountMetadataFile { os_log(.error, log: log, "Save to disk failed: %@.", error.localizedDescription) } } - + } private extension AccountMetadataFile { diff --git a/Modules/Account/Sources/Account/Feedly/OAuthAuthorizationCodeGranting.swift b/Modules/Account/Sources/Account/Feedly/OAuthAuthorizationCodeGranting.swift index 713788fbd..fb16d7574 100644 --- a/Modules/Account/Sources/Account/Feedly/OAuthAuthorizationCodeGranting.swift +++ b/Modules/Account/Sources/Account/Feedly/OAuthAuthorizationCodeGranting.swift @@ -17,7 +17,7 @@ public struct OAuthAuthorizationClient: Equatable { public var redirectUri: String public var state: String? public var secret: String - + public init(id: String, redirectUri: String, state: String?, secret: String) { self.id = id self.redirectUri = redirectUri @@ -34,20 +34,20 @@ public struct OAuthAuthorizationRequest { public var redirectUri: String public var scope: String public var state: String? - + public init(clientId: String, redirectUri: String, scope: String, state: String?) { self.clientId = clientId self.redirectUri = redirectUri self.scope = scope self.state = state } - + public var queryItems: [URLQueryItem] { return [ URLQueryItem(name: "response_type", value: responseType), URLQueryItem(name: "client_id", value: clientId), URLQueryItem(name: "scope", value: scope), - URLQueryItem(name: "redirect_uri", value: redirectUri), + URLQueryItem(name: "redirect_uri", value: redirectUri) ] } } @@ -60,7 +60,7 @@ public struct OAuthAuthorizationResponse { } public extension OAuthAuthorizationResponse { - + init(url: URL, client: OAuthAuthorizationClient) throws { guard let scheme = url.scheme, client.redirectUri.hasPrefix(scheme) else { throw URLError(.unsupportedURL) @@ -75,10 +75,10 @@ public extension OAuthAuthorizationResponse { guard let codeValue = code?.value, !codeValue.isEmpty else { throw URLError(.unsupportedURL) } - + let state = queryItems.first { $0.name.lowercased() == "state" } let stateValue = state?.value - + self.init(code: codeValue, state: stateValue) } } @@ -89,7 +89,7 @@ public struct OAuthAuthorizationErrorResponse: Error { public var error: OAuthAuthorizationError public var state: String? public var errorDescription: String? - + public var localizedDescription: String { return errorDescription ?? error.rawValue } @@ -115,11 +115,11 @@ public struct OAuthAccessTokenRequest: Encodable { public var redirectUri: String public var state: String? public var clientId: String - + // Possibly not part of the standard but specific to certain implementations (e.g.: Feedly). public var clientSecret: String public var scope: String - + public init(authorizationResponse: OAuthAuthorizationResponse, scope: String, client: OAuthAuthorizationClient) { self.code = authorizationResponse.code self.redirectUri = client.redirectUri @@ -152,22 +152,21 @@ public struct OAuthAuthorizationGrant: Equatable { /// https://tools.ietf.org/html/rfc6749#section-4.1 public protocol OAuthAuthorizationCodeGrantRequesting { associatedtype AccessTokenResponse: OAuthAccessTokenResponse - + /// Provides the URL request that allows users to consent to the client having access to their information. Typically loaded by a web view. /// - Parameter request: The information about the client requesting authorization to be granted access tokens. /// - Parameter baseUrlComponents: The scheme and host of the url except for the path. static func authorizationCodeUrlRequest(for request: OAuthAuthorizationRequest, baseUrlComponents: URLComponents) -> URLRequest - - + /// Performs the request for the access token given an authorization code. /// - Parameter authorizationRequest: The authorization code and other information the authorization server requires to grant the client access tokens on the user's behalf. /// - Parameter completion: On success, the access token response appropriate for concrete type's service. On failure, possibly a `URLError` or `OAuthAuthorizationErrorResponse` value. - func requestAccessToken(_ authorizationRequest: OAuthAccessTokenRequest, completion: @escaping (Result) -> ()) + func requestAccessToken(_ authorizationRequest: OAuthAccessTokenRequest, completion: @escaping (Result) -> Void) } protocol OAuthAuthorizationGranting: AccountDelegate { - + static func oauthAuthorizationCodeGrantRequest() -> URLRequest - - static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completion: @escaping (Result) -> ()) + + static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completion: @escaping (Result) -> Void) } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift index 8c0ba5b03..1f11b42e4 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestStarredArticleIdsOperation.swift @@ -25,12 +25,12 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation { private let database: SyncDatabase private var remoteEntryIds = Set() private let log: OSLog - + convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { let resource = FeedlyTagResourceId.Global.saved(for: userId) self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log) } - + init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { self.account = account self.resource = resource @@ -38,92 +38,92 @@ final class FeedlyIngestStarredArticleIdsOperation: FeedlyOperation { self.database = database self.log = log } - + override func run() { getStreamIds(nil) } - + private func getStreamIds(_ continuation: String?) { service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: nil, completion: didGetStreamIds(_:)) } - + private func didGetStreamIds(_ result: Result) { guard !isCanceled else { didFinish() return } - + switch result { case .success(let streamIds): - + remoteEntryIds.formUnion(streamIds.ids) - + guard let continuation = streamIds.continuation else { removeEntryIdsWithPendingStatus() return } - + getStreamIds(continuation) - + case .failure(let error): didFinish(with: error) } } - + /// Do not override pending statuses with the remote statuses of the same articles, otherwise an article will temporarily re-acquire the remote status before the pending status is pushed and subseqently pulled. private func removeEntryIdsWithPendingStatus() { guard !isCanceled else { didFinish() return } - + database.selectPendingStarredStatusArticleIDs { result in switch result { case .success(let pendingArticleIds): self.remoteEntryIds.subtract(pendingArticleIds) - + self.updateStarredStatuses() - + case .failure(let error): self.didFinish(with: error) } } } - + private func updateStarredStatuses() { guard !isCanceled else { didFinish() return } - + account.fetchStarredArticleIDs { result in switch result { case .success(let localStarredArticleIDs): self.processStarredArticleIDs(localStarredArticleIDs) - + case .failure(let error): self.didFinish(with: error) } } } - + func processStarredArticleIDs(_ localStarredArticleIDs: Set) { guard !isCanceled else { didFinish() return } - + let remoteStarredArticleIDs = remoteEntryIds - + let group = DispatchGroup() - + final class StarredStatusResults { var markAsStarredError: Error? var markAsUnstarredError: Error? } - + let results = StarredStatusResults() - + group.enter() account.markAsStarred(remoteStarredArticleIDs) { result in if case .failure(let error) = result { diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift index e5d71060d..f03136b59 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlySendArticleStatusesOperation.swift @@ -11,7 +11,6 @@ import Articles import SyncDatabase import os.log - /// Take changes to statuses of articles locally and apply them to the corresponding the articles remotely. final class FeedlySendArticleStatusesOperation: FeedlyOperation { @@ -24,7 +23,7 @@ final class FeedlySendArticleStatusesOperation: FeedlyOperation { self.service = service self.log = log } - + override func run() { os_log(.debug, log: log, "Sending article statuses...") @@ -33,7 +32,7 @@ final class FeedlySendArticleStatusesOperation: FeedlyOperation { self.didFinish() return } - + switch result { case .success(let syncStatuses): self.processStatuses(syncStatuses) @@ -51,7 +50,7 @@ private extension FeedlySendArticleStatusesOperation { (.read, false, .unread), (.read, true, .read), (.starred, true, .saved), - (.starred, false, .unsaved), + (.starred, false, .unsaved) ] let group = DispatchGroup() diff --git a/Modules/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift b/Modules/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift index 0e0ed5775..14de3c16f 100644 --- a/Modules/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift +++ b/Modules/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift @@ -28,7 +28,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } - var accountMetadata: AccountMetadata? = nil + var accountMetadata: AccountMetadata? var refreshProgress = DownloadProgress(numberOfTasks: 0) let caller: NewsBlurAPICaller @@ -59,14 +59,14 @@ final class NewsBlurAccountDelegate: AccountDelegate { database = SyncDatabase(databaseFilePath: dataFolder.appending("/DB.sqlite3")) } - func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable: Any], completion: @escaping () -> Void) { completion() } - - func refreshAll(for account: Account, completion: @escaping (Result) -> ()) { + + func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { refreshProgress.reset() - + self.refreshProgress.addToNumberOfTasksAndRemaining(4) refreshFeeds(for: account) { result in @@ -135,8 +135,8 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } } - - func sendArticleStatus(for account: Account, completion: @escaping (Result) -> ()) { + + func sendArticleStatus(for account: Account, completion: @escaping (Result) -> Void) { os_log(.debug, log: log, "Sending story statuses...") database.selectForProcessing { result in @@ -209,7 +209,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } - func refreshArticleStatus(for account: Account, completion: @escaping (Result) -> ()) { + func refreshArticleStatus(for account: Account, completion: @escaping (Result) -> Void) { os_log(.debug, log: log, "Refreshing story statuses...") let group = DispatchGroup() @@ -349,18 +349,18 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } - func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result) -> ()) { + func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result) -> Void) { completion(.success(())) } - func createFolder(for account: Account, name: String, completion: @escaping (Result) -> ()) { + func createFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) { self.refreshProgress.addToNumberOfTasksAndRemaining(1) caller.addFolder(named: name) { result in self.refreshProgress.completeTask() switch result { - case .success(): + case .success: if let folder = account.ensureFolder(with: name) { completion(.success(folder)) } else { @@ -372,7 +372,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } - func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> ()) { + func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { guard let folderToRename = folder.name else { completion(.failure(NewsBlurError.invalidParameter)) return @@ -397,7 +397,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { folder.name = name } - func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> ()) { + func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { guard let folderToRemove = folder.name else { completion(.failure(NewsBlurError.invalidParameter)) return @@ -427,7 +427,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } - func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> ()) { + func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { refreshProgress.addToNumberOfTasksAndRemaining(1) let folderName = (container as? Folder)?.name @@ -446,7 +446,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } - func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result) -> ()) { + func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result) -> Void) { guard let feedID = feed.externalID else { completion(.failure(NewsBlurError.invalidParameter)) return @@ -473,7 +473,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } - func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result) -> ()) { + func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result) -> Void) { guard let folder = container as? Folder else { DispatchQueue.main.async { if let account = container as? Account { @@ -492,11 +492,11 @@ final class NewsBlurAccountDelegate: AccountDelegate { completion(.success(())) } - func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result) -> ()) { + func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { deleteFeed(for: account, with: feed, from: container, completion: completion) } - func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result) -> ()) { + func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result) -> Void) { guard let feedID = feed.externalID else { completion(.failure(NewsBlurError.invalidParameter)) return @@ -515,7 +515,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { case .success: from.removeFeed(feed) to.addFeed(feed) - + completion(.success(())) case .failure(let error): completion(.failure(error)) @@ -523,7 +523,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } - func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result) -> ()) { + func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result) -> Void) { if let existingFeed = account.existingFeed(withURL: feed.url) { account.addFeed(existingFeed, to: container) { result in switch result { @@ -545,7 +545,7 @@ final class NewsBlurAccountDelegate: AccountDelegate { } } - func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> ()) { + func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> Void) { guard let folderName = folder.name else { completion(.failure(NewsBlurError.invalidParameter)) return @@ -613,13 +613,13 @@ final class NewsBlurAccountDelegate: AccountDelegate { } func accountWillBeDeleted(_ account: Account) { - caller.logout() { _ in } + caller.logout { _ in } } - class func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result) -> ()) { + class func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result) -> Void) { let caller = NewsBlurAPICaller(transport: transport) caller.credentials = credentials - caller.validateCredentials() { result in + caller.validateCredentials { result in DispatchQueue.main.async { completion(result) } diff --git a/Modules/SyncDatabase/Package.swift b/Modules/SyncDatabase/Package.swift index 4b30bc82d..46f2d557e 100644 --- a/Modules/SyncDatabase/Package.swift +++ b/Modules/SyncDatabase/Package.swift @@ -9,12 +9,12 @@ let package = Package( .library( name: "SyncDatabase", type: .dynamic, - targets: ["SyncDatabase"]), + targets: ["SyncDatabase"]) ], dependencies: [ .package(path: "../RSCore"), .package(path: "../Articles"), - .package(path: "../RSDatabase"), + .package(path: "../RSDatabase") ], targets: [ .target( @@ -22,9 +22,9 @@ let package = Package( dependencies: [ "RSCore", "RSDatabase", - "Articles", + "Articles" ], swiftSettings: [.unsafeFlags(["-warnings-as-errors"])] - ), + ) ] ) diff --git a/Modules/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift b/Modules/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift index b02419d84..e01f1a1b1 100644 --- a/Modules/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift +++ b/Modules/SyncDatabase/Sources/SyncDatabase/SyncDatabase.swift @@ -10,7 +10,7 @@ import Foundation import RSCore import RSDatabase -public typealias SyncStatusesResult = Result, DatabaseError> +public typealias SyncStatusesResult = Result<[SyncStatus], DatabaseError> public typealias SyncStatusesCompletionBlock = (SyncStatusesResult) -> Void public typealias SyncStatusArticleIDsResult = Result, DatabaseError> @@ -35,7 +35,7 @@ public struct SyncDatabase { public func insertStatuses(_ statuses: [SyncStatus], completion: @escaping DatabaseCompletionBlock) { syncStatusTable.insertStatuses(statuses, completion: completion) } - + public func selectForProcessing(limit: Int? = nil, completion: @escaping SyncStatusesCompletionBlock) { return syncStatusTable.selectForProcessing(limit: limit, completion: completion) } @@ -47,11 +47,11 @@ public struct SyncDatabase { public func selectPendingReadStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { syncStatusTable.selectPendingReadStatusArticleIDs(completion: completion) } - + public func selectPendingStarredStatusArticleIDs(completion: @escaping SyncStatusArticleIDsCompletionBlock) { syncStatusTable.selectPendingStarredStatusArticleIDs(completion: completion) } - + public func resetAllSelectedForProcessing(completion: DatabaseCompletionBlock? = nil) { syncStatusTable.resetAllSelectedForProcessing(completion: completion) } @@ -59,7 +59,7 @@ public struct SyncDatabase { public func resetSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { syncStatusTable.resetSelectedForProcessing(articleIDs, completion: completion) } - + public func deleteSelectedForProcessing(_ articleIDs: [String], completion: DatabaseCompletionBlock? = nil) { syncStatusTable.deleteSelectedForProcessing(articleIDs, completion: completion) } @@ -81,7 +81,7 @@ public struct SyncDatabase { // MARK: - Private private extension SyncDatabase { - + static let tableCreationStatements = """ CREATE TABLE if not EXISTS syncStatus (articleID TEXT NOT NULL, key TEXT NOT NULL, flag BOOL NOT NULL DEFAULT 0, selected BOOL NOT NULL DEFAULT 0, PRIMARY KEY (articleID, key)); diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 055c54849..a07da9d21 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -13,7 +13,7 @@ import Articles import SafariServices class ArticleViewController: UIViewController { - + typealias State = (extractedArticle: ExtractedArticle?, isShowingExtractedArticle: Bool, articleExtractorButtonState: ArticleExtractorButtonState, @@ -25,26 +25,26 @@ class ArticleViewController: UIViewController { @IBOutlet private weak var readBarButtonItem: UIBarButtonItem! @IBOutlet private weak var starBarButtonItem: UIBarButtonItem! @IBOutlet private weak var actionBarButtonItem: UIBarButtonItem! - + @IBOutlet private var searchBar: ArticleSearchBar! @IBOutlet private var searchBarBottomConstraint: NSLayoutConstraint! private var defaultControls: [UIBarButtonItem]? - + private var pageViewController: UIPageViewController! - + private var currentWebViewController: WebViewController? { return pageViewController?.viewControllers?.first as? WebViewController } - + private var articleExtractorButton: ArticleExtractorButton = { let button = ArticleExtractorButton(type: .system) button.frame = CGRect(x: 0, y: 0, width: 44.0, height: 44.0) button.setImage(AppAssets.articleExtractorOff, for: .normal) return button }() - + weak var coordinator: SceneCoordinator! - + private let poppableDelegate = PoppableGestureRecognizerDelegate() var article: Article? { @@ -60,7 +60,7 @@ class ArticleViewController: UIViewController { updateUI() } } - + var restoreScrollPosition: (isShowingExtractedArticle: Bool, articleWindowScrollY: Int)? { didSet { if let rsp = restoreScrollPosition { @@ -68,7 +68,7 @@ class ArticleViewController: UIViewController { } } } - + var currentState: State? { guard let controller = currentWebViewController else { return nil} return State(extractedArticle: controller.extractedArticle, @@ -76,14 +76,14 @@ class ArticleViewController: UIViewController { articleExtractorButtonState: controller.articleExtractorButtonState, windowScrollY: controller.windowScrollY) } - + var restoreState: State? - + private let keyboardManager = KeyboardManager(type: .detail) override var keyCommands: [UIKeyCommand]? { return keyboardManager.keyCommands } - + override func viewDidLoad() { super.viewDidLoad() @@ -99,15 +99,15 @@ class ArticleViewController: UIViewController { ]) fullScreenTapZone.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapNavigationBar))) navigationItem.titleView = fullScreenTapZone - + articleExtractorButton.addTarget(self, action: #selector(toggleArticleExtractor(_:)), for: .touchUpInside) toolbarItems?.insert(UIBarButtonItem(customView: articleExtractorButton), at: 6) - + if let parentNavController = navigationController?.parent as? UINavigationController { poppableDelegate.navigationController = parentNavController parentNavController.interactivePopGestureRecognizer?.delegate = poppableDelegate } - + pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:]) pageViewController.delegate = self pageViewController.dataSource = self @@ -128,7 +128,7 @@ class ArticleViewController: UIViewController { view.topAnchor.constraint(equalTo: pageViewController.view.topAnchor), view.bottomAnchor.constraint(equalTo: pageViewController.view.bottomAnchor) ]) - + let controller: WebViewController if let state = restoreState { controller = createWebViewController(article, updateView: false) @@ -139,18 +139,18 @@ class ArticleViewController: UIViewController { } else { controller = createWebViewController(article, updateView: true) } - + if let rsp = restoreScrollPosition { controller.setScrollPosition(isShowingExtractedArticle: rsp.isShowingExtractedArticle, articleWindowScrollY: rsp.articleWindowScrollY) } articleExtractorButton.buttonState = controller.articleExtractorButtonState - + self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil) if AppDefaults.shared.logicalArticleFullscreenEnabled { controller.hideBars() } - + // Search bar searchBar.translatesAutoresizingMaskIntoConstraints = false NotificationCenter.default.addObserver(self, selector: #selector(beginFind(_:)), name: .FindInArticle, object: nil) @@ -158,10 +158,10 @@ class ArticleViewController: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChangeFrame(_:)), name: UIWindow.keyboardWillChangeFrameNotification, object: nil) searchBar.delegate = self view.bringSubviewToFront(searchBar) - + updateUI() } - + override func viewWillAppear(_ animated: Bool) { let hideToolbars = AppDefaults.shared.logicalArticleFullscreenEnabled if hideToolbars { @@ -176,19 +176,19 @@ class ArticleViewController: UIViewController { super.viewDidAppear(true) coordinator.isArticleViewControllerPending = false } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if searchBar != nil && !searchBar.isHidden { endFind() } } - + override func viewSafeAreaInsetsDidChange() { // This will animate if the show/hide bars animation is happening. view.layoutIfNeeded() } - + override func willTransition(to newCollection: UITraitCollection, with coordinator: any UIViewControllerTransitionCoordinator) { // We only want to show bars when rotating to horizontalSizeClass == .regular // (i.e., big) iPhones to resolve crash #4483. @@ -196,9 +196,9 @@ class ArticleViewController: UIViewController { currentWebViewController?.showBars() } } - + func updateUI() { - + guard let article = article else { articleExtractorButton.isEnabled = false nextUnreadBarButtonItem.isEnabled = false @@ -215,11 +215,11 @@ class ArticleViewController: UIViewController { nextArticleBarButtonItem.isEnabled = coordinator.isNextArticleAvailable readBarButtonItem.isEnabled = true starBarButtonItem.isEnabled = true - + let permalinkPresent = article.preferredLink != nil articleExtractorButton.isEnabled = permalinkPresent && !AppDefaults.shared.isDeveloperBuild actionBarButtonItem.isEnabled = permalinkPresent - + if article.status.read { readBarButtonItem.image = AppAssets.circleOpenImage readBarButtonItem.isEnabled = article.isAvailableToMarkUnread @@ -229,7 +229,7 @@ class ArticleViewController: UIViewController { readBarButtonItem.isEnabled = true readBarButtonItem.accLabelText = NSLocalizedString("Selected - Mark Article Unread", comment: "Selected - Mark Article Unread") } - + if article.status.starred { starBarButtonItem.image = AppAssets.starClosedImage starBarButtonItem.accLabelText = NSLocalizedString("Selected - Star Article", comment: "Selected - Star Article") @@ -238,13 +238,13 @@ class ArticleViewController: UIViewController { starBarButtonItem.accLabelText = NSLocalizedString("Star Article", comment: "Star Article") } } - + // MARK: Notifications - + @objc dynamic func unreadCountDidChange(_ notification: Notification) { updateUI() } - + @objc func statusesDidChange(_ note: Notification) { guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set else { return @@ -260,14 +260,14 @@ class ArticleViewController: UIViewController { @objc func contentSizeCategoryDidChange(_ note: Notification) { currentWebViewController?.fullReload() } - + @objc func willEnterForeground(_ note: Notification) { // The toolbar will come back on you if you don't hide it again if AppDefaults.shared.logicalArticleFullscreenEnabled { currentWebViewController?.hideBars() } } - + // MARK: Actions @objc func didTapNavigationBar() { @@ -281,27 +281,27 @@ class ArticleViewController: UIViewController { @IBAction func toggleArticleExtractor(_ sender: Any) { currentWebViewController?.toggleArticleExtractor() } - + @IBAction func nextUnread(_ sender: Any) { coordinator.selectNextUnread() } - + @IBAction func prevArticle(_ sender: Any) { coordinator.selectPrevArticle() } - + @IBAction func nextArticle(_ sender: Any) { coordinator.selectNextArticle() } - + @IBAction func toggleRead(_ sender: Any) { coordinator.toggleReadForCurrentArticle() } - + @IBAction func toggleStar(_ sender: Any) { coordinator.toggleStarredForCurrentArticle() } - + @IBAction func showActivityDialog(_ sender: Any) { currentWebViewController?.showActivityDialog(popOverBarButtonItem: actionBarButtonItem) } @@ -309,13 +309,13 @@ class ArticleViewController: UIViewController { @objc func toggleReaderView(_ sender: Any?) { currentWebViewController?.toggleArticleExtractor() } - + // MARK: Keyboard Shortcuts @objc func navigateToTimeline(_ sender: Any?) { coordinator.navigateToTimeline() } - + // MARK: API func focus() { @@ -337,7 +337,7 @@ class ArticleViewController: UIViewController { func scrollPageUp() { currentWebViewController?.scrollPageUp() } - + func stopArticleExtractorIfProcessing() { currentWebViewController?.stopArticleExtractorIfProcessing() } @@ -345,7 +345,7 @@ class ArticleViewController: UIViewController { func openInAppBrowser() { currentWebViewController?.openInAppBrowser() } - + func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) { currentWebViewController?.setScrollPosition(isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) } @@ -358,29 +358,29 @@ public extension Notification.Name { } extension ArticleViewController: SearchBarDelegate { - + func searchBar(_ searchBar: ArticleSearchBar, textDidChange searchText: String) { currentWebViewController?.searchText(searchText) { found in searchBar.resultsCount = found.count - + if let index = found.index { searchBar.selectedResult = index + 1 } } } - + func doneWasPressed(_ searchBar: ArticleSearchBar) { NotificationCenter.default.post(name: .EndFindInArticle, object: nil) } - + func nextWasPressed(_ searchBar: ArticleSearchBar) { if searchBar.selectedResult < searchBar.resultsCount { currentWebViewController?.selectNextSearchResult() searchBar.selectedResult += 1 } } - + func previousWasPressed(_ searchBar: ArticleSearchBar) { if searchBar.selectedResult > 1 { currentWebViewController?.selectPreviousSearchResult() @@ -390,14 +390,14 @@ extension ArticleViewController: SearchBarDelegate { } extension ArticleViewController { - + @objc func beginFind(_ _: Any? = nil) { searchBar.isHidden = false navigationController?.setToolbarHidden(true, animated: true) currentWebViewController?.additionalSafeAreaInsets.bottom = searchBar.frame.height searchBar.becomeFirstResponder() } - + @objc func endFind(_ _: Any? = nil) { searchBar.resignFirstResponder() searchBar.isHidden = true @@ -405,13 +405,13 @@ extension ArticleViewController { currentWebViewController?.additionalSafeAreaInsets.bottom = 0 currentWebViewController?.endSearch() } - + @objc func keyboardWillChangeFrame(_ notification: Notification) { if !searchBar.isHidden, let duration = notification.userInfo?[UIWindow.keyboardAnimationDurationUserInfoKey] as? Double, let curveRaw = notification.userInfo?[UIWindow.keyboardAnimationCurveUserInfoKey] as? UInt, let frame = notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect { - + let curve = UIView.AnimationOptions(rawValue: curveRaw) let newHeight = view.safeAreaLayoutGuide.layoutFrame.maxY - frame.minY currentWebViewController?.additionalSafeAreaInsets.bottom = newHeight + searchBar.frame.height + 10 @@ -421,26 +421,25 @@ extension ArticleViewController { }) } } - -} +} // MARK: WebViewControllerDelegate extension ArticleViewController: WebViewControllerDelegate { - + func webViewController(_ webViewController: WebViewController, articleExtractorButtonStateDidUpdate buttonState: ArticleExtractorButtonState) { if webViewController === currentWebViewController { articleExtractorButton.buttonState = buttonState } } - + } // MARK: UIPageViewControllerDataSource extension ArticleViewController: UIPageViewControllerDataSource { - + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let webViewController = viewController as? WebViewController, let currentArticle = webViewController.article, @@ -449,7 +448,7 @@ extension ArticleViewController: UIPageViewControllerDataSource { } return createWebViewController(article) } - + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let webViewController = viewController as? WebViewController, let currentArticle = webViewController.article, @@ -458,7 +457,7 @@ extension ArticleViewController: UIPageViewControllerDataSource { } return createWebViewController(article) } - + } // MARK: UIPageViewControllerDelegate @@ -468,19 +467,19 @@ extension ArticleViewController: UIPageViewControllerDelegate { func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { guard finished, completed else { return } guard let article = currentWebViewController?.article else { return } - + coordinator.selectArticle(article, animations: [.select, .scroll, .navigation]) articleExtractorButton.buttonState = currentWebViewController?.articleExtractorButtonState ?? .off - + previousViewControllers.compactMap({ $0 as? WebViewController }).forEach({ $0.stopWebViewActivity() }) } - + } // MARK: UIGestureRecognizerDelegate extension ArticleViewController: UIGestureRecognizerDelegate { - + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { return true } @@ -492,13 +491,13 @@ extension ArticleViewController: UIGestureRecognizerDelegate { } return false } - + } // MARK: Private private extension ArticleViewController { - + func createWebViewController(_ article: Article?, updateView: Bool = true) -> WebViewController { let controller = WebViewController() controller.coordinator = coordinator @@ -506,5 +505,5 @@ private extension ArticleViewController { controller.setArticle(article, updateView: updateView) return controller } - + }