From 3e2fb4eb9a0697156c5449448df353d4b722f730 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Fri, 26 Jun 2026 18:09:46 +1200 Subject: [PATCH 1/4] 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/4] 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 From a33ea91a94069025899e3c492ef5da768653bf38 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 2 Jul 2026 10:37:49 +1200 Subject: [PATCH 3/4] Remove unused stackViewTopConstraint from MenuItemEditingHeaderView The constraint was removed in 2019 but the readonly property was left behind, so it has been nil ever since. The status bar appearance update method that mutated it was a no-op, so it is removed along with its call sites. --- .../Controllers/MenuItemEditingViewController.m | 2 -- .../Menus/Views/MenuItemEditingHeaderView.h | 5 ----- .../Menus/Views/MenuItemEditingHeaderView.m | 13 ------------- 3 files changed, 20 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Menus/Controllers/MenuItemEditingViewController.m b/WordPress/Classes/ViewRelated/Menus/Controllers/MenuItemEditingViewController.m index 5b192203e377..4771e36c7546 100644 --- a/WordPress/Classes/ViewRelated/Menus/Controllers/MenuItemEditingViewController.m +++ b/WordPress/Classes/ViewRelated/Menus/Controllers/MenuItemEditingViewController.m @@ -144,7 +144,6 @@ - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender - (BOOL)prefersStatusBarHidden { - [self.headerView setNeedsTopConstraintsUpdateForStatusBarAppearence:self.headerView.hidden]; return self.headerView.hidden; } @@ -229,7 +228,6 @@ - (void)updateLayoutIfNeeded [self.stackView layoutIfNeeded]; [self setNeedsStatusBarAppearanceUpdate]; - [self.headerView setNeedsTopConstraintsUpdateForStatusBarAppearence:self.headerView.hidden]; } - (void)updateLayoutIfNeededAnimated diff --git a/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.h b/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.h index 84f017443160..f67d8121d474 100644 --- a/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.h +++ b/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.h @@ -22,11 +22,6 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, strong, readonly) UITextField *textField; -/** - Helper method for updating the layout based on statusBar changes. - */ -- (void)setNeedsTopConstraintsUpdateForStatusBarAppearence:(BOOL)hidden; - @end @protocol MenuItemEditingHeaderViewDelegate diff --git a/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.m b/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.m index 2b30cbbccb4b..b6425639f4ed 100644 --- a/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.m +++ b/WordPress/Classes/ViewRelated/Menus/Views/MenuItemEditingHeaderView.m @@ -9,7 +9,6 @@ @interface MenuItemEditingHeaderView () @property (nonatomic, strong, readonly) UIStackView *stackView; -@property (nonatomic, strong, readonly) NSLayoutConstraint *stackViewTopConstraint; @property (nonatomic, strong, readonly) UIImageView *iconView; @end @@ -131,18 +130,6 @@ - (CGFloat)defaultStackDesignMargin return ceilf(MenusDesignDefaultContentSpacing / 2.0); } -- (void)setNeedsTopConstraintsUpdateForStatusBarAppearence:(BOOL)hidden -{ - if (hidden) { - - self.stackViewTopConstraint.constant = [self defaultStackDesignMargin]; - - } else { - - self.stackViewTopConstraint.constant = [self defaultStackDesignMargin] + self.window.windowScene.statusBarManager.statusBarFrame.size.height; - } -} - - (void)setItemType:(NSString *)itemType { if (_itemType != itemType) { From 5164be9138d44daed83cca9670c8c98c7757444b Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 2 Jul 2026 10:56:40 +1200 Subject: [PATCH 4/4] Pass the Reader cover image size to cells instead of a window --- .../Reader/Cards/ReaderPostCell.swift | 26 ++++--------------- .../ReaderStreamViewController.swift | 19 +++++++++++--- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift b/WordPress/Classes/ViewRelated/Reader/Cards/ReaderPostCell.swift index 3a75e3e24b9e..6344ddb73220 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, window: UIWindow?) { + func configure(with viewModel: ReaderPostCellViewModel, isCompact: Bool, coverSize: ImageSize) { self.isCompact = isCompact view.isCompact = isCompact - view.configure(with: viewModel, window: window) + view.configure(with: viewModel, coverSize: coverSize) accessibilityLabel = "\(viewModel.author). \(viewModel.title). \(viewModel.details)" } @@ -54,14 +54,6 @@ final class ReaderPostCell: ReaderStreamBaseCell { super.updateConstraints() } - static func preferredCoverSize(in window: UIWindow, isCompact: Bool) -> ImageSize { - var coverWidth = ReaderPostCell.regularCoverWidth - if isCompact { - coverWidth = min(window.bounds.width, window.bounds.height) - ReaderStreamBaseCell.insets.left * 2 - } - return ImageSize(scaling: CGSize(width: coverWidth, height: coverWidth), in: window) - } - func getViewForZoomTransition() -> UIView { view } @@ -105,8 +97,6 @@ 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 @@ -315,9 +305,8 @@ private final class ReaderPostCellView: UIView { // MARK: Configure (ViewModel) - func configure(with viewModel: ReaderPostCellViewModel, window: UIWindow?) { + func configure(with viewModel: ReaderPostCellViewModel, coverSize: ImageSize) { self.viewModel = viewModel - self.hostWindow = window setAvatar(with: viewModel) buttonAuthor.configuration?.attributedTitle = AttributedString( @@ -332,7 +321,7 @@ private final class ReaderPostCellView: UIView { imageView.isHidden = viewModel.imageURL == nil if let imageURL = viewModel.imageURL { - imageView.setImage(with: imageURL, size: preferredCoverSize) + imageView.setImage(with: imageURL, size: coverSize) } if viewModel.isSeen == true { @@ -353,11 +342,6 @@ private final class ReaderPostCellView: UIView { } } - private var preferredCoverSize: ImageSize? { - guard let window = window ?? hostWindow else { return nil } - return ReaderPostCell.preferredCoverSize(in: window, isCompact: isCompact) - } - private func configureToolbar(with viewModel: ReaderPostToolbarViewModel) { buttons.bookmark.configuration = { var configuration = buttons.bookmark.configuration ?? .plain() @@ -567,7 +551,7 @@ private extension ReaderPostCellView { #Preview { let cell = ReaderPostCellView() - cell.configure(with: .mock(), window: nil) + cell.configure(with: .mock(), coverSize: ImageSize(scaling: CGSize(width: 358, height: 358), scale: 3)) 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 ab12a13d556b..a8b4fca32a18 100644 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Controllers/ReaderStreamViewController.swift @@ -202,6 +202,20 @@ import AutomatticTracks } } + /// The target size for post cover images. Derived from the portrait-width + /// of the view so the fetched size is rotation-invariant. + private var coverSize: ImageSize { + var width = ReaderPostCell.regularCoverWidth + if isCompact { + let size = view.bounds.size + width = min(size.width, size.height) - ReaderStreamBaseCell.insets.left * 2 + } + return ImageSize( + scaling: CGSize(width: width, height: width), + scale: traitCollection.displayScale + ) + } + private let isStandaloneAppModeEnabled = BuildSettings.current.brand == .reader private var emptyStateView: UIView? { @@ -1526,7 +1540,7 @@ extension ReaderStreamViewController: WPTableViewHandlerDelegate { viewModel.viewController = self let cell = tableConfiguration.postCell(in: tableView, for: indexPath) - cell.configure(with: viewModel, isCompact: isCompact, window: view.window) + cell.configure(with: viewModel, isCompact: isCompact, coverSize: coverSize) cell.isSeparatorHidden = !showsSeparator return cell } @@ -1695,8 +1709,7 @@ extension ReaderStreamViewController: UITableViewDataSourcePrefetching { } private func makeImageRequests(for indexPaths: [IndexPath]) -> [ImageRequest] { - guard let window = view.window else { return [] } - let targetSize = ReaderPostCell.preferredCoverSize(in: window, isCompact: isCompact) + let targetSize = coverSize return indexPaths.compactMap { guard let imageURL = getPost(at: $0)?.featuredImageURLForDisplay() else { return nil