// // MediaPreviewViewController.swift // Mastodon // // Created by MainasuK Cirno on 2021-4-28. // import os.log import UIKit import Combine import Pageboy 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) } } var disposeBag = Set() var viewModel: MediaPreviewViewModel! 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 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) } } extension MediaPreviewViewController { override func viewDidLoad() { super.viewDidLoad() overrideUserInterfaceStyle = .dark visualEffectView.frame = view.bounds view.addSubview(visualEffectView) pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false addChild(pagingViewController) visualEffectView.contentView.addSubview(pagingViewController.view) NSLayoutConstraint.activate([ visualEffectView.topAnchor.constraint(equalTo: pagingViewController.view.topAnchor), visualEffectView.bottomAnchor.constraint(equalTo: pagingViewController.view.bottomAnchor), visualEffectView.leadingAnchor.constraint(equalTo: pagingViewController.view.leadingAnchor), visualEffectView.trailingAnchor.constraint(equalTo: pagingViewController.view.trailingAnchor), ]) 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) 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), ]) viewModel.mediaPreviewImageViewControllerDelegate = self pagingViewController.interPageSpacing = 10 pagingViewController.delegate = self pagingViewController.dataSource = viewModel closeButton.addTarget(self, action: #selector(MediaPreviewViewController.closeButtonPressed(_:)), for: .touchUpInside) // bind view model viewModel.currentPage .receive(on: DispatchQueue.main) .sink { [weak self] index in guard let self = self else { return } switch self.viewModel.pushTransitionItem.source { case .mosaic(let mosaicImageViewContainer): UIView.animate(withDuration: 0.3) { mosaicImageViewContainer.setImageViews(alpha: 1) mosaicImageViewContainer.setImageView(alpha: 0, index: index) } case .profileAvatar, .profileBanner: break } } .store(in: &disposeBag) } } 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) } } // MARK: - MediaPreviewingViewController extension MediaPreviewViewController: MediaPreviewingViewController { func isInteractiveDismissible() -> Bool { if let mediaPreviewImageViewController = pagingViewController.currentViewController as? MediaPreviewImageViewController { let previewImageView = mediaPreviewImageViewController.previewImageView // TODO: allow zooming pan dismiss guard previewImageView.zoomScale == previewImageView.minimumZoomScale else { return false } let safeAreaInsets = previewImageView.safeAreaInsets let statusBarFrameHeight = view.window?.windowScene?.statusBarManager?.statusBarFrame.height ?? 0 let dismissible = previewImageView.contentOffset.y <= -(safeAreaInsets.top - statusBarFrameHeight) + 3 // add 3pt tolerance os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: dismissible %s", ((#file as NSString).lastPathComponent), #line, #function, dismissible ? "true" : "false") return dismissible } os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: dismissible false", ((#file as NSString).lastPathComponent), #line, #function) return false } } // MARK: - PageboyViewControllerDelegate extension MediaPreviewViewController: PageboyViewControllerDelegate { func pageboyViewController( _ pageboyViewController: PageboyViewController, willScrollToPageAt index: PageboyViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool ) { // do nothing } func pageboyViewController( _ pageboyViewController: PageboyViewController, didScrollTo position: CGPoint, direction: PageboyViewController.NavigationDirection, animated: Bool ) { // do nothing } func pageboyViewController( _ pageboyViewController: PageboyViewController, didScrollToPageAt index: PageboyViewController.PageIndex, direction: PageboyViewController.NavigationDirection, animated: Bool ) { // update page control // pageControl.currentPage = index viewModel.currentPage.value = index } func pageboyViewController( _ pageboyViewController: PageboyViewController, didReloadWith currentViewController: UIViewController, currentPageIndex: PageboyViewController.PageIndex ) { // do nothing } } // MARK: - MediaPreviewImageViewControllerDelegate extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate { func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer) { // do nothing } func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, longPressGestureRecognizerDidTrigger longPressGestureRecognizer: UILongPressGestureRecognizer) { // do nothing } func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, contextMenuActionPerform action: MediaPreviewImageViewController.ContextMenuAction) { switch action { case .savePhoto: switch viewController.viewModel.item { case .status(let meta): context.photoLibraryService.saveImage(url: meta.url) .sink { [weak self] completion in guard let self = self else { return } switch completion { case .failure(let error): guard let error = error as? PhotoLibraryService.PhotoLibraryError, case .noPermission = error else { return } let alertController = SettingService.openSettingsAlertController(title: L10n.Common.Alerts.SavePhotoFailure.title, message: L10n.Common.Alerts.SavePhotoFailure.message) self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) case .finished: break } } receiveValue: { _ in // do nothing } .store(in: &context.disposeBag) case .local(let meta): context.photoLibraryService.save(image: meta.image, withNotificationFeedback: true) } case .share: let applicationActivities: [UIActivity] = [ SafariActivity(sceneCoordinator: self.coordinator) ] let activityViewController = UIActivityViewController( activityItems: viewController.viewModel.item.activityItems, applicationActivities: applicationActivities ) activityViewController.popoverPresentationController?.sourceView = viewController.previewImageView.imageView self.present(activityViewController, animated: true, completion: nil) } } } extension MediaPreviewViewController { var closeKeyCommand: UIKeyCommand { UIKeyCommand( title: L10n.Scene.Preview.Keyboard.closePreview, image: nil, action: #selector(MediaPreviewViewController.closePreviewKeyCommandHandler(_:)), input: "i", modifierFlags: [], propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .off ) } var showNextKeyCommand: UIKeyCommand { UIKeyCommand( title: L10n.Scene.Preview.Keyboard.closePreview, image: nil, action: #selector(MediaPreviewViewController.showNextKeyCommandHandler(_:)), input: "j", modifierFlags: [], propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .off ) } var showPreviousKeyCommand: UIKeyCommand { UIKeyCommand( title: L10n.Scene.Preview.Keyboard.closePreview, image: nil, action: #selector(MediaPreviewViewController.showPreviousKeyCommandHandler(_:)), input: "k", modifierFlags: [], propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .off ) } override var keyCommands: [UIKeyCommand] { return [ closeKeyCommand, showNextKeyCommand, showPreviousKeyCommand, ] } @objc private func closePreviewKeyCommandHandler(_ sender: UIKeyCommand) { dismiss(animated: true, completion: nil) } @objc private func showNextKeyCommandHandler(_ sender: UIKeyCommand) { pagingViewController.scrollToPage(.next, animated: true, completion: nil) } @objc private func showPreviousKeyCommandHandler(_ sender: UIKeyCommand) { pagingViewController.scrollToPage(.previous, animated: true, completion: nil) } }