diff --git a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift index abde73120c2c..ae7c55d6bdd8 100644 --- a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift +++ b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserCell.swift @@ -13,7 +13,7 @@ public enum ThemeAction { case tryCustomize case view - static func activeActionsForTheme(_ theme: Theme) ->[ThemeAction] { + static func activeActionsForTheme(_ theme: Theme) -> [ThemeAction] { if theme.custom { if theme.hasDetailsURL() { return [customize, details] @@ -23,7 +23,7 @@ public enum ThemeAction { return [customize, details, support] } - static func inactiveActionsForTheme(_ theme: Theme) ->[ThemeAction] { + static func inactiveActionsForTheme(_ theme: Theme) -> [ThemeAction] { if theme.custom { if theme.hasDetailsURL() { return [tryCustomize, activate, details] @@ -100,6 +100,29 @@ open class ThemeBrowserCell: UICollectionViewCell { @objc open var showPriceInformation: Bool = false open weak var presenter: ThemePresenter? + /// Replaces the action button with a spinner while the theme is being activated. + var isActivating: Bool = false { + didSet { + actionButton.isHidden = isActivating + if isActivating { + activatingIndicator.startAnimating() + } else { + activatingIndicator.stopAnimating() + } + } + } + + private lazy var activatingIndicator: UIActivityIndicatorView = { + let indicator = UIActivityIndicatorView(style: .medium) + indicator.hidesWhenStopped = true + indicator.accessibilityLabel = NSLocalizedString( + "themeBrowser.cell.activating", + value: "Activating", + comment: "Accessibility label for the spinner shown while a theme is being activated" + ) + return indicator + }() + fileprivate var placeholderImage = UIImage(named: "theme-loading") fileprivate var activeEllipsisImage = UIImage(named: "icon-menu-ellipsis-white") fileprivate var inactiveEllipsisImage = UIImage(named: "icon-menu-ellipsis") @@ -109,9 +132,12 @@ open class ThemeBrowserCell: UICollectionViewCell { override open var isHighlighted: Bool { didSet { let alphaFinal: CGFloat = isHighlighted ? 0.3 : 0 - UIView.animate(withDuration: 0.2, animations: { [weak self] in - self?.highlightView.alpha = alphaFinal - }) + UIView.animate( + withDuration: 0.2, + animations: { [weak self] in + self?.highlightView.alpha = alphaFinal + } + ) } } @@ -120,6 +146,13 @@ open class ThemeBrowserCell: UICollectionViewCell { actionButton.isExclusiveTouch = true + infoBar.addSubview(activatingIndicator) + activatingIndicator.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + activatingIndicator.centerXAnchor.constraint(equalTo: actionButton.centerXAnchor), + activatingIndicator.centerYAnchor.constraint(equalTo: actionButton.centerYAnchor) + ]) + layer.cornerRadius = 12 layer.cornerCurve = .continuous layer.borderColor = UIColor.separator.cgColor @@ -135,6 +168,7 @@ open class ThemeBrowserCell: UICollectionViewCell { theme = nil presenter = nil showPriceInformation = false + isActivating = false } fileprivate func refreshGUI() { @@ -220,20 +254,25 @@ open class ThemeBrowserCell: UICollectionViewCell { } fileprivate func refreshScreenshotImage(_ imageUrl: String) { - let imageUrlWithWidth = imageUrlForWidth( imageUrl: imageUrl ) + let imageUrlWithWidth = imageUrlForWidth(imageUrl: imageUrl) let screenshotUrl = URL(string: imageUrlWithWidth) imageView.backgroundColor = Styles.placeholderColor activityView.startAnimating() - imageView.downloadImage(from: screenshotUrl, success: { [weak self] _ in - self?.showScreenshot() - }, failure: { [weak self] error in - if let error = error as NSError?, error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled { + imageView.downloadImage( + from: screenshotUrl, + success: { [weak self] _ in + self?.showScreenshot() + }, + failure: { [weak self] error in + let nsError = error as NSError? + if nsError?.domain == NSURLErrorDomain && nsError?.code == NSURLErrorCancelled { return } DDLogError("Error loading theme screenshot: \(String(describing: error?.localizedDescription))") self?.showPlaceholder() - }) + } + ) } // MARK: - Actions @@ -245,13 +284,17 @@ open class ThemeBrowserCell: UICollectionViewCell { let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - let themeActions = theme.isCurrentTheme() ? ThemeAction.activeActionsForTheme(theme) : ThemeAction.inactiveActionsForTheme(theme) + let themeActions = + theme.isCurrentTheme() + ? ThemeAction.activeActionsForTheme(theme) : ThemeAction.inactiveActionsForTheme(theme) themeActions.forEach { themeAction in - alertController.addActionWithTitle(themeAction.title, - style: .default, - handler: { (_: UIAlertAction) in - themeAction.present(theme, presenter) - }) + alertController.addActionWithTitle( + themeAction.title, + style: .default, + handler: { (_: UIAlertAction) in + themeAction.present(theme, presenter) + } + ) } let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel action title") @@ -290,7 +333,10 @@ extension ThemeBrowserCell: Accessible { private func prepareActionButtonForVoiceOver() { actionButton.isAccessibilityElement = true - actionButton.accessibilityLabel = NSLocalizedString("More", comment: "Action button to display more available options") + actionButton.accessibilityLabel = NSLocalizedString( + "More", + comment: "Action button to display more available options" + ) actionButton.accessibilityTraits = .button } diff --git a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift index d35b74e7dbc4..56a32f530a0a 100644 --- a/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift +++ b/WordPress/Classes/ViewRelated/Themes/ThemeBrowserViewController.swift @@ -205,8 +205,6 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou fileprivate var activityIndicator: UIActivityIndicatorView = { let indicatorView = UIActivityIndicatorView(style: .medium) indicatorView.frame = themesLoaderFrame - //TODO update color with white headers - indicatorView.color = .white indicatorView.startAnimating() return indicatorView }() @@ -252,6 +250,9 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou fileprivate var presentingTheme: Theme? + /// The theme whose activation request is in flight. Drives the grid cell spinner across cell reuse and reloads. + private var activatingThemeId: String? + private var noResultsViewController: NoResultsViewController? private struct NoResultsTitles { @@ -620,6 +621,7 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou cell.presenter = self cell.theme = themeAtIndexPath(indexPath) + cell.isActivating = activatingThemeId != nil && cell.theme?.themeId == activatingThemeId if sections[indexPath.section] == .themes { syncMoreThemesIfNeeded(indexPath) @@ -939,10 +941,12 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou @objc var onWebkitViewControllerClose: (() -> Void)? @objc open func activateTheme(_ theme: Theme?) { - guard let theme, !theme.isCurrentTheme() else { + guard let theme, !theme.isCurrentTheme(), activatingThemeId == nil else { return } + activatingThemeId = theme.themeId + collectionView?.reloadData() updateActivateButton(isLoading: true) _ = themeService.activate( @@ -955,6 +959,7 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou blog: self?.blog ) + self?.activatingThemeId = nil self?.collectionView?.reloadData() let successTitle = NSLocalizedString( @@ -966,10 +971,6 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou 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" - ) self?.updateActivateButton(isLoading: false) @@ -978,13 +979,6 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou message: successMessage, 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.presentFromRootViewController() }, @@ -998,6 +992,8 @@ open class ThemeBrowserViewController: UIViewController, UICollectionViewDataSou comment: "Title of alert when theme activation fails" ) + self?.activatingThemeId = nil + self?.collectionView?.reloadData() self?.activityIndicator.stopAnimating() self?.activateButton?.customView = nil