From 6ddb0170775836f476ecf39798c6f3b4a539fe09 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 09:46:33 -0500 Subject: [PATCH 01/18] Rename ImageLoadingController --- WordPress/Classes/Utility/Media/AsyncImageView.swift | 4 ++-- ...eViewController.swift => ImageLoadingController.swift} | 2 +- .../Utility/Media/UIImageView+ImageDownloader.swift | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) rename WordPress/Classes/Utility/Media/{ImageViewController.swift => ImageLoadingController.swift} (98%) diff --git a/WordPress/Classes/Utility/Media/AsyncImageView.swift b/WordPress/Classes/Utility/Media/AsyncImageView.swift index 307e3ae6e94e..b7bcea67c2f2 100644 --- a/WordPress/Classes/Utility/Media/AsyncImageView.swift +++ b/WordPress/Classes/Utility/Media/AsyncImageView.swift @@ -9,7 +9,7 @@ final class AsyncImageView: UIView { private let imageView = GIFImageView() private var errorView: UIImageView? private var spinner: UIActivityIndicatorView? - private let controller = ImageViewController() + private let controller = ImageLoadingController() enum LoadingStyle { /// Shows a secondary background color during the download. @@ -88,7 +88,7 @@ final class AsyncImageView: UIView { controller.setImage(with: imageURL, host: host, size: size, completion: completion) } - private func setState(_ state: ImageViewController.State) { + private func setState(_ state: ImageLoadingController.State) { imageView.isHidden = true errorView?.isHidden = true spinner?.stopAnimating() diff --git a/WordPress/Classes/Utility/Media/ImageViewController.swift b/WordPress/Classes/Utility/Media/ImageLoadingController.swift similarity index 98% rename from WordPress/Classes/Utility/Media/ImageViewController.swift rename to WordPress/Classes/Utility/Media/ImageLoadingController.swift index 053e6018b072..a226bd4219c1 100644 --- a/WordPress/Classes/Utility/Media/ImageViewController.swift +++ b/WordPress/Classes/Utility/Media/ImageLoadingController.swift @@ -4,7 +4,7 @@ import WordPressMedia /// A convenience class for managing image downloads for individual views. @MainActor -final class ImageViewController { +final class ImageLoadingController { var downloader: ImageDownloader = .shared var onStateChanged: (State) -> Void = { _ in } diff --git a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift index 303c9a9f60f4..45ab051e78ab 100644 --- a/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift +++ b/WordPress/Classes/Utility/Media/UIImageView+ImageDownloader.swift @@ -31,11 +31,11 @@ struct ImageViewExtensions { controller.setImage(with: imageURL, host: host, size: size, completion: completion) } - var controller: ImageViewController { - if let controller = objc_getAssociatedObject(imageView, ImageViewExtensions.controllerKey) as? ImageViewController { + var controller: ImageLoadingController { + if let controller = objc_getAssociatedObject(imageView, ImageViewExtensions.controllerKey) as? ImageLoadingController { return controller } - let controller = ImageViewController() + let controller = ImageLoadingController() controller.onStateChanged = { [weak imageView] in guard let imageView else { return } setState($0, for: imageView) @@ -44,7 +44,7 @@ struct ImageViewExtensions { return controller } - private func setState(_ state: ImageViewController.State, for imageView: UIImageView) { + private func setState(_ state: ImageLoadingController.State, for imageView: UIImageView) { switch state { case .loading: break From 6e9cd7095131ab4f2336c710ff1dcc87794b8b78 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 20:40:52 -0500 Subject: [PATCH 02/18] Add LightboxViewController to replace WPImageViewController --- .../LightboxImagePageViewController.swift | 75 ++++++++++ .../Lightbox/LightboxImageScrollView.swift | 132 ++++++++++++++++++ .../Media/Lightbox/LightboxItem.swift | 7 + .../Lightbox/LightboxViewController.swift | 118 ++++++++++++++++ 4 files changed, 332 insertions(+) create mode 100644 WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift create mode 100644 WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImageScrollView.swift create mode 100644 WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift create mode 100644 WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift new file mode 100644 index 000000000000..34c3e8899dad --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift @@ -0,0 +1,75 @@ +import UIKit +import WordPressUI + +final class LightboxImagePageViewController: UIViewController { + private(set) var scrollView = LightboxImageScrollView() + private let controller = ImageLoadingController() + private let image: LightboxItem + private let activityIndicator = UIActivityIndicatorView() + private var errorView: UIImageView? + + init(image: LightboxItem) { + self.image = image + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.addSubview(scrollView) + + activityIndicator.hidesWhenStopped = true + view.addSubview(activityIndicator) + activityIndicator.pinCenter() + + scrollView.onDismissTapped = { [weak self] in + self?.parent?.presentingViewController?.dismiss(animated: true) + } + + controller.onStateChanged = { [weak self] in + self?.setState($0) + } + + controller.setImage(with: image.sourceURL, host: image.host) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + if scrollView.frame != view.bounds { + scrollView.frame = view.bounds + scrollView.configureLayout() + } + } + + private func setState(_ state: ImageLoadingController.State) { + switch state { + case .loading: + if scrollView.imageView.image == nil { + activityIndicator.startAnimating() + } + case .success(let image): + activityIndicator.stopAnimating() + scrollView.configure(with: image) + case .failure: + activityIndicator.stopAnimating() + makeErrorView().isHidden = false + } + } + + private func makeErrorView() -> UIImageView { + if let errorView { + return errorView + } + let errorView = UIImageView(image: UIImage(systemName: "exclamationmark.triangle")) + errorView.tintColor = .separator + view.addSubview(errorView) + errorView.pinCenter() + self.errorView = errorView + return errorView + } +} diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImageScrollView.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImageScrollView.swift new file mode 100644 index 000000000000..5cd72ac7c9a5 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImageScrollView.swift @@ -0,0 +1,132 @@ +import UIKit +import Gifu +import WordPressUI + +final class LightboxImageScrollView: UIScrollView, UIScrollViewDelegate { + let imageView = GIFImageView() + + var onDismissTapped: (() -> Void)? + + // MARK: - Initializers + + override init(frame: CGRect) { + super.init(frame: frame) + + setupView() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: Configuration + + func configure(with image: UIImage) { + imageView.configure(image: image) + configureImageView() + } + + private func setupView() { + addSubview(imageView) + + imageView.contentMode = .scaleAspectFit + imageView.clipsToBounds = true + imageView.isUserInteractionEnabled = true + + delegate = self + isMultipleTouchEnabled = true + minimumZoomScale = 1 + maximumZoomScale = 3 + showsHorizontalScrollIndicator = false + showsVerticalScrollIndicator = false + + let doubleTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(didRecognizeDoubleTap)) + doubleTapRecognizer.numberOfTapsRequired = 2 + doubleTapRecognizer.numberOfTouchesRequired = 1 + addGestureRecognizer(doubleTapRecognizer) + + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(didRecognizeTap)) + addGestureRecognizer(tapRecognizer) + + tapRecognizer.require(toFail: doubleTapRecognizer) + } + + // MARK: Recognizers + + @objc private func didRecognizeDoubleTap(_ recognizer: UITapGestureRecognizer) { + let zoomScale = zoomScale > minimumZoomScale ? minimumZoomScale : maximumZoomScale + let width = bounds.size.width / zoomScale + let height = bounds.size.height / zoomScale + + let location = recognizer.location(in: imageView) + let x = location.x - (width / 2.0) + let y = location.y - (height / 2.0) + + let rect = CGRect(x: x, y: y, width: width, height: height) + zoom(to: rect, animated: true) + } + + @objc private func didRecognizeTap(_ recognizer: UITapGestureRecognizer) { + onDismissTapped?() + } + + // MARK: Layout + + func configureLayout() { + contentSize = bounds.size + imageView.frame = bounds + zoomScale = minimumZoomScale + + configureImageView() + } + + private func configureImageView() { + guard let image = imageView.image else { + return centerImageView() + } + + let imageViewSize = imageView.frame.size + let imageSize = image.size + let actualImageSize: CGSize + + if imageSize.width / imageSize.height > imageViewSize.width / imageViewSize.height { + actualImageSize = CGSize( + width: imageViewSize.width, + height: imageViewSize.width / imageSize.width * imageSize.height) + } else { + actualImageSize = CGSize( + width: imageViewSize.height / imageSize.height * imageSize.width, + height: imageViewSize.height) + } + + imageView.frame = CGRect(origin: CGPoint.zero, size: actualImageSize) + + centerImageView() + } + + private func centerImageView() { + var newFrame = imageView.frame + if newFrame.size.width < bounds.size.width { + newFrame.origin.x = (bounds.size.width - newFrame.size.width) / 2.0 + } else { + newFrame.origin.x = 0.0 + } + + if newFrame.size.height < bounds.size.height { + newFrame.origin.y = (bounds.size.height - newFrame.size.height) / 2.0 + } else { + newFrame.origin.y = 0.0 + } + imageView.frame = newFrame + } + + // MARK: UIScrollViewDelegate + + func viewForZooming(in scrollView: UIScrollView) -> UIView? { + imageView + } + + func scrollViewDidZoom(_ scrollView: UIScrollView) { + centerImageView() + } +} diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift new file mode 100644 index 000000000000..9d6d58d3b251 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift @@ -0,0 +1,7 @@ +import Foundation +import WordPressMedia + +struct LightboxItem { + let sourceURL: URL + var host: MediaHost? +} diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift new file mode 100644 index 000000000000..5b7b331fbaf8 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -0,0 +1,118 @@ +import UIKit +import WordPressMedia +import WordPressUI +import UniformTypeIdentifiers + +/// A fullscreen preview of a set of media assets. +final class LightboxViewController: UIViewController { + private var pageVC: LightboxImagePageViewController? + private var items: [LightboxItem] + + init(items: [LightboxItem]) { + assert(items.count == 1, "Current API supports only one item at a time") + self.items = items + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .black + + if let item = items.first { + show(item) + } + + addCloseButton() + } + + private func show(_ item: LightboxItem) { + let pageVC = LightboxImagePageViewController(image: item) + pageVC.willMove(toParent: self) + addChild(pageVC) + view.addSubview(pageVC.view) + pageVC.view.pinEdges() + pageVC.didMove(toParent: self) + self.pageVC = pageVC + } + + private func addCloseButton() { + let button = UIButton(type: .system) + let image = UIImage(systemName: "xmark.circle.fill")? + .withConfiguration(UIImage.SymbolConfiguration(font: .systemFont(ofSize: 22, weight: .medium))) + .applyingSymbolConfiguration(UIImage.SymbolConfiguration(paletteColors: [.lightGray, .opaqueSeparator.withAlphaComponent(0.2)])) + button.setImage(image, for: []) + button.addTarget(self, action: #selector(buttonCloseTapped), for: .primaryActionTriggered) + button.accessibilityLabel = SharedStrings.Button.close + view.addSubview(button) + button.pinEdges([.top, .trailing], to: view.safeAreaLayoutGuide, insets: UIEdgeInsets(.all, 8)) + } + + @objc private func buttonCloseTapped() { + presentingViewController?.dismiss(animated: true) + } + + // MARK: Presentation + + func configureZoomTransition(souceItemProvider: @escaping (UIViewController) -> UIView) { + if #available(iOS 18.0, *) { + let options = UIViewController.Transition.ZoomOptions() + options.alignmentRectProvider = { context in + // For more info, see https://douglashill.co/zoom-transitions/#Zooming-to-only-part-of-the-destination-view + let detailViewController = context.zoomedViewController as! LightboxViewController + let detailsView: UIView = detailViewController.pageVC?.scrollView.imageView ?? detailViewController.view + return detailsView.convert(detailsView.bounds, to: detailViewController.view) + } + preferredTransition = .zoom(options: options) { context in + souceItemProvider(context.zoomedViewController) + } + } else { + modalTransitionStyle = .crossDissolve + } + } + + func configureZoomTransition(sourceView: UIView) { + configureZoomTransition { _ in sourceView } + } +} + +@available(iOS 17, *) +#Preview { + UINavigationController(rootViewController: LightboxDemoViewController()) +} + +/// An example of ``LightboxController`` usage. +final class LightboxDemoViewController: UIViewController { + let imageView = UIImageView() + let images: [LightboxItem] = [ + LightboxItem(sourceURL: URL(string: "https://github.com/user-attachments/assets/5a1d0d95-8ce6-4a87-8175-d67396511143")!) + ] + + override func viewDidLoad() { + super.viewDidLoad() + + view.addSubview(imageView) + imageView.pinCenter() + NSLayoutConstraint.activate([ + imageView.widthAnchor.constraint(equalToConstant: 120), + imageView.heightAnchor.constraint(equalToConstant: 80), + ]) + + Task { @MainActor in + imageView.image = try? await ImageDownloader.shared.image(from: images[0].sourceURL) + } + + imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imageTapped))) + imageView.isUserInteractionEnabled = true + } + + @objc private func imageTapped() { + let lightboxVC = LightboxViewController(items: images) + lightboxVC.configureZoomTransition(sourceView: imageView) + present(lightboxVC, animated: true) + } +} From 386dc50914e5b7d750ad6834e0cddec7a931be4a Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 20:47:16 -0500 Subject: [PATCH 03/18] Integrate LightboxViewController in Reader --- .../Media/Lightbox/LightboxViewController.swift | 4 ++-- .../Reader/Detail/ReaderDetailCoordinator.swift | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index 5b7b331fbaf8..1478e0ff6686 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -58,7 +58,7 @@ final class LightboxViewController: UIViewController { // MARK: Presentation - func configureZoomTransition(souceItemProvider: @escaping (UIViewController) -> UIView) { + func configureZoomTransition(souceItemProvider: @escaping (UIViewController) -> UIView?) { if #available(iOS 18.0, *) { let options = UIViewController.Transition.ZoomOptions() options.alignmentRectProvider = { context in @@ -75,7 +75,7 @@ final class LightboxViewController: UIViewController { } } - func configureZoomTransition(sourceView: UIView) { + func configureZoomTransition(sourceView: UIView?) { configureZoomTransition { _ in sourceView } } } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index 4aa343b2e725..6fa7c685325b 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -1,5 +1,6 @@ import Foundation import WordPressShared +import WordPressMedia import Combine class ReaderDetailCoordinator { @@ -283,12 +284,10 @@ class ReaderDetailCoordinator { func presentImage(_ url: URL) { WPAnalytics.trackReader(.readerArticleImageTapped) - let imageViewController = WPImageViewController(url: url) - imageViewController.readerPost = post - imageViewController.modalTransitionStyle = .crossDissolve - imageViewController.modalPresentationStyle = .fullScreen - - viewController?.present(imageViewController, animated: true) + let image = LightboxItem(sourceURL: url, host: post.map(MediaHost.init)) + let lightboxVC = LightboxViewController(items: [image]) + lightboxVC.configureZoomTransition(sourceView: nil) + viewController?.present(lightboxVC, animated: true) } /// Open the postURL in a separated view controller From 23968d9e228056ab76ac0093c43dceea4d5b3ab1 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 21:03:07 -0500 Subject: [PATCH 04/18] Add Media support in LightboxViewController --- .../Media/ImageLoadingController.swift | 26 +++++++++++++++++++ .../LightboxImagePageViewController.swift | 17 +++++++++--- .../Media/Lightbox/LightboxItem.swift | 7 ++++- .../Lightbox/LightboxViewController.swift | 11 ++++---- .../Detail/ReaderDetailCoordinator.swift | 4 +-- 5 files changed, 53 insertions(+), 12 deletions(-) diff --git a/WordPress/Classes/Utility/Media/ImageLoadingController.swift b/WordPress/Classes/Utility/Media/ImageLoadingController.swift index a226bd4219c1..a1e0a2c41933 100644 --- a/WordPress/Classes/Utility/Media/ImageLoadingController.swift +++ b/WordPress/Classes/Utility/Media/ImageLoadingController.swift @@ -6,6 +6,7 @@ import WordPressMedia @MainActor final class ImageLoadingController { var downloader: ImageDownloader = .shared + var service: MediaImageService = .shared var onStateChanged: (State) -> Void = { _ in } private(set) var task: Task? @@ -61,4 +62,29 @@ final class ImageLoadingController { } } } + + func setImage( + with media: Media, + size: MediaImageService.ImageSize + ) { + task?.cancel() + + if let image = service.getCachedThumbnail(for: .init(media), size: size) { + onStateChanged(.success(image)) + } else { + onStateChanged(.loading) + task = Task { @MainActor [service, weak self] in + do { + let image = try await service.image(for: media, size: size) + // This line guarantees that if you cancel on the main thread, + // none of the `onStateChanged` callbacks get called. + guard !Task.isCancelled else { return } + self?.onStateChanged(.success(image)) + } catch { + guard !Task.isCancelled else { return } + self?.onStateChanged(.failure(error)) + } + } + } + } } diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift index 34c3e8899dad..9666a6c95bf4 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift @@ -4,12 +4,12 @@ import WordPressUI final class LightboxImagePageViewController: UIViewController { private(set) var scrollView = LightboxImageScrollView() private let controller = ImageLoadingController() - private let image: LightboxItem + private let item: LightboxItem private let activityIndicator = UIActivityIndicatorView() private var errorView: UIImageView? - init(image: LightboxItem) { - self.image = image + init(item: LightboxItem) { + self.item = item super.init(nibName: nil, bundle: nil) } @@ -34,7 +34,7 @@ final class LightboxImagePageViewController: UIViewController { self?.setState($0) } - controller.setImage(with: image.sourceURL, host: image.host) + startFetching() } override func viewDidLayoutSubviews() { @@ -46,6 +46,15 @@ final class LightboxImagePageViewController: UIViewController { } } + private func startFetching() { + switch item { + case .asset(let asset): + controller.setImage(with: asset.sourceURL, host: asset.host) + case .media(let media): + controller.setImage(with: media, size: .original) + } + } + private func setState(_ state: ImageLoadingController.State) { switch state { case .loading: diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift index 9d6d58d3b251..da374bd737cd 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift @@ -1,7 +1,12 @@ import Foundation import WordPressMedia -struct LightboxItem { +enum LightboxItem { + case asset(LightboxAsset) + case media(Media) +} + +struct LightboxAsset { let sourceURL: URL var host: MediaHost? } diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index 1478e0ff6686..5456272b07a5 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -31,7 +31,7 @@ final class LightboxViewController: UIViewController { } private func show(_ item: LightboxItem) { - let pageVC = LightboxImagePageViewController(image: item) + let pageVC = LightboxImagePageViewController(item: item) pageVC.willMove(toParent: self) addChild(pageVC) view.addSubview(pageVC.view) @@ -87,9 +87,10 @@ final class LightboxViewController: UIViewController { /// An example of ``LightboxController`` usage. final class LightboxDemoViewController: UIViewController { - let imageView = UIImageView() - let images: [LightboxItem] = [ - LightboxItem(sourceURL: URL(string: "https://github.com/user-attachments/assets/5a1d0d95-8ce6-4a87-8175-d67396511143")!) + private let imageView = UIImageView() + private let imageURL = URL(string: "https://github.com/user-attachments/assets/5a1d0d95-8ce6-4a87-8175-d67396511143")! + private let images: [LightboxItem] = [ + .asset(LightboxAsset(sourceURL: imageURL)) ] override func viewDidLoad() { @@ -103,7 +104,7 @@ final class LightboxDemoViewController: UIViewController { ]) Task { @MainActor in - imageView.image = try? await ImageDownloader.shared.image(from: images[0].sourceURL) + imageView.image = try? await ImageDownloader.shared.image(from: imageURL) } imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imageTapped))) diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index 6fa7c685325b..d4faad148602 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -284,8 +284,8 @@ class ReaderDetailCoordinator { func presentImage(_ url: URL) { WPAnalytics.trackReader(.readerArticleImageTapped) - let image = LightboxItem(sourceURL: url, host: post.map(MediaHost.init)) - let lightboxVC = LightboxViewController(items: [image]) + let image = LightboxAsset(sourceURL: url, host: post.map(MediaHost.init)) + let lightboxVC = LightboxViewController(items: [.asset(image)]) lightboxVC.configureZoomTransition(sourceView: nil) viewController?.present(lightboxVC, animated: true) } From 72031aa8e4bc1fde0fbdedccdcb9a096537bd46b Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 21:06:27 -0500 Subject: [PATCH 05/18] Add convenience init to LightboxViewController --- .../Media/Lightbox/LightboxViewController.swift | 10 ++++++---- .../Reader/Detail/ReaderDetailCoordinator.swift | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index 5456272b07a5..bdb6c72a12a9 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -8,6 +8,11 @@ final class LightboxViewController: UIViewController { private var pageVC: LightboxImagePageViewController? private var items: [LightboxItem] + convenience init(sourceURL: URL, host: MediaHost? = nil) { + let asset = LightboxAsset(sourceURL: sourceURL, host: host) + self.init(items: [.asset(asset)]) + } + init(items: [LightboxItem]) { assert(items.count == 1, "Current API supports only one item at a time") self.items = items @@ -89,9 +94,6 @@ final class LightboxViewController: UIViewController { final class LightboxDemoViewController: UIViewController { private let imageView = UIImageView() private let imageURL = URL(string: "https://github.com/user-attachments/assets/5a1d0d95-8ce6-4a87-8175-d67396511143")! - private let images: [LightboxItem] = [ - .asset(LightboxAsset(sourceURL: imageURL)) - ] override func viewDidLoad() { super.viewDidLoad() @@ -112,7 +114,7 @@ final class LightboxDemoViewController: UIViewController { } @objc private func imageTapped() { - let lightboxVC = LightboxViewController(items: images) + let lightboxVC = LightboxViewController(sourceURL: imageURL) lightboxVC.configureZoomTransition(sourceView: imageView) present(lightboxVC, animated: true) } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index d4faad148602..c3188087320c 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -284,8 +284,8 @@ class ReaderDetailCoordinator { func presentImage(_ url: URL) { WPAnalytics.trackReader(.readerArticleImageTapped) - let image = LightboxAsset(sourceURL: url, host: post.map(MediaHost.init)) - let lightboxVC = LightboxViewController(items: [.asset(image)]) + let host = post.map(MediaHost.init) + let lightboxVC = LightboxViewController(sourceURL: url, host: host) lightboxVC.configureZoomTransition(sourceView: nil) viewController?.present(lightboxVC, animated: true) } From 1ab5e6a1cdc10807764bba2863a735aae7753d35 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 21:22:23 -0500 Subject: [PATCH 06/18] Integrate LightboxViewController in SiteMedia --- .../ViewRelated/Cells/MediaItemHeaderView.swift | 2 +- .../Media/Lightbox/LightboxViewController.swift | 11 +++++++++++ .../ViewRelated/Media/MediaItemViewController.swift | 9 ++++----- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift b/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift index 9c1d66b83e9b..6c5b67854f6a 100644 --- a/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Cells/MediaItemHeaderView.swift @@ -4,7 +4,7 @@ import WordPressShared import WordPressMedia final class MediaItemHeaderView: UIView { - private let imageView = CachedAnimatedImageView() + let imageView = CachedAnimatedImageView() private let errorView = UIImageView() private let videoIconView = PlayIconView() private let loadingIndicator = UIActivityIndicatorView(style: .large) diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index bdb6c72a12a9..e025ddca948b 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -8,11 +8,18 @@ final class LightboxViewController: UIViewController { private var pageVC: LightboxImagePageViewController? private var items: [LightboxItem] + /// A thumbnail to display during transition and for the initial image download. + var thumbnail: UIImage? + convenience init(sourceURL: URL, host: MediaHost? = nil) { let asset = LightboxAsset(sourceURL: sourceURL, host: host) self.init(items: [.asset(asset)]) } + convenience init(media: Media) { + self.init(items: [.media(media)]) + } + init(items: [LightboxItem]) { assert(items.count == 1, "Current API supports only one item at a time") self.items = items @@ -42,6 +49,10 @@ final class LightboxViewController: UIViewController { view.addSubview(pageVC.view) pageVC.view.pinEdges() pageVC.didMove(toParent: self) + if let thumbnail { + pageVC.scrollView.configure(with: thumbnail) + self.thumbnail = nil + } self.pageVC = pageVC } diff --git a/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift b/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift index 2d1e29ddb088..79cff2797f91 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaItemViewController.swift @@ -179,11 +179,10 @@ final class MediaItemViewController: UITableViewController { } private func presentImageViewControllerForMedia() { - let controller = WPImageViewController(media: self.media) - controller.modalTransitionStyle = .crossDissolve - controller.modalPresentationStyle = .fullScreen - - self.present(controller, animated: true) + let controller = LightboxViewController(media: media) + controller.thumbnail = headerView.imageView.image + controller.configureZoomTransition(sourceView: headerView.imageView) + present(controller, animated: true) } private func presentVideoViewControllerForMedia() { From 0a1ade1f9cbd23a8b13fcd958f69e94f2b848d56 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 23 Dec 2024 21:30:51 -0500 Subject: [PATCH 07/18] Integrate LightboxViewController in ReaderDetailsCoordinator (cover image) --- .../Reader/Detail/ReaderDetailCoordinator.swift | 11 ++++++----- .../WordPressTest/ReaderDetailCoordinatorTests.swift | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index c3188087320c..b342169fe6dd 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -493,11 +493,12 @@ class ReaderDetailCoordinator { guard let post, let imageURL = post.featuredImage.flatMap(URL.init) else { return } - let controller = WPImageViewController(url: imageURL) - controller.readerPost = post - controller.modalTransitionStyle = .crossDissolve - controller.modalPresentationStyle = .fullScreen - viewController?.present(controller, animated: true) + let lightboxVC = LightboxViewController(sourceURL: imageURL, host: MediaHost(with: post)) + MainActor.assumeIsolated { + lightboxVC.thumbnail = sender.image + } + lightboxVC.configureZoomTransition(sourceView: sender) + viewController?.present(lightboxVC, animated: true) } private func followSite(completion: @escaping () -> Void) { diff --git a/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift b/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift index ea25945dabb2..e7861fc233af 100644 --- a/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift +++ b/WordPress/WordPressTest/ReaderDetailCoordinatorTests.swift @@ -194,7 +194,7 @@ class ReaderDetailCoordinatorTests: CoreDataTestCase { coordinator.handle(URL(string: "https://wordpress.com/image.png")!) - expect(viewMock.didCallPresentWith).to(beAKindOf(WPImageViewController.self)) + expect(viewMock.didCallPresentWith).to(beAKindOf(LightboxViewController.self)) } /// Present an URL in a new Reader Detail screen From 74157da6ffb26bb27b937f7bd655991b70c29033 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 08:34:50 -0500 Subject: [PATCH 08/18] INtegrate in DefaultContentCoordinator --- WordPress/Classes/Utility/ContentCoordinator.swift | 7 +++---- .../Media/Lightbox/LightboxImagePageViewController.swift | 2 ++ .../Classes/ViewRelated/Media/Lightbox/LightboxItem.swift | 1 + .../Media/Lightbox/LightboxViewController.swift | 8 ++++++-- .../Reader/Detail/ReaderDetailCoordinator.swift | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/WordPress/Classes/Utility/ContentCoordinator.swift b/WordPress/Classes/Utility/ContentCoordinator.swift index 27d63494accd..8da84d65c2a8 100644 --- a/WordPress/Classes/Utility/ContentCoordinator.swift +++ b/WordPress/Classes/Utility/ContentCoordinator.swift @@ -142,10 +142,9 @@ struct DefaultContentCoordinator: ContentCoordinator { } func displayFullscreenImage(_ image: UIImage) { - let imageViewController = WPImageViewController(image: image) - imageViewController.modalTransitionStyle = .crossDissolve - imageViewController.modalPresentationStyle = .fullScreen - controller?.present(imageViewController, animated: true) + let lightboxVC = LightboxViewController(.image(image)) + lightboxVC.configureZoomTransition() + controller?.present(lightboxVC, animated: true) } func displayPlugin(withSlug pluginSlug: String, on siteSlug: String) throws { diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift index 9666a6c95bf4..f18934f37aac 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxImagePageViewController.swift @@ -48,6 +48,8 @@ final class LightboxImagePageViewController: UIViewController { private func startFetching() { switch item { + case .image(let image): + setState(.success(image)) case .asset(let asset): controller.setImage(with: asset.sourceURL, host: asset.host) case .media(let media): diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift index da374bd737cd..69e37075929e 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxItem.swift @@ -2,6 +2,7 @@ import Foundation import WordPressMedia enum LightboxItem { + case image(UIImage) case asset(LightboxAsset) case media(Media) } diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index e025ddca948b..d4fb52a3efe1 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -20,7 +20,11 @@ final class LightboxViewController: UIViewController { self.init(items: [.media(media)]) } - init(items: [LightboxItem]) { + convenience init(_ item: LightboxItem) { + self.init(items: [item]) + } + + private init(items: [LightboxItem]) { assert(items.count == 1, "Current API supports only one item at a time") self.items = items super.init(nibName: nil, bundle: nil) @@ -91,7 +95,7 @@ final class LightboxViewController: UIViewController { } } - func configureZoomTransition(sourceView: UIView?) { + func configureZoomTransition(sourceView: UIView? = nil) { configureZoomTransition { _ in sourceView } } } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index b342169fe6dd..e87f08a5818a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -286,7 +286,7 @@ class ReaderDetailCoordinator { let host = post.map(MediaHost.init) let lightboxVC = LightboxViewController(sourceURL: url, host: host) - lightboxVC.configureZoomTransition(sourceView: nil) + lightboxVC.configureZoomTransition() viewController?.present(lightboxVC, animated: true) } From f6defb76e490d67624c93c9636711fa2b4dfc8c2 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 08:49:37 -0500 Subject: [PATCH 09/18] Integrate LightboxViewController in Guteberg --- .../Networking/MediaHost+AbstractPost.swift | 21 ----- .../Classes/Networking/MediaHost+Blog.swift | 38 --------- .../Networking/MediaHost+Extensions.swift | 80 +++++++++++++++++++ .../Networking/MediaHost+ReaderPost.swift | 32 -------- .../Classes/Utility/Media/ImageLoader.swift | 9 +-- .../Blaze/Overlay/BlazePostPreviewView.swift | 6 +- .../RichCommentContentRenderer.swift | 2 +- .../Gutenberg/GutenbergViewController.swift | 17 +--- .../NewGutenbergViewController.swift | 17 +--- .../Pages/Views/PageListCell.swift | 4 +- .../Post/Views/PostCompactCell.swift | 6 +- .../ViewRelated/Post/Views/PostListCell.swift | 4 +- .../Detail/ReaderDetailCoordinator.swift | 2 +- .../Views/ReaderDetailFeaturedImageView.swift | 2 +- 14 files changed, 97 insertions(+), 143 deletions(-) delete mode 100644 WordPress/Classes/Networking/MediaHost+AbstractPost.swift delete mode 100644 WordPress/Classes/Networking/MediaHost+Blog.swift create mode 100644 WordPress/Classes/Networking/MediaHost+Extensions.swift delete mode 100644 WordPress/Classes/Networking/MediaHost+ReaderPost.swift diff --git a/WordPress/Classes/Networking/MediaHost+AbstractPost.swift b/WordPress/Classes/Networking/MediaHost+AbstractPost.swift deleted file mode 100644 index d9a9b41a3d1f..000000000000 --- a/WordPress/Classes/Networking/MediaHost+AbstractPost.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import WordPressMedia - -/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily -/// initialize it from a given `AbstractPost`. -/// -extension MediaHost { - enum AbstractPostError: Swift.Error { - case baseInitializerError(error: BlogError) - } - - init(with post: AbstractPost, failure: (AbstractPostError) -> ()) { - self.init( - with: post.blog, - failure: { error in - // We just associate a post with the underlying error for simpler debugging. - failure(AbstractPostError.baseInitializerError(error: error)) - } - ) - } -} diff --git a/WordPress/Classes/Networking/MediaHost+Blog.swift b/WordPress/Classes/Networking/MediaHost+Blog.swift deleted file mode 100644 index a1e1411b0ed9..000000000000 --- a/WordPress/Classes/Networking/MediaHost+Blog.swift +++ /dev/null @@ -1,38 +0,0 @@ -import Foundation -import WordPressMedia - -/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily -/// initialize it from a given `Blog`. -/// -extension MediaHost { - enum BlogError: Swift.Error { - case baseInitializerError(error: Error) - } - - init(with blog: Blog) { - self.init(with: blog) { error in - // We'll log the error, so we know it's there, but we won't halt execution. - WordPressAppDelegate.crashLogging?.logError(error) - } - } - - init(with blog: Blog, failure: (BlogError) -> ()) { - let isAtomic = blog.isAtomic() - self.init(with: blog, isAtomic: isAtomic, failure: failure) - } - - init(with blog: Blog, isAtomic: Bool, failure: (BlogError) -> ()) { - self.init( - isAccessibleThroughWPCom: blog.isAccessibleThroughWPCom(), - isPrivate: blog.isPrivate(), - isAtomic: isAtomic, - siteID: blog.dotComID?.intValue, - username: blog.usernameForSite, - authToken: blog.authToken, - failure: { error in - // We just associate a blog with the underlying error for simpler debugging. - failure(BlogError.baseInitializerError(error: error)) - } - ) - } -} diff --git a/WordPress/Classes/Networking/MediaHost+Extensions.swift b/WordPress/Classes/Networking/MediaHost+Extensions.swift new file mode 100644 index 000000000000..ef6a44815f0d --- /dev/null +++ b/WordPress/Classes/Networking/MediaHost+Extensions.swift @@ -0,0 +1,80 @@ +import Foundation +import WordPressMedia + +/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily +/// initialize it from a given `AbstractPost`. +/// +extension MediaHost { + init(_ post: AbstractPost) { + self.init(with: post.blog, failure: { error in + // We just associate a post with the underlying error for simpler debugging. + WordPressAppDelegate.crashLogging?.logError(error) + }) + } +} + +/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily +/// initialize it from a given `Blog`. +/// +extension MediaHost { + enum BlogError: Swift.Error { + case baseInitializerError(error: Error) + } + + init(with blog: Blog) { + self.init(with: blog) { error in + // We'll log the error, so we know it's there, but we won't halt execution. + WordPressAppDelegate.crashLogging?.logError(error) + } + } + + init(with blog: Blog, failure: (BlogError) -> ()) { + let isAtomic = blog.isAtomic() + self.init(with: blog, isAtomic: isAtomic, failure: failure) + } + + init(with blog: Blog, isAtomic: Bool, failure: (BlogError) -> ()) { + self.init( + isAccessibleThroughWPCom: blog.isAccessibleThroughWPCom(), + isPrivate: blog.isPrivate(), + isAtomic: isAtomic, + siteID: blog.dotComID?.intValue, + username: blog.usernameForSite, + authToken: blog.authToken, + failure: { error in + // We just associate a blog with the underlying error for simpler debugging. + failure(BlogError.baseInitializerError(error: error)) + } + ) + } +} + +/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily +/// initialize it from a given `Blog`. +/// +extension MediaHost { + init(_ post: ReaderPost) { + let isAccessibleThroughWPCom = post.isWPCom || post.isJetpack + + // This is the only way in which we can obtain the username and authToken here. + // It'd be nice if all data was associated with an account instead, for transparency + // and cleanliness of the code - but this'll have to do for now. + + // We allow a nil account in case the user connected only self-hosted sites. + let account = try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext) + let username = account?.username + let authToken = account?.authToken + + self.init( + isAccessibleThroughWPCom: isAccessibleThroughWPCom, + isPrivate: post.isBlogPrivate, + isAtomic: post.isBlogAtomic, + siteID: post.siteID?.intValue, + username: username, + authToken: authToken, + failure: { error in + WordPressAppDelegate.crashLogging?.logError(error) + } + ) + } +} diff --git a/WordPress/Classes/Networking/MediaHost+ReaderPost.swift b/WordPress/Classes/Networking/MediaHost+ReaderPost.swift deleted file mode 100644 index 70be952500ec..000000000000 --- a/WordPress/Classes/Networking/MediaHost+ReaderPost.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -import WordPressMedia - -/// Extends `MediaRequestAuthenticator.MediaHost` so that we can easily -/// initialize it from a given `Blog`. -/// -extension MediaHost { - init(with post: ReaderPost) { - let isAccessibleThroughWPCom = post.isWPCom || post.isJetpack - - // This is the only way in which we can obtain the username and authToken here. - // It'd be nice if all data was associated with an account instead, for transparency - // and cleanliness of the code - but this'll have to do for now. - - // We allow a nil account in case the user connected only self-hosted sites. - let account = try? WPAccount.lookupDefaultWordPressComAccount(in: ContextManager.shared.mainContext) - let username = account?.username - let authToken = account?.authToken - - self.init( - isAccessibleThroughWPCom: isAccessibleThroughWPCom, - isPrivate: post.isBlogPrivate, - isAtomic: post.isBlogAtomic, - siteID: post.siteID?.intValue, - username: username, - authToken: authToken, - failure: { error in - WordPressAppDelegate.crashLogging?.logError(error) - } - ) - } -} diff --git a/WordPress/Classes/Utility/Media/ImageLoader.swift b/WordPress/Classes/Utility/Media/ImageLoader.swift index 09c65e8e208a..edfbfe771365 100644 --- a/WordPress/Classes/Utility/Media/ImageLoader.swift +++ b/WordPress/Classes/Utility/Media/ImageLoader.swift @@ -83,18 +83,13 @@ import WordPressMedia @objc(loadImageWithURL:fromPost:preferredSize:placeholder:success:error:) func loadImage(with url: URL, from post: AbstractPost, preferredSize size: CGSize = .zero, placeholder: UIImage?, success: ImageLoaderSuccessBlock?, error: ImageLoaderFailureBlock?) { - let host = MediaHost(with: post, failure: { error in - WordPressAppDelegate.crashLogging?.logError(error) - }) - + let host = MediaHost(post) loadImage(with: url, from: host, preferredSize: size, placeholder: placeholder, success: success, error: error) } @objc(loadImageWithURL:fromReaderPost:preferredSize:placeholder:success:error:) func loadImage(with url: URL, from readerPost: ReaderPost, preferredSize size: CGSize = .zero, placeholder: UIImage?, success: ImageLoaderSuccessBlock?, error: ImageLoaderFailureBlock?) { - - let host = MediaHost(with: readerPost) - loadImage(with: url, from: host, preferredSize: size, placeholder: placeholder, success: success, error: error) + loadImage(with: url, from: MediaHost(readerPost), preferredSize: size, placeholder: placeholder, success: success, error: error) } /// Load an image from a specific post, using the given URL. Supports animated images (gifs) as well. diff --git a/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift b/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift index 3cdbd57745c3..c84e478be3a7 100644 --- a/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift +++ b/WordPress/Classes/ViewRelated/Blaze/Overlay/BlazePostPreviewView.swift @@ -96,13 +96,9 @@ final class BlazePostPreviewView: UIView { if let url = post.featuredImageURL { featuredImageView.isHidden = false - let host = MediaHost(with: post, failure: { error in - // We'll log the error, so we know it's there, but we won't halt execution. - WordPressAppDelegate.crashLogging?.logError(error) - }) let preferredSize = CGSize(width: featuredImageView.frame.width, height: featuredImageView.frame.height) .scaled(by: UITraitCollection.current.displayScale) - featuredImageView.setImage(with: url, host: host, size: preferredSize) + featuredImageView.setImage(with: url, host: MediaHost(post), size: preferredSize) } else { featuredImageView.isHidden = true diff --git a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift b/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift index 11bdb6bae598..51f042e12672 100644 --- a/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift +++ b/WordPress/Classes/ViewRelated/Comments/Views/Detail/ContentRenderer/RichCommentContentRenderer.swift @@ -79,7 +79,7 @@ private extension RichCommentContentRenderer { WordPressAppDelegate.crashLogging?.logError(error) }) } else if let post = comment.post as? ReaderPost, post.isBlogPrivate { - return MediaHost(with: post) + return MediaHost(post) } return .publicSite diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift index 8d886b989925..758bf6f4abff 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressMedia import Gutenberg import Aztec import WordPressFlux @@ -897,19 +898,9 @@ extension GutenbergViewController: GutenbergBridgeDelegate { } func gutenbergDidRequestImagePreview(with fullSizeUrl: URL, thumbUrl: URL?) { - navigationController?.definesPresentationContext = true - - let controller: WPImageViewController - if let image = AnimatedImageCache.shared.cachedStaticImage(url: fullSizeUrl) { - controller = WPImageViewController(image: image) - } else { - controller = WPImageViewController(externalMediaURL: fullSizeUrl) - } - - controller.post = self.post - controller.modalTransitionStyle = .crossDissolve - controller.modalPresentationStyle = .overCurrentContext - self.present(controller, animated: true) + let lightboxVC = LightboxViewController(sourceURL: fullSizeUrl, host: MediaHost(post)) + lightboxVC.configureZoomTransition() + present(lightboxVC, animated: true) } func gutenbergDidRequestUnsupportedBlockFallback(for block: Block) { diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index e55b31d38e84..8b8516e62d7c 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -1,4 +1,5 @@ import UIKit +import WordPressMedia import AutomatticTracks import GutenbergKit import SafariServices @@ -452,19 +453,9 @@ extension NewGutenbergViewController { // TODO: are we going to show this natively? func gutenbergDidRequestImagePreview(with fullSizeUrl: URL, thumbUrl: URL?) { - navigationController?.definesPresentationContext = true - - let controller: WPImageViewController - if let image = AnimatedImageCache.shared.cachedStaticImage(url: fullSizeUrl) { - controller = WPImageViewController(image: image) - } else { - controller = WPImageViewController(externalMediaURL: fullSizeUrl) - } - - controller.post = self.post - controller.modalTransitionStyle = .crossDissolve - controller.modalPresentationStyle = .overCurrentContext - self.present(controller, animated: true) + let lightboxVC = LightboxViewController(sourceURL: fullSizeUrl, host: MediaHost(post)) + lightboxVC.configureZoomTransition() + present(lightboxVC, animated: true) } // TODO: reimplement diff --git a/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift b/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift index da0e1fda2c86..7d6534431d1c 100644 --- a/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift +++ b/WordPress/Classes/ViewRelated/Pages/Views/PageListCell.swift @@ -62,9 +62,7 @@ final class PageListCell: UITableViewCell, AbstractPostListCell, PostSearchResul featuredImageView.isHidden = viewModel.imageURL == nil if let imageURL = viewModel.imageURL { - let host = MediaHost(with: viewModel.page) { error in - WordPressAppDelegate.crashLogging?.logError(error) - } + let host = MediaHost(viewModel.page) let thumbnailURL = MediaImageService.getResizedImageURL(for: imageURL, blog: viewModel.page.blog, size: Constants.imageSize.scaled(by: UIScreen.main.scale)) featuredImageView.setImage(with: thumbnailURL, host: host) } diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift index 5d5229ffc34f..0dbfe52398c5 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostCompactCell.swift @@ -78,11 +78,7 @@ final class PostCompactCell: UITableViewCell, Reusable { if let post, let url = post.featuredImageURL { featuredImageView.isHidden = false - let host = MediaHost(with: post, failure: { error in - // We'll log the error, so we know it's there, but we won't halt execution. - WordPressAppDelegate.crashLogging?.logError(error) - }) - + let host = MediaHost(post) let targetSize = Constants.imageSize.scaled(by: traitCollection.displayScale) featuredImageView.setImage(with: url, host: host, size: targetSize) } else { diff --git a/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift b/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift index 6d68e35f1264..8ec4cb3c89dd 100644 --- a/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift +++ b/WordPress/Classes/ViewRelated/Post/Views/PostListCell.swift @@ -75,9 +75,7 @@ final class PostListCell: UITableViewCell, AbstractPostListCell, PostSearchResul featuredImageView.isHidden = viewModel.imageURL == nil featuredImageView.layer.opacity = viewModel.syncStateViewModel.isEditable ? 1 : 0.25 if let imageURL = viewModel.imageURL { - let host = MediaHost(with: viewModel.post) { error in - WordPressAppDelegate.crashLogging?.logError(error) - } + let host = MediaHost(viewModel.post) let thumbnailURL = MediaImageService.getResizedImageURL(for: imageURL, blog: viewModel.post.blog, size: Constants.imageSize.scaled(by: UIScreen.main.scale)) featuredImageView.setImage(with: thumbnailURL, host: host) } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift index e87f08a5818a..c037dfef9f3a 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/ReaderDetailCoordinator.swift @@ -493,7 +493,7 @@ class ReaderDetailCoordinator { guard let post, let imageURL = post.featuredImage.flatMap(URL.init) else { return } - let lightboxVC = LightboxViewController(sourceURL: imageURL, host: MediaHost(with: post)) + let lightboxVC = LightboxViewController(sourceURL: imageURL, host: MediaHost(post)) MainActor.assumeIsolated { lightboxVC.thumbnail = sender.image } diff --git a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift index 05ea92bd8e68..063a836debc5 100644 --- a/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift +++ b/WordPress/Classes/ViewRelated/Reader/Detail/Views/ReaderDetailFeaturedImageView.swift @@ -223,7 +223,7 @@ class ReaderDetailFeaturedImageView: UIView, NibLoadable { completionHandler(CGSize(width: 1000, height: 1000 * ReaderPostCell.coverAspectRatio)) } - imageView.setImage(with: imageURL, host: MediaHost(with: post)) { [weak self] result in + imageView.setImage(with: imageURL, host: MediaHost(post)) { [weak self] result in guard let self else { return } switch result { case .success: From 5da2855366e1898e51783dab1c2c1cdcf4bd9a31 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 09:01:25 -0500 Subject: [PATCH 10/18] Integrate LightboxViewController in ExternalMediaPickerViewController --- .../ExternalMediaPickerViewController.swift | 1 + .../Lightbox/LightboxViewController.swift | 19 +++++++++++---- .../Preview/MediaPreviewController.swift | 23 +++++++++---------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift index f57cefc59aa4..62ad231900e3 100644 --- a/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/External/ExternalMediaPickerViewController.swift @@ -222,6 +222,7 @@ final class ExternalMediaPickerViewController: UIViewController, UICollectionVie let viewController = MediaPreviewController() viewController.dataSource = self let navigation = UINavigationController(rootViewController: viewController) + navigation.modalPresentationStyle = .fullScreen present(navigation, animated: true) } diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index d4fb52a3efe1..3904df25c070 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -11,6 +11,13 @@ final class LightboxViewController: UIViewController { /// A thumbnail to display during transition and for the initial image download. var thumbnail: UIImage? + var configuration: Configuration + + struct Configuration { + var backgroundColor: UIColor = .black + var showsCloseButton = true + } + convenience init(sourceURL: URL, host: MediaHost? = nil) { let asset = LightboxAsset(sourceURL: sourceURL, host: host) self.init(items: [.asset(asset)]) @@ -20,13 +27,14 @@ final class LightboxViewController: UIViewController { self.init(items: [.media(media)]) } - convenience init(_ item: LightboxItem) { + convenience init(_ item: LightboxItem, configuration: Configuration = .init()) { self.init(items: [item]) } - private init(items: [LightboxItem]) { + private init(items: [LightboxItem], configuration: Configuration = .init()) { assert(items.count == 1, "Current API supports only one item at a time") self.items = items + self.configuration = configuration super.init(nibName: nil, bundle: nil) } @@ -37,13 +45,14 @@ final class LightboxViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .black + view.backgroundColor = configuration.backgroundColor if let item = items.first { show(item) } - - addCloseButton() + if configuration.showsCloseButton { + addCloseButton() + } } private func show(_ item: LightboxItem) { diff --git a/WordPress/Classes/ViewRelated/Media/Preview/MediaPreviewController.swift b/WordPress/Classes/ViewRelated/Media/Preview/MediaPreviewController.swift index 6407c281c991..294ea77c9e06 100644 --- a/WordPress/Classes/ViewRelated/Media/Preview/MediaPreviewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Preview/MediaPreviewController.swift @@ -22,6 +22,8 @@ final class MediaPreviewController: UIViewController, UIPageViewControllerDataSo override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .systemBackground + configureNavigationItems() configurePageViewController() updateNavigationForCurrentViewController() @@ -52,26 +54,27 @@ final class MediaPreviewController: UIViewController, UIPageViewControllerDataSo } } - private func makePageViewController(at index: Int) -> MediaPreviewItemViewController? { + private func makePageViewController(at index: Int) -> LightboxViewController? { guard index >= 0 && index < numberOfItems, let item = dataSource?.previewController(self, previewItemAt: index) else { return nil } - let viewController = MediaPreviewItemViewController(externalMediaURL: item.url) - viewController.shouldDismissWithGestures = false - viewController.index = index + let viewController = LightboxViewController(sourceURL: item.url) + viewController.configuration.showsCloseButton = false + viewController.configuration.backgroundColor = .systemBackground + viewController.view.tag = index return viewController } // MARK: - UIPageViewControllerDataSource func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - let index = (viewController as! MediaPreviewItemViewController).index + let index = (viewController as! LightboxViewController).view.tag return makePageViewController(at: index - 1) } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - let index = (viewController as! MediaPreviewItemViewController).index + let index = (viewController as! LightboxViewController).view.tag return makePageViewController(at: index + 1) } @@ -82,17 +85,13 @@ final class MediaPreviewController: UIViewController, UIPageViewControllerDataSo } private func updateNavigationForCurrentViewController() { - guard let viewController = pageViewController.viewControllers?.first as? MediaPreviewItemViewController else { + guard let viewController = pageViewController.viewControllers?.first as? LightboxViewController else { return } - navigationItem.title = String(format: Strings.title, String(viewController.index + 1), String(numberOfItems)) + navigationItem.title = String(format: Strings.title, String(viewController.view.tag + 1), String(numberOfItems)) } } -private final class MediaPreviewItemViewController: WPImageViewController { - var index = 0 -} - private enum Strings { static let title = NSLocalizedString("mediaPreview.NofM", value: "%@ of %@", comment: "Navigation title for media preview. Example: 1 of 3") } From d2ad70aeeeec16f78e44aa591f81d19f29b9a5f6 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 10:07:08 -0500 Subject: [PATCH 11/18] Integrate LightboxViewController in PostSettingsViewController (featured image) --- .../PostSettingsViewController+Swift.swift | 39 +++++++++++++++++++ .../Post/PostSettingsViewController.m | 25 +++++------- .../PostSettingsViewController_Internal.h | 2 + 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index b781321cfb0f..6cd78ab87e76 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -251,6 +251,45 @@ extension PostSettingsViewController { } } +// MARK: - PostSettingsViewController (Featued Image) + +extension PostSettingsViewController { + @objc func showFeaturedImageSelector() { + guard let featuredImage = apost.featuredImage else { + return wpAssertionFailure("featured image missing") + } + + let lightboxVC = LightboxViewController(media: featuredImage) + lightboxVC.configuration.backgroundColor = .systemBackground + lightboxVC.configuration.showsCloseButton = false + lightboxVC.edgesForExtendedLayout = [] + + lightboxVC.navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: UIAction { [weak self] _ in + self?.dismiss(animated: true) + }) + + lightboxVC.toolbarItems = [ + UIBarButtonItem(title: SharedStrings.Button.remove, image: UIImage(systemName: "trash"), target: self, action: #selector(buttonRemoveFeaturedImageTapped)) + ] + + let navigationVC = UINavigationController(rootViewController: lightboxVC) + navigationVC.isToolbarHidden = false + navigationVC.view.backgroundColor = .systemBackground + self.present(navigationVC, animated: true) + } + + @objc private func buttonRemoveFeaturedImageTapped(_ sender: UIBarButtonItem) { + let alert = UIAlertController(title: Strings.confirmFeaturedImageRemoval, message: nil, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: SharedStrings.Button.cancel, style: .cancel)) + alert.addAction(UIAlertAction(title: SharedStrings.Button.remove, style: .destructive, handler: { [weak self] _ in + self?.removeFeaturedImage() + })) + alert.popoverPresentationController?.sourceItem = sender + (presentedViewController ?? self).present(alert, animated: true) + } +} + private enum Strings { + static let confirmFeaturedImageRemoval = NSLocalizedString("postSettings.confirmFeaturedImageRemovalAlert.title", value: "Remove this Featured Image?", comment: "Prompt when removing a featured image from a post") static let warningPostWillBePublishedAlertMessage = NSLocalizedString("postSettings.warningPostWillBePublishedAlertMessage", value: "By changing the visibility to 'Private', the post will be published immediately", comment: "An alert message explaning that by changing the visibility to private, the post will be published immediately to your site") } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 24aeecebf6af..91ab80619591 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -1030,21 +1030,6 @@ - (void)showEditShareMessageController [self.navigationController pushViewController:vc animated:YES]; } -- (void)showFeaturedImageSelector -{ - if (self.apost.featuredImage && self.featuredImage) { - FeaturedImageViewController *featuredImageVC; - if (self.animatedFeaturedImageData) { - featuredImageVC = [[FeaturedImageViewController alloc] initWithGifData:self.animatedFeaturedImageData]; - } else { - featuredImageVC = [[FeaturedImageViewController alloc] initWithImage:self.featuredImage]; - } - featuredImageVC.delegate = self; - UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:featuredImageVC]; - [self presentViewController:navigationController animated:YES completion:nil]; - } -} - - (void)showEditSlugController { SettingsMultiTextViewController *vc = [[SettingsMultiTextViewController alloc] initWithText:self.apost.slugForDisplay @@ -1238,4 +1223,14 @@ - (void)FeaturedImageViewControllerOnRemoveImageButtonPressed:(FeaturedImageView [self.featuredImageDelegate gutenbergDidRequestFeaturedImageId:nil]; } +- (void)removeFeaturedImage { + [WPAnalytics trackEvent:WPAnalyticsEventEditorPostFeaturedImageChanged properties:@{@"via": @"settings", @"action": @"removed"}]; + self.featuredImage = nil; + self.animatedFeaturedImageData = nil; + [self.apost setFeaturedImage:nil]; + [self dismissViewControllerAnimated:YES completion:nil]; + [self.tableView reloadData]; + [self.featuredImageDelegate gutenbergDidRequestFeaturedImageId:nil]; +} + @end diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h index d6ed3545b9cc..25cd07f874ba 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController_Internal.h @@ -28,4 +28,6 @@ typedef enum { @property (nullable, nonatomic, strong) WPProgressTableViewCell *progressCell; +- (void)removeFeaturedImage; + @end From 176ab0e1b6d57741954e80da61d21247411bed94 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 10:08:39 -0500 Subject: [PATCH 12/18] Remove FeaturedImageViewController (ObjC) --- .../Post/FeaturedImageViewController.h | 13 -- .../Post/FeaturedImageViewController.m | 127 ------------------ .../Post/PostSettingsViewController.m | 17 +-- 3 files changed, 2 insertions(+), 155 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.h delete mode 100644 WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.m diff --git a/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.h b/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.h deleted file mode 100644 index c7d4133fd7b2..000000000000 --- a/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.h +++ /dev/null @@ -1,13 +0,0 @@ -#import "WPImageViewController.h" - -@class FeaturedImageViewController; - -@protocol FeaturedImageViewControllerDelegate -- (void)FeaturedImageViewControllerOnRemoveImageButtonPressed:(FeaturedImageViewController *)controller; -@end - -@interface FeaturedImageViewController : WPImageViewController - -@property (weak, nonatomic) id delegate; - -@end diff --git a/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.m b/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.m deleted file mode 100644 index 7bbf9dcabca0..000000000000 --- a/WordPress/Classes/ViewRelated/Post/FeaturedImageViewController.m +++ /dev/null @@ -1,127 +0,0 @@ -#import "FeaturedImageViewController.h" - -#import "Media.h" -#import "WordPress-Swift.h" - - -@interface FeaturedImageViewController () - -@property (nonatomic, strong) NSURL *url; -@property (nonatomic, strong) UIImage *image; - -@property (nonatomic, strong) UIBarButtonItem *doneButton; -@property (nonatomic, strong) UIBarButtonItem *removeButton; - -@end - -@implementation FeaturedImageViewController - -@dynamic url; -@dynamic image; - -#pragma mark - Life Cycle Methods - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.title = NSLocalizedString(@"Featured Image", @"Title for the Featured Image view"); - self.view.backgroundColor = [UIColor murielBasicBackground]; - self.navigationItem.leftBarButtonItems = @[self.doneButton]; - self.navigationItem.rightBarButtonItems = @[self.removeButton]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - // Super class will hide the status bar by default - [self hideBars:NO animated:NO]; - - // Called here to be sure the view is complete in case we need to present a popover from the toolbar. - [self loadImage]; -} - -#pragma mark - Appearance Related Methods - -- (UIBarButtonItem *)doneButton -{ - if (!_doneButton) { - _doneButton = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Done", @"Label for confirm feature image of a post") - style:UIBarButtonItemStylePlain - target:self - action:@selector(confirmFeaturedImage)]; - } - return _doneButton; -} - -- (UIBarButtonItem *)removeButton -{ - if (!_removeButton) { - UIBarButtonItem *button = [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Remove", @"Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post.") - style:UIBarButtonItemStylePlain - target:self - action:@selector(removeFeaturedImage)]; - NSString *title = NSLocalizedString(@"Remove Featured Image", @"Accessibility Label for the Remove Feature Image icon. Tapping will show a confirmation screen for removing the feature image from the post."); - button.accessibilityLabel = title; - button.accessibilityIdentifier = @"Remove Featured Image"; - _removeButton = button; - } - - return _removeButton; -} - - -- (void)hideBars:(BOOL)hide animated:(BOOL)animated -{ - [super hideBars:hide animated:animated]; - - if (self.navigationController.navigationBarHidden != hide) { - [self.navigationController setNavigationBarHidden:hide animated:animated]; - } - - [self centerImage]; - [UIView animateWithDuration:0.3 animations:^{ - if (hide) { - self.view.backgroundColor = [UIColor blackColor]; - } else { - self.view.backgroundColor = [UIColor murielBasicBackground]; - } - }]; -} - -#pragma mark - Action Methods - -- (void)handleImageTapped:(UITapGestureRecognizer *)tgr -{ - BOOL hide = !self.navigationController.navigationBarHidden; - [self hideBars:hide animated:YES]; -} - -- (void)removeFeaturedImage -{ - UIAlertController *alertController = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Remove this Featured Image?", @"Prompt when removing a featured image from a post") - message:nil - preferredStyle:UIAlertControllerStyleActionSheet]; - [alertController addActionWithTitle:NSLocalizedString(@"Cancel", "Cancel a prompt") - style:UIAlertActionStyleCancel - handler:nil]; - [alertController addActionWithTitle:NSLocalizedString(@"Remove", @"Remove an image/posts/etc") - style:UIAlertActionStyleDestructive - handler:^(UIAlertAction * __unused alertAction) { - if (self.delegate) { - [self.delegate FeaturedImageViewControllerOnRemoveImageButtonPressed:self]; - } - }]; - alertController.popoverPresentationController.barButtonItem = self.removeButton; - [self presentViewController:alertController animated:YES completion:nil]; - -} - -- (void)confirmFeaturedImage -{ - [self.presentingViewController dismissViewControllerAnimated:YES completion:nil]; -} - - -@end diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 91ab80619591..afc9825e1b19 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -1,6 +1,5 @@ #import "PostSettingsViewController.h" #import "PostSettingsViewController_Internal.h" -#import "FeaturedImageViewController.h" #import "Media.h" #import "PostFeaturedImageCell.h" #import "SettingsSelectionViewController.h" @@ -53,8 +52,7 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { @interface PostSettingsViewController () +PostCategoriesViewControllerDelegate, PostFeaturedImageCellDelegate> @property (nonatomic, strong) AbstractPost *apost; @property (nonatomic, strong) NSArray *postMetaSectionRows; @@ -1210,18 +1208,7 @@ - (void)updateFeaturedImageCell:(PostFeaturedImageCell *)cell [self.tableView reloadSections:featuredImageSectionSet withRowAnimation:UITableViewRowAnimationNone]; } -#pragma mark - FeaturedImageViewControllerDelegate - -- (void)FeaturedImageViewControllerOnRemoveImageButtonPressed:(FeaturedImageViewController *)controller -{ - [WPAnalytics trackEvent:WPAnalyticsEventEditorPostFeaturedImageChanged properties:@{@"via": @"settings", @"action": @"removed"}]; - self.featuredImage = nil; - self.animatedFeaturedImageData = nil; - [self.apost setFeaturedImage:nil]; - [self dismissViewControllerAnimated:YES completion:nil]; - [self.tableView reloadData]; - [self.featuredImageDelegate gutenbergDidRequestFeaturedImageId:nil]; -} +#pragma mark - Featured Image - (void)removeFeaturedImage { [WPAnalytics trackEvent:WPAnalyticsEventEditorPostFeaturedImageChanged properties:@{@"via": @"settings", @"action": @"removed"}]; From 9f4da0d72302c6c6ed02f65f60ffd100485b4f59 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 10:38:29 -0500 Subject: [PATCH 13/18] Rewrite PostFeaturedImageCell --- .../ViewRelated/Cells/PostFeaturedImageCell.h | 21 ---- .../ViewRelated/Cells/PostFeaturedImageCell.m | 96 ------------------- .../Cells/PostFeaturedImageCell.swift | 27 ++++++ .../Post/PostSettingsViewController.m | 53 +--------- 4 files changed, 30 insertions(+), 167 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.h delete mode 100644 WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.m create mode 100644 WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift diff --git a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.h b/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.h deleted file mode 100644 index ccbca15a7e47..000000000000 --- a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.h +++ /dev/null @@ -1,21 +0,0 @@ -@import WordPressShared; - -@class AbstractPost; -@class PostFeaturedImageCell; - -@protocol PostFeaturedImageCellDelegate -- (void)postFeatureImageCellDidFinishLoadingImage:(nonnull PostFeaturedImageCell *)cell; -- (void)postFeatureImageCell:(nonnull PostFeaturedImageCell *)cell didFinishLoadingAnimatedImageWithData:(nullable NSData *)animationData; -- (void)postFeatureImageCell:(nonnull PostFeaturedImageCell *)cell didFinishLoadingImageWithError:(nullable NSError *)error; -@end - -@interface PostFeaturedImageCell : WPTableViewCell - -extern CGFloat const PostFeaturedImageCellMargin; - -@property (weak, nonatomic, nullable) id delegate; -@property (strong, nonatomic, readonly, nullable) UIImage *image; - -- (void)setImageWithURL:(nonnull NSURL *)url inPost:(nonnull AbstractPost *)post withSize:(CGSize)size; - -@end diff --git a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.m b/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.m deleted file mode 100644 index 40331ca999b7..000000000000 --- a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.m +++ /dev/null @@ -1,96 +0,0 @@ -#import "PostFeaturedImageCell.h" -#import "WordPress-Swift.h" - -CGFloat const PostFeaturedImageCellMargin = 15.0f; - -@interface PostFeaturedImageCell () - -@property (nonatomic, strong) CachedAnimatedImageView *featuredImageView; -@property (nonatomic, strong) ImageLoader *imageLoader; - -@end - -@implementation PostFeaturedImageCell - -- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier -{ - self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; - if (self) { - [self setup]; - } - return self; -} - -- (void)setup -{ - [self layoutImageView]; - _imageLoader = [[ImageLoader alloc] initWithImageView:self.featuredImageView gifStrategy:GIFStrategyLargeGIFs]; - self.accessibilityLabel = NSLocalizedString(@"A featured image is set. Tap to change it.", @"Label for image that is set as a feature image for post/page"); - self.accessibilityIdentifier = @"CurrentFeaturedImage"; -} - -- (void)setImageWithURL:(NSURL *)url inPost:(AbstractPost *)post withSize:(CGSize)size -{ - __weak PostFeaturedImageCell *weakSelf = self; - [self.imageLoader loadImageWithURL:url fromPost:post preferredSize:size placeholder:nil success:^{ - [weakSelf informDelegateImageLoaded]; - } error:^(NSError * _Nullable error) { - if (weakSelf && weakSelf.delegate) { - [weakSelf.delegate postFeatureImageCell:weakSelf didFinishLoadingImageWithError:error]; - } - }]; -} - -- (void)informDelegateImageLoaded -{ - if (self.delegate == nil) { - return; - } - - if (self.featuredImageView.animatedGifData) { - [self.delegate postFeatureImageCell:self didFinishLoadingAnimatedImageWithData:self.featuredImageView.animatedGifData]; - } else { - [self.delegate postFeatureImageCellDidFinishLoadingImage:self]; - } -} - -- (UIImage *)image -{ - return self.featuredImageView.image; -} - -- (void)prepareForReuse -{ - [super prepareForReuse]; - [self.featuredImageView prepForReuse]; -} - -#pragma mark - Helpers - -- (CachedAnimatedImageView *)featuredImageView -{ - if (!_featuredImageView) { - _featuredImageView = [[CachedAnimatedImageView alloc] init]; - _featuredImageView.contentMode = UIViewContentModeScaleAspectFill; - _featuredImageView.clipsToBounds = YES; - _featuredImageView.translatesAutoresizingMaskIntoConstraints = NO; - } - - return _featuredImageView; -} - -- (void)layoutImageView -{ - UIView *imageView = self.featuredImageView; - - [self.contentView addSubview:imageView]; - UILayoutGuide *readableGuide = self.contentView.readableContentGuide; - [NSLayoutConstraint activateConstraints:@[ - [imageView.leadingAnchor constraintEqualToAnchor:readableGuide.leadingAnchor], - [imageView.trailingAnchor constraintEqualToAnchor:readableGuide.trailingAnchor], - [imageView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:PostFeaturedImageCellMargin], - [imageView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-PostFeaturedImageCellMargin] - ]]; -} - -@end diff --git a/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift b/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift new file mode 100644 index 000000000000..461d23fd669f --- /dev/null +++ b/WordPress/Classes/ViewRelated/Cells/PostFeaturedImageCell.swift @@ -0,0 +1,27 @@ +import UIKit +import WordPressUI +import WordPressMedia + +final class PostFeaturedImageCell: UITableViewCell { + let featuredImageView = AsyncImageView() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + featuredImageView.configuration.loadingStyle = .spinner + + contentView.addSubview(featuredImageView) + featuredImageView.pinEdges() + NSLayoutConstraint.activate([ + featuredImageView.heightAnchor.constraint(equalTo: featuredImageView.widthAnchor, multiplier: ReaderPostCell.coverAspectRatio) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func setImage(withURL url: URL, post: AbstractPost) { + featuredImageView.setImage(with: url, host: MediaHost(post)) + } +} diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index afc9825e1b19..711d82dbce99 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -1,7 +1,6 @@ #import "PostSettingsViewController.h" #import "PostSettingsViewController_Internal.h" #import "Media.h" -#import "PostFeaturedImageCell.h" #import "SettingsSelectionViewController.h" #import "SharingDetailViewController.h" #import "WPTableViewActivityCell.h" @@ -40,7 +39,6 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { }; static CGFloat CellHeight = 44.0f; -static CGFloat LoadingIndicatorHeight = 28.0f; static NSString *const PostSettingsAnalyticsTrackingSource = @"post_settings"; static NSString *const TableViewActivityCellIdentifier = @"TableViewActivityCellIdentifier"; @@ -52,13 +50,12 @@ typedef NS_ENUM(NSInteger, PostSettingsRow) { @interface PostSettingsViewController () +PostCategoriesViewControllerDelegate> @property (nonatomic, strong) AbstractPost *apost; @property (nonatomic, strong) NSArray *postMetaSectionRows; @property (nonatomic, strong) NSArray *formatsList; @property (nonatomic, strong) UIImage *featuredImage; -@property (nonatomic, strong) NSData *animatedFeaturedImageData; @property (nonatomic, readonly) CGSize featuredImageSize; @@ -443,11 +440,7 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa if (sectionId == PostSettingsSectionFeaturedImage) { if ([self isUploadingMedia]) { - return CellHeight + (2.f * PostFeaturedImageCellMargin); - } else if (self.featuredImage) { - return self.featuredImageSize.height + 2.f * PostFeaturedImageCellMargin; - } else { - return LoadingIndicatorHeight + 2.f * PostFeaturedImageCellMargin; + return CellHeight; } } @@ -717,10 +710,7 @@ - (UITableViewCell *)cellForFeaturedImageUploadProgressAtIndexPath:(NSIndexPath - (UITableViewCell *)cellForFeaturedImageWithURL:(nonnull NSURL *)featuredURL atIndexPath:(NSIndexPath *)indexPath { PostFeaturedImageCell *featuredImageCell = [self.tableView dequeueReusableCellWithIdentifier:TableViewFeaturedImageCellIdentifier forIndexPath:indexPath]; - featuredImageCell.delegate = self; - [WPStyleGuide configureTableViewCell:featuredImageCell]; - - [featuredImageCell setImageWithURL:featuredURL inPost:self.apost withSize:self.featuredImageSize]; + [featuredImageCell setImageWithURL:featuredURL post:self.apost]; featuredImageCell.tag = PostSettingsRowFeaturedImage; return featuredImageCell; } @@ -1090,7 +1080,6 @@ - (void)showTagsPicker - (CGSize)featuredImageSize { CGFloat width = CGRectGetWidth(self.view.frame); - width = width - (PostFeaturedImageCellMargin * 2); // left and right cell margins CGFloat height = ceilf(width * 0.66); return CGSizeMake(width, height); } @@ -1173,47 +1162,11 @@ - (void)postCategoriesViewController:(PostCategoriesViewController *)controller } } -#pragma mark - PostFeaturedImageCellDelegate - -- (void)postFeatureImageCell:(PostFeaturedImageCell *)cell didFinishLoadingAnimatedImageWithData:(NSData *)animationData -{ - if (self.animatedFeaturedImageData == nil) { - self.animatedFeaturedImageData = animationData; - [self updateFeaturedImageCell:cell]; - } -} - -- (void)postFeatureImageCellDidFinishLoadingImage:(PostFeaturedImageCell *)cell -{ - self.animatedFeaturedImageData = nil; - if (!self.featuredImage) { - [self updateFeaturedImageCell:cell]; - } -} - -- (void)postFeatureImageCell:(PostFeaturedImageCell *)cell didFinishLoadingImageWithError:(NSError *)error -{ - self.featuredImage = nil; - if (error) { - NSIndexPath *featureImageCellPath = [NSIndexPath indexPathForRow:0 inSection:[self.sections indexOfObject:@(PostSettingsSectionFeaturedImage)]]; - [self featuredImageFailedLoading:featureImageCellPath withError:error]; - } -} - -- (void)updateFeaturedImageCell:(PostFeaturedImageCell *)cell -{ - self.featuredImage = cell.image; - NSInteger featuredImageSection = [self.sections indexOfObject:@(PostSettingsSectionFeaturedImage)]; - NSIndexSet *featuredImageSectionSet = [NSIndexSet indexSetWithIndex:featuredImageSection]; - [self.tableView reloadSections:featuredImageSectionSet withRowAnimation:UITableViewRowAnimationNone]; -} - #pragma mark - Featured Image - (void)removeFeaturedImage { [WPAnalytics trackEvent:WPAnalyticsEventEditorPostFeaturedImageChanged properties:@{@"via": @"settings", @"action": @"removed"}]; self.featuredImage = nil; - self.animatedFeaturedImageData = nil; [self.apost setFeaturedImage:nil]; [self dismissViewControllerAnimated:YES completion:nil]; [self.tableView reloadData]; From e5bee355d2b18b54311b9033f37fc13cc1af156c Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 11:01:25 -0500 Subject: [PATCH 14/18] Integrate LightboxViewController in ReaderCommentsViewController --- .../Comments/ReaderCommentsViewController.m | 39 +------------------ .../ReaderCommentsViewController.swift | 28 ++++++++++++- 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m index d7b27018dace..2caa6da68a6d 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.m @@ -5,7 +5,6 @@ #import "ReaderPost.h" #import "ReaderPostService.h" #import "UIView+Subviews.h" -#import "WPImageViewController.h" #import "WPTableViewHandler.h" #import "SuggestionsTableView.h" #import "WordPress-Swift.h" @@ -1269,30 +1268,12 @@ - (BOOL)textView:(UITextView *)textView shouldInteractWithURL:(NSURL *)URL inRan - (void)richContentView:(WPRichContentView *)richContentView didReceiveImageAction:(WPRichTextImage *)image { - UIViewController *controller = nil; - BOOL isSupportedNatively = [WPImageViewController isUrlSupported:image.linkURL]; - - if (image.imageView.animatedGifData) { - controller = [[WPImageViewController alloc] initWithGifData:image.imageView.animatedGifData]; - } else if (isSupportedNatively) { - controller = [[WPImageViewController alloc] initWithImage:image.imageView.image andURL:image.linkURL]; - } else if (image.linkURL) { - [self presentWebViewControllerWithURL:image.linkURL]; - return; - } else if (image.imageView.image) { - controller = [[WPImageViewController alloc] initWithImage:image.imageView.image]; - } - - if (controller) { - controller.modalTransitionStyle = UIModalTransitionStyleCrossDissolve; - controller.modalPresentationStyle = UIModalPresentationFullScreen; - [self presentViewController:controller animated:YES completion:nil]; - } + [self showFullScreenImage:image from:richContentView]; } - (void)interactWithURL:(NSURL *)URL { - [self presentWebViewControllerWithURL:URL]; + [self presentWebViewControllerWith:URL]; } - (BOOL)richContentViewShouldUpdateLayoutForAttachments:(WPRichContentView *)richContentView @@ -1310,22 +1291,6 @@ - (void)richContentViewDidUpdateLayoutForAttachments:(WPRichContentView *)richCo [self updateTableViewForAttachments]; } -- (void)presentWebViewControllerWithURL:(NSURL *)URL -{ - NSURL *linkURL = URL; - NSURLComponents *components = [NSURLComponents componentsWithString:[URL absoluteString]]; - if (!components.host) { - linkURL = [components URLRelativeToURL:[NSURL URLWithString:self.post.blogURL]]; - } - - WebViewControllerConfiguration *configuration = [[WebViewControllerConfiguration alloc] initWithUrl:linkURL]; - [configuration authenticateWithDefaultAccount]; - [configuration setAddsWPComReferrer:YES]; - UIViewController *webViewController = [WebViewControllerFactory controllerWithConfiguration:configuration source:@"reader_comments"]; - UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:webViewController]; - [self presentViewController:navController animated:YES completion:nil]; -} - - (void)textViewDidChangeSelection:(UITextView *)textView { if (!textView.selectedTextRange.isEmpty) { diff --git a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift index 53fe1a417810..5cd7277542ad 100644 --- a/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift +++ b/WordPress/Classes/ViewRelated/Reader/Comments/ReaderCommentsViewController.swift @@ -8,7 +8,7 @@ extension NSNotification.Name { static let ReaderCommentModifiedNotification = NSNotification.Name(rawValue: "ReaderCommentModifiedNotification") } -@objc public extension ReaderCommentsViewController { +@objc extension ReaderCommentsViewController { func shouldShowSuggestions(for siteID: NSNumber?) -> Bool { guard let siteID, let blog = Blog.lookup(withID: siteID, in: ContextManager.shared.mainContext) else { return false } return SuggestionService.shared.shouldShowSuggestions(for: blog) @@ -26,6 +26,32 @@ extension NSNotification.Name { navigationController?.pushViewController(controller, animated: true) } + @objc func showFullScreenImage(_ image: WPRichTextImage, from contentView: WPRichContentView) { + if let contentURL = image.contentURL { + let lightboxVC = LightboxViewController(sourceURL: contentURL) + lightboxVC.configureZoomTransition() + present(lightboxVC, animated: true) + } else if let linkURL = image.linkURL { + presentWebViewController(with: linkURL) + } + } + + @objc func presentWebViewController(with url: URL) { + var linkURL = url + if let components = URLComponents(string: url.absoluteString), components.host == nil { + linkURL = components.url(relativeTo: URL(string: self.post.blogURL)) ?? linkURL + } + let configuration = WebViewControllerConfiguration(url: linkURL) + configuration.authenticateWithDefaultAccount() + configuration.addsWPComReferrer = true + let webVC = WebViewControllerFactory.controller( + configuration: configuration, + source: "reader_comments" + ) + let navigationVC = UINavigationController(rootViewController: webVC) + self.present(navigationVC, animated: true, completion: nil) + } + // MARK: New Comment Threads func configuredHeaderView(for tableView: UITableView) -> UIView { From 19b004a2416cb2689811045f46546c0984f2a3a0 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 11:38:58 -0500 Subject: [PATCH 15/18] Update WPRichTextImage to use AsyncImageView --- .../Utility/Media/AsyncImageView.swift | 10 +++ .../Views/WPRichText/WPRichContentView.swift | 2 +- .../Views/WPRichText/WPRichTextImage.swift | 71 +++++-------------- .../WPRichTextMediaAttachment.swift | 6 +- 4 files changed, 31 insertions(+), 58 deletions(-) diff --git a/WordPress/Classes/Utility/Media/AsyncImageView.swift b/WordPress/Classes/Utility/Media/AsyncImageView.swift index b7bcea67c2f2..5f071c43ae31 100644 --- a/WordPress/Classes/Utility/Media/AsyncImageView.swift +++ b/WordPress/Classes/Utility/Media/AsyncImageView.swift @@ -30,6 +30,8 @@ final class AsyncImageView: UIView { /// By default, `background`. var loadingStyle = LoadingStyle.background + + var passTouchesToSuperview = false } var configuration = Configuration() { @@ -145,6 +147,14 @@ final class AsyncImageView: UIView { self.errorView = errorView return errorView } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if configuration.passTouchesToSuperview && self.bounds.contains(point) { + // Pass the touch to the superview + return nil + } + return super.hitTest(point, with: event) + } } extension GIFImageView { diff --git a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift index e9901f908a62..223f26f637bc 100644 --- a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift +++ b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichContentView.swift @@ -268,7 +268,7 @@ extension WPRichContentView: WPTextAttachmentManagerDelegate { /// fileprivate func richTextImage(with size: CGSize, _ url: URL, _ attachment: WPTextAttachment) -> WPRichTextImage { let image = WPRichTextImage(frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) - image.addTarget(self, action: #selector(type(of: self).handleImageTapped(_:)), for: .touchUpInside) + image.addTarget(self, action: #selector(handleImageTapped), for: .touchUpInside) image.contentURL = url image.linkURL = linkURLForImageAttachment(attachment) return image diff --git a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift index 1eed7a9b8b71..3853c08f8701 100644 --- a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift +++ b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextImage.swift @@ -1,22 +1,17 @@ import UIKit import WordPressMedia +import Gifu -open class WPRichTextImage: UIControl, WPRichTextMediaAttachment { +class WPRichTextImage: UIControl, WPRichTextMediaAttachment { // MARK: Properties var contentURL: URL? var linkURL: URL? - @objc fileprivate(set) var imageView: CachedAnimatedImageView + @objc fileprivate(set) var imageView: AsyncImageView - fileprivate lazy var imageLoader: ImageLoader = { - let imageLoader = ImageLoader(imageView: imageView, gifStrategy: .largeGIFs) - imageLoader.photonQuality = Constants.readerPhotonQuality - return imageLoader - }() - - override open var frame: CGRect { + override var frame: CGRect { didSet { // If Voice Over is enabled, the OS will query for the accessibilityPath // to know what region of the screen to highlight. If the path is nil @@ -28,12 +23,9 @@ open class WPRichTextImage: UIControl, WPRichTextMediaAttachment { // MARK: Lifecycle - deinit { - imageView.clean() - } - override init(frame: CGRect) { - imageView = CachedAnimatedImageView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height)) + imageView = AsyncImageView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height)) + imageView.configuration.passTouchesToSuperview = true imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] imageView.contentMode = .scaleAspectFit imageView.isAccessibilityElement = true @@ -43,26 +35,8 @@ open class WPRichTextImage: UIControl, WPRichTextMediaAttachment { addSubview(imageView) } - required public init?(coder aDecoder: NSCoder) { - imageView = aDecoder.decodeObject(forKey: UIImage.classNameWithoutNamespaces()) as! CachedAnimatedImageView - contentURL = aDecoder.decodeObject(forKey: "contentURL") as! URL? - linkURL = aDecoder.decodeObject(forKey: "linkURL") as! URL? - - super.init(coder: aDecoder) - } - - override open func encode(with aCoder: NSCoder) { - aCoder.encode(imageView, forKey: UIImage.classNameWithoutNamespaces()) - - if let url = contentURL { - aCoder.encode(url, forKey: "contentURL") - } - - if let url = linkURL { - aCoder.encode(url, forKey: "linkURL") - } - - super.encode(with: aCoder) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } // MARK: Public Methods @@ -83,33 +57,22 @@ open class WPRichTextImage: UIControl, WPRichTextMediaAttachment { return } - let successHandler: (() -> Void)? = { - onSuccess?() - } - - let errorHandler: ((Error?) -> Void)? = { error in - onError?(error) + imageView.setImage(with: contentURL, host: host) { result in + switch result { + case .success: onSuccess?() + case .failure(let error): onError?(error) + } } - - imageLoader.loadImage(with: contentURL, from: host, preferredSize: size, placeholder: nil, success: successHandler, error: errorHandler) } func contentSize() -> CGSize { - let size = imageView.intrinsicContentSize - guard size.height > 0, size.width > 0 else { - return CGSize(width: 1.0, height: 1.0) + guard let size = imageView.image?.size, size.height > 0, size.width > 0 else { + return CGSize(width: 44.0, height: 44.0) } - return imageView.intrinsicContentSize + return size } func clean() { - imageView.clean() - imageView.prepForReuse() - } -} - -private extension WPRichTextImage { - enum Constants { - static let readerPhotonQuality: UInt = 65 + imageView.prepareForReuse() } } diff --git a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextMediaAttachment.swift b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextMediaAttachment.swift index 6d549bd0c03c..d8b0e5a3bd2d 100644 --- a/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextMediaAttachment.swift +++ b/WordPress/Classes/ViewRelated/Views/WPRichText/WPRichTextMediaAttachment.swift @@ -1,8 +1,8 @@ import Foundation @objc protocol WPRichTextMediaAttachment: NSObjectProtocol { - var contentURL: URL? {get set} - var linkURL: URL? {get set} - var frame: CGRect {get set} + var contentURL: URL? { get set } + var linkURL: URL? { get set } + var frame: CGRect { get set } func contentSize() -> CGSize } From 496d1789af5f59edc87bb80e49e5d0599b70b105 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 11:43:42 -0500 Subject: [PATCH 16/18] Automatically pick thumbnail when available --- .../Media/Lightbox/LightboxViewController.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift index 3904df25c070..ec1058a8fe75 100644 --- a/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift +++ b/WordPress/Classes/ViewRelated/Media/Lightbox/LightboxViewController.swift @@ -106,7 +106,23 @@ final class LightboxViewController: UIViewController { func configureZoomTransition(sourceView: UIView? = nil) { configureZoomTransition { _ in sourceView } + if let sourceView, thumbnail == nil { + MainActor.assumeIsolated { + thumbnail = getThumbnail(fromSourceView: sourceView) + } + } + } +} + +@MainActor +private func getThumbnail(fromSourceView sourceView: UIView) -> UIImage? { + if let imageView = sourceView as? AsyncImageView { + return imageView.image + } + if let imageView = sourceView as? UIImageView { + return imageView.image } + return nil } @available(iOS 17, *) From 02415962406131f9f4cb4d3a81d42f4f00584874 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 11:47:35 -0500 Subject: [PATCH 17/18] Remove WPImageViewController --- .../System/WordPress-Bridging-Header.h | 1 - .../WPImageViewController+Swift.swift | 21 - .../Controllers/WPImageViewController.h | 33 -- .../Controllers/WPImageViewController.m | 536 ------------------ 4 files changed, 591 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController+Swift.swift delete mode 100644 WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.h delete mode 100644 WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.m diff --git a/WordPress/Classes/System/WordPress-Bridging-Header.h b/WordPress/Classes/System/WordPress-Bridging-Header.h index 53304de77aec..cce86c7a3683 100644 --- a/WordPress/Classes/System/WordPress-Bridging-Header.h +++ b/WordPress/Classes/System/WordPress-Bridging-Header.h @@ -85,7 +85,6 @@ #import "WPAuthTokenIssueSolver.h" #import "WPUploadStatusButton.h" #import "WPError.h" -#import "WPImageViewController.h" #import "WPStyleGuide+Pages.h" #import "WPStyleGuide+WebView.h" #import "WPTableViewHandler.h" diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController+Swift.swift b/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController+Swift.swift deleted file mode 100644 index 5101200f9094..000000000000 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController+Swift.swift +++ /dev/null @@ -1,21 +0,0 @@ -import UIKit -import WordPressMedia - -extension WPImageViewController { - @objc func loadOriginalImage(for media: Media, success: @escaping (UIImage) -> Void, failure: @escaping (Error) -> Void) { - Task { @MainActor in - do { - let image = try await MediaImageService.shared.image(for: media, size: .original) - success(image) - } catch { - failure(error) - } - } - } - - @objc func startAnimationIfNeeded(for image: UIImage, in imageView: CachedAnimatedImageView?) { - if let gif = image as? AnimatedImage, let data = gif.gifData { - imageView?.animate(withGIFData: data) - } - } -} diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.h b/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.h deleted file mode 100644 index 09e4d8252ad7..000000000000 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.h +++ /dev/null @@ -1,33 +0,0 @@ -#import - -@import Photos; - -@class Media; -@class AbstractPost; -@class ReaderPost; - -NS_ASSUME_NONNULL_BEGIN -@interface WPImageViewController : UIViewController - -@property (nonatomic, assign) BOOL shouldDismissWithGestures; -@property (nonatomic, weak) AbstractPost* post; -@property (nonatomic, weak) ReaderPost* readerPost; - -- (instancetype)initWithImage:(UIImage *)image; -- (instancetype)initWithURL:(NSURL *)url; -- (instancetype)initWithMedia:(Media *)media; - -- (instancetype)initWithGifData:(NSData *)data; -- (instancetype)initWithExternalMediaURL:(NSURL *)url; - -- (instancetype)initWithImage:(nullable UIImage *)image andURL:(nullable NSURL *)url; -- (instancetype)initWithImage:(nullable UIImage *)image andMedia:(nullable Media *)media; - -- (void)loadImage; -- (void)hideBars:(BOOL)hide animated:(BOOL)animated; -- (void)centerImage; - -+ (BOOL)isUrlSupported:(NSURL *)url; - -@end -NS_ASSUME_NONNULL_END diff --git a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.m b/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.m deleted file mode 100644 index a6fa1e690641..000000000000 --- a/WordPress/Classes/ViewRelated/Reader/Controllers/WPImageViewController.m +++ /dev/null @@ -1,536 +0,0 @@ -#import "WPImageViewController.h" -#import "WordPress-Swift.h" -@import Gridicons; - -static CGFloat const MaximumZoomScale = 4.0; -static CGFloat const MinimumZoomScale = 0.1; - -@interface WPImageViewController () - -@property (nonatomic, strong) NSURL *url; -@property (nonatomic, strong) UIImage *image; -@property (nonatomic, strong) Media *media; -@property (nonatomic, strong) NSData *data; -@property (nonatomic) BOOL isExternal; - -@property (nonatomic, assign) BOOL isLoadingImage; -@property (nonatomic, assign) BOOL isFirstLayout; -@property (nonatomic, strong) UIScrollView *scrollView; -@property (nonatomic, strong) CachedAnimatedImageView *imageView; -@property (nonatomic, strong) ImageLoader *imageLoader; -@property (nonatomic, assign) BOOL shouldHideStatusBar; -@property (nonatomic, strong) CircularProgressView *activityIndicatorView; - -@property (nonatomic) FlingableViewHandler *flingableViewHandler; -@property (nonatomic, strong) UITapGestureRecognizer *singleTapGesture; -@property (nonatomic, strong) UITapGestureRecognizer *doubleTapGesture; - -@end - -@implementation WPImageViewController - -#pragma mark - LifeCycle Methods - -- (instancetype)initWithImage:(UIImage *)image -{ - return [self initWithImage:image andURL:nil]; -} - -- (instancetype)initWithURL:(NSURL *)url -{ - return [self initWithImage:nil andURL:url]; -} - -- (instancetype)initWithMedia:(Media *)media -{ - return [self initWithImage:nil andMedia:media]; -} - -- (instancetype)initWithGifData:(NSData *)data -{ - self = [super init]; - if (self) { - _data = data; - [self commonInit]; - } - return self; -} - -- (instancetype)initWithImage:(UIImage *)image andURL:(NSURL *)url -{ - self = [super init]; - if (self) { - _image = [image copy]; - _url = url; - [self commonInit]; - } - return self; -} - -- (instancetype)initWithImage:(UIImage *)image andMedia:(Media *)media -{ - self = [super init]; - if (self) { - _image = [image copy]; - _media = media; - [self commonInit]; - } - return self; -} - -- (instancetype)initWithExternalMediaURL:(NSURL *)url -{ - self = [super init]; - if (self) { - _image = nil; - _url = url; - _isExternal = YES; - [self commonInit]; - } - return self; -} - -- (void)commonInit -{ - _shouldDismissWithGestures = YES; - _isFirstLayout = YES; -} - -- (void)setIsLoadingImage:(BOOL)isLoadingImage -{ - _isLoadingImage = isLoadingImage; - - if (isLoadingImage) { - [self.activityIndicatorView startAnimating]; - } else { - [self.activityIndicatorView stopAnimating]; - } -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - - self.view.backgroundColor = [UIColor blackColor]; - CGRect frame = self.view.frame; - frame = CGRectMake(0.0f, 0.0f, frame.size.width, frame.size.height); - - [self setupScrollView:frame]; - [self setupImageViewWidth:frame]; - [self setupImageLoader]; - - self.doubleTapGesture = [self setupTapGestureWithNumberOfTaps:2 onView:self.imageView]; - self.singleTapGesture = [self setupTapGestureWithNumberOfTaps:1 onView:self.scrollView]; - [self.singleTapGesture requireGestureRecognizerToFail:self.doubleTapGesture]; - - [self setupFlingableView]; - [self setupActivityIndicator]; - [self layoutActivityIndicator]; - - [self setupAccessibility]; - - [self loadImage]; -} - -- (void)setupActivityIndicator -{ - self.activityIndicatorView = [[CircularProgressView alloc] initWithStyle:CircularProgressViewStyleWhite]; - AccessoryView *errorView = [[AccessoryView alloc] init]; - errorView.imageView.image = [UIImage gridiconOfType:GridiconTypeNoticeOutline]; - errorView.label.text = NSLocalizedString(@"Error", @"Generic error."); - self.activityIndicatorView.errorView = errorView; -} - -- (void)layoutActivityIndicator -{ - self.activityIndicatorView.translatesAutoresizingMaskIntoConstraints = NO; - [self.view addSubview:self.activityIndicatorView]; - NSArray *constraints = @[ - [self.activityIndicatorView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor], - [self.activityIndicatorView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor] - ]; - - [NSLayoutConstraint activateConstraints:constraints]; -} - -- (void)setupFlingableView -{ - self.flingableViewHandler = [[FlingableViewHandler alloc] initWithTargetView:self.scrollView]; - self.flingableViewHandler.delegate = self; - self.flingableViewHandler.isActive = self.shouldDismissWithGestures; -} - -- (UITapGestureRecognizer *)setupTapGestureWithNumberOfTaps:(NSInteger)taps onView:(UIView*)view -{ - UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(handleTapGesture:)]; - [gesture setNumberOfTapsRequired:taps]; - [view addGestureRecognizer:gesture]; - return gesture; -} - -- (void)setupScrollView:(CGRect)frame { - self.scrollView = [[UIScrollView alloc] initWithFrame:frame]; - self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; - self.scrollView.maximumZoomScale = MaximumZoomScale; - self.scrollView.minimumZoomScale = MinimumZoomScale; - self.scrollView.scrollsToTop = NO; - self.scrollView.delegate = self; - [self.view addSubview:self.scrollView]; -} - -- (void)setupImageViewWidth:(CGRect)frame -{ - self.imageView = [[CachedAnimatedImageView alloc] initWithFrame:frame]; - self.imageView.gifStrategy = GIFStrategyLargeGIFs; - self.imageView.contentMode = UIViewContentModeScaleAspectFit; - self.imageView.shouldShowLoadingIndicator = NO; - self.imageView.userInteractionEnabled = YES; - [self.scrollView addSubview:self.imageView]; -} - -- (void)setupImageLoader -{ - self.imageLoader = [[ImageLoader alloc] initWithImageView:self.imageView gifStrategy:GIFStrategyLargeGIFs]; -} - -- (void)loadImage -{ - if (self.isLoadingImage) { - return; - } - - if (self.image != nil) { - [self updateImageView]; - } else if (self.url && self.isExternal) { - [self loadImageFromExternalURL]; - } else if (self.url) { - [self loadImageFromURL]; - } else if (self.media) { - [self loadImageFromMedia]; - } else if (self.data) { - [self loadImageFromGifData]; - } -} - -- (void)updateImageView -{ - self.imageView.image = self.image; - [self.imageView sizeToFit]; - self.scrollView.contentSize = self.imageView.image.size; - [self centerImage]; - -} - -- (void)loadImageFromURL -{ - self.isLoadingImage = YES; - __weak __typeof__(self) weakSelf = self; - if (self.readerPost != NULL) { - [self.imageLoader loadImageWithURL:self.url fromReaderPost:self.readerPost preferredSize:CGSizeZero placeholder:self.image success:^{ - weakSelf.isLoadingImage = NO; - weakSelf.image = weakSelf.imageView.image; - [weakSelf updateImageView]; - } error:^(NSError * _Nullable error) { - [weakSelf.activityIndicatorView showError]; - DDLogError(@"Error loading image: %@", error); - }]; - } else { - [_imageView downloadImageUsingRequest:[NSURLRequest requestWithURL:self.url] - placeholderImage:self.image - success:^(UIImage *image) { - weakSelf.image = image; - [weakSelf updateImageView]; - weakSelf.isLoadingImage = NO; - } failure:^(NSError *error) { - DDLogError(@"Error loading image: %@", error); - [weakSelf.activityIndicatorView showError]; - }]; - } -} - -- (void)loadImageFromMedia -{ - self.imageView.image = self.image; - self.isLoadingImage = YES; - [self.activityIndicatorView startAnimating]; - - __weak __typeof__(self) weakSelf = self; - [self loadOriginalImageFor:self.media success:^(UIImage * _Nonnull image) { - weakSelf.isLoadingImage = NO; - weakSelf.image = image; - [weakSelf updateImageView]; - [weakSelf startAnimationIfNeededFor:image in:weakSelf.imageView]; - } failure:^(NSError * _Nonnull error) { - [weakSelf.activityIndicatorView showError]; - DDLogError(@"Error loading image: %@", error); - }]; -} - -- (void)loadImageFromGifData -{ - self.isLoadingImage = YES; - - __weak __typeof__(self) weakSelf = self; - dispatch_async(dispatch_get_main_queue(), ^{ - self.image = [[UIImage alloc] initWithData: self.data]; - [weakSelf updateImageView]; - }); - [self.imageView setAnimatedImage:self.data success:^{ - dispatch_async(dispatch_get_main_queue(), ^{ - weakSelf.isLoadingImage = NO; - }); - }]; -} - -- (void)loadImageFromExternalURL -{ - self.isLoadingImage = YES; - - __weak __typeof__(self) weakSelf = self; - [self.imageLoader loadImageWithURL:self.url - fromPost:self.post - preferredSize:CGSizeZero - placeholder:nil - success:^{ - weakSelf.isLoadingImage = NO; - weakSelf.image = weakSelf.imageView.image; - [weakSelf updateImageView]; - } error:^(NSError * _Nullable error) { - [weakSelf.activityIndicatorView showError]; - DDLogError(@"Error loading image: %@", error); - }]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self hideBars:YES animated:animated]; -} - -- (void)viewDidLayoutSubviews -{ - [super viewDidLayoutSubviews]; - if (self.isFirstLayout) { - [self centerImage]; - self.isFirstLayout = NO; - } -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - [self hideBars:NO animated:animated]; -} - -- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id)coordinator -{ - [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; - [coordinator animateAlongsideTransition:^(id _Nonnull __unused context) { - [self centerImage]; - } completion:nil]; -} - -- (BOOL)prefersHomeIndicatorAutoHidden -{ - return self.shouldHideStatusBar; -} - -#pragma mark - Instance Methods - -- (void)setShouldDismissWithGestures:(BOOL)shouldDismissWithGestures -{ - _shouldDismissWithGestures = shouldDismissWithGestures; - self.flingableViewHandler.isActive = shouldDismissWithGestures; -} - -- (void)hideBars:(BOOL)hide animated:(BOOL)animated -{ - self.shouldHideStatusBar = hide; - - // Force an update of the status bar appearance and visiblity - if (animated) { - [UIView animateWithDuration:0.3 - animations:^{ - [self setNeedsStatusBarAppearanceUpdate]; - [self setNeedsUpdateOfHomeIndicatorAutoHidden]; - }]; - } else { - [self setNeedsStatusBarAppearanceUpdate]; - - [self setNeedsUpdateOfHomeIndicatorAutoHidden]; - } -} - -- (void)centerImage -{ - CGFloat scaleWidth = CGRectGetWidth(self.scrollView.frame) / self.imageView.image.size.width; - CGFloat scaleHeight = CGRectGetHeight(self.scrollView.frame) / self.imageView.image.size.height; - - self.scrollView.minimumZoomScale = MIN(scaleWidth, scaleHeight); - self.scrollView.zoomScale = self.scrollView.minimumZoomScale; - - [self scrollViewDidZoom:self.scrollView]; -} - -- (void)handleTapGesture:(UITapGestureRecognizer *)tapGesture -{ - if ([tapGesture isEqual:self.singleTapGesture]) { - [self handleImageTappedWith:tapGesture]; - } else if ([tapGesture isEqual:self.doubleTapGesture]) { - [self handleImageDoubleTappedWidth:tapGesture]; - } -} - -- (void)handleImageTappedWith:(UITapGestureRecognizer *)tgr -{ - if (self.shouldDismissWithGestures) { - [self dismissViewControllerAnimated:YES completion:nil]; - } -} - -- (void)handleImageDoubleTappedWidth:(UITapGestureRecognizer *)tgr -{ - if (self.scrollView.zoomScale > self.scrollView.minimumZoomScale) { - [self.scrollView setZoomScale:self.scrollView.minimumZoomScale animated:YES]; - return; - } - - CGPoint point = [tgr locationInView:self.imageView]; - CGSize size = self.scrollView.frame.size; - - CGFloat w = size.width / self.scrollView.maximumZoomScale; - CGFloat h = size.height / self.scrollView.maximumZoomScale; - CGFloat x = point.x - (w / 2.0f); - CGFloat y = point.y - (h / 2.0f); - - CGRect rect = CGRectMake(x, y, w, h); - [self.scrollView zoomToRect:rect animated:YES]; -} - -#pragma mark - UIScrollView Delegate - -- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView -{ - if (self.imageView.image) { - return self.imageView; - } - return nil; -} - -- (void)scrollViewDidZoom:(UIScrollView *)scrollView -{ - CGSize size = scrollView.frame.size; - CGRect frame = self.imageView.frame; - - if (frame.size.width < size.width) { - frame.origin.x = (size.width - frame.size.width) / 2; - } else { - frame.origin.x = 0; - } - - if (frame.size.height < size.height) { - frame.origin.y = (size.height - frame.size.height) / 2; - } else { - frame.origin.y = 0; - } - - self.imageView.frame = frame; - - [self updateFlingableViewHandlerActiveState]; -} - -- (void)updateFlingableViewHandlerActiveState -{ - if (!self.shouldDismissWithGestures) { - return; - } - BOOL isScrollViewZoomedOut = (self.scrollView.zoomScale == self.scrollView.minimumZoomScale); - - self.flingableViewHandler.isActive = isScrollViewZoomedOut; -} - -#pragma mark - Status bar management - -- (BOOL)prefersStatusBarHidden -{ - return self.shouldHideStatusBar; -} - -- (UIStatusBarStyle)preferredStatusBarStyle -{ - return UIStatusBarStyleLightContent; -} - -- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation -{ - return UIStatusBarAnimationFade; -} - -#pragma mark - Static Helpers - -+ (BOOL)isUrlSupported:(NSURL *)url -{ - // Safeguard - if (!url) { - return NO; - } - - // We only support: PNG + JPG + JPEG + GIF - NSString *absoluteURL = url.absoluteString; - - NSArray *types = @[@".png", @".jpg", @".gif", @".jpeg"]; - for (NSString *type in types) { - if (NSNotFound != [[absoluteURL lowercaseString] rangeOfString:type].location) { - return YES; - } - } - - return NO; -} - -#pragma mark - FlingableViewHandlerDelegate - -- (void)flingableViewHandlerDidBeginRecognizingGesture:(FlingableViewHandler *)handler -{ - self.scrollView.multipleTouchEnabled = NO; -} - -- (void)flingableViewHandlerDidEndRecognizingGesture:(FlingableViewHandler *)handler { - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self dismissViewControllerAnimated:YES completion:nil]; - }); -} - -- (void)flingableViewHandlerWasCancelled:(FlingableViewHandler *)handler -{ - self.scrollView.multipleTouchEnabled = YES; -} - -#pragma mark - Accessibility - -- (void)setupAccessibility -{ - self.imageView.isAccessibilityElement = YES; - self.imageView.accessibilityTraits = UIAccessibilityTraitImage; - - if (self.media != nil && self.media.title != nil) { - self.imageView.accessibilityLabel = [NSString stringWithFormat:NSLocalizedString(@"Fullscreen view of image %@. Double tap to dismiss", @"Accessibility label for when image is shown to user in full screen, with instructions on how to dismiss the screen. Placeholder is the title of the image"), self.media.title]; - } - else { - self.imageView.accessibilityLabel = NSLocalizedString(@"Fullscreen view of image. Double tap to dismiss", @"Accessibility label for when image is shown to user in full screen, with instructions on how to dismiss the screen"); - } - -} - -- (BOOL)accessibilityPerformEscape -{ - // Dismiss when self receives the VoiceOver escape gesture (Z). This does not seem to happen - // automatically if self is presented modally by itself (i.e. not inside a - // UINavigationController). - [self dismissViewControllerAnimated:YES completion:nil]; - return YES; -} - -@end From 9e825446b55236b07785e32c9d44b61fbf4fe665 Mon Sep 17 00:00:00 2001 From: kean Date: Tue, 24 Dec 2024 11:53:27 -0500 Subject: [PATCH 18/18] Update release notes --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 2a225326cbcd..c0b0a5025042 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,5 +1,6 @@ 25.7 ----- +* [**] Add new lightbox screen for images with modern transitions and enhanced performance [#23922] 25.6 -----