From dd95724d140be1736d2da622c015450fe54c2078 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Thu, 24 Nov 2022 14:28:39 -0500 Subject: [PATCH 01/24] Factor out code for the close button --- Mastodon.xcodeproj/project.pbxproj | 4 ++ Mastodon/Scene/MediaPreview/HUDButton.swift | 59 +++++++++++++++++++ .../MediaPreviewViewController.swift | 47 +++------------ ...wViewControllerAnimatedTransitioning.swift | 10 ++-- 4 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 Mastodon/Scene/MediaPreview/HUDButton.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 74b7841a5..aca99ccfb 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -98,6 +98,7 @@ 62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D42893708A00B205C5 /* BookmarkViewModel+Diffable.swift */; }; 85904C02293BC0EB0011C817 /* ImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85904C01293BC0EB0011C817 /* ImageProvider.swift */; }; 85904C04293BC1940011C817 /* URLActivityItemWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */; }; + 85BC11B1292FF92C00E191CD /* HUDButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BC11B0292FF92C00E191CD /* HUDButton.swift */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; }; D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; }; @@ -620,6 +621,7 @@ 819CEC9DCAD8E8E7BD85A7BB /* Pods-Mastodon.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk.xcconfig"; sourceTree = ""; }; 85904C01293BC0EB0011C817 /* ImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProvider.swift; sourceTree = ""; }; 85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLActivityItemWithMetadata.swift; sourceTree = ""; }; + 85BC11B0292FF92C00E191CD /* HUDButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDButton.swift; sourceTree = ""; }; 8850E70A1D5FF51432E43653 /* Pods-Mastodon-MastodonUITests.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.asdk - release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.asdk - release.xcconfig"; sourceTree = ""; }; 8E79CCBE51FBC3F7FE8CF49F /* Pods-MastodonTests.release snapshot.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release snapshot.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release snapshot.xcconfig"; sourceTree = ""; }; 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.debug.xcconfig"; sourceTree = ""; }; @@ -1974,6 +1976,7 @@ DBB45B5727B39FCC002DC5A7 /* Video */, DB6180F026391CAB0018D199 /* Image */, DB6180E1263919780018D199 /* Paging */, + 85BC11B0292FF92C00E191CD /* HUDButton.swift */, DB6180DC263918E30018D199 /* MediaPreviewViewController.swift */, DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */, ); @@ -3419,6 +3422,7 @@ 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */, 2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */, DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */, + 85BC11B1292FF92C00E191CD /* HUDButton.swift in Sources */, DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */, DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */, DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */, diff --git a/Mastodon/Scene/MediaPreview/HUDButton.swift b/Mastodon/Scene/MediaPreview/HUDButton.swift new file mode 100644 index 000000000..b08f95eb4 --- /dev/null +++ b/Mastodon/Scene/MediaPreview/HUDButton.swift @@ -0,0 +1,59 @@ +// +// HUDButton.swift +// Mastodon +// +// Created by Jed Fox on 2022-11-24. +// + +import UIKit +import MastodonUI + +class HUDButton: UIView { + + static let height: CGFloat = 30 + + let background: UIVisualEffectView = { + let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)) + backgroundView.alpha = 0.9 + backgroundView.layer.masksToBounds = true + backgroundView.layer.cornerRadius = HUDButton.height * 0.5 + return backgroundView + }() + + let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemUltraThinMaterial))) + + let button: UIButton = { + let button = HighlightDimmableButton() + button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) + button.imageView?.tintColor = .label + return button + }() + + init(configure: (UIButton) -> Void) { + super.init(frame: .zero) + + configure(button) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + + func _init() { + translatesAutoresizingMaskIntoConstraints = false + background.translatesAutoresizingMaskIntoConstraints = false + addSubview(background) + background.pinToParent() + vibrancyView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + background.contentView.addSubview(vibrancyView) + + button.translatesAutoresizingMaskIntoConstraints = false + vibrancyView.contentView.addSubview(button) + button.pinToParent() + NSLayoutConstraint.activate([ + heightAnchor.constraint(equalToConstant: HUDButton.height).priority(.defaultHigh), + ]) + } +} diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift index dee6c00ea..c25d099a0 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift @@ -16,8 +16,6 @@ import MastodonLocalization final class MediaPreviewViewController: UIViewController, NeedsDependency { - static let closeButtonSize = CGSize(width: 30, height: 30) - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } @@ -27,23 +25,9 @@ final class MediaPreviewViewController: UIViewController, NeedsDependency { let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) let pagingViewController = MediaPreviewPagingViewController() - let closeButtonBackground: UIVisualEffectView = { - let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)) - backgroundView.alpha = 0.9 - backgroundView.layer.masksToBounds = true - backgroundView.layer.cornerRadius = MediaPreviewViewController.closeButtonSize.width * 0.5 - return backgroundView - }() - - let closeButtonBackgroundVisualEffectView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemUltraThinMaterial))) - - let closeButton: UIButton = { - let button = HighlightDimmableButton() - button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) - button.imageView?.tintColor = .label + let closeButton = HUDButton { button in button.setImage(UIImage(systemName: "xmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .bold))!, for: .normal) - return button - }() + } deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -67,25 +51,12 @@ extension MediaPreviewViewController { visualEffectView.contentView.addSubview(pagingViewController.view) visualEffectView.pinTo(to: pagingViewController.view) pagingViewController.didMove(toParent: self) - - closeButtonBackground.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(closeButtonBackground) - NSLayoutConstraint.activate([ - closeButtonBackground.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 12), - closeButtonBackground.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor) - ]) - closeButtonBackgroundVisualEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - closeButtonBackground.contentView.addSubview(closeButtonBackgroundVisualEffectView) - closeButton.translatesAutoresizingMaskIntoConstraints = false - closeButtonBackgroundVisualEffectView.contentView.addSubview(closeButton) + view.addSubview(closeButton) NSLayoutConstraint.activate([ - closeButton.topAnchor.constraint(equalTo: closeButtonBackgroundVisualEffectView.topAnchor), - closeButton.leadingAnchor.constraint(equalTo: closeButtonBackgroundVisualEffectView.leadingAnchor), - closeButtonBackgroundVisualEffectView.trailingAnchor.constraint(equalTo: closeButton.trailingAnchor), - closeButtonBackgroundVisualEffectView.bottomAnchor.constraint(equalTo: closeButton.bottomAnchor), - closeButton.heightAnchor.constraint(equalToConstant: MediaPreviewViewController.closeButtonSize.height).priority(.defaultHigh), - closeButton.widthAnchor.constraint(equalToConstant: MediaPreviewViewController.closeButtonSize.width).priority(.defaultHigh), + closeButton.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 12), + closeButton.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + closeButton.widthAnchor.constraint(equalToConstant: HUDButton.height).priority(.defaultHigh), ]) viewModel.mediaPreviewImageViewControllerDelegate = self @@ -94,7 +65,7 @@ extension MediaPreviewViewController { pagingViewController.delegate = self pagingViewController.dataSource = viewModel - closeButton.addTarget(self, action: #selector(MediaPreviewViewController.closeButtonPressed(_:)), for: .touchUpInside) + closeButton.button.addTarget(self, action: #selector(MediaPreviewViewController.closeButtonPressed(_:)), for: .touchUpInside) // bind view model viewModel.$currentPage @@ -126,7 +97,7 @@ extension MediaPreviewViewController { let attachment = previewContext.attachments[index] return attachment.kind == .video // not hide buttno for audio }() - self.closeButtonBackground.isHidden = needsHideCloseButton + self.closeButton.isHidden = needsHideCloseButton default: break } @@ -139,7 +110,7 @@ extension MediaPreviewViewController { .sink { [weak self] showingChrome in UIView.animate(withDuration: 0.3) { self?.setNeedsStatusBarAppearanceUpdate() - self?.closeButtonBackground.alpha = showingChrome ? 1 : 0 + self?.closeButton.alpha = showingChrome ? 1 : 0 } } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index ace6048c5..7bf81f0b0 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -81,7 +81,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { transitionItem.transitionView = transitionImageView transitionContext.containerView.addSubview(transitionImageView) - toVC.closeButtonBackground.alpha = 0 + toVC.closeButton.alpha = 0 if UIAccessibility.isReduceTransparencyEnabled { toVC.visualEffectView.alpha = 0 @@ -101,7 +101,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { toVC.pagingViewController.view.alpha = 1 transitionImageView.removeFromSuperview() UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) { - toVC.closeButtonBackground.alpha = 1 + toVC.closeButton.alpha = 1 } transitionContext.completeTransition(position == .end) } @@ -140,11 +140,11 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { // update close button UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) { - fromVC.closeButtonBackground.alpha = 0 + fromVC.closeButton.alpha = 0 } animator.addCompletion { position in UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) { - fromVC.closeButtonBackground.alpha = position == .end ? 0 : 1 + fromVC.closeButton.alpha = position == .end ? 0 : 1 } } @@ -202,7 +202,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { mediaPreviewTransitionContext.snapshot.contentMode = .scaleAspectFill mediaPreviewTransitionContext.snapshot.clipsToBounds = true transitionMaskView.addSubview(mediaPreviewTransitionContext.snapshot) - fromVC.view.bringSubviewToFront(fromVC.closeButtonBackground) + fromVC.view.bringSubviewToFront(fromVC.closeButton) transitionItem.transitionView = mediaPreviewTransitionContext.transitionView transitionItem.snapshotTransitioning = mediaPreviewTransitionContext.snapshot From 4014fb41f18832bdea1e9d6e314a14919309dda4 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 26 Nov 2022 09:36:57 -0500 Subject: [PATCH 02/24] Allow pinning to a view with padding --- .../Sources/MastodonExtension/UIEdgeInsets.swift | 14 ++++++++++++++ .../Sources/MastodonExtension/UIView.swift | 15 +++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonExtension/UIEdgeInsets.swift diff --git a/MastodonSDK/Sources/MastodonExtension/UIEdgeInsets.swift b/MastodonSDK/Sources/MastodonExtension/UIEdgeInsets.swift new file mode 100644 index 000000000..8436ff5d2 --- /dev/null +++ b/MastodonSDK/Sources/MastodonExtension/UIEdgeInsets.swift @@ -0,0 +1,14 @@ +// +// UIEdgeInsets.swift +// +// +// Created by Jed Fox on 2022-11-24. +// + +import UIKit + +extension UIEdgeInsets { + public static func constant(_ offset: CGFloat) -> Self { + UIEdgeInsets(top: offset, left: offset, bottom: offset, right: offset) + } +} diff --git a/MastodonSDK/Sources/MastodonExtension/UIView.swift b/MastodonSDK/Sources/MastodonExtension/UIView.swift index bfa253680..84f87eb20 100644 --- a/MastodonSDK/Sources/MastodonExtension/UIView.swift +++ b/MastodonSDK/Sources/MastodonExtension/UIView.swift @@ -48,20 +48,19 @@ extension UIView { } public extension UIView { - @discardableResult - func pinToParent() -> [NSLayoutConstraint] { - pinTo(to: self.superview) + func pinToParent(padding: UIEdgeInsets = .zero) -> [NSLayoutConstraint] { + pinTo(to: self.superview, padding: padding) } @discardableResult - func pinTo(to view: UIView?) -> [NSLayoutConstraint] { + func pinTo(to view: UIView?, padding: UIEdgeInsets = .zero) -> [NSLayoutConstraint] { guard let pinToView = view else { return [] } let constraints = [ - topAnchor.constraint(equalTo: pinToView.topAnchor), - leadingAnchor.constraint(equalTo: pinToView.leadingAnchor), - trailingAnchor.constraint(equalTo: pinToView.trailingAnchor), - bottomAnchor.constraint(equalTo: pinToView.bottomAnchor), + topAnchor.constraint(equalTo: pinToView.topAnchor, constant: padding.top), + leadingAnchor.constraint(equalTo: pinToView.leadingAnchor, constant: padding.left), + trailingAnchor.constraint(equalTo: pinToView.trailingAnchor, constant: -padding.right), + bottomAnchor.constraint(equalTo: pinToView.bottomAnchor, constant: -padding.bottom), ] NSLayoutConstraint.activate(constraints) return constraints From e8e15f3a0e8252362146b94099dc87ff744a362c Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 26 Nov 2022 09:38:14 -0500 Subject: [PATCH 03/24] Remove support for previewing local images (it was unused) --- .../MediaPreviewImageViewController.swift | 39 +++++++------------ .../Image/MediaPreviewImageViewModel.swift | 11 +----- .../MediaPreviewViewController.swift | 39 +++---------------- .../MediaPreview/MediaPreviewViewModel.swift | 12 +++--- 4 files changed, 28 insertions(+), 73 deletions(-) diff --git a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift index 513110d29..68bc0219f 100644 --- a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift +++ b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift @@ -68,30 +68,21 @@ extension MediaPreviewImageViewController { let previewImageViewContextMenuInteraction = UIContextMenuInteraction(delegate: self) previewImageView.addInteraction(previewImageViewContextMenuInteraction) - switch viewModel.item { - case .remote(let imageContext): - previewImageView.imageView.accessibilityLabel = imageContext.altText - - if let thumbnail = imageContext.thumbnail { - previewImageView.imageView.image = thumbnail - previewImageView.setup(image: thumbnail, container: self.previewImageView, forceUpdate: true) - } - - previewImageView.imageView.setImage( - url: imageContext.assetURL, - placeholder: imageContext.thumbnail, - scaleToSize: nil - ) { [weak self] image in - guard let self = self else { return } - guard let image = image else { return } - self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true) - } - - case .local(let imageContext): - let image = imageContext.image - previewImageView.imageView.image = image - previewImageView.setup(image: image, container: previewImageView, forceUpdate: true) - + previewImageView.imageView.accessibilityLabel = viewModel.item.altText + + if let thumbnail = viewModel.item.thumbnail { + previewImageView.imageView.image = thumbnail + previewImageView.setup(image: thumbnail, container: self.previewImageView, forceUpdate: true) + } + + previewImageView.imageView.setImage( + url: viewModel.item.assetURL, + placeholder: viewModel.item.thumbnail, + scaleToSize: nil + ) { [weak self] image in + guard let self = self else { return } + guard let image = image else { return } + self.previewImageView.setup(image: image, container: self.previewImageView, forceUpdate: true) } } diff --git a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewModel.swift b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewModel.swift index e82118f78..3a4d9edd2 100644 --- a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewModel.swift +++ b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewModel.swift @@ -30,19 +30,10 @@ class MediaPreviewImageViewModel { extension MediaPreviewImageViewModel { - public enum ImagePreviewItem { - case remote(RemoteImageContext) - case local(LocalImageContext) - } - - public struct RemoteImageContext { + public struct ImagePreviewItem { let assetURL: URL? let thumbnail: UIImage? let altText: String? } - - public struct LocalImageContext { - let image: UIImage - } } diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift index c25d099a0..e6de7c2e9 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift @@ -241,19 +241,8 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate { ) { switch action { case .savePhoto: - let _savePublisher: AnyPublisher? = { - switch viewController.viewModel.item { - case .remote(let previewContext): - guard let assetURL = previewContext.assetURL else { return nil } - return context.photoLibraryService.save(imageSource: .url(assetURL)) - case .local(let previewContext): - return context.photoLibraryService.save(imageSource: .image(previewContext.image)) - } - }() - guard let savePublisher = _savePublisher else { - return - } - savePublisher + guard let assetURL = viewController.viewModel.item.assetURL else { return } + context.photoLibraryService.save(imageSource: .url(assetURL)) .sink { [weak self] completion in guard let self = self else { return } switch completion { @@ -277,20 +266,9 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate { } .store(in: &context.disposeBag) case .copyPhoto: - let _copyPublisher: AnyPublisher? = { - switch viewController.viewModel.item { - case .remote(let previewContext): - guard let assetURL = previewContext.assetURL else { return nil } - return context.photoLibraryService.copy(imageSource: .url(assetURL)) - case .local(let previewContext): - return context.photoLibraryService.copy(imageSource: .image(previewContext.image)) - } - }() - guard let copyPublisher = _copyPublisher else { - return - } + guard let assetURL = viewController.viewModel.item.assetURL else { return } - copyPublisher + context.photoLibraryService.copy(imageSource: .url(assetURL)) .sink { completion in switch completion { case .failure(let error): @@ -309,13 +287,8 @@ extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate { let activityViewController = UIActivityViewController( activityItems: { var activityItems: [Any] = [] - switch viewController.viewModel.item { - case .remote(let previewContext): - if let assetURL = previewContext.assetURL { - activityItems.append(assetURL) - } - case .local(let previewContext): - activityItems.append(previewContext.image) + if let assetURL = viewController.viewModel.item.assetURL { + activityItems.append(assetURL) } return activityItems }(), diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift index 3f60b19e5..3d6c4c14d 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -51,11 +51,11 @@ final class MediaPreviewViewModel: NSObject { let viewController = MediaPreviewImageViewController() let viewModel = MediaPreviewImageViewModel( context: context, - item: .remote(.init( + item: .init( assetURL: attachment.assetURL.flatMap { URL(string: $0) }, thumbnail: previewContext.thumbnail(at: i), altText: attachment.altDescription - )) + ) ) viewController.viewModel = viewModel viewControllers.append(viewController) @@ -87,11 +87,11 @@ final class MediaPreviewViewModel: NSObject { let viewController = MediaPreviewImageViewController() let viewModel = MediaPreviewImageViewModel( context: context, - item: .remote(.init( + item: .init( assetURL: previewContext.assetURL.flatMap { URL(string: $0) }, thumbnail: previewContext.thumbnail, altText: nil - )) + ) ) viewController.viewModel = viewModel viewControllers.append(viewController) @@ -99,11 +99,11 @@ final class MediaPreviewViewModel: NSObject { let viewController = MediaPreviewImageViewController() let viewModel = MediaPreviewImageViewModel( context: context, - item: .remote(.init( + item: .init( assetURL: previewContext.assetURL.flatMap { URL(string: $0) }, thumbnail: previewContext.thumbnail, altText: nil - )) + ) ) viewController.viewModel = viewModel viewControllers.append(viewController) From 582d1cf295bda717b7bc3a71990c7cf3a74b5d28 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 26 Nov 2022 09:39:24 -0500 Subject: [PATCH 04/24] Add an ALT button to the media preview to display alt text --- Mastodon.xcodeproj/project.pbxproj | 4 ++ .../MediaPreview/AltViewController.swift | 72 +++++++++++++++++++ Mastodon/Scene/MediaPreview/HUDButton.swift | 7 ++ .../MediaPreviewImageViewController.swift | 4 ++ .../MediaPreviewViewController.swift | 22 +++++- .../MediaPreview/MediaPreviewViewModel.swift | 11 +-- .../MediaPreviewVideoViewController.swift | 4 ++ .../Video/MediaPreviewVideoViewModel.swift | 9 +++ 8 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 Mastodon/Scene/MediaPreview/AltViewController.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index aca99ccfb..afb0c30ab 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -99,6 +99,7 @@ 85904C02293BC0EB0011C817 /* ImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85904C01293BC0EB0011C817 /* ImageProvider.swift */; }; 85904C04293BC1940011C817 /* URLActivityItemWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */; }; 85BC11B1292FF92C00E191CD /* HUDButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BC11B0292FF92C00E191CD /* HUDButton.swift */; }; + 85BC11B32932414900E191CD /* AltViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BC11B22932414900E191CD /* AltViewController.swift */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; }; D87BFC8B291D5C6B00FEE264 /* MastodonLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87BFC8A291D5C6B00FEE264 /* MastodonLoginView.swift */; }; @@ -622,6 +623,7 @@ 85904C01293BC0EB0011C817 /* ImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProvider.swift; sourceTree = ""; }; 85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLActivityItemWithMetadata.swift; sourceTree = ""; }; 85BC11B0292FF92C00E191CD /* HUDButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDButton.swift; sourceTree = ""; }; + 85BC11B22932414900E191CD /* AltViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltViewController.swift; sourceTree = ""; }; 8850E70A1D5FF51432E43653 /* Pods-Mastodon-MastodonUITests.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.asdk - release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.asdk - release.xcconfig"; sourceTree = ""; }; 8E79CCBE51FBC3F7FE8CF49F /* Pods-MastodonTests.release snapshot.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release snapshot.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release snapshot.xcconfig"; sourceTree = ""; }; 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.debug.xcconfig"; sourceTree = ""; }; @@ -1978,6 +1980,7 @@ DB6180E1263919780018D199 /* Paging */, 85BC11B0292FF92C00E191CD /* HUDButton.swift */, DB6180DC263918E30018D199 /* MediaPreviewViewController.swift */, + 85BC11B22932414900E191CD /* AltViewController.swift */, DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */, ); path = MediaPreview; @@ -3539,6 +3542,7 @@ DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */, 2D4AD8A226316CD200613EFC /* SelectedAccountSection.swift in Sources */, DB3EA8F1281B9EF600598866 /* DiscoveryCommunityViewModel+Diffable.swift in Sources */, + 85BC11B32932414900E191CD /* AltViewController.swift in Sources */, DB63F775279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift in Sources */, DB98EB5927B109890082E365 /* ReportSupplementaryViewController.swift in Sources */, DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */, diff --git a/Mastodon/Scene/MediaPreview/AltViewController.swift b/Mastodon/Scene/MediaPreview/AltViewController.swift new file mode 100644 index 000000000..f15f1d84d --- /dev/null +++ b/Mastodon/Scene/MediaPreview/AltViewController.swift @@ -0,0 +1,72 @@ +// +// AltViewController.swift +// Mastodon +// +// Created by Jed Fox on 2022-11-26. +// + +import SwiftUI + +class AltViewController: UIViewController { + var alt: String? + let label = UILabel() + + convenience init(alt: String?, sourceView: UIView?) { + self.init(nibName: nil, bundle: nil) + self.alt = alt + self.modalPresentationStyle = .popover + self.popoverPresentationController?.delegate = self + self.popoverPresentationController?.permittedArrowDirections = .up + self.popoverPresentationController?.sourceView = sourceView + self.overrideUserInterfaceStyle = .dark + } + + @objc override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + @MainActor required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.lineBreakStrategy = .standard + label.font = .preferredFont(forTextStyle: .callout) + label.text = alt ?? "ummmmmmm tbd but you shouldn’t see this" + + view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(label) + + NSLayoutConstraint.activate( + NSLayoutConstraint.constraints(withVisualFormat: "V:|-[label]-|", metrics: nil, views: ["label": label]) + ) + NSLayoutConstraint.activate( + NSLayoutConstraint.constraints(withVisualFormat: "H:|-[label]-|", metrics: nil, views: ["label": label]) + ) + NSLayoutConstraint.activate([ + label.widthAnchor.constraint(lessThanOrEqualToConstant: 400), + ]) + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + UIView.performWithoutAnimation { + preferredContentSize = CGSize( + width: label.intrinsicContentSize.width + view.layoutMargins.left + view.layoutMargins.right, + height: label.intrinsicContentSize.height + view.layoutMargins.top + view.layoutMargins.bottom + ) + } + } +} + +// MARK: UIPopoverPresentationControllerDelegate +extension AltViewController: UIPopoverPresentationControllerDelegate { + func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle { + .none + } +} diff --git a/Mastodon/Scene/MediaPreview/HUDButton.swift b/Mastodon/Scene/MediaPreview/HUDButton.swift index b08f95eb4..0089ebcb1 100644 --- a/Mastodon/Scene/MediaPreview/HUDButton.swift +++ b/Mastodon/Scene/MediaPreview/HUDButton.swift @@ -25,7 +25,9 @@ class HUDButton: UIView { let button: UIButton = { let button = HighlightDimmableButton() button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) + button.contentEdgeInsets = .constant(7) button.imageView?.tintColor = .label + button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold)) return button }() @@ -56,4 +58,9 @@ class HUDButton: UIView { heightAnchor.constraint(equalToConstant: HUDButton.height).priority(.defaultHigh), ]) } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold)) + } } diff --git a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift index 68bc0219f..c87788aa1 100644 --- a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift +++ b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift @@ -110,6 +110,10 @@ extension MediaPreviewImageViewController: MediaPreviewPage { } } } + + var altText: String? { + viewModel.item.altText + } } // MARK: - ImageAnalysisInteractionDelegate diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift index e6de7c2e9..3c9821eb6 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift @@ -29,6 +29,10 @@ final class MediaPreviewViewController: UIViewController, NeedsDependency { button.setImage(UIImage(systemName: "xmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .bold))!, for: .normal) } + let altButton = HUDButton { button in + button.setTitle("ALT", for: .normal) + } + deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) } @@ -58,7 +62,13 @@ extension MediaPreviewViewController { closeButton.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), closeButton.widthAnchor.constraint(equalToConstant: HUDButton.height).priority(.defaultHigh), ]) - + + view.addSubview(altButton) + NSLayoutConstraint.activate([ + altButton.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 12), + altButton.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + ]) + viewModel.mediaPreviewImageViewControllerDelegate = self pagingViewController.interPageSpacing = 10 @@ -66,7 +76,8 @@ extension MediaPreviewViewController { pagingViewController.dataSource = viewModel closeButton.button.addTarget(self, action: #selector(MediaPreviewViewController.closeButtonPressed(_:)), for: .touchUpInside) - + altButton.button.addTarget(self, action: #selector(MediaPreviewViewController.altButtonPressed(_:)), for: .touchUpInside) + // bind view model viewModel.$currentPage .receive(on: DispatchQueue.main) @@ -144,7 +155,12 @@ extension MediaPreviewViewController { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) dismiss(animated: true, completion: nil) } - + + @objc private func altButtonPressed(_ sender: UIButton) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + present(AltViewController(alt: viewModel.viewControllers[viewModel.currentPage].altText, sourceView: sender), animated: true) + } + } // MARK: - MediaPreviewingViewController diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift index 3d6c4c14d..7cdee02fe 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -14,6 +14,7 @@ import MastodonCore protocol MediaPreviewPage: UIViewController { func setShowingChrome(_ showingChrome: Bool) + var altText: String? { get } } final class MediaPreviewViewModel: NSObject { @@ -27,9 +28,9 @@ final class MediaPreviewViewModel: NSObject { @Published var currentPage: Int @Published var showingChrome = true - + // output - let viewControllers: [UIViewController] + let viewControllers: [MediaPreviewPage] private var disposeBag: Set = [] @@ -65,7 +66,8 @@ final class MediaPreviewViewModel: NSObject { context: context, item: .gif(.init( assetURL: attachment.assetURL.flatMap { URL(string: $0) }, - previewURL: attachment.previewURL.flatMap { URL(string: $0) } + previewURL: attachment.previewURL.flatMap { URL(string: $0) }, + altText: attachment.altDescription )) ) viewController.viewModel = viewModel @@ -76,7 +78,8 @@ final class MediaPreviewViewModel: NSObject { context: context, item: .video(.init( assetURL: attachment.assetURL.flatMap { URL(string: $0) }, - previewURL: attachment.previewURL.flatMap { URL(string: $0) } + previewURL: attachment.previewURL.flatMap { URL(string: $0) }, + altText: attachment.altDescription )) ) viewController.viewModel = viewModel diff --git a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift index e924f38d4..b91705cc5 100644 --- a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift +++ b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift @@ -111,6 +111,10 @@ extension MediaPreviewVideoViewController: MediaPreviewPage { func setShowingChrome(_ showingChrome: Bool) { // TODO: does this do anything? } + + var altText: String? { + viewModel.item.altText + } } // MARK: - AVPlayerViewControllerDelegate diff --git a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift index 97e5f955b..9cc9666dd 100644 --- a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift +++ b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift @@ -125,17 +125,26 @@ extension MediaPreviewVideoViewModel { case .gif(let mediaContext): return mediaContext.assetURL } } + + var altText: String? { + switch self { + case .video(let mediaContext): return mediaContext.altText + case .gif(let mediaContext): return mediaContext.altText + } + } } struct RemoteVideoContext { let assetURL: URL? let previewURL: URL? + let altText: String? // let thumbnail: UIImage? } struct RemoteGIFContext { let assetURL: URL? let previewURL: URL? + let altText: String? } } From ed580541f0597c82e80d453c05522f440531c724 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 26 Nov 2022 14:29:30 -0500 Subject: [PATCH 05/24] Merge top buttons into a single parent view (also fix tapping just outside a HUDButton) --- Mastodon/Scene/MediaPreview/HUDButton.swift | 8 ++++ .../MediaPreviewViewController.swift | 46 +++++++++++++++---- ...wViewControllerAnimatedTransitioning.swift | 14 +++--- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/Mastodon/Scene/MediaPreview/HUDButton.swift b/Mastodon/Scene/MediaPreview/HUDButton.swift index 0089ebcb1..e82fba0f6 100644 --- a/Mastodon/Scene/MediaPreview/HUDButton.swift +++ b/Mastodon/Scene/MediaPreview/HUDButton.swift @@ -63,4 +63,12 @@ class HUDButton: UIView { super.traitCollectionDidChange(previousTraitCollection) button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold)) } + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + button.point(inside: button.convert(point, from: self), with: event) + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + button + } } diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift index 3c9821eb6..eaf740a2b 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift @@ -24,7 +24,32 @@ final class MediaPreviewViewController: UIViewController, NeedsDependency { let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial)) let pagingViewController = MediaPreviewPagingViewController() - + + let topToolbar: UIStackView = { + class TouchTransparentStackView: UIStackView { + // allow button hit boxes to grow outside of this view’s bounds + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + subviews.contains { $0.point(inside: $0.convert(point, from: self), with: event) } + } + + // allow taps on blank areas to pass through + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let view = super.hitTest(point, with: event) + if view == self { + return nil + } + return view + } + } + + let stackView = TouchTransparentStackView() + stackView.axis = .horizontal + stackView.distribution = .equalSpacing + stackView.alignment = .fill + stackView.translatesAutoresizingMaskIntoConstraints = false + return stackView + }() + let closeButton = HUDButton { button in button.setImage(UIImage(systemName: "xmark", withConfiguration: UIImage.SymbolConfiguration(pointSize: 16, weight: .bold))!, for: .normal) } @@ -56,18 +81,19 @@ extension MediaPreviewViewController { visualEffectView.pinTo(to: pagingViewController.view) pagingViewController.didMove(toParent: self) - view.addSubview(closeButton) + view.addSubview(topToolbar) + NSLayoutConstraint.activate([ + topToolbar.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 12), + topToolbar.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), + topToolbar.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), + ]) + + topToolbar.addArrangedSubview(closeButton) NSLayoutConstraint.activate([ - closeButton.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 12), - closeButton.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor), closeButton.widthAnchor.constraint(equalToConstant: HUDButton.height).priority(.defaultHigh), ]) - view.addSubview(altButton) - NSLayoutConstraint.activate([ - altButton.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor, constant: 12), - altButton.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor), - ]) + topToolbar.addArrangedSubview(altButton) viewModel.mediaPreviewImageViewControllerDelegate = self @@ -121,7 +147,7 @@ extension MediaPreviewViewController { .sink { [weak self] showingChrome in UIView.animate(withDuration: 0.3) { self?.setNeedsStatusBarAppearanceUpdate() - self?.closeButton.alpha = showingChrome ? 1 : 0 + self?.topToolbar.alpha = showingChrome ? 1 : 0 } } .store(in: &disposeBag) diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index 7bf81f0b0..44cb68428 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -81,8 +81,8 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { transitionItem.transitionView = transitionImageView transitionContext.containerView.addSubview(transitionImageView) - toVC.closeButton.alpha = 0 - + toVC.topToolbar.alpha = 0 + if UIAccessibility.isReduceTransparencyEnabled { toVC.visualEffectView.alpha = 0 } @@ -101,7 +101,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { toVC.pagingViewController.view.alpha = 1 transitionImageView.removeFromSuperview() UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) { - toVC.closeButton.alpha = 1 + toVC.topToolbar.alpha = 1 } transitionContext.completeTransition(position == .end) } @@ -138,13 +138,13 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { } } - // update close button + // update top toolbar UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) { - fromVC.closeButton.alpha = 0 + fromVC.topToolbar.alpha = 0 } animator.addCompletion { position in UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseInOut]) { - fromVC.closeButton.alpha = position == .end ? 0 : 1 + fromVC.topToolbar.alpha = position == .end ? 0 : 1 } } @@ -202,7 +202,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { mediaPreviewTransitionContext.snapshot.contentMode = .scaleAspectFill mediaPreviewTransitionContext.snapshot.clipsToBounds = true transitionMaskView.addSubview(mediaPreviewTransitionContext.snapshot) - fromVC.view.bringSubviewToFront(fromVC.closeButton) + fromVC.view.bringSubviewToFront(fromVC.topToolbar) transitionItem.transitionView = mediaPreviewTransitionContext.transitionView transitionItem.snapshotTransitioning = mediaPreviewTransitionContext.snapshot From 501e17bf18089d074135217528bc117762c8a6c6 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 26 Nov 2022 15:11:13 -0500 Subject: [PATCH 06/24] Hide the ALT button when alt text is unavailable --- .../Scene/MediaPreview/AltViewController.swift | 12 ++++-------- .../Image/MediaPreviewImageViewController.swift | 4 ---- .../MediaPreviewViewController.swift | 17 ++++++++++++++++- .../MediaPreview/MediaPreviewViewModel.swift | 9 ++++++++- .../Video/MediaPreviewVideoViewController.swift | 4 ---- .../Video/MediaPreviewVideoViewModel.swift | 7 ------- 6 files changed, 28 insertions(+), 25 deletions(-) diff --git a/Mastodon/Scene/MediaPreview/AltViewController.swift b/Mastodon/Scene/MediaPreview/AltViewController.swift index f15f1d84d..318514ae3 100644 --- a/Mastodon/Scene/MediaPreview/AltViewController.swift +++ b/Mastodon/Scene/MediaPreview/AltViewController.swift @@ -8,12 +8,12 @@ import SwiftUI class AltViewController: UIViewController { - var alt: String? + private var alt: String let label = UILabel() - convenience init(alt: String?, sourceView: UIView?) { - self.init(nibName: nil, bundle: nil) + init(alt: String, sourceView: UIView?) { self.alt = alt + super.init(nibName: nil, bundle: nil) self.modalPresentationStyle = .popover self.popoverPresentationController?.delegate = self self.popoverPresentationController?.permittedArrowDirections = .up @@ -21,10 +21,6 @@ class AltViewController: UIViewController { self.overrideUserInterfaceStyle = .dark } - @objc override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - } - @MainActor required dynamic init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -37,7 +33,7 @@ class AltViewController: UIViewController { label.lineBreakMode = .byWordWrapping label.lineBreakStrategy = .standard label.font = .preferredFont(forTextStyle: .callout) - label.text = alt ?? "ummmmmmm tbd but you shouldn’t see this" + label.text = alt view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(label) diff --git a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift index c87788aa1..68bc0219f 100644 --- a/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift +++ b/Mastodon/Scene/MediaPreview/Image/MediaPreviewImageViewController.swift @@ -110,10 +110,6 @@ extension MediaPreviewImageViewController: MediaPreviewPage { } } } - - var altText: String? { - viewModel.item.altText - } } // MARK: - ImageAnalysisInteractionDelegate diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift index eaf740a2b..d4a415f5f 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift @@ -141,6 +141,20 @@ extension MediaPreviewViewController { } .store(in: &disposeBag) + viewModel.$altText + .receive(on: DispatchQueue.main) + .sink { [weak self] altText in + guard let self else { return } + UIView.animate(withDuration: 0.3) { + if altText == nil { + self.altButton.alpha = 0 + } else { + self.altButton.alpha = 1 + } + } + } + .store(in: &disposeBag) + viewModel.$showingChrome .receive(on: DispatchQueue.main) .removeDuplicates() @@ -184,7 +198,8 @@ extension MediaPreviewViewController { @objc private func altButtonPressed(_ sender: UIButton) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - present(AltViewController(alt: viewModel.viewControllers[viewModel.currentPage].altText, sourceView: sender), animated: true) + guard let alt = viewModel.altText else { return } + present(AltViewController(alt: alt, sourceView: sender), animated: true) } } diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift index 7cdee02fe..a6b604d6f 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewModel.swift @@ -14,7 +14,6 @@ import MastodonCore protocol MediaPreviewPage: UIViewController { func setShowingChrome(_ showingChrome: Bool) - var altText: String? { get } } final class MediaPreviewViewModel: NSObject { @@ -28,6 +27,7 @@ final class MediaPreviewViewModel: NSObject { @Published var currentPage: Int @Published var showingChrome = true + @Published var altText: String? // output let viewControllers: [MediaPreviewPage] @@ -43,8 +43,11 @@ final class MediaPreviewViewModel: NSObject { self.item = item var currentPage = 0 var viewControllers: [MediaPreviewPage] = [] + var getAltText = { (page: Int) -> String? in nil } switch item { case .attachment(let previewContext): + getAltText = { previewContext.attachments[$0].altDescription } + currentPage = previewContext.initialIndex for (i, attachment) in previewContext.attachments.enumerated() { switch attachment.kind { @@ -117,6 +120,10 @@ final class MediaPreviewViewModel: NSObject { self.transitionItem = transitionItem super.init() + self.$currentPage + .map(getAltText) + .assign(to: &$altText) + for viewController in viewControllers { self.$showingChrome .sink { [weak viewController] showingChrome in diff --git a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift index b91705cc5..e924f38d4 100644 --- a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift +++ b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewController.swift @@ -111,10 +111,6 @@ extension MediaPreviewVideoViewController: MediaPreviewPage { func setShowingChrome(_ showingChrome: Bool) { // TODO: does this do anything? } - - var altText: String? { - viewModel.item.altText - } } // MARK: - AVPlayerViewControllerDelegate diff --git a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift index 9cc9666dd..a6542d464 100644 --- a/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift +++ b/Mastodon/Scene/MediaPreview/Video/MediaPreviewVideoViewModel.swift @@ -125,13 +125,6 @@ extension MediaPreviewVideoViewModel { case .gif(let mediaContext): return mediaContext.assetURL } } - - var altText: String? { - switch self { - case .video(let mediaContext): return mediaContext.altText - case .gif(let mediaContext): return mediaContext.altText - } - } } struct RemoteVideoContext { From 754b0a7eb073740799b7c878925fd4f56e7036f8 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 26 Nov 2022 15:14:43 -0500 Subject: [PATCH 07/24] Move HUDButton to MastodonUI --- Mastodon.xcodeproj/project.pbxproj | 4 ---- .../MastodonUI/View/Button}/HUDButton.swift | 15 +++++++-------- 2 files changed, 7 insertions(+), 12 deletions(-) rename {Mastodon/Scene/MediaPreview => MastodonSDK/Sources/MastodonUI/View/Button}/HUDButton.swift (82%) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index afb0c30ab..0ee9fa7e8 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -98,7 +98,6 @@ 62FD27D52893708A00B205C5 /* BookmarkViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62FD27D42893708A00B205C5 /* BookmarkViewModel+Diffable.swift */; }; 85904C02293BC0EB0011C817 /* ImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85904C01293BC0EB0011C817 /* ImageProvider.swift */; }; 85904C04293BC1940011C817 /* URLActivityItemWithMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */; }; - 85BC11B1292FF92C00E191CD /* HUDButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BC11B0292FF92C00E191CD /* HUDButton.swift */; }; 85BC11B32932414900E191CD /* AltViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85BC11B22932414900E191CD /* AltViewController.swift */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; C24C97032922F30500BAE8CB /* RefreshControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C24C97022922F30500BAE8CB /* RefreshControl.swift */; }; @@ -622,7 +621,6 @@ 819CEC9DCAD8E8E7BD85A7BB /* Pods-Mastodon.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.asdk.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.asdk.xcconfig"; sourceTree = ""; }; 85904C01293BC0EB0011C817 /* ImageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProvider.swift; sourceTree = ""; }; 85904C03293BC1940011C817 /* URLActivityItemWithMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLActivityItemWithMetadata.swift; sourceTree = ""; }; - 85BC11B0292FF92C00E191CD /* HUDButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDButton.swift; sourceTree = ""; }; 85BC11B22932414900E191CD /* AltViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltViewController.swift; sourceTree = ""; }; 8850E70A1D5FF51432E43653 /* Pods-Mastodon-MastodonUITests.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.asdk - release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.asdk - release.xcconfig"; sourceTree = ""; }; 8E79CCBE51FBC3F7FE8CF49F /* Pods-MastodonTests.release snapshot.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release snapshot.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release snapshot.xcconfig"; sourceTree = ""; }; @@ -1978,7 +1976,6 @@ DBB45B5727B39FCC002DC5A7 /* Video */, DB6180F026391CAB0018D199 /* Image */, DB6180E1263919780018D199 /* Paging */, - 85BC11B0292FF92C00E191CD /* HUDButton.swift */, DB6180DC263918E30018D199 /* MediaPreviewViewController.swift */, 85BC11B22932414900E191CD /* AltViewController.swift */, DB6180F926391F2E0018D199 /* MediaPreviewViewModel.swift */, @@ -3425,7 +3422,6 @@ 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */, 2A1FE47C2938BB2600784BF1 /* FollowedTagsViewModel+DiffableDataSource.swift in Sources */, DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */, - 85BC11B1292FF92C00E191CD /* HUDButton.swift in Sources */, DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */, DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */, DB3E6FFA2807C47900B035AE /* DiscoveryForYouViewModel+Diffable.swift in Sources */, diff --git a/Mastodon/Scene/MediaPreview/HUDButton.swift b/MastodonSDK/Sources/MastodonUI/View/Button/HUDButton.swift similarity index 82% rename from Mastodon/Scene/MediaPreview/HUDButton.swift rename to MastodonSDK/Sources/MastodonUI/View/Button/HUDButton.swift index e82fba0f6..78579bb1e 100644 --- a/Mastodon/Scene/MediaPreview/HUDButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Button/HUDButton.swift @@ -6,11 +6,10 @@ // import UIKit -import MastodonUI -class HUDButton: UIView { +public class HUDButton: UIView { - static let height: CGFloat = 30 + public static let height: CGFloat = 30 let background: UIVisualEffectView = { let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)) @@ -22,7 +21,7 @@ class HUDButton: UIView { let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .systemUltraThinMaterial))) - let button: UIButton = { + public let button: UIButton = { let button = HighlightDimmableButton() button.expandEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10) button.contentEdgeInsets = .constant(7) @@ -31,7 +30,7 @@ class HUDButton: UIView { return button }() - init(configure: (UIButton) -> Void) { + public init(configure: (UIButton) -> Void) { super.init(frame: .zero) configure(button) @@ -59,16 +58,16 @@ class HUDButton: UIView { ]) } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 15, weight: .bold)) } - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { button.point(inside: button.convert(point, from: self), with: event) } - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { button } } From 7ab6ea0d2344f8b8be9c10b52e43d17f6a34dd09 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 26 Nov 2022 15:53:09 -0500 Subject: [PATCH 08/24] Make alt text selectable --- .../MediaPreview/AltViewController.swift | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/MediaPreview/AltViewController.swift b/Mastodon/Scene/MediaPreview/AltViewController.swift index 318514ae3..b8f486890 100644 --- a/Mastodon/Scene/MediaPreview/AltViewController.swift +++ b/Mastodon/Scene/MediaPreview/AltViewController.swift @@ -9,7 +9,7 @@ import SwiftUI class AltViewController: UIViewController { private var alt: String - let label = UILabel() + let label = UITextView() init(alt: String, sourceView: UIView?) { self.alt = alt @@ -29,10 +29,20 @@ class AltViewController: UIViewController { super.viewDidLoad() label.translatesAutoresizingMaskIntoConstraints = false - label.numberOfLines = 0 - label.lineBreakMode = .byWordWrapping - label.lineBreakStrategy = .standard + label.textContainer.maximumNumberOfLines = 0 + label.textContainer.lineBreakMode = .byWordWrapping + label.textContainerInset = UIEdgeInsets( + top: 8, + left: 0, + bottom: -label.textContainer.lineFragmentPadding, + right: 0 + ) label.font = .preferredFont(forTextStyle: .callout) + label.isScrollEnabled = false + label.backgroundColor = .clear + label.isOpaque = false + label.isEditable = false + label.tintColor = .white label.text = alt view.translatesAutoresizingMaskIntoConstraints = false @@ -42,7 +52,7 @@ class AltViewController: UIViewController { NSLayoutConstraint.constraints(withVisualFormat: "V:|-[label]-|", metrics: nil, views: ["label": label]) ) NSLayoutConstraint.activate( - NSLayoutConstraint.constraints(withVisualFormat: "H:|-[label]-|", metrics: nil, views: ["label": label]) + NSLayoutConstraint.constraints(withVisualFormat: "H:|-(8)-[label]-(8)-|", metrics: nil, views: ["label": label]) ) NSLayoutConstraint.activate([ label.widthAnchor.constraint(lessThanOrEqualToConstant: 400), From dbf95f726c1eceb728dc52e6051342e050244042 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Sat, 26 Nov 2022 16:37:48 -0500 Subject: [PATCH 09/24] fix preferredContentSize --- Mastodon/Scene/MediaPreview/AltViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mastodon/Scene/MediaPreview/AltViewController.swift b/Mastodon/Scene/MediaPreview/AltViewController.swift index b8f486890..305487397 100644 --- a/Mastodon/Scene/MediaPreview/AltViewController.swift +++ b/Mastodon/Scene/MediaPreview/AltViewController.swift @@ -63,7 +63,7 @@ class AltViewController: UIViewController { super.viewDidLayoutSubviews() UIView.performWithoutAnimation { preferredContentSize = CGSize( - width: label.intrinsicContentSize.width + view.layoutMargins.left + view.layoutMargins.right, + width: label.intrinsicContentSize.width + 16, height: label.intrinsicContentSize.height + view.layoutMargins.top + view.layoutMargins.bottom ) } From 26aff2d627a71c686fed1ae29ab46426182d9f2c Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 10:09:18 -0500 Subject: [PATCH 10/24] MediaView: remove unused property --- MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift index 54560a8ce..fd2d10872 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift @@ -16,12 +16,6 @@ public final class MediaView: UIView { var _disposeBag = Set() public static let cornerRadius: CGFloat = 0 - public static let durationFormatter: DateComponentsFormatter = { - let formatter = DateComponentsFormatter() - formatter.zeroFormattingBehavior = .pad - formatter.allowedUnits = [.minute, .second] - return formatter - }() public static let placeholderImage = UIImage.placeholder(color: .systemGray6) public let container = TouchBlockingView() From c9a74055198b0a8aff76abf62678456c41cf14f1 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 11:03:57 -0500 Subject: [PATCH 11/24] Add altDescription attribute to MediaView.Configuration values --- .../Extension/CGSize+Hashable.swift | 15 +++++++++++ .../Content/MediaView+Configuration.swift | 26 +++++++------------ .../MastodonUI/View/Content/MediaView.swift | 3 ++- .../View/Content/NewsView+Configuration.swift | 3 ++- 4 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/Extension/CGSize+Hashable.swift diff --git a/MastodonSDK/Sources/MastodonUI/Extension/CGSize+Hashable.swift b/MastodonSDK/Sources/MastodonUI/Extension/CGSize+Hashable.swift new file mode 100644 index 000000000..0578d543e --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Extension/CGSize+Hashable.swift @@ -0,0 +1,15 @@ +// +// CGSize.swift +// +// +// Created by Jed Fox on 2022-12-20. +// + +import Foundation + +extension CGSize: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(width) + hasher.combine(height) + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift index 438baff7e..1da4e5392 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift @@ -101,19 +101,16 @@ extension MediaView.Configuration { public struct ImageInfo: Hashable { public let aspectRadio: CGSize public let assetURL: String? + public let altDescription: String? public init( aspectRadio: CGSize, - assetURL: String? + assetURL: String?, + altDescription: String? ) { self.aspectRadio = aspectRadio self.assetURL = assetURL - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(aspectRadio.width) - hasher.combine(aspectRadio.height) - assetURL.flatMap { hasher.combine($0) } + self.altDescription = altDescription } } @@ -121,26 +118,21 @@ extension MediaView.Configuration { public let aspectRadio: CGSize public let assetURL: String? public let previewURL: String? + public let altDescription: String? public let durationMS: Int? public init( aspectRadio: CGSize, assetURL: String?, previewURL: String?, + altDescription: String?, durationMS: Int? ) { self.aspectRadio = aspectRadio self.assetURL = assetURL self.previewURL = previewURL self.durationMS = durationMS - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(aspectRadio.width) - hasher.combine(aspectRadio.height) - assetURL.flatMap { hasher.combine($0) } - previewURL.flatMap { hasher.combine($0) } - durationMS.flatMap { hasher.combine($0) } + self.altDescription = altDescription } } @@ -187,6 +179,7 @@ extension MediaView { aspectRadio: attachment.size, assetURL: attachment.assetURL, previewURL: attachment.previewURL, + altDescription: attachment.altDescription, durationMS: attachment.durationMS ) } @@ -199,7 +192,8 @@ extension MediaView { case .image: let info = MediaView.Configuration.ImageInfo( aspectRadio: attachment.size, - assetURL: attachment.assetURL + assetURL: attachment.assetURL, + altDescription: attachment.altDescription ) return .init( info: .image(info: info), diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift index fd2d10872..2e1a751c0 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift @@ -189,7 +189,8 @@ extension MediaView { private func bindVideo(configuration: Configuration, info: Configuration.VideoInfo) { let imageInfo = Configuration.ImageInfo( aspectRadio: info.aspectRadio, - assetURL: info.previewURL + assetURL: info.previewURL, + altDescription: info.altDescription ) bindImage(configuration: configuration, info: imageInfo) } diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NewsView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NewsView+Configuration.swift index 7f44232aa..560721b74 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NewsView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NewsView+Configuration.swift @@ -39,7 +39,8 @@ extension NewsView { let configuration = MediaView.Configuration( info: .image(info: .init( aspectRadio: CGSize(width: link.width, height: link.height), - assetURL: link.image + assetURL: link.image, + altDescription: nil )), blurhash: link.blurhash ) From 28b52533f9039b774a571c9fe70e05a9202a96d7 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 11:45:20 -0500 Subject: [PATCH 12/24] =?UTF-8?q?Add=20a=20non-functional=20=E2=80=9CALT?= =?UTF-8?q?=E2=80=9D=20button=20to=20MediaView?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/Content/MediaAltTextOverlay.swift | 62 +++++++++++++++++++ .../MastodonUI/View/Content/MediaView.swift | 36 +++++++++++ 2 files changed, 98 insertions(+) create mode 100644 MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift new file mode 100644 index 000000000..91aeb8993 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift @@ -0,0 +1,62 @@ +// +// MediaAltTextOverlay.swift +// +// +// Created by Jed Fox on 2022-12-20. +// + +import SwiftUI + +@available(iOS 15.0, *) +struct MediaAltTextOverlay: View { + var altDescription: String? + + @State private var showingAlt = false + + var body: some View { + HStack { + VStack { + Spacer(minLength: 0) + if altDescription != nil { + Button("ALT") {} + .buttonStyle(AltButtonStyle()) + } + } + Spacer(minLength: 0) + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + .onChange(of: altDescription) { _ in + showingAlt = false + } + } +} + +@available(iOS 15.0, *) +private struct AltButtonStyle: ButtonStyle { + @Environment(\.pixelLength) private var pixelLength + func makeBody(configuration: Configuration) -> some View { + configuration.label + .font(.caption.weight(.semibold)) + .foregroundColor(.white) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .background(Color.black.opacity(0.85)) + .cornerRadius(4) + .opacity(configuration.isPressed ? 0.5 : 1) + .overlay( + .white.opacity(0.4), + in: RoundedRectangle(cornerRadius: 4) + .inset(by: -0.5) + .stroke(lineWidth: 0.5) + ) + } +} + +@available(iOS 15.0, *) +struct MediaAltTextOverlay_Previews: PreviewProvider { + static var previews: some View { + MediaAltTextOverlay(altDescription: nil) + MediaAltTextOverlay(altDescription: "Hello, world!") + } +} diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift index 2e1a751c0..8ce96674e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift @@ -10,6 +10,7 @@ import AVKit import UIKit import Combine import AlamofireImage +import SwiftUI public final class MediaView: UIView { @@ -71,6 +72,20 @@ public final class MediaView: UIView { return label }() + let _altViewController: UIViewController! = { + if #available(iOS 15.0, *) { + let vc = UIHostingController(rootView: MediaAltTextOverlay()) + vc.view.backgroundColor = .clear + return vc + } else { + return nil + } + }() + @available(iOS 15.0, *) + var altViewController: UIHostingController { + _altViewController as! UIHostingController + } + public override init(frame: CGRect) { super.init(frame: frame) _init() @@ -133,6 +148,7 @@ extension MediaView { imageView.translatesAutoresizingMaskIntoConstraints = false container.addSubview(imageView) imageView.pinToParent() + layoutAlt() } private func bindImage(configuration: Configuration, info: Configuration.ImageInfo) { @@ -151,6 +167,9 @@ extension MediaView { self.imageView.image = image } .store(in: &configuration.disposeBag) + if #available(iOS 15.0, *) { + altViewController.rootView.altDescription = info.altDescription + } } private func layoutGIF() { @@ -161,6 +180,8 @@ extension MediaView { setupIndicatorViewHierarchy() playerIndicatorLabel.attributedText = NSAttributedString(string: "GIF") + + layoutAlt() } private func bindGIF(configuration: Configuration, info: Configuration.VideoInfo) { @@ -171,6 +192,9 @@ extension MediaView { // auto play for GIF player.play() + if #available(iOS 15.0, *) { + altViewController.rootView.altDescription = info.altDescription + } } private func layoutVideo() { @@ -223,6 +247,14 @@ extension MediaView { .store(in: &_disposeBag) } + private func layoutAlt() { + if #available(iOS 15.0, *) { + altViewController.view.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(altViewController.view) + altViewController.view.pinToParent() + } + } + public func prepareForReuse() { _disposeBag.removeAll() @@ -258,6 +290,10 @@ extension MediaView { container.removeFromSuperview() container.removeConstraints(container.constraints) + if #available(iOS 15.0, *) { + altViewController.rootView.altDescription = nil + } + // reset configuration configuration = nil } From 4bcf76740f11c98c739c888fb0969da1a6657114 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 12:36:31 -0500 Subject: [PATCH 13/24] Render alt text --- .../View/Content/MediaAltTextOverlay.swift | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift index 91aeb8993..173fe53ac 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift @@ -14,15 +14,33 @@ struct MediaAltTextOverlay: View { @State private var showingAlt = false var body: some View { - HStack { - VStack { - Spacer(minLength: 0) - if altDescription != nil { - Button("ALT") {} - .buttonStyle(AltButtonStyle()) + GeometryReader { geom in + ZStack { + if let altDescription { + if showingAlt { + HStack(alignment: .top) { + Text(altDescription) + Spacer() + Button(action: { showingAlt = false }) { + Image(systemName: "xmark.circle.fill") + .frame(width: 20, height: 20) + } + } + .padding(8) + .frame(width: geom.size.width) + .fixedSize() + } else { + Button("ALT") { showingAlt = true } + .fixedSize() + .buttonStyle(AltButtonStyle()) + } } } - Spacer(minLength: 0) + .foregroundColor(.white) + .tint(.white) + .background(Color.black.opacity(0.85)) + .cornerRadius(4) + .frame(width: geom.size.width, height: geom.size.height, alignment: .bottomLeading) } .padding(.horizontal, 16) .padding(.vertical, 8) @@ -34,29 +52,21 @@ struct MediaAltTextOverlay: View { @available(iOS 15.0, *) private struct AltButtonStyle: ButtonStyle { - @Environment(\.pixelLength) private var pixelLength func makeBody(configuration: Configuration) -> some View { configuration.label .font(.caption.weight(.semibold)) - .foregroundColor(.white) .padding(.horizontal, 8) .padding(.vertical, 3) - .background(Color.black.opacity(0.85)) - .cornerRadius(4) .opacity(configuration.isPressed ? 0.5 : 1) - .overlay( - .white.opacity(0.4), - in: RoundedRectangle(cornerRadius: 4) - .inset(by: -0.5) - .stroke(lineWidth: 0.5) - ) } } @available(iOS 15.0, *) struct MediaAltTextOverlay_Previews: PreviewProvider { static var previews: some View { - MediaAltTextOverlay(altDescription: nil) MediaAltTextOverlay(altDescription: "Hello, world!") + .frame(height: 300) + .background(Color.gray) + .previewLayout(.sizeThatFits) } } From 7235ba3fb28a0db04645f6e160858fb44e584343 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 12:36:43 -0500 Subject: [PATCH 14/24] Spring transition --- .../MastodonUI/View/Content/MediaAltTextOverlay.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift index 173fe53ac..594ed48ae 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift @@ -12,6 +12,7 @@ struct MediaAltTextOverlay: View { var altDescription: String? @State private var showingAlt = false + @Namespace private var namespace var body: some View { GeometryReader { geom in @@ -26,13 +27,16 @@ struct MediaAltTextOverlay: View { .frame(width: 20, height: 20) } } + .transition(.scale(scale: 0.1, anchor: .bottomLeading)) .padding(8) .frame(width: geom.size.width) .fixedSize() + .matchedGeometryEffect(id: "background", in: namespace) } else { Button("ALT") { showingAlt = true } .fixedSize() .buttonStyle(AltButtonStyle()) + .matchedGeometryEffect(id: "background", in: namespace) } } } @@ -40,6 +44,7 @@ struct MediaAltTextOverlay: View { .tint(.white) .background(Color.black.opacity(0.85)) .cornerRadius(4) + .animation(.spring(response: 0.25), value: showingAlt) .frame(width: geom.size.width, height: geom.size.height, alignment: .bottomLeading) } .padding(.horizontal, 16) From 1461b314ff5cb916e119bd3060b97e1826648649 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 12:36:57 -0500 Subject: [PATCH 15/24] Add a thin white border --- .../MastodonUI/View/Content/MediaAltTextOverlay.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift index 594ed48ae..09aeaa9e0 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift @@ -44,6 +44,12 @@ struct MediaAltTextOverlay: View { .tint(.white) .background(Color.black.opacity(0.85)) .cornerRadius(4) + .overlay( + .white.opacity(0.5), + in: RoundedRectangle(cornerRadius: 4) + .inset(by: -0.5) + .stroke(lineWidth: 0.5) + ) .animation(.spring(response: 0.25), value: showingAlt) .frame(width: geom.size.width, height: geom.size.height, alignment: .bottomLeading) } From 019a9920f04b781068cbef5202e508f3c8f64d23 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 12:56:04 -0500 Subject: [PATCH 16/24] better animations? or at least different --- .../View/Content/MediaAltTextOverlay.swift | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift index 09aeaa9e0..1bab0bc24 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift @@ -27,16 +27,22 @@ struct MediaAltTextOverlay: View { .frame(width: 20, height: 20) } } - .transition(.scale(scale: 0.1, anchor: .bottomLeading)) .padding(8) - .frame(width: geom.size.width) - .fixedSize() - .matchedGeometryEffect(id: "background", in: namespace) + .matchedGeometryEffect(id: "background", in: namespace, properties: .position) + .transition( + .scale(scale: 0.2, anchor: .bottomLeading) + .combined(with: .opacity) + ) } else { Button("ALT") { showingAlt = true } - .fixedSize() - .buttonStyle(AltButtonStyle()) - .matchedGeometryEffect(id: "background", in: namespace) + .font(.caption.weight(.semibold)) + .padding(.horizontal, 8) + .padding(.vertical, 3) + .matchedGeometryEffect(id: "background", in: namespace, properties: .position) + .transition( + .scale(scale: 3, anchor: .trailing) + .combined(with: .opacity) + ) } } } @@ -50,7 +56,7 @@ struct MediaAltTextOverlay: View { .inset(by: -0.5) .stroke(lineWidth: 0.5) ) - .animation(.spring(response: 0.25), value: showingAlt) + .animation(.spring(response: 0.3), value: showingAlt) .frame(width: geom.size.width, height: geom.size.height, alignment: .bottomLeading) } .padding(.horizontal, 16) @@ -61,17 +67,6 @@ struct MediaAltTextOverlay: View { } } -@available(iOS 15.0, *) -private struct AltButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .font(.caption.weight(.semibold)) - .padding(.horizontal, 8) - .padding(.vertical, 3) - .opacity(configuration.isPressed ? 0.5 : 1) - } -} - @available(iOS 15.0, *) struct MediaAltTextOverlay_Previews: PreviewProvider { static var previews: some View { From 042c4968322a362cf96d1a82cfb10dc92482db67 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 13:11:42 -0500 Subject: [PATCH 17/24] Fix image sizing --- .../Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift index 1bab0bc24..fec17f1ee 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaAltTextOverlay.swift @@ -24,6 +24,8 @@ struct MediaAltTextOverlay: View { Spacer() Button(action: { showingAlt = false }) { Image(systemName: "xmark.circle.fill") + .resizable() + .aspectRatio(contentMode: .fill) .frame(width: 20, height: 20) } } From 7553b0aae64fadf534f58ecc3c6ade7171ded3c1 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 13:23:14 -0500 Subject: [PATCH 18/24] Fix MediaView accessibility --- .../Sources/MastodonUI/View/Content/MediaView.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift index 8ce96674e..814e48e6a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift @@ -127,18 +127,18 @@ extension MediaView { case .image(let info): layoutImage() bindImage(configuration: configuration, info: info) - accessibilityLabel = "Show image" // TODO: i18n + accessibilityHint = "Expands the image. Double-tap and hold to show actions" // TODO: i18n case .gif(let info): layoutGIF() bindGIF(configuration: configuration, info: info) - accessibilityLabel = "Show GIF" // TODO: i18n + accessibilityHint = "Expands the GIF. Double-tap and hold to show actions" // TODO: i18n case .video(let info): layoutVideo() bindVideo(configuration: configuration, info: info) - accessibilityLabel = "Show video player" // TODO: i18n + accessibilityHint = "Shows video player. Double-tap and hold to show actions" // TODO: i18n } - accessibilityHint = "Tap then hold to show menu" // TODO: i18n + accessibilityTraits.insert([.button, .image]) layoutBlurhash() bindBlurhash(configuration: configuration) @@ -167,6 +167,8 @@ extension MediaView { self.imageView.image = image } .store(in: &configuration.disposeBag) + + accessibilityLabel = info.altDescription if #available(iOS 15.0, *) { altViewController.rootView.altDescription = info.altDescription } @@ -192,6 +194,8 @@ extension MediaView { // auto play for GIF player.play() + + accessibilityLabel = info.altDescription if #available(iOS 15.0, *) { altViewController.rootView.altDescription = info.altDescription } From cd9e013a40555bde3382fd8a2fa1f156e02d84b9 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 14:01:46 -0500 Subject: [PATCH 19/24] Fix HUDButton hitTest method --- MastodonSDK/Sources/MastodonUI/View/Button/HUDButton.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Button/HUDButton.swift b/MastodonSDK/Sources/MastodonUI/View/Button/HUDButton.swift index 78579bb1e..566100bb6 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Button/HUDButton.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Button/HUDButton.swift @@ -68,6 +68,10 @@ public class HUDButton: UIView { } public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - button + if self.point(inside: point, with: event) { + return button + } else { + return nil + } } } From 5adce841efbdff8943a13d689ad3a66ecc23213d Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 14:22:38 -0500 Subject: [PATCH 20/24] =?UTF-8?q?Label=20images=20as=20=E2=80=9C[alt],=20a?= =?UTF-8?q?ttachment=203=20of=204=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../input/Base.lproj/app.json | 1 + Localization/app.json | 1 + .../Generated/Strings.swift | 4 +++ .../Resources/Base.lproj/Localizable.strings | 1 + .../Content/MediaView+Configuration.swift | 26 +++++++++++++---- .../MastodonUI/View/Content/MediaView.swift | 28 +++++++++++++------ 6 files changed, 46 insertions(+), 15 deletions(-) diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index 4a7112553..243d60b9a 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -144,6 +144,7 @@ "tap_to_reveal": "Tap to reveal", "load_embed": "Load Embed", "link_via_user": "%s via %s", + "media_label": "%s, attachment %d of %d", "poll": { "vote": "Vote", "closed": "Closed" diff --git a/Localization/app.json b/Localization/app.json index 4a7112553..243d60b9a 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -144,6 +144,7 @@ "tap_to_reveal": "Tap to reveal", "load_embed": "Load Embed", "link_via_user": "%s via %s", + "media_label": "%s, attachment %d of %d", "poll": { "vote": "Vote", "closed": "Closed" diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index fff18cf38..75f348a0c 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -298,6 +298,10 @@ public enum L10n { public static let loadEmbed = L10n.tr("Localizable", "Common.Controls.Status.LoadEmbed", fallback: "Load Embed") /// Tap anywhere to reveal public static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning", fallback: "Tap anywhere to reveal") + /// %@, attachment %d of %d + public static func mediaLabel(_ p1: Any, _ p2: Int, _ p3: Int) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.MediaLabel", String(describing: p1), p2, p3, fallback: "%@, attachment %d of %d") + } /// Sensitive Content public static let sensitiveContent = L10n.tr("Localizable", "Common.Controls.Status.SensitiveContent", fallback: "Sensitive Content") /// Show Post diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index aab9bd6cd..830f887b8 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -117,6 +117,7 @@ Please check your internet connection."; "Common.Controls.Status.LinkViaUser" = "%@ via %@"; "Common.Controls.Status.LoadEmbed" = "Load Embed"; "Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal"; +"Common.Controls.Status.MediaLabel" = "%@, attachment %d of %d"; "Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; "Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; "Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift index 1da4e5392..05c8eee14 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift @@ -21,6 +21,8 @@ extension MediaView { public let info: Info public let blurhash: String? + public let index: Int + public let total: Int @Published public var isReveal = true @Published public var previewImage: UIImage? @@ -29,10 +31,14 @@ extension MediaView { public init( info: MediaView.Configuration.Info, - blurhash: String? + blurhash: String?, + index: Int, + total: Int ) { self.info = info self.blurhash = blurhash + self.index = index + self.total = total } public var aspectRadio: CGSize { @@ -186,7 +192,7 @@ extension MediaView { let status = status.reblog ?? status let attachments = status.attachments - let configurations = attachments.map { attachment -> MediaView.Configuration in + let configurations = attachments.enumerated().map { (idx, attachment) -> MediaView.Configuration in let configuration: MediaView.Configuration = { switch attachment.kind { case .image: @@ -197,25 +203,33 @@ extension MediaView { ) return .init( info: .image(info: info), - blurhash: attachment.blurhash + blurhash: attachment.blurhash, + index: idx, + total: attachments.count ) case .video: let info = videoInfo(from: attachment) return .init( info: .video(info: info), - blurhash: attachment.blurhash + blurhash: attachment.blurhash, + index: idx, + total: attachments.count ) case .gifv: let info = videoInfo(from: attachment) return .init( info: .gif(info: info), - blurhash: attachment.blurhash + blurhash: attachment.blurhash, + index: idx, + total: attachments.count ) case .audio: let info = videoInfo(from: attachment) return .init( info: .video(info: info), - blurhash: attachment.blurhash + blurhash: attachment.blurhash, + index: idx, + total: attachments.count ) } // end switch }() diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift index 814e48e6a..1b471d34a 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift @@ -11,6 +11,7 @@ import UIKit import Combine import AlamofireImage import SwiftUI +import MastodonLocalization public final class MediaView: UIView { @@ -168,12 +169,9 @@ extension MediaView { } .store(in: &configuration.disposeBag) - accessibilityLabel = info.altDescription - if #available(iOS 15.0, *) { - altViewController.rootView.altDescription = info.altDescription - } + bindAlt(configuration: configuration, altDescription: info.altDescription) } - + private func layoutGIF() { // use view controller as View here playerViewController.view.translatesAutoresizingMaskIntoConstraints = false @@ -195,10 +193,7 @@ extension MediaView { // auto play for GIF player.play() - accessibilityLabel = info.altDescription - if #available(iOS 15.0, *) { - altViewController.rootView.altDescription = info.altDescription - } + bindAlt(configuration: configuration, altDescription: info.altDescription) } private func layoutVideo() { @@ -223,6 +218,21 @@ extension MediaView { bindImage(configuration: configuration, info: imageInfo) } + private func bindAlt(configuration: Configuration, altDescription: String?) { + if configuration.total > 1 { + accessibilityLabel = L10n.Common.Controls.Status.mediaLabel( + altDescription ?? "", + configuration.index + 1, + configuration.total + ) + } else { + accessibilityLabel = altDescription + } + if #available(iOS 15.0, *) { + altViewController.rootView.altDescription = altDescription + } + } + private func layoutBlurhash() { blurhashImageView.translatesAutoresizingMaskIntoConstraints = false container.addSubview(blurhashImageView) From a9534e480aa305a4fa1955aa48163cfc46735461 Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Tue, 20 Dec 2022 14:44:49 -0500 Subject: [PATCH 21/24] FIx NewsView+Configuration.swift --- .../MastodonUI/View/Content/NewsView+Configuration.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/NewsView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/NewsView+Configuration.swift index 560721b74..8403be756 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/NewsView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/NewsView+Configuration.swift @@ -42,7 +42,9 @@ extension NewsView { assetURL: link.image, altDescription: nil )), - blurhash: link.blurhash + blurhash: link.blurhash, + index: 1, + total: 1 ) imageView.setup(configuration: configuration) From ff502a48684a7901d60d214e8fac8aea2f5d5b7e Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Wed, 21 Dec 2022 19:25:39 -0500 Subject: [PATCH 22/24] Remove some os_logs --- Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift index d4a415f5f..4ff54934b 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift @@ -192,12 +192,10 @@ extension MediaPreviewViewController { extension MediaPreviewViewController { @objc private func closeButtonPressed(_ sender: UIButton) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) dismiss(animated: true, completion: nil) } @objc private func altButtonPressed(_ sender: UIButton) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) guard let alt = viewModel.altText else { return } present(AltViewController(alt: alt, sourceView: sender), animated: true) } From dc6a86f846132ae6e6474c80b1661a84ddf98d3c Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Wed, 21 Dec 2022 19:29:12 -0500 Subject: [PATCH 23/24] Extract out TouchTransparentStackView --- .../MediaPreviewViewController.swift | 16 ------------ .../Container/TouchTransparentStackView.swift | 26 +++++++++++++++++++ 2 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonUI/View/Container/TouchTransparentStackView.swift diff --git a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift index 4ff54934b..658bca33d 100644 --- a/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift +++ b/Mastodon/Scene/MediaPreview/MediaPreviewViewController.swift @@ -26,22 +26,6 @@ final class MediaPreviewViewController: UIViewController, NeedsDependency { let pagingViewController = MediaPreviewPagingViewController() let topToolbar: UIStackView = { - class TouchTransparentStackView: UIStackView { - // allow button hit boxes to grow outside of this view’s bounds - override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { - subviews.contains { $0.point(inside: $0.convert(point, from: self), with: event) } - } - - // allow taps on blank areas to pass through - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - let view = super.hitTest(point, with: event) - if view == self { - return nil - } - return view - } - } - let stackView = TouchTransparentStackView() stackView.axis = .horizontal stackView.distribution = .equalSpacing diff --git a/MastodonSDK/Sources/MastodonUI/View/Container/TouchTransparentStackView.swift b/MastodonSDK/Sources/MastodonUI/View/Container/TouchTransparentStackView.swift new file mode 100644 index 000000000..e519c0cf7 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/View/Container/TouchTransparentStackView.swift @@ -0,0 +1,26 @@ +// +// TouchTransparentStackView.swift +// +// +// Created by Jed Fox on 2022-12-21. +// + +import UIKit + +/// A subclass of `UIStackView` that allows touches that aren’t captured by any +/// of its subviews to pass through to views beneath this view in the Z-order. +public class TouchTransparentStackView: UIStackView { + // allow subview hit boxes to grow outside of this view’s bounds + public override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + subviews.contains { $0.point(inside: $0.convert(point, from: self), with: event) } + } + + // allow taps on blank areas to pass through + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let view = super.hitTest(point, with: event) + if view == self { + return nil + } + return view + } +} From f5c6529341da0f3967c1f58e92b491311b7384ba Mon Sep 17 00:00:00 2001 From: Jed Fox Date: Wed, 21 Dec 2022 19:38:05 -0500 Subject: [PATCH 24/24] i18n for accessibility hint --- .../StringsConvertor/input/Base.lproj/app.json | 7 ++++++- Localization/app.json | 7 ++++++- .../MastodonLocalization/Generated/Strings.swift | 16 ++++++++++++---- .../Resources/Base.lproj/Localizable.strings | 5 ++++- .../MastodonUI/View/Content/MediaView.swift | 8 ++++---- 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/Localization/StringsConvertor/input/Base.lproj/app.json b/Localization/StringsConvertor/input/Base.lproj/app.json index 243d60b9a..2959c9597 100644 --- a/Localization/StringsConvertor/input/Base.lproj/app.json +++ b/Localization/StringsConvertor/input/Base.lproj/app.json @@ -144,7 +144,6 @@ "tap_to_reveal": "Tap to reveal", "load_embed": "Load Embed", "link_via_user": "%s via %s", - "media_label": "%s, attachment %d of %d", "poll": { "vote": "Vote", "closed": "Closed" @@ -188,6 +187,12 @@ "unknown_language": "Unknown", "unknown_provider": "Unknown", "show_original": "Shown Original" + }, + "media": { + "accessibility_label": "%s, attachment %d of %d", + "expand_image_hint": "Expands the image. Double-tap and hold to show actions", + "expand_gif_hint": "Expands the GIF. Double-tap and hold to show actions", + "expand_video_hint": "Shows the video player. Double-tap and hold to show actions" } }, "friendship": { diff --git a/Localization/app.json b/Localization/app.json index 243d60b9a..2959c9597 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -144,7 +144,6 @@ "tap_to_reveal": "Tap to reveal", "load_embed": "Load Embed", "link_via_user": "%s via %s", - "media_label": "%s, attachment %d of %d", "poll": { "vote": "Vote", "closed": "Closed" @@ -188,6 +187,12 @@ "unknown_language": "Unknown", "unknown_provider": "Unknown", "show_original": "Shown Original" + }, + "media": { + "accessibility_label": "%s, attachment %d of %d", + "expand_image_hint": "Expands the image. Double-tap and hold to show actions", + "expand_gif_hint": "Expands the GIF. Double-tap and hold to show actions", + "expand_video_hint": "Shows the video player. Double-tap and hold to show actions" } }, "friendship": { diff --git a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift index 75f348a0c..a3466fad6 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift +++ b/MastodonSDK/Sources/MastodonLocalization/Generated/Strings.swift @@ -298,10 +298,6 @@ public enum L10n { public static let loadEmbed = L10n.tr("Localizable", "Common.Controls.Status.LoadEmbed", fallback: "Load Embed") /// Tap anywhere to reveal public static let mediaContentWarning = L10n.tr("Localizable", "Common.Controls.Status.MediaContentWarning", fallback: "Tap anywhere to reveal") - /// %@, attachment %d of %d - public static func mediaLabel(_ p1: Any, _ p2: Int, _ p3: Int) -> String { - return L10n.tr("Localizable", "Common.Controls.Status.MediaLabel", String(describing: p1), p2, p3, fallback: "%@, attachment %d of %d") - } /// Sensitive Content public static let sensitiveContent = L10n.tr("Localizable", "Common.Controls.Status.SensitiveContent", fallback: "Sensitive Content") /// Show Post @@ -344,6 +340,18 @@ public enum L10n { /// Undo reblog public static let unreblog = L10n.tr("Localizable", "Common.Controls.Status.Actions.Unreblog", fallback: "Undo reblog") } + public enum Media { + /// %@, attachment %d of %d + public static func accessibilityLabel(_ p1: Any, _ p2: Int, _ p3: Int) -> String { + return L10n.tr("Localizable", "Common.Controls.Status.Media.AccessibilityLabel", String(describing: p1), p2, p3, fallback: "%@, attachment %d of %d") + } + /// Expands the GIF. Double-tap and hold to show actions + public static let expandGifHint = L10n.tr("Localizable", "Common.Controls.Status.Media.ExpandGifHint", fallback: "Expands the GIF. Double-tap and hold to show actions") + /// Expands the image. Double-tap and hold to show actions + public static let expandImageHint = L10n.tr("Localizable", "Common.Controls.Status.Media.ExpandImageHint", fallback: "Expands the image. Double-tap and hold to show actions") + /// Shows the video player. Double-tap and hold to show actions + public static let expandVideoHint = L10n.tr("Localizable", "Common.Controls.Status.Media.ExpandVideoHint", fallback: "Shows the video player. Double-tap and hold to show actions") + } public enum MetaEntity { /// Email address: %@ public static func email(_ p1: Any) -> String { diff --git a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings index 830f887b8..5280f2dcd 100644 --- a/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings +++ b/MastodonSDK/Sources/MastodonLocalization/Resources/Base.lproj/Localizable.strings @@ -116,8 +116,11 @@ Please check your internet connection."; "Common.Controls.Status.ContentWarning" = "Content Warning"; "Common.Controls.Status.LinkViaUser" = "%@ via %@"; "Common.Controls.Status.LoadEmbed" = "Load Embed"; +"Common.Controls.Status.Media.AccessibilityLabel" = "%@, attachment %d of %d"; +"Common.Controls.Status.Media.ExpandGifHint" = "Expands the GIF. Double-tap and hold to show actions"; +"Common.Controls.Status.Media.ExpandImageHint" = "Expands the image. Double-tap and hold to show actions"; +"Common.Controls.Status.Media.ExpandVideoHint" = "Shows the video player. Double-tap and hold to show actions"; "Common.Controls.Status.MediaContentWarning" = "Tap anywhere to reveal"; -"Common.Controls.Status.MediaLabel" = "%@, attachment %d of %d"; "Common.Controls.Status.MetaEntity.Email" = "Email address: %@"; "Common.Controls.Status.MetaEntity.Hashtag" = "Hashtag: %@"; "Common.Controls.Status.MetaEntity.Mention" = "Show Profile: %@"; diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift index 1b471d34a..a140d9737 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView.swift @@ -128,15 +128,15 @@ extension MediaView { case .image(let info): layoutImage() bindImage(configuration: configuration, info: info) - accessibilityHint = "Expands the image. Double-tap and hold to show actions" // TODO: i18n + accessibilityHint = L10n.Common.Controls.Status.Media.expandImageHint case .gif(let info): layoutGIF() bindGIF(configuration: configuration, info: info) - accessibilityHint = "Expands the GIF. Double-tap and hold to show actions" // TODO: i18n + accessibilityHint = L10n.Common.Controls.Status.Media.expandGifHint case .video(let info): layoutVideo() bindVideo(configuration: configuration, info: info) - accessibilityHint = "Shows video player. Double-tap and hold to show actions" // TODO: i18n + accessibilityHint = L10n.Common.Controls.Status.Media.expandVideoHint } accessibilityTraits.insert([.button, .image]) @@ -220,7 +220,7 @@ extension MediaView { private func bindAlt(configuration: Configuration, altDescription: String?) { if configuration.total > 1 { - accessibilityLabel = L10n.Common.Controls.Status.mediaLabel( + accessibilityLabel = L10n.Common.Controls.Status.Media.accessibilityLabel( altDescription ?? "", configuration.index + 1, configuration.total