// GalleryViewController.swift // Tusker // // Created by Shadowfacts on 6/13/19. // Copyright © 2019 Shadowfacts. All rights reserved. // import UIKit import Pachyderm import AVFoundation import AVKit class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController { weak var avPlayerViewControllerDelegate: AVPlayerViewControllerDelegate? let attachments: [Attachment] let sourceViews: WeakArray let startIndex: Int var pages: [UIViewController]! var currentIndex: Int { guard let vc = viewControllers?.first, let index = pages.firstIndex(of: vc) else { fatalError() } return index } var animationSourceView: UIImageView? { sourceViews[currentIndex] } var largeImageController: LargeImageViewController? { // use protocol because page controllers may be loading or non-loading VCs (pages[currentIndex] as? LargeImageAnimatableViewController)?.largeImageController } var animationImage: UIImage? { if let page = pages[currentIndex] as? LargeImageAnimatableViewController, let image = page.animationImage { return image } else { return animationSourceView?.image } } var animationGifData: Data? { let attachment = attachments[currentIndex] if attachment.url.pathExtension == "gif" { return ImageCache.attachments.getData(attachment.url) } else { return nil } } var dismissInteractionController: LargeImageInteractionController? override var prefersStatusBarHidden: Bool { return true } override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { return .none } override var childForHomeIndicatorAutoHidden: UIViewController? { return viewControllers?.first } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { if UIDevice.current.userInterfaceIdiom == .phone { return .allButUpsideDown } else { return .all } } init(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) { self.attachments = attachments self.sourceViews = WeakArray(sourceViews) self.startIndex = startIndex super.init(transitionStyle: .scroll, navigationOrientation: .horizontal) modalPresentationStyle = .fullScreen } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() self.pages = attachments.enumerated().map { (index, attachment) in switch attachment.kind { case .image: let vc = LoadingLargeImageViewController(attachment: attachment) vc.shrinkGestureEnabled = false vc.animationSourceView = sourceViews[index] return vc case .video, .audio: let vc = GalleryPlayerViewController() vc.player = AVPlayer(url: attachment.url) vc.delegate = avPlayerViewControllerDelegate vc.attachment = attachment return vc case .gifv: // Passing the source view to the LargeImageGifvContentView is a crappy workaround for not // having the video size directly inside the content view. This will break when there // are more than 4 attachments and there is a gifv at index >= 3 (the More... button will show // in place of the fourth attachment, so there aren't source views for the attachments at index >= 3). // Really, what should happen is the LargeImageGifvContentView should get the size of the video from // the AVFoundation instead of the source view. // This isn't a priority as only Mastodon converts gifs to gifvs, and Mastodon (in its default configuration, // I don't know about forks) doesn't allow more than four attachments, meaning there will always be a source view. let gifvContentView = LargeImageGifvContentView(attachment: attachment, source: sourceViews[index]!) let vc = LargeImageViewController(contentView: gifvContentView, description: attachment.description, sourceView: nil) vc.shrinkGestureEnabled = false return vc default: fatalError() } } setViewControllers([pages[startIndex]], direction: .forward, animated: false) self.dataSource = self self.delegate = self overrideUserInterfaceStyle = .dark dismissInteractionController = LargeImageInteractionController(viewController: self) } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if let vc = pages[currentIndex] as? AVPlayerViewController { // when the gallery is first shown, after the transition finishes, the controls for the player controller appear semi-transparent // hiding the controls and then immediately reshowing them makes sure they're visible when the gallery is presented vc.showsPlaybackControls = false vc.showsPlaybackControls = true // begin playing the video as soon as we appear vc.player?.play() } } // MARK: - Page View Controller Data Source func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let index = pages.firstIndex(of: viewController), index > 0 else { return nil } return pages[index - 1] } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let index = pages.firstIndex(of: viewController), index < pages.count - 1 else { return nil } return pages[index + 1] } // MARK: - Page View Controller Delegate func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { if let pending = pendingViewControllers.first as? LoadingLargeImageViewController, let current = viewControllers!.first as? LoadingLargeImageViewController { pending.controlsVisible = current.controlsVisible } if let pending = pendingViewControllers.first as? AVPlayerViewController { // show controls and begin playing when the player page becomes visible pending.showsPlaybackControls = true pending.player?.play() } } }