From 3e2fb4eb9a0697156c5449448df353d4b722f730 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 26 Jun 2026 18:09:46 +1200 Subject: [PATCH 1/2] Format view-local-window-reads files before migration --- .../NUX/JetpackPrologueViewController.swift | 2 +- .../3D Touch/WP3DTouchShortcutCreator.swift | 104 +++-- .../Reader/Cards/ReaderPostCell.swift | 176 ++++++-- .../ReaderStreamViewController.swift | 393 +++++++++++++----- .../Themes/ThemeBrowserViewController.swift | 339 ++++++++++----- 5 files changed, 730 insertions(+), 284 deletions(-) diff --git a/WordPress/Classes/Jetpack/NUX/JetpackPrologueViewController.swift b/WordPress/Classes/Jetpack/NUX/JetpackPrologueViewController.swift index 30c25adb326a..91ae0189d57c 100644 --- a/WordPress/Classes/Jetpack/NUX/JetpackPrologueViewController.swift +++ b/WordPress/Classes/Jetpack/NUX/JetpackPrologueViewController.swift @@ -151,7 +151,7 @@ extension JetpackPrologueViewController: InfiniteScrollViewDelegate { /// /// - Returns: Points per second. private func rateForAngle(angle: Double) -> CGFloat { - return -angle * Self.Constants.angleRateMultiplier + -angle * Self.Constants.angleRateMultiplier } /// Returns the angle in degrees of the device independently of the view's orientation. diff --git a/WordPress/Classes/System/3D Touch/WP3DTouchShortcutCreator.swift b/WordPress/Classes/System/3D Touch/WP3DTouchShortcutCreator.swift index aed878791390..30503bf953f0 100644 --- a/WordPress/Classes/System/3D Touch/WP3DTouchShortcutCreator.swift +++ b/WordPress/Classes/System/3D Touch/WP3DTouchShortcutCreator.swift @@ -8,15 +8,15 @@ public protocol ApplicationShortcutsProvider { extension UIApplication: ApplicationShortcutsProvider { @objc public var is3DTouchAvailable: Bool { - return mainWindow?.traitCollection.forceTouchCapability == .available + mainWindow?.traitCollection.forceTouchCapability == .available } } open class WP3DTouchShortcutCreator: NSObject { enum LoggedIn3DTouchShortcutIndex: Int { case notifications = 0, - stats, - newPost + stats, + newPost } var shortcutsProvider: ApplicationShortcutsProvider @@ -50,18 +50,43 @@ open class WP3DTouchShortcutCreator: NSObject { fileprivate func registerForNotifications() { let notificationCenter = NotificationCenter.default - notificationCenter.addObserver(self, selector: #selector(WP3DTouchShortcutCreator.createLoggedInShortcuts), name: NSNotification.Name(rawValue: WordPressAuthenticationManager.WPSigninDidFinishNotification), object: nil) - notificationCenter.addObserver(self, selector: #selector(WP3DTouchShortcutCreator.createLoggedInShortcuts), name: .WPRecentSitesChanged, object: nil) - notificationCenter.addObserver(self, selector: #selector(WP3DTouchShortcutCreator.createLoggedInShortcuts), name: .WPBlogUpdated, object: nil) - notificationCenter.addObserver(self, selector: #selector(WP3DTouchShortcutCreator.createLoggedInShortcuts), name: .wpAccountDefaultWordPressComAccountChanged, object: nil) + notificationCenter.addObserver( + self, + selector: #selector(WP3DTouchShortcutCreator.createLoggedInShortcuts), + name: NSNotification.Name(rawValue: WordPressAuthenticationManager.WPSigninDidFinishNotification), + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(WP3DTouchShortcutCreator.createLoggedInShortcuts), + name: .WPRecentSitesChanged, + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(WP3DTouchShortcutCreator.createLoggedInShortcuts), + name: .WPBlogUpdated, + object: nil + ) + notificationCenter.addObserver( + self, + selector: #selector(WP3DTouchShortcutCreator.createLoggedInShortcuts), + name: .wpAccountDefaultWordPressComAccountChanged, + object: nil + ) } fileprivate func loggedOutShortcutArray() -> [UIApplicationShortcutItem] { - let logInShortcut = UIMutableApplicationShortcutItem(type: WP3DTouchShortcutHandler.ShortcutIdentifier.LogIn.type, - localizedTitle: NSLocalizedString("Log In", comment: "Log In 3D Touch Shortcut"), - localizedSubtitle: nil, - icon: UIApplicationShortcutIcon(systemImageName: "arrow.right.square"), - userInfo: [WP3DTouchShortcutHandler.applicationShortcutUserInfoIconKey: WP3DTouchShortcutHandler.ShortcutIdentifier.LogIn.rawValue as NSSecureCoding]) + let logInShortcut = UIMutableApplicationShortcutItem( + type: WP3DTouchShortcutHandler.ShortcutIdentifier.LogIn.type, + localizedTitle: NSLocalizedString("Log In", comment: "Log In 3D Touch Shortcut"), + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(systemImageName: "arrow.right.square"), + userInfo: [ + WP3DTouchShortcutHandler.applicationShortcutUserInfoIconKey: WP3DTouchShortcutHandler.ShortcutIdentifier + .LogIn.rawValue as NSSecureCoding + ] + ) return [logInShortcut] } @@ -72,30 +97,45 @@ open class WP3DTouchShortcutCreator: NSObject { defaultBlogName = Blog.lastUsedOrFirst(in: mainContext)?.settings?.name } - let notificationsShortcut = UIMutableApplicationShortcutItem(type: WP3DTouchShortcutHandler.ShortcutIdentifier.Notifications.type, - localizedTitle: NSLocalizedString("Notifications", comment: "Notifications 3D Touch Shortcut"), - localizedSubtitle: nil, - icon: UIApplicationShortcutIcon(systemImageName: "bell"), - userInfo: [WP3DTouchShortcutHandler.applicationShortcutUserInfoIconKey: WP3DTouchShortcutHandler.ShortcutIdentifier.Notifications.rawValue as NSSecureCoding]) - - let statsShortcut = UIMutableApplicationShortcutItem(type: WP3DTouchShortcutHandler.ShortcutIdentifier.Stats.type, - localizedTitle: NSLocalizedString("Stats", comment: "Stats 3D Touch Shortcut"), - localizedSubtitle: defaultBlogName, - icon: UIApplicationShortcutIcon(systemImageName: "chart.bar"), - userInfo: [WP3DTouchShortcutHandler.applicationShortcutUserInfoIconKey: WP3DTouchShortcutHandler.ShortcutIdentifier.Stats.rawValue as NSSecureCoding]) - - let newPostShortcut = UIMutableApplicationShortcutItem(type: WP3DTouchShortcutHandler.ShortcutIdentifier.NewPost.type, - localizedTitle: NSLocalizedString("New Post", comment: "New Post 3D Touch Shortcut"), - localizedSubtitle: defaultBlogName, - icon: UIApplicationShortcutIcon(systemImageName: "square.and.pencil"), - userInfo: [WP3DTouchShortcutHandler.applicationShortcutUserInfoIconKey: WP3DTouchShortcutHandler.ShortcutIdentifier.NewPost.rawValue as NSSecureCoding]) + let notificationsShortcut = UIMutableApplicationShortcutItem( + type: WP3DTouchShortcutHandler.ShortcutIdentifier.Notifications.type, + localizedTitle: NSLocalizedString("Notifications", comment: "Notifications 3D Touch Shortcut"), + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(systemImageName: "bell"), + userInfo: [ + WP3DTouchShortcutHandler.applicationShortcutUserInfoIconKey: WP3DTouchShortcutHandler.ShortcutIdentifier + .Notifications.rawValue as NSSecureCoding + ] + ) + + let statsShortcut = UIMutableApplicationShortcutItem( + type: WP3DTouchShortcutHandler.ShortcutIdentifier.Stats.type, + localizedTitle: NSLocalizedString("Stats", comment: "Stats 3D Touch Shortcut"), + localizedSubtitle: defaultBlogName, + icon: UIApplicationShortcutIcon(systemImageName: "chart.bar"), + userInfo: [ + WP3DTouchShortcutHandler.applicationShortcutUserInfoIconKey: WP3DTouchShortcutHandler.ShortcutIdentifier + .Stats.rawValue as NSSecureCoding + ] + ) + + let newPostShortcut = UIMutableApplicationShortcutItem( + type: WP3DTouchShortcutHandler.ShortcutIdentifier.NewPost.type, + localizedTitle: NSLocalizedString("New Post", comment: "New Post 3D Touch Shortcut"), + localizedSubtitle: defaultBlogName, + icon: UIApplicationShortcutIcon(systemImageName: "square.and.pencil"), + userInfo: [ + WP3DTouchShortcutHandler.applicationShortcutUserInfoIconKey: WP3DTouchShortcutHandler.ShortcutIdentifier + .NewPost.rawValue as NSSecureCoding + ] + ) return [notificationsShortcut, statsShortcut, newPostShortcut] } @objc fileprivate func createLoggedInShortcuts() { - DispatchQueue.main.async {[weak self]() in + DispatchQueue.main.async { [weak self] () in guard let strongSelf = self else { return } @@ -132,7 +172,7 @@ open class WP3DTouchShortcutCreator: NSObject { } fileprivate func hasWordPressComAccount() -> Bool { - return AccountHelper.isDotcomAvailable() + AccountHelper.isDotcomAvailable() } fileprivate func doesCurrentBlogSupportStats() -> Bool { @@ -144,6 +184,6 @@ open class WP3DTouchShortcutCreator: NSObject { } fileprivate func hasBlog() -> Bool { - return Blog.count(in: mainContext) > 0 + Blog.count(in: mainContext) > 0 } } diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index ed867ea6f0c0..8a5eb6fc02b8 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift @@ -18,7 +18,7 @@ final class ReaderPostCell: ReaderStreamBaseCell { view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ view.topAnchor.constraint(equalTo: contentView.topAnchor), - view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).withPriority(999), + view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).withPriority(999) ]) } @@ -47,7 +47,10 @@ final class ReaderPostCell: ReaderStreamBaseCell { override func updateConstraints() { NSLayoutConstraint.deactivate(contentViewConstraints) - contentViewConstraints = view.pinEdges(.horizontal, to: isCompact ? contentView : contentView.readableContentGuide) + contentViewConstraints = view.pinEdges( + .horizontal, + to: isCompact ? contentView : contentView.readableContentGuide + ) super.updateConstraints() } @@ -81,10 +84,15 @@ private final class ReaderPostCellView: UIView { private lazy var toolbarView = UIStackView(buttons.allButtons) let buttons = ReaderPostToolbarButtons() - private lazy var postPreview = UIStackView(axis: .vertical, alignment: .leading, spacing: 12, [ - UIStackView(axis: .vertical, spacing: 4, [titleLabel, detailsLabel]), - imageView - ]) + private lazy var postPreview = UIStackView( + axis: .vertical, + alignment: .leading, + spacing: 12, + [ + UIStackView(axis: .vertical, spacing: 4, [titleLabel, detailsLabel]), + imageView + ] + ) var isCompact: Bool = true { didSet { @@ -213,13 +221,19 @@ private final class ReaderPostCellView: UIView { NSLayoutConstraint.deactivate(imageViewConstraints) if isCompact { imageViewConstraints = [ - imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: ReaderPostCell.coverAspectRatio), + imageView.heightAnchor.constraint( + equalTo: imageView.widthAnchor, + multiplier: ReaderPostCell.coverAspectRatio + ), imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, constant: -(insets.left * 2)), imageView.widthAnchor.constraint(equalTo: widthAnchor).withPriority(150) ] } else { imageViewConstraints = [ - imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: ReaderPostCell.coverAspectRatio), + imageView.heightAnchor.constraint( + equalTo: imageView.widthAnchor, + multiplier: ReaderPostCell.coverAspectRatio + ), imageView.widthAnchor.constraint(equalToConstant: ReaderPostCell.regularCoverWidth) ] } @@ -233,11 +247,14 @@ private final class ReaderPostCellView: UIView { private func setupActions() { buttonAuthor.addTarget(self, action: #selector(buttonAuthorTapped), for: .primaryActionTriggered) buttonMore.showsMenuAsPrimaryAction = true - buttonMore.menu = UIMenu(options: .displayInline, children: [ - UIDeferredMenuElement.uncached { [weak self] callback in - callback(self?.makeMoreMenu() ?? []) - } - ]) + buttonMore.menu = UIMenu( + options: .displayInline, + children: [ + UIDeferredMenuElement.uncached { [weak self] callback in + callback(self?.makeMoreMenu() ?? []) + } + ] + ) buttons.bookmark.addTarget(self, action: #selector(buttonBookmarkTapped), for: .primaryActionTriggered) buttons.reblog.addTarget(self, action: #selector(buttonReblogTapped), for: .primaryActionTriggered) buttons.comment.addTarget(self, action: #selector(buttonCommentTapped), for: .primaryActionTriggered) @@ -272,9 +289,10 @@ private final class ReaderPostCellView: UIView { toolbar.likeCount += 1 configureToolbar(with: toolbar) UINotificationFeedbackGenerator().notificationOccurred(.success) - buttons.like.imageView?.fadeInWithRotationAnimation { _ in - viewModel.toggleLike() - } + buttons.like.imageView? + .fadeInWithRotationAnimation { _ in + viewModel.toggleLike() + } } else { viewModel.toggleLike() } @@ -289,7 +307,8 @@ private final class ReaderPostCellView: UIView { topic: viewController.readerTopic, anchor: buttonMore, viewController: viewController - ).makeMenu() + ) + .makeMenu() } // MARK: Configure (ViewModel) @@ -298,7 +317,10 @@ private final class ReaderPostCellView: UIView { self.viewModel = viewModel setAvatar(with: viewModel) - buttonAuthor.configuration?.attributedTitle = AttributedString(viewModel.author, attributes: Self.authorAttributes) + buttonAuthor.configuration?.attributedTitle = AttributedString( + viewModel.author, + attributes: Self.authorAttributes + ) timeLabel.text = viewModel.time titleLabel.text = viewModel.title @@ -336,7 +358,9 @@ private final class ReaderPostCellView: UIView { private func configureToolbar(with viewModel: ReaderPostToolbarViewModel) { buttons.bookmark.configuration = { var configuration = buttons.bookmark.configuration ?? .plain() - configuration.image = viewModel.isBookmarked ? WPStyleGuide.ReaderDetail.saveSelectedToolbarIcon : WPStyleGuide.ReaderDetail.saveToolbarIcon + configuration.image = + viewModel.isBookmarked + ? WPStyleGuide.ReaderDetail.saveSelectedToolbarIcon : WPStyleGuide.ReaderDetail.saveToolbarIcon configuration.baseForegroundColor = viewModel.isBookmarked ? UIAppColor.primary : .secondaryLabel return configuration }() @@ -345,20 +369,28 @@ private final class ReaderPostCellView: UIView { buttons.comment.isHidden = !viewModel.isCommentsEnabled if viewModel.isCommentsEnabled { - buttons.comment.configuration?.attributedTitle = AttributedString(kFormatted(viewModel.commentCount), attributes: AttributeContainer([ - .font: font, - .foregroundColor: UIColor.secondaryLabel - ])) + buttons.comment.configuration?.attributedTitle = AttributedString( + kFormatted(viewModel.commentCount), + attributes: AttributeContainer([ + .font: font, + .foregroundColor: UIColor.secondaryLabel + ]) + ) } buttons.like.isHidden = !viewModel.isLikesEnabled if viewModel.isLikesEnabled { buttons.like.configuration = { var configuration = buttons.like.configuration ?? .plain() - configuration.attributedTitle = AttributedString(kFormatted(viewModel.likeCount), attributes: AttributeContainer([ - .font: font, - .foregroundColor: viewModel.isLiked ? UIAppColor.primary : UIColor.secondaryLabel - ])) - configuration.image = viewModel.isLiked ? WPStyleGuide.ReaderDetail.likeSelectedToolbarIcon : WPStyleGuide.ReaderDetail.likeToolbarIcon + configuration.attributedTitle = AttributedString( + kFormatted(viewModel.likeCount), + attributes: AttributeContainer([ + .font: font, + .foregroundColor: viewModel.isLiked ? UIAppColor.primary : UIColor.secondaryLabel + ]) + ) + configuration.image = + viewModel.isLiked + ? WPStyleGuide.ReaderDetail.likeSelectedToolbarIcon : WPStyleGuide.ReaderDetail.likeToolbarIcon configuration.baseForegroundColor = viewModel.isLiked ? UIAppColor.primary : .secondaryLabel return configuration }() @@ -367,13 +399,18 @@ private final class ReaderPostCellView: UIView { private func setAvatar(with viewModel: ReaderPostCellViewModel) { avatarView.setPlaceholder(UIImage(named: "post-blavatar-placeholder")) - let avatarSize = ImageSize(scaling: CGSize(width: ReaderPostCell.avatarSize, height: ReaderPostCell.avatarSize), in: self) + let avatarSize = ImageSize( + scaling: CGSize(width: ReaderPostCell.avatarSize, height: ReaderPostCell.avatarSize), + in: self + ) if let avatarURL = viewModel.avatarURL { avatarView.setImage(with: avatarURL, size: avatarSize) } else { - viewModel.$avatarURL.compactMap({ $0 }).sink { [weak self] in - self?.avatarView.setImage(with: $0, size: avatarSize) - }.store(in: &cancellables) + viewModel.$avatarURL.compactMap({ $0 }) + .sink { [weak self] in + self?.avatarView.setImage(with: $0, size: avatarSize) + } + .store(in: &cancellables) } } @@ -383,7 +420,9 @@ private final class ReaderPostCellView: UIView { seenCheckmark.image = UIImage( systemName: "checkmark", - withConfiguration: UIImage.SymbolConfiguration(font: .preferredFont(forTextStyle: .caption1).withWeight(.medium)) + withConfiguration: UIImage.SymbolConfiguration( + font: .preferredFont(forTextStyle: .caption1).withWeight(.medium) + ) ) seenCheckmark.tintColor = .secondaryLabel } @@ -417,7 +456,8 @@ private func makeAuthorButton() -> UIButton { return UIButton(configuration: configuration) } -private func makeButton(image: UIImage? = nil, font: UIFont = UIFont.preferredFont(forTextStyle: .footnote)) -> UIButton { +private func makeButton(image: UIImage? = nil, font: UIFont = UIFont.preferredFont(forTextStyle: .footnote)) -> UIButton +{ var configuration = UIButton.Configuration.plain() configuration.image = image configuration.imagePadding = 2 @@ -439,8 +479,16 @@ private func kFormatted(_ count: Int) -> String { private extension ReaderPostCellView { func setupAccessibility() { - buttonAuthor.accessibilityHint = NSLocalizedString("reader.post.buttonSite.accessibilityHint", value: "Opens the site details", comment: "Accessibility hint for the site header") - buttonMore.accessibilityLabel = NSLocalizedString("reader.post.moreMenu.accessibilityLabel", value: "More actions", comment: "Button accessibility label") + buttonAuthor.accessibilityHint = NSLocalizedString( + "reader.post.buttonSite.accessibilityHint", + value: "Opens the site details", + comment: "Accessibility hint for the site header" + ) + buttonMore.accessibilityLabel = NSLocalizedString( + "reader.post.moreMenu.accessibilityLabel", + value: "More actions", + comment: "Button accessibility label" + ) buttonAuthor.accessibilityIdentifier = "reader-author-button" buttonMore.accessibilityIdentifier = "reader-more-button" @@ -451,16 +499,60 @@ private extension ReaderPostCellView { } func configureToolbarAccessibility(with viewModel: ReaderPostToolbarViewModel) { - buttons.bookmark.accessibilityLabel = viewModel.isBookmarked ? NSLocalizedString("reader.post.buttonRemoveBookmark.accessibilityLint", value: "Remove bookmark", comment: "Button accessibility label") : NSLocalizedString("reader.post.buttonBookmark.accessibilityLabel", value: "Bookmark", comment: "Button accessibility label") - buttons.reblog.accessibilityLabel = NSLocalizedString("reader.post.buttonReblog.accessibilityLabel", value: "Reblog", comment: "Button accessibility label") + buttons.bookmark.accessibilityLabel = + viewModel.isBookmarked + ? NSLocalizedString( + "reader.post.buttonRemoveBookmark.accessibilityLint", + value: "Remove bookmark", + comment: "Button accessibility label" + ) + : NSLocalizedString( + "reader.post.buttonBookmark.accessibilityLabel", + value: "Bookmark", + comment: "Button accessibility label" + ) + buttons.reblog.accessibilityLabel = NSLocalizedString( + "reader.post.buttonReblog.accessibilityLabel", + value: "Reblog", + comment: "Button accessibility label" + ) buttons.comment.accessibilityLabel = { - let label = NSLocalizedString("reader.post.buttonComment.accessibilityLabel", value: "Show comments", comment: "Button accessibility label") - let count = String(format: NSLocalizedString("reader.post.numberOfComments.accessibilityLabel", value: "%@ comments", comment: "Accessibility label showing total number of comments"), viewModel.commentCount.description) + let label = NSLocalizedString( + "reader.post.buttonComment.accessibilityLabel", + value: "Show comments", + comment: "Button accessibility label" + ) + let count = String( + format: NSLocalizedString( + "reader.post.numberOfComments.accessibilityLabel", + value: "%@ comments", + comment: "Accessibility label showing total number of comments" + ), + viewModel.commentCount.description + ) return "\(label). \(count)." }() buttons.like.accessibilityLabel = { - let label = viewModel.isLiked ? NSLocalizedString("reader.post.buttonRemoveLike.accessibilityLabel", value: "Remove like", comment: "Button accessibility label") : NSLocalizedString("reader.post.buttonLike.accessibilityLabel", value: "Like", comment: "Button accessibility label") - let count = String(format: NSLocalizedString("reader.post.numberOfLikes.accessibilityLabel", value: "%@ likes", comment: "Accessibility label showing total number of likes"), viewModel.likeCount.description) + let label = + viewModel.isLiked + ? NSLocalizedString( + "reader.post.buttonRemoveLike.accessibilityLabel", + value: "Remove like", + comment: "Button accessibility label" + ) + : NSLocalizedString( + "reader.post.buttonLike.accessibilityLabel", + value: "Like", + comment: "Button accessibility label" + ) + let count = String( + format: NSLocalizedString( + "reader.post.numberOfLikes.accessibilityLabel", + value: "%@ likes", + comment: "Accessibility label showing total number of likes" + ), + viewModel.likeCount.description + ) return "\(label). \(count)." }() } diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index c23e9d49406b..68eb95f1738a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -38,7 +38,7 @@ import AutomatticTracks // MARK: - Properties var tableView: UITableView! { - return tableViewController.tableView + tableViewController.tableView } private var syncHelpers: [ReaderAbstractTopic: WPContentSyncHelper] = [:] @@ -119,9 +119,7 @@ import AutomatticTracks } private var isLoadingDiscover: Bool { - return readerTopic == nil && - contentType == .topic && - siteID == ReaderHelpers.discoverSiteID + readerTopic == nil && contentType == .topic && siteID == ReaderHelpers.discoverSiteID } /// The topic can be nil while a site or tag topic is being fetched, hence, optional. @@ -134,7 +132,8 @@ import AutomatticTracks syncHelper?.delegate = self if let newTopic = readerTopic, - let context = newTopic.managedObjectContext { + let context = newTopic.managedObjectContext + { newTopic.inUse = true ContextManager.shared.save(context) } @@ -288,7 +287,7 @@ import AutomatticTracks } override func awakeAfter(using aDecoder: NSCoder) -> Any? { - return super.awakeAfter(using: aDecoder) + super.awakeAfter(using: aDecoder) } override func viewDidLoad() { @@ -306,7 +305,12 @@ import AutomatticTracks navigationItem.largeTitleDisplayMode = .never - NotificationCenter.default.addObserver(self, selector: #selector(postSeenToggled(_:)), name: .ReaderPostSeenToggled, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(postSeenToggled(_:)), + name: .ReaderPostSeenToggled, + object: nil + ) configureCloseButtonIfNeeded() setupSavedPostsSettingsBarButtonItemIfNeeded() @@ -343,7 +347,11 @@ import AutomatticTracks } let mainContext = ContextManager.shared.mainContext - NotificationCenter.default.removeObserver(self, name: NSNotification.Name.NSManagedObjectContextDidSave, object: mainContext) + NotificationCenter.default.removeObserver( + self, + name: NSNotification.Name.NSManagedObjectContextDidSave, + object: mainContext + ) bumpStats() registerUserActivity() @@ -390,7 +398,12 @@ import AutomatticTracks if isNotificationsBarButtonEnabled && traitCollection.horizontalSizeClass == .regular { notificationsButtonCancellable = notificationsButtonViewModel.$image.sink { [weak self] in guard let self else { return } - let button = UIBarButtonItem(image: $0, style: .plain, target: self, action: #selector(buttonShowNotificationsTapped)) + let button = UIBarButtonItem( + image: $0, + style: .plain, + target: self, + action: #selector(buttonShowNotificationsTapped) + ) button.tag = NavigationItemTag.notifications.rawValue addRightBarButtonItem(button, after: .share) } @@ -428,13 +441,15 @@ import AutomatticTracks } let service = ReaderTopicService(coreDataStack: ContextManager.shared) - service.siteTopicForSite(withID: siteID, + service.siteTopicForSite( + withID: siteID, isFeed: isFeed, success: { [weak self] (objectID: NSManagedObjectID?, _: Bool) in let context = ContextManager.shared.mainContext guard let objectID, - let topic = (try? context.existingObject(with: objectID)) as? ReaderAbstractTopic else { + let topic = (try? context.existingObject(with: objectID)) as? ReaderAbstractTopic + else { DDLogError("Reader: Error retriving an existing site topic by its objectID") if self?.isLoadingDiscover ?? false { self?.updateContent(synchronize: false) @@ -449,7 +464,8 @@ import AutomatticTracks self?.updateContent(synchronize: false) } self?.displayLoadingStreamFailed() - }) + } + ) } /// Fetches a tag topic for the value of the `tagSlug` property @@ -461,17 +477,22 @@ import AutomatticTracks return wpAssertionFailure("tag slug is missing") } let service = ReaderTopicService(coreDataStack: ContextManager.shared) - service.tagTopicForTag(withSlug: tagSlug, success: { [weak self] objectID in - let context = ContextManager.shared.mainContext - guard let objectID, let topic = (try? context.existingObject(with: objectID)) as? ReaderAbstractTopic else { - DDLogError("Reader: Error retriving an existing tag topic by its objectID") + service.tagTopicForTag( + withSlug: tagSlug, + success: { [weak self] objectID in + let context = ContextManager.shared.mainContext + guard let objectID, let topic = (try? context.existingObject(with: objectID)) as? ReaderAbstractTopic + else { + DDLogError("Reader: Error retriving an existing tag topic by its objectID") + self?.displayLoadingStreamFailed() + return + } + self?.readerTopic = topic + }, + failure: { [weak self] _ in self?.displayLoadingStreamFailed() - return } - self?.readerTopic = topic - }, failure: { [weak self] _ in - self?.displayLoadingStreamFailed() - }) + ) } // MARK: - Setup @@ -490,7 +511,11 @@ import AutomatticTracks } @objc func configureRefreshControl() { - refreshControl.addTarget(self, action: #selector(ReaderStreamViewController.handleRefresh(_:)), for: .valueChanged) + refreshControl.addTarget( + self, + action: #selector(ReaderStreamViewController.handleRefresh(_:)), + for: .valueChanged + ) } private func setupContentHandler() { @@ -602,7 +627,12 @@ import AutomatticTracks private func configureCloseButtonIfNeeded() { if isModal() { - navigationItem.leftBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "xmark"), style: .plain, target: self, action: #selector(closeButtonTapped)) + navigationItem.leftBarButtonItem = UIBarButtonItem( + image: UIImage(systemName: "xmark"), + style: .plain, + target: self, + action: #selector(closeButtonTapped) + ) } } @@ -648,7 +678,9 @@ import AutomatticTracks private func removeBlockedPosts() { // Fetch account - guard let account = try? WPAccount.lookupDefaultWordPressComAccount(in: viewContext), let userID = account.userID else { + guard let account = try? WPAccount.lookupDefaultWordPressComAccount(in: viewContext), + let userID = account.userID + else { return } @@ -661,7 +693,8 @@ import AutomatticTracks // Site Predicate if let topic = readerTopic as? ReaderSiteTopic, - let blocked = BlockedSite.findOne(accountID: userID, blogID: topic.siteID, context: viewContext) { + let blocked = BlockedSite.findOne(accountID: userID, blogID: topic.siteID, context: viewContext) + { predicates.append(NSPredicate(format: "\(#keyPath(ReaderPost.siteID)) = %@", blocked.blogID)) } @@ -690,10 +723,14 @@ import AutomatticTracks /// Update the post card when a site is blocked from post details. /// - func readerSiteBlockingController(_ controller: ReaderPostBlockingController, didBlockSiteOfPost post: ReaderPost, result: Result) { + func readerSiteBlockingController( + _ controller: ReaderPostBlockingController, + didBlockSiteOfPost post: ReaderPost, + result: Result + ) { guard case .success = result, - let post = (try? viewContext.existingObject(with: post.objectID)) as? ReaderPost, - let indexPath = content.indexPath(forObject: post) + let post = (try? viewContext.existingObject(with: post.objectID)) as? ReaderPost, + let indexPath = content.indexPath(forObject: post) else { return } @@ -702,10 +739,14 @@ import AutomatticTracks tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.fade) } - func readerSiteBlockingController(_ controller: ReaderPostBlockingController, didEndBlockingPostAuthor post: ReaderPost, result: Result) { + func readerSiteBlockingController( + _ controller: ReaderPostBlockingController, + didEndBlockingPostAuthor post: ReaderPost, + result: Result + ) { guard case .success = result, - let post = (try? viewContext.existingObject(with: post.objectID)) as? ReaderPost, - let indexPath = content.indexPath(forObject: post) + let post = (try? viewContext.existingObject(with: post.objectID)) as? ReaderPost, + let indexPath = content.indexPath(forObject: post) else { return } @@ -724,10 +765,11 @@ import AutomatticTracks tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.fade) - ReaderBlockSiteAction(asBlocked: false).execute(with: post, context: viewContext) { [weak self] in - self?.recentlyBlockedSitePostObjectIDs.add(objectID) - self?.tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.fade) - } + ReaderBlockSiteAction(asBlocked: false) + .execute(with: post, context: viewContext) { [weak self] in + self?.recentlyBlockedSitePostObjectIDs.add(objectID) + self?.tableView.reloadRows(at: [indexPath], with: UITableView.RowAnimation.fade) + } } // MARK: - Actions @@ -755,7 +797,8 @@ import AutomatticTracks func removePost(_ post: ReaderPost) { togglePostSave(post) - let notice = Notice(title: Strings.postRemoved, actionTitle: SharedStrings.Button.undo) { [weak self] accepted in + let notice = Notice(title: Strings.postRemoved, actionTitle: SharedStrings.Button.undo) { + [weak self] accepted in guard accepted else { return } self?.togglePostSave(post) } @@ -783,7 +826,8 @@ import AutomatticTracks } guard let topic = readerTopic, - let properties = topicPropertyForStats() else { + let properties = topicPropertyForStats() + else { return } @@ -845,7 +889,7 @@ import AutomatticTracks } private func canSync() -> Bool { - return (readerTopic != nil || isLoadingDiscover) && connectionAvailable() + (readerTopic != nil || isLoadingDiscover) && connectionAvailable() } /// Kicks off a "background" sync without updating the UI if certain conditions @@ -879,7 +923,7 @@ import AutomatticTracks } let lastSynced = topic.lastSynced ?? Date(timeIntervalSince1970: 0) - let interval = Int( Date().timeIntervalSince(lastSynced)) + let interval = Int(Date().timeIntervalSince(lastSynced)) if forceSync || (canSync() && (interval >= refreshInterval || topicPostsCount == 0)) { syncHelper?.syncContentWithUserInteraction(false) @@ -891,7 +935,7 @@ import AutomatticTracks /// Returns the number of posts for the current topic /// This allows the count to be overridden by subclasses var topicPostsCount: Int { - return readerTopic?.posts.count ?? 0 + readerTopic?.posts.count ?? 0 } /// Used to fetch new content in response to a background refresh event. /// Not intended for use as part of a user interaction. See syncIfAppropriate instead. @@ -899,39 +943,60 @@ import AutomatticTracks @objc func backgroundFetch(_ completionHandler: @escaping ((UIBackgroundFetchResult) -> Void)) { let lastSeenPostID = (content.content?.first as? ReaderPost)?.postID ?? -1 - syncHelper?.backgroundSync(success: { [weak self, weak lastSeenPostID] in - let newestFetchedPostID = (self?.content.content?.first as? ReaderPost)?.postID ?? -1 - if lastSeenPostID == newestFetchedPostID { - completionHandler(.noData) - } else { - if let numberOfRows = self?.tableView?.numberOfRows(inSection: 0), numberOfRows > 0 { - self?.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) + syncHelper? + .backgroundSync( + success: { [weak self, weak lastSeenPostID] in + let newestFetchedPostID = (self?.content.content?.first as? ReaderPost)?.postID ?? -1 + if lastSeenPostID == newestFetchedPostID { + completionHandler(.noData) + } else { + if let numberOfRows = self?.tableView?.numberOfRows(inSection: 0), numberOfRows > 0 { + self?.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) + } + completionHandler(.newData) + } + }, + failure: { _ in + completionHandler(.failed) } - completionHandler(.newData) - } - }, failure: { _ in - completionHandler(.failed) - }) + ) } private func syncFillingGap(_ indexPath: IndexPath) { if !canSync() { - let alertTitle = NSLocalizedString("Unable to Load Posts", comment: "Title of a prompt saying the app needs an internet connection before it can load posts") - let alertMessage = NSLocalizedString("Please check your internet connection and try again.", comment: "Politely asks the user to check their internet connection before trying again. ") - let alertController = UIAlertController(title: alertTitle, + let alertTitle = NSLocalizedString( + "Unable to Load Posts", + comment: "Title of a prompt saying the app needs an internet connection before it can load posts" + ) + let alertMessage = NSLocalizedString( + "Please check your internet connection and try again.", + comment: "Politely asks the user to check their internet connection before trying again. " + ) + let alertController = UIAlertController( + title: alertTitle, message: alertMessage, - preferredStyle: .alert) + preferredStyle: .alert + ) alertController.addCancelActionWithTitle(SharedStrings.Button.ok, handler: nil) alertController.presentFromRootViewController() return } if let syncHelper, syncHelper.isSyncing { - let alertTitle = NSLocalizedString("Busy", comment: "Title of a prompt letting the user know that they must wait until the current aciton completes.") - let alertMessage = NSLocalizedString("Please wait until the current fetch completes.", comment: "Asks the user to wait until the currently running fetch request completes.") - let alertController = UIAlertController(title: alertTitle, + let alertTitle = NSLocalizedString( + "Busy", + comment: + "Title of a prompt letting the user know that they must wait until the current aciton completes." + ) + let alertMessage = NSLocalizedString( + "Please wait until the current fetch completes.", + comment: "Asks the user to wait until the currently running fetch request completes." + ) + let alertController = UIAlertController( + title: alertTitle, message: alertMessage, - preferredStyle: .alert) + preferredStyle: .alert + ) alertController.addCancelActionWithTitle(SharedStrings.Button.ok, handler: nil) alertController.presentFromRootViewController() @@ -975,7 +1040,11 @@ import AutomatticTracks self.fetch(for: topic, success: successBlock, failure: failureBlock) } - func fetch(for originalTopic: ReaderAbstractTopic, success: @escaping ((_ count: Int, _ hasMore: Bool) -> Void), failure: @escaping ((_ error: Error?) -> Void)) { + func fetch( + for originalTopic: ReaderAbstractTopic, + success: @escaping ((_ count: Int, _ hasMore: Bool) -> Void), + failure: @escaping ((_ error: Error?) -> Void) + ) { coreDataStack.performAndSave { context in guard let topic = (try? context.existingObject(with: originalTopic.objectID)) as? ReaderAbstractTopic else { DDLogError("Error: Could not retrieve an existing topic via its objectID") @@ -988,7 +1057,13 @@ import AutomatticTracks } else if let topic = topic as? ReaderTagTopic { self.readerPostStreamService.fetchPosts(for: topic, success: success, failure: failure) } else { - self.readerPostService.fetchUnblockedPosts(topic: topic, earlierThan: Date(), forceRetry: true, success: success, failure: failure) + self.readerPostService.fetchUnblockedPosts( + topic: topic, + earlierThan: Date(), + forceRetry: true, + success: success, + failure: failure + ) } } } @@ -1016,7 +1091,8 @@ import AutomatticTracks let sortDate = post.sortDate coreDataStack.performAndSave { [weak self] context in - guard let topicInContext = (try? context.existingObject(with: topic.objectID)) as? ReaderAbstractTopic else { + guard let topicInContext = (try? context.existingObject(with: topic.objectID)) as? ReaderAbstractTopic + else { DDLogError("Error: Could not retrieve an existing topic via its objectID") return } @@ -1048,9 +1124,21 @@ import AutomatticTracks let service = ReaderPostService(coreDataStack: ContextManager.shared) if ReaderHelpers.isTopicSearchTopic(topicInContext) { assertionFailure("Search topics should no have a gap to fill.") - service.fetchPosts(for: topicInContext, atOffset: 0, deletingEarlier: true, success: successBlock, failure: failureBlock) + service.fetchPosts( + for: topicInContext, + atOffset: 0, + deletingEarlier: true, + success: successBlock, + failure: failureBlock + ) } else { - service.fetchPosts(for: topicInContext, earlierThan: sortDate, deletingEarlier: true, success: successBlock, failure: failureBlock) + service.fetchPosts( + for: topicInContext, + earlierThan: sortDate, + deletingEarlier: true, + success: successBlock, + failure: failureBlock + ) } } } @@ -1086,7 +1174,11 @@ import AutomatticTracks } } - private func fetchMore(for originalTopic: ReaderAbstractTopic, success: @escaping ((Int, Bool) -> Void), failure: @escaping ((Error?) -> Void)) { + private func fetchMore( + for originalTopic: ReaderAbstractTopic, + success: @escaping ((Int, Bool) -> Void), + failure: @escaping ((Error?) -> Void) + ) { guard let posts = content.content, let post = posts.last as? ReaderPost, @@ -1105,11 +1197,27 @@ import AutomatticTracks if ReaderHelpers.isTopicSearchTopic(topic) { let service = ReaderPostService(coreDataStack: ContextManager.shared) let offset = UInt(self.content.contentCount) - service.fetchPosts(for: topic, atOffset: UInt(offset), deletingEarlier: false, success: success, failure: failure) + service.fetchPosts( + for: topic, + atOffset: UInt(offset), + deletingEarlier: false, + success: success, + failure: failure + ) } else if let topic = topic as? ReaderTagTopic { - self.readerPostStreamService.fetchPosts(for: topic, isFirstPage: false, success: success, failure: failure) + self.readerPostStreamService.fetchPosts( + for: topic, + isFirstPage: false, + success: success, + failure: failure + ) } else { - self.readerPostService.fetchUnblockedPosts(topic: topic, earlierThan: sortDate, success: success, failure: failure) + self.readerPostService.fetchUnblockedPosts( + topic: topic, + earlierThan: sortDate, + success: success, + failure: failure + ) } } } @@ -1134,9 +1242,10 @@ import AutomatticTracks // mark as seen/unseen option. guard let userInfo = notification.userInfo, - let post = userInfo[ReaderNotificationKeys.post] as? ReaderPost, - let indexPath = content.indexPath(forObject: post), - let cellPost: ReaderPost = content.object(at: indexPath) else { + let post = userInfo[ReaderNotificationKeys.post] as? ReaderPost, + let indexPath = content.indexPath(forObject: post), + let cellPost: ReaderPost = content.object(at: indexPath) + else { return } @@ -1151,22 +1260,28 @@ import AutomatticTracks // avoids returning readerPosts that do not belong to a topic (e.g. those // loaded from a notification). We can do this by specifying that self // has to exist within an empty set. - let predicateForNilTopic = contentType == .saved ? - NSPredicate(format: "isSavedForLater == YES") : - NSPredicate(format: "topic = NULL AND SELF in %@", [String]()) + let predicateForNilTopic = + contentType == .saved + ? NSPredicate(format: "isSavedForLater == YES") + : NSPredicate(format: "topic = NULL AND SELF in %@", [String]()) guard let topic = readerTopic else { return predicateForNilTopic } - guard let topicInContext = (try? viewContext.existingObject(with: topic.objectID)) as? ReaderAbstractTopic else { + guard let topicInContext = (try? viewContext.existingObject(with: topic.objectID)) as? ReaderAbstractTopic + else { DDLogError("Error: Could not retrieve an existing topic via its objectID") return predicateForNilTopic } // swiftlint:disable:next empty_count if recentlyBlockedSitePostObjectIDs.count > 0 { - return NSPredicate(format: "topic = %@ AND (isSiteBlocked = NO OR SELF in %@)", topicInContext, recentlyBlockedSitePostObjectIDs) + return NSPredicate( + format: "topic = %@ AND (isSiteBlocked = NO OR SELF in %@)", + topicInContext, + recentlyBlockedSitePostObjectIDs + ) } return NSPredicate(format: "topic = %@ AND isSiteBlocked = NO", topicInContext) @@ -1196,12 +1311,16 @@ import AutomatticTracks } let service = ReaderTopicService(coreDataStack: ContextManager.shared) - service.toggleFollowing(forTag: topic, success: { - completion?(true) - }, failure: { (_: Error?) in - generator.notificationOccurred(.error) - completion?(false) - }) + service.toggleFollowing( + forTag: topic, + success: { + completion?(true) + }, + failure: { (_: Error?) in + generator.notificationOccurred(.error) + completion?(false) + } + ) } func getPost(at indexPath: IndexPath) -> ReaderPost? { @@ -1229,7 +1348,12 @@ extension ReaderStreamViewController: ReaderStreamHeaderDelegate { extension ReaderStreamViewController: WPContentSyncHelperDelegate { - func syncHelper(_ syncHelper: WPContentSyncHelper, syncContentWithUserInteraction userInteraction: Bool, success: ((_ hasMore: Bool) -> Void)?, failure: ((_ error: NSError) -> Void)?) { + func syncHelper( + _ syncHelper: WPContentSyncHelper, + syncContentWithUserInteraction userInteraction: Bool, + success: ((_ hasMore: Bool) -> Void)?, + failure: ((_ error: NSError) -> Void)? + ) { displayLoadingViewIfNeeded() if syncIsFillingGap { syncItemsForGap(success, failure: failure) @@ -1238,7 +1362,11 @@ extension ReaderStreamViewController: WPContentSyncHelperDelegate { } } - func syncHelper(_ syncHelper: WPContentSyncHelper, syncMoreWithSuccess success: ((_ hasMore: Bool) -> Void)?, failure: ((_ error: NSError) -> Void)?) { + func syncHelper( + _ syncHelper: WPContentSyncHelper, + syncMoreWithSuccess success: ((_ hasMore: Bool) -> Void)?, + failure: ((_ error: NSError) -> Void)? + ) { loadMoreItems(success, failure: failure) } @@ -1254,7 +1382,8 @@ extension ReaderStreamViewController: WPContentSyncHelperDelegate { cleanupAfterSync(refresh: false) if let count = content.content?.count, - count == 0 { + count == 0 + { displayLoadingStreamFailed() } } @@ -1346,15 +1475,15 @@ extension ReaderStreamViewController: WPTableViewHandlerDelegate { } func tableView(_ aTableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension + UITableView.automaticDimension } func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - return 0.0 + 0.0 } func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - return 0.0 + 0.0 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -1443,14 +1572,16 @@ extension ReaderStreamViewController: WPTableViewHandlerDelegate { /// - When there are no ongoing blocking requests. private func syncMoreContentIfNeeded(for tableView: UITableView, indexPathForVisibleRow indexPath: IndexPath) { let criticalRow = tableView.numberOfRows(inSection: indexPath.section) - loadMoreThreashold - guard let syncHelper, (indexPath.section == tableView.numberOfSections - 1) && (indexPath.row >= criticalRow) else { + guard let syncHelper, (indexPath.section == tableView.numberOfSections - 1) && (indexPath.row >= criticalRow) + else { return } - let shouldLoadMoreItems = syncHelper.hasMoreContent - && !syncHelper.isSyncing - && !cleanupAndRefreshAfterScrolling - && !siteBlockingController.isBlockingPosts - && !readerPostService.isSilentlyFetchingPosts + let shouldLoadMoreItems = + syncHelper.hasMoreContent + && !syncHelper.isSyncing + && !cleanupAndRefreshAfterScrolling + && !siteBlockingController.isBlockingPosts + && !readerPostService.isSilentlyFetchingPosts if shouldLoadMoreItems { syncHelper.syncMoreContent() } @@ -1531,18 +1662,25 @@ extension ReaderStreamViewController: WPTableViewHandlerDelegate { // Do nothing } - func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + func tableView( + _ tableView: UITableView, + contextMenuConfigurationForRowAt indexPath: IndexPath, + point: CGPoint + ) -> UIContextMenuConfiguration? { guard let post = getPost(at: indexPath) else { return nil } return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in guard let self else { return nil } - return UIMenu(children: ReaderPostMenu( - post: post, - topic: readerTopic, - anchor: self.tableView.cellForRow(at: indexPath) ?? self.view, - viewController: self - ).makeMenu()) + return UIMenu( + children: ReaderPostMenu( + post: post, + topic: readerTopic, + anchor: self.tableView.cellForRow(at: indexPath) ?? self.view, + viewController: self + ) + .makeMenu() + ) } } } @@ -1572,16 +1710,18 @@ extension ReaderStreamViewController: UITableViewDataSourcePrefetching { extension ReaderStreamViewController: SearchableActivityConvertable { var activityType: String { - return WPActivityType.reader.rawValue + WPActivityType.reader.rawValue } var activityTitle: String { - return SharedStrings.Reader.title + SharedStrings.Reader.title } var activityKeywords: Set? { - let keyWordString = NSLocalizedString("wordpress, reader, articles, posts, blog post, followed, discover, likes, my likes, tags, topics", - comment: "This is a comma-separated list of keywords used for spotlight indexing of the 'Reader' tab.") + let keyWordString = NSLocalizedString( + "wordpress, reader, articles, posts, blog post, followed, discover, likes, my likes, tags, topics", + comment: "This is a comma-separated list of keywords used for spotlight indexing of the 'Reader' tab." + ) let keywordArray = keyWordString.arrayOfTags() guard !keywordArray.isEmpty else { @@ -1645,9 +1785,18 @@ extension ReaderStreamViewController { } struct ResultsStatusText { - static let loadingErrorTitle = NSLocalizedString("Problem loading content", comment: "Error message title informing the user that reader content could not be loaded.") - static let loadingErrorMessage = NSLocalizedString("Sorry. The content could not be loaded.", comment: "A short error message letting the user know the requested reader content could not be loaded.") - static let noConnectionTitle = NSLocalizedString("Unable to Sync", comment: "Title of error prompt shown when a sync the user initiated fails.") + static let loadingErrorTitle = NSLocalizedString( + "Problem loading content", + comment: "Error message title informing the user that reader content could not be loaded." + ) + static let loadingErrorMessage = NSLocalizedString( + "Sorry. The content could not be loaded.", + comment: "A short error message letting the user know the requested reader content could not be loaded." + ) + static let noConnectionTitle = NSLocalizedString( + "Unable to Sync", + comment: "Title of error prompt shown when a sync the user initiated fails." + ) } } @@ -1655,12 +1804,14 @@ extension ReaderStreamViewController { extension ReaderStreamViewController: NetworkAwareUI { func contentIsEmpty() -> Bool { - return content.contentCount == 0 + content.contentCount == 0 } func noConnectionMessage() -> String { - return NSLocalizedString("No internet connection. Some content may be unavailable while offline.", - comment: "Error message shown when the user is browsing Reader without an internet connection.") + NSLocalizedString( + "No internet connection. Some content may be unavailable while offline.", + comment: "Error message shown when the user is browsing Reader without an internet connection." + ) } } @@ -1679,7 +1830,11 @@ extension ReaderStreamViewController: NetworkStatusDelegate { // MARK: - UIViewControllerTransitioningDelegate // extension ReaderStreamViewController: UIViewControllerTransitioningDelegate { - func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + func presentationController( + forPresented presented: UIViewController, + presenting: UIViewController?, + source: UIViewController + ) -> UIPresentationController? { guard presented is FancyAlertViewController else { return nil } @@ -1717,6 +1872,14 @@ extension ReaderStreamViewController: ContentIdentifiable { } private enum Strings { - static let postRemoved = NSLocalizedString("reader.savedPostRemovedNotificationTitle", value: "Saved post removed", comment: "Notification title for when saved post is removed") - static let savedPostsSettingsAccessibilityLabel = NSLocalizedString("reader.savedPosts.settings.button.accessibilityLabel", value: "Saved posts settings", comment: "Accessibility label for the button that opens saved Reader posts import and export settings") + static let postRemoved = NSLocalizedString( + "reader.savedPostRemovedNotificationTitle", + value: "Saved post removed", + comment: "Notification title for when saved post is removed" + ) + static let savedPostsSettingsAccessibilityLabel = NSLocalizedString( + "reader.savedPosts.settings.button.accessibilityLabel", + value: "Saved posts settings", + comment: "Accessibility label for the button that opens saved Reader posts import and export settings" + ) } diff --git a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift index 310ea3b6910c..476239cabe6a 100644 --- a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift +++ b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift @@ -61,11 +61,14 @@ public protocol ThemePresenter: AnyObject { /// Invalidates the layout whenever the collection view's bounds change @objc open class ThemeBrowserCollectionViewLayout: UICollectionViewFlowLayout { open override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { - return shouldInvalidateForNewBounds(newBounds) + shouldInvalidateForNewBounds(newBounds) } - open override func invalidationContext(forBoundsChange newBounds: CGRect) -> UICollectionViewFlowLayoutInvalidationContext { - let context = super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext + open override func invalidationContext( + forBoundsChange newBounds: CGRect + ) -> UICollectionViewFlowLayoutInvalidationContext { + let context = + super.invalidationContext(forBoundsChange: newBounds) as! UICollectionViewFlowLayoutInvalidationContext context.invalidateFlowLayoutDelegateMetrics = shouldInvalidateForNewBounds(newBounds) return context @@ -78,7 +81,11 @@ public protocol ThemePresenter: AnyObject { } } -@objc open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, NSFetchedResultsControllerDelegate, UISearchControllerDelegate, UISearchResultsUpdating, ThemePresenter, WPContentSyncHelperDelegate { +@objc +open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, + UICollectionViewDelegateFlowLayout, NSFetchedResultsControllerDelegate, UISearchControllerDelegate, + UISearchResultsUpdating, ThemePresenter, WPContentSyncHelperDelegate +{ // MARK: - Constants @@ -100,18 +107,18 @@ public protocol ThemePresenter: AnyObject { // swiftlint:disable:next weak_delegate fileprivate lazy var customizerNavigationDelegate: ThemeWebNavigationDelegate = { - return ThemeWebNavigationDelegate() + ThemeWebNavigationDelegate() }() /** * @brief The FRCs this VC will use to display filtered content. */ fileprivate lazy var themesController: NSFetchedResultsController = { - return self.createThemesFetchedResultsController() + self.createThemesFetchedResultsController() }() fileprivate lazy var customThemesController: NSFetchedResultsController = { - return self.createThemesFetchedResultsController() + self.createThemesFetchedResultsController() }() fileprivate func createThemesFetchedResultsController() -> NSFetchedResultsController { @@ -123,18 +130,19 @@ public protocol ThemePresenter: AnyObject { fetchRequest: fetchRequest, managedObjectContext: self.themeService.coreDataStack.mainContext, sectionNameKeyPath: nil, - cacheName: nil) + cacheName: nil + ) frc.delegate = self return frc } fileprivate var themeCount: NSInteger { - return themesController.fetchedObjects?.count ?? 0 + themesController.fetchedObjects?.count ?? 0 } fileprivate var customThemeCount: NSInteger { - return blog.supports(BlogFeature.customThemes) ? (customThemesController.fetchedObjects?.count ?? 0) : 0 + blog.supports(BlogFeature.customThemes) ? (customThemesController.fetchedObjects?.count ?? 0) : 0 } // Absolute count of available themes for the site, as it comes from the ThemeService @@ -153,16 +161,24 @@ public protocol ThemePresenter: AnyObject { fileprivate var themesHeader: ThemeBrowserSectionHeaderView? { didSet { - themesHeader?.descriptionLabel.text = NSLocalizedString("WordPress.com Themes", - comment: "Title for the WordPress.com themes section, should be the same as in Calypso").localizedUppercase + themesHeader?.descriptionLabel.text = + NSLocalizedString( + "WordPress.com Themes", + comment: "Title for the WordPress.com themes section, should be the same as in Calypso" + ) + .localizedUppercase themesHeader?.themeCount = totalThemeCount > 0 ? totalThemeCount : themeCount } } fileprivate var customThemesHeader: ThemeBrowserSectionHeaderView? { didSet { - customThemesHeader?.descriptionLabel.text = NSLocalizedString("Uploaded themes", - comment: "Title for the user uploaded themes section, should be the same as in Calypso").localizedUppercase + customThemesHeader?.descriptionLabel.text = + NSLocalizedString( + "Uploaded themes", + comment: "Title for the user uploaded themes section, should be the same as in Calypso" + ) + .localizedUppercase customThemesHeader?.themeCount = totalCustomThemeCount > 0 ? totalCustomThemeCount : customThemeCount } } @@ -177,13 +193,13 @@ public protocol ThemePresenter: AnyObject { fetchThemes() reloadThemes() } - } + } } fileprivate var suspendedSearch = "" @objc func resumingSearch() -> Bool { - return !suspendedSearch.trim().isEmpty + !suspendedSearch.trim().isEmpty } fileprivate var activityIndicator: UIActivityIndicatorView = { @@ -193,7 +209,7 @@ public protocol ThemePresenter: AnyObject { indicatorView.color = .white indicatorView.startAnimating() return indicatorView - }() + }() open var filterType: ThemeType = ThemeType.mayPurchase ? .all : .free @@ -239,12 +255,18 @@ public protocol ThemePresenter: AnyObject { private var noResultsViewController: NoResultsViewController? private struct NoResultsTitles { - static let noThemes = NSLocalizedString("No themes matching your search", comment: "Text displayed when theme name search has no matches") - static let fetchingThemes = NSLocalizedString("Fetching Themes...", comment: "Text displayed while fetching themes") + static let noThemes = NSLocalizedString( + "No themes matching your search", + comment: "Text displayed when theme name search has no matches" + ) + static let fetchingThemes = NSLocalizedString( + "Fetching Themes...", + comment: "Text displayed while fetching themes" + ) } private var noResultsShown: Bool { - return noResultsViewController?.parent != nil + noResultsViewController?.parent != nil } private var isFirstAppearance = true @@ -278,13 +300,13 @@ public protocol ThemePresenter: AnyObject { fileprivate typealias Styles = WPStyleGuide.Themes - /** - * @brief Convenience method for browser instantiation - * - * @param blog The blog to browse themes for - * - * @returns ThemeBrowserViewController instance - */ + /** + * @brief Convenience method for browser instantiation + * + * @param blog The blog to browse themes for + * + * @returns ThemeBrowserViewController instance + */ @objc open class func browserWithBlog(_ blog: Blog) -> ThemeBrowserViewController { let storyboard = UIStoryboard(name: "ThemeBrowser", bundle: .keystone) let viewController = storyboard.instantiateInitialViewController() as! ThemeBrowserViewController @@ -304,8 +326,8 @@ public protocol ThemePresenter: AnyObject { title = NSLocalizedString("Themes", comment: "Title of Themes browser page") fetchThemes() - sections = (themeCount == 0 && customThemeCount == 0) ? [.customThemes, .themes] : - [.info, .customThemes, .themes] + sections = + (themeCount == 0 && customThemeCount == 0) ? [.customThemes, .themes] : [.info, .customThemes, .themes] configureSearchController() @@ -327,9 +349,17 @@ public protocol ThemePresenter: AnyObject { searchController.delegate = self searchController.searchResultsUpdater = self - collectionView.register(ThemeBrowserSectionHeaderView.defaultNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: ThemeBrowserViewController.reuseIdentifierForThemesHeader) + collectionView.register( + ThemeBrowserSectionHeaderView.defaultNib, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: ThemeBrowserViewController.reuseIdentifierForThemesHeader + ) - collectionView.register(ThemeBrowserSectionHeaderView.defaultNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: ThemeBrowserViewController.reuseIdentifierForCustomThemesHeader) + collectionView.register( + ThemeBrowserSectionHeaderView.defaultNib, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, + withReuseIdentifier: ThemeBrowserViewController.reuseIdentifierForCustomThemesHeader + ) } open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { @@ -378,8 +408,18 @@ public protocol ThemePresenter: AnyObject { } fileprivate func registerForKeyboardNotifications() { - NotificationCenter.default.addObserver(self, selector: #selector(ThemeBrowserViewController.keyboardDidShow(_:)), name: UIResponder.keyboardDidShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(ThemeBrowserViewController.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(ThemeBrowserViewController.keyboardDidShow(_:)), + name: UIResponder.keyboardDidShowNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(ThemeBrowserViewController.keyboardWillHide(_:)), + name: UIResponder.keyboardWillHideNotification, + object: nil + ) } fileprivate func unregisterForKeyboardNotifications() { @@ -390,7 +430,7 @@ public protocol ThemePresenter: AnyObject { @objc open func keyboardDidShow(_ notification: Foundation.Notification) { let keyboardFrame = localKeyboardFrameFromNotification(notification) let keyboardHeight = collectionView.frame.maxY - keyboardFrame.origin.y -// + // collectionView.contentInset.bottom = keyboardHeight collectionView.verticalScrollIndicatorInsets.bottom = keyboardHeight } @@ -405,7 +445,7 @@ public protocol ThemePresenter: AnyObject { fileprivate func localKeyboardFrameFromNotification(_ notification: Foundation.Notification) -> CGRect { let key = UIResponder.keyboardFrameEndUserInfoKey guard let keyboardFrame = (notification.userInfo?[key] as? NSValue)?.cgRectValue else { - return .zero + return .zero } // Convert the frame from window coordinates @@ -417,7 +457,8 @@ public protocol ThemePresenter: AnyObject { fileprivate func updateActiveTheme() { let lastActiveThemeId = blog.currentThemeId - _ = themeService.getActiveTheme(for: blog, + _ = themeService.getActiveTheme( + for: blog, success: { [weak self] (theme: Theme?) in if lastActiveThemeId != theme?.themeId { self?.collectionView?.collectionViewLayout.invalidateLayout() @@ -425,7 +466,8 @@ public protocol ThemePresenter: AnyObject { }, failure: { error in DDLogError("Error updating active theme: \(String(describing: error?.localizedDescription))") - }) + } + ) } fileprivate func setupThemesSyncHelper() { @@ -439,9 +481,9 @@ public protocol ThemePresenter: AnyObject { } fileprivate func syncContent() { - if themesSyncHelper.syncContent() && - (!blog.supports(BlogFeature.customThemes) || - customThemesSyncHelper.syncContent()) { + if themesSyncHelper.syncContent() + && (!blog.supports(BlogFeature.customThemes) || customThemesSyncHelper.syncContent()) + { updateResults() } } @@ -453,14 +495,20 @@ public protocol ThemePresenter: AnyObject { } } - private func syncThemePage(_ page: NSInteger, search: String, success: ((_ hasMore: Bool) -> Void)?, failure: ((_ error: NSError) -> Void)?) { + private func syncThemePage( + _ page: NSInteger, + search: String, + success: ((_ hasMore: Bool) -> Void)?, + failure: ((_ error: NSError) -> Void)? + ) { assert(page > 0) themesSyncingPage = page - _ = themeService.getThemesFor(blog, + _ = themeService.getThemesFor( + blog, page: themesSyncingPage, search: search, sync: page == 1, - success: {[weak self](_: [Theme]?, hasMore: Bool, themeCount: NSInteger) in + success: { [weak self] (_: [Theme]?, hasMore: Bool, themeCount: NSInteger) in if let success { success(hasMore) } @@ -469,16 +517,19 @@ public protocol ThemePresenter: AnyObject { failure: { error in DDLogError("Error syncing themes: \(String(describing: error?.localizedDescription))") if let failure, - let error { + let error + { failure(error as NSError) } - }) + } + ) } fileprivate func syncCustomThemes(success: ((_ hasMore: Bool) -> Void)?, failure: ((_ error: NSError) -> Void)?) { - _ = themeService.getCustomThemes(for: blog, + _ = themeService.getCustomThemes( + for: blog, sync: true, - success: {[weak self](_: [Theme]?, hasMore: Bool, themeCount: NSInteger) in + success: { [weak self] (_: [Theme]?, hasMore: Bool, themeCount: NSInteger) in if let success { success(hasMore) } @@ -487,10 +538,12 @@ public protocol ThemePresenter: AnyObject { failure: { error in DDLogError("Error syncing themes: \(String(describing: error?.localizedDescription))") if let failure, - let error { + let error + { failure(error as NSError) } - }) + } + ) } @objc open func currentTheme() -> Theme? { @@ -509,7 +562,12 @@ public protocol ThemePresenter: AnyObject { // MARK: - WPContentSyncHelperDelegate - public func syncHelper(_ syncHelper: WPContentSyncHelper, syncContentWithUserInteraction userInteraction: Bool, success: ((_ hasMore: Bool) -> Void)?, failure: ((_ error: NSError) -> Void)?) { + public func syncHelper( + _ syncHelper: WPContentSyncHelper, + syncContentWithUserInteraction userInteraction: Bool, + success: ((_ hasMore: Bool) -> Void)?, + failure: ((_ error: NSError) -> Void)? + ) { if syncHelper == themesSyncHelper { syncThemePage(1, search: searchName, success: success, failure: failure) } else if syncHelper == customThemesSyncHelper { @@ -517,7 +575,11 @@ public protocol ThemePresenter: AnyObject { } } - public func syncHelper(_ syncHelper: WPContentSyncHelper, syncMoreWithSuccess success: ((_ hasMore: Bool) -> Void)?, failure: ((_ error: NSError) -> Void)?) { + public func syncHelper( + _ syncHelper: WPContentSyncHelper, + syncMoreWithSuccess success: ((_ hasMore: Bool) -> Void)?, + failure: ((_ error: NSError) -> Void)? + ) { if syncHelper == themesSyncHelper { let nextPage = themesSyncingPage + 1 syncThemePage(nextPage, search: searchName, success: success, failure: failure) @@ -552,9 +614,14 @@ public protocol ThemePresenter: AnyObject { } } - open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + open func collectionView( + _ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath + ) -> UICollectionViewCell { - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ThemeBrowserCell.reuseIdentifier, for: indexPath) as! ThemeBrowserCell + let cell = + collectionView.dequeueReusableCell(withReuseIdentifier: ThemeBrowserCell.reuseIdentifier, for: indexPath) + as! ThemeBrowserCell cell.presenter = self cell.theme = themeAtIndexPath(indexPath) @@ -566,29 +633,52 @@ public protocol ThemePresenter: AnyObject { return cell } - open func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + open func collectionView( + _ collectionView: UICollectionView, + viewForSupplementaryElementOfKind kind: String, + at indexPath: IndexPath + ) -> UICollectionReusableView { switch kind { case UICollectionView.elementKindSectionHeader: if sections[indexPath.section] == .info { - let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ThemeBrowserHeaderView.reuseIdentifier, for: indexPath) as! ThemeBrowserHeaderView + let header = + collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: ThemeBrowserHeaderView.reuseIdentifier, + for: indexPath + ) as! ThemeBrowserHeaderView header.presenter = self return header } else { // We don't want the collectionView to reuse the section headers // since we need to keep a reference to them to update the counts if sections[indexPath.section] == .customThemes { - customThemesHeader = (collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ThemeBrowserViewController.reuseIdentifierForCustomThemesHeader, for: indexPath) as! ThemeBrowserSectionHeaderView) + customThemesHeader = + (collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: ThemeBrowserViewController.reuseIdentifierForCustomThemesHeader, + for: indexPath + ) as! ThemeBrowserSectionHeaderView) customThemesHeader?.isHidden = customThemeCount == 0 return customThemesHeader! } else { - themesHeader = (collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ThemeBrowserViewController.reuseIdentifierForCustomThemesHeader, for: indexPath) as! ThemeBrowserSectionHeaderView) + themesHeader = + (collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: ThemeBrowserViewController.reuseIdentifierForCustomThemesHeader, + for: indexPath + ) as! ThemeBrowserSectionHeaderView) themesHeader?.isHidden = themeCount == 0 return themesHeader! } } case UICollectionView.elementKindSectionFooter: - let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "ThemeBrowserFooterView", for: indexPath) + let footer = collectionView.dequeueReusableSupplementaryView( + ofKind: kind, + withReuseIdentifier: "ThemeBrowserFooterView", + for: indexPath + ) return footer default: fatalError("Unexpected theme browser element") @@ -596,7 +686,7 @@ public protocol ThemePresenter: AnyObject { } open func numberOfSections(in collectionView: UICollectionView) -> Int { - return sections.count + sections.count } // MARK: - UICollectionViewDelegate @@ -613,11 +703,16 @@ public protocol ThemePresenter: AnyObject { // MARK: - UICollectionViewDelegateFlowLayout - open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: NSInteger) -> CGSize { + open func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + referenceSizeForHeaderInSection section: NSInteger + ) -> CGSize { switch sections[section] { case .themes, .customThemes: if !hideSectionHeaders - && blog.supports(BlogFeature.customThemes) { + && blog.supports(BlogFeature.customThemes) + { return CGSize(width: 0, height: ThemeBrowserSectionHeaderView.height) } return .zero @@ -629,21 +724,33 @@ public protocol ThemePresenter: AnyObject { } } - open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + open func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath + ) -> CGSize { let parentViewWidth = collectionView.frame.size.width return Styles.cellSizeForFrameWidth(parentViewWidth) } - open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { - guard sections[section] == .themes && themesSyncHelper.isLoadingMore else { - return CGSize.zero - } + open func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + referenceSizeForFooterInSection section: Int + ) -> CGSize { + guard sections[section] == .themes && themesSyncHelper.isLoadingMore else { + return CGSize.zero + } - return CGSize(width: 0, height: Styles.footerHeight) + return CGSize(width: 0, height: Styles.footerHeight) } - open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + open func collectionView( + _ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + insetForSectionAt section: Int + ) -> UIEdgeInsets { switch sections[section] { case .customThemes: if !blog.supports(BlogFeature.customThemes) { @@ -694,7 +801,8 @@ public protocol ThemePresenter: AnyObject { let previouslyHadRemoteSearch = self.searchName.count >= 3 // Create a new timer for debounce - searchDebounceTimer = Timer.scheduledTimer(withTimeInterval: searchDebounceInterval, repeats: false) { [weak self] _ in + searchDebounceTimer = Timer.scheduledTimer(withTimeInterval: searchDebounceInterval, repeats: false) { + [weak self] _ in guard let self else { return } self.searchName = searchText @@ -772,7 +880,7 @@ public protocol ThemePresenter: AnyObject { // MARK: - NSFetchedResultsController helpers fileprivate func browsePredicate() -> NSPredicate? { - return browsePredicateThemesWithCustomValue(false) + browsePredicateThemesWithCustomValue(false) } fileprivate func customThemesBrowsePredicate() -> NSPredicate? { @@ -842,53 +950,82 @@ public protocol ThemePresenter: AnyObject { updateActivateButton(isLoading: true) - _ = themeService.activate(theme, + _ = themeService.activate( + theme, for: blog, success: { [weak self] (theme: Theme?) in - WPAppAnalytics.track(.themesChangedTheme, properties: ["theme_id": theme?.themeId ?? ""], blog: self?.blog) + WPAppAnalytics.track( + .themesChangedTheme, + properties: ["theme_id": theme?.themeId ?? ""], + blog: self?.blog + ) self?.collectionView?.reloadData() - let successTitle = NSLocalizedString("Theme Activated", comment: "Title of alert when theme activation succeeds") - let successFormat = NSLocalizedString("Thanks for choosing %@ by %@", comment: "Message of alert when theme activation succeeds") + let successTitle = NSLocalizedString( + "Theme Activated", + comment: "Title of alert when theme activation succeeds" + ) + let successFormat = NSLocalizedString( + "Thanks for choosing %@ by %@", + comment: "Message of alert when theme activation succeeds" + ) let successMessage = String(format: successFormat, theme?.name ?? "", theme?.author ?? "") - let manageTitle = NSLocalizedString("Manage site", comment: "Return to blog screen action when theme activation succeeds") + let manageTitle = NSLocalizedString( + "Manage site", + comment: "Return to blog screen action when theme activation succeeds" + ) self?.updateActivateButton(isLoading: false) - let alertController = UIAlertController(title: successTitle, + let alertController = UIAlertController( + title: successTitle, message: successMessage, - preferredStyle: .alert) - alertController.addActionWithTitle(manageTitle, + preferredStyle: .alert + ) + alertController.addActionWithTitle( + manageTitle, style: .default, handler: { [weak self] (_: UIAlertAction) in _ = self?.navigationController?.popViewController(animated: true) - }) - alertController.addDefaultActionWithTitle(SharedStrings.Button.ok, handler: nil) + } + ) + alertController.addDefaultActionWithTitle(SharedStrings.Button.ok, handler: nil) alertController.presentFromRootViewController() }, failure: { [weak self] error in - DDLogError("Error activating theme \(String(describing: theme.themeId)): \(String(describing: error?.localizedDescription))") + DDLogError( + "Error activating theme \(String(describing: theme.themeId)): \(String(describing: error?.localizedDescription))" + ) - let errorTitle = NSLocalizedString("Activation Error", comment: "Title of alert when theme activation fails") + let errorTitle = NSLocalizedString( + "Activation Error", + comment: "Title of alert when theme activation fails" + ) self?.activityIndicator.stopAnimating() self?.activateButton?.customView = nil - let alertController = UIAlertController(title: errorTitle, + let alertController = UIAlertController( + title: errorTitle, message: error?.localizedDescription, - preferredStyle: .alert) - alertController.addDefaultActionWithTitle(SharedStrings.Button.ok, handler: nil) + preferredStyle: .alert + ) + alertController.addDefaultActionWithTitle(SharedStrings.Button.ok, handler: nil) alertController.presentFromRootViewController() - }) + } + ) } @objc open func installThemeAndPresentCustomizer(_ theme: Theme) { - _ = themeService.installTheme(theme, + _ = themeService.installTheme( + theme, for: blog, success: { [weak self] in self?.presentUrlForTheme(theme, url: theme.customizeUrl(), activeButton: !theme.isCurrentTheme()) - }, failure: nil) + }, + failure: nil + ) } @objc open func presentCustomizeForTheme(_ theme: Theme?) { @@ -942,10 +1079,18 @@ public protocol ThemePresenter: AnyObject { configuration.onClose = onClose let title = activeButton ? ThemeAction.activate.title : ThemeAction.active.title - activateButton = UIBarButtonItem(title: title, style: .plain, target: self, action: #selector(ThemeBrowserViewController.activatePresentingTheme)) + activateButton = UIBarButtonItem( + title: title, + style: .plain, + target: self, + action: #selector(ThemeBrowserViewController.activatePresentingTheme) + ) activateButton?.isEnabled = !theme.isCurrentTheme() - let webViewController = WebViewControllerFactory.controller(configuration: configuration, source: "theme_browser") + let webViewController = WebViewControllerFactory.controller( + configuration: configuration, + source: "theme_browser" + ) webViewController.navigationItem.rightBarButtonItem = activateButton let navigation = UINavigationController(rootViewController: webViewController) @@ -957,9 +1102,12 @@ public protocol ThemePresenter: AnyObject { } if searchController != nil && searchController.isActive { - searchController.dismiss(animated: true, completion: { - self.present(navigation, animated: true) - }) + searchController.dismiss( + animated: true, + completion: { + self.present(navigation, animated: true) + } + ) } else { present(navigation, animated: true) } @@ -1001,7 +1149,10 @@ private extension ThemeBrowserViewController { if searchController.isActive { noResultsViewController.configureForNoSearchResults(title: NoResultsTitles.noThemes) } else { - noResultsViewController.configure(title: NoResultsTitles.fetchingThemes, accessoryView: NoResultsViewController.loadingAccessoryView()) + noResultsViewController.configure( + title: NoResultsTitles.fetchingThemes, + accessoryView: NoResultsViewController.loadingAccessoryView() + ) } addChild(noResultsViewController) From 4f1fa0cdebd2ba876bb015806e2d7e93940c3af0 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 26 Jun 2026 18:09:46 +1200 Subject: [PATCH 2/2] Read the window from the view hierarchy across the app --- .../NUX/JetpackPrologueViewController.swift | 2 +- .../3D Touch/WP3DTouchShortcutCreator.swift | 9 ++------- .../AztecPostViewController.swift | 2 +- .../Menus/Views/MenuItemEditingHeaderView.m | 2 +- .../Reader/Cards/ReaderPostCell.swift | 19 ++++++++++++------- .../ReaderStreamViewController.swift | 2 +- .../Themes/ThemeBrowserViewController.swift | 19 +++++++------------ 7 files changed, 25 insertions(+), 30 deletions(-) diff --git a/WordPress/Classes/Jetpack/NUX/JetpackPrologueViewController.swift b/WordPress/Classes/Jetpack/NUX/JetpackPrologueViewController.swift index 91ae0189d57c..65a488dec97e 100644 --- a/WordPress/Classes/Jetpack/NUX/JetpackPrologueViewController.swift +++ b/WordPress/Classes/Jetpack/NUX/JetpackPrologueViewController.swift @@ -169,7 +169,7 @@ extension JetpackPrologueViewController: InfiniteScrollViewDelegate { let angleRad: Double - switch UIApplication.shared.currentStatusBarOrientation { + switch view.window?.windowScene?.interfaceOrientation ?? .unknown { case .portrait: angleRad = attitude.pitch case .portraitUpsideDown: diff --git a/WordPress/Classes/System/3D Touch/WP3DTouchShortcutCreator.swift b/WordPress/Classes/System/3D Touch/WP3DTouchShortcutCreator.swift index 30503bf953f0..c2f645a5ddbb 100644 --- a/WordPress/Classes/System/3D Touch/WP3DTouchShortcutCreator.swift +++ b/WordPress/Classes/System/3D Touch/WP3DTouchShortcutCreator.swift @@ -8,7 +8,8 @@ public protocol ApplicationShortcutsProvider { extension UIApplication: ApplicationShortcutsProvider { @objc public var is3DTouchAvailable: Bool { - mainWindow?.traitCollection.forceTouchCapability == .available + connectedScenes.compactMap { ($0 as? UIWindowScene)?.keyWindow }.first?.traitCollection.forceTouchCapability + == .available } } @@ -165,12 +166,6 @@ open class WP3DTouchShortcutCreator: NSObject { shortcutsProvider.shortcutItems = loggedOutShortcutArray() } - fileprivate func is3DTouchAvailable() -> Bool { - let window = UIApplication.shared.mainWindow - - return window?.traitCollection.forceTouchCapability == .available - } - fileprivate func hasWordPressComAccount() -> Bool { AccountHelper.isDotcomAvailable() } diff --git a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift index 7094a993b266..74ad0bd52ff6 100644 --- a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift @@ -2312,7 +2312,7 @@ extension AztecPostViewController { // Let's assume a sensible default for the keyboard height based on orientation let keyboardFrameRatioDefault = - UIApplication.shared.currentStatusBarOrientation.isPortrait + view.window?.windowScene?.interfaceOrientation.isPortrait ?? true ? Constants.mediaPickerKeyboardHeightRatioPortrait : Constants.mediaPickerKeyboardHeightRatioLandscape let keyboardHeightDefault = (keyboardFrameRatioDefault * UIScreen.main.bounds.height) diff --git a/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.m b/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.m index adff56ba55d4..2b30cbbccb4b 100644 --- a/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.m +++ b/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.m @@ -139,7 +139,7 @@ - (void)setNeedsTopConstraintsUpdateForStatusBarAppearence:(BOOL)hidden } else { - self.stackViewTopConstraint.constant = [self defaultStackDesignMargin] + [[UIApplication sharedApplication] currentStatusBarFrame].size.height; + self.stackViewTopConstraint.constant = [self defaultStackDesignMargin] + self.window.windowScene.statusBarManager.statusBarFrame.size.height; } } diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index 8a5eb6fc02b8..3a75e3e24b9e 100644 --- a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift +++ b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift @@ -32,11 +32,11 @@ final class ReaderPostCell: ReaderStreamBaseCell { view.prepareForReuse() } - func configure(with viewModel: ReaderPostCellViewModel, isCompact: Bool) { + func configure(with viewModel: ReaderPostCellViewModel, isCompact: Bool, window: UIWindow?) { self.isCompact = isCompact view.isCompact = isCompact - view.configure(with: viewModel) + view.configure(with: viewModel, window: window) accessibilityLabel = "\(viewModel.author). \(viewModel.title). \(viewModel.details)" } @@ -105,6 +105,8 @@ private final class ReaderPostCellView: UIView { private var viewModel: ReaderPostCellViewModel? // important: has to retain + private weak var hostWindow: UIWindow? + private var toolbarViewHeightConstraint: NSLayoutConstraint? private var imageViewConstraints: [NSLayoutConstraint] = [] private var isSeenCheckmarkConfigured = false @@ -313,8 +315,9 @@ private final class ReaderPostCellView: UIView { // MARK: Configure (ViewModel) - func configure(with viewModel: ReaderPostCellViewModel) { + func configure(with viewModel: ReaderPostCellViewModel, window: UIWindow?) { self.viewModel = viewModel + self.hostWindow = window setAvatar(with: viewModel) buttonAuthor.configuration?.attributedTitle = AttributedString( @@ -351,7 +354,7 @@ private final class ReaderPostCellView: UIView { } private var preferredCoverSize: ImageSize? { - guard let window = window ?? UIApplication.shared.mainWindow else { return nil } + guard let window = window ?? hostWindow else { return nil } return ReaderPostCell.preferredCoverSize(in: window, isCompact: isCompact) } @@ -456,8 +459,10 @@ private func makeAuthorButton() -> UIButton { return UIButton(configuration: configuration) } -private func makeButton(image: UIImage? = nil, font: UIFont = UIFont.preferredFont(forTextStyle: .footnote)) -> UIButton -{ +private func makeButton( + image: UIImage? = nil, + font: UIFont = UIFont.preferredFont(forTextStyle: .footnote) +) -> UIButton { var configuration = UIButton.Configuration.plain() configuration.image = image configuration.imagePadding = 2 @@ -562,7 +567,7 @@ private extension ReaderPostCellView { #Preview { let cell = ReaderPostCellView() - cell.configure(with: .mock()) + cell.configure(with: .mock(), window: nil) cell.isCompact = true let vc = UIViewController() diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift index 68eb95f1738a..ab12a13d556b 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -1526,7 +1526,7 @@ extension ReaderStreamViewController: WPTableViewHandlerDelegate { viewModel.viewController = self let cell = tableConfiguration.postCell(in: tableView, for: indexPath) - cell.configure(with: viewModel, isCompact: isCompact) + cell.configure(with: viewModel, isCompact: isCompact, window: view.window) cell.isSeparatorHidden = !showsSeparator return cell } diff --git a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift index 476239cabe6a..d35b74e7dbc4 100644 --- a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift +++ b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift @@ -85,7 +85,7 @@ public protocol ThemePresenter: AnyObject { open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout, NSFetchedResultsControllerDelegate, UISearchControllerDelegate, UISearchResultsUpdating, ThemePresenter, WPContentSyncHelperDelegate -{ +{ // swiftlint:disable:this opening_brace // MARK: - Constants @@ -274,17 +274,12 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou /** * @brief Load theme screenshots at maximum displayed width */ - @objc open var screenshotWidth: Int = { - guard let window = UIApplication.shared.mainWindow else { - assertionFailure("The mainWindow is not set") - return Int(Styles.imageWidthForFrameWidth(852)) - } - let windowSize = window.bounds.size - let vWidth = Styles.imageWidthForFrameWidth(windowSize.width) - let hWidth = Styles.imageWidthForFrameWidth(windowSize.height) - let maxWidth = Int(max(hWidth, vWidth)) - return maxWidth - }() + @objc open var screenshotWidth: Int { + let size = view.window?.bounds.size ?? view.bounds.size + let vWidth = Styles.imageWidthForFrameWidth(size.width) + let hWidth = Styles.imageWidthForFrameWidth(size.height) + return Int(max(hWidth, vWidth)) + } /** * @brief The themes service we'll use in this VC and its helpers