// // GalleryViewController.swift // GalleryVC // // Created by Shadowfacts on 12/28/23. // import UIKit public class GalleryViewController: UIPageViewController { let galleryDataSource: GalleryDataSource let initialItemIndex: Int private let _itemsCount: Int private var itemsCount: Int { get { precondition(_itemsCount == galleryDataSource.galleryItemsCount(), "GalleryDataSource item count cannot change") return _itemsCount } } var currentItemViewController: GalleryItemViewController { viewControllers![0] as! GalleryItemViewController } private var dismissInteraction: GalleryDismissInteraction! private var presentationAnimationCompletionHandlers: [() -> Void] = [] override public var prefersStatusBarHidden: Bool { true } override public var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { .none } override public var childForHomeIndicatorAutoHidden: UIViewController? { currentItemViewController } public init(dataSource: GalleryDataSource, initialItemIndex: Int) { self.galleryDataSource = dataSource self.initialItemIndex = initialItemIndex self._itemsCount = dataSource.galleryItemsCount() precondition(initialItemIndex >= 0 && initialItemIndex < _itemsCount, "initialItemIndex is out of bounds") super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [ .interPageSpacing: 50 ]) modalPresentationStyle = .fullScreen transitioningDelegate = self } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public override func viewDidLoad() { super.viewDidLoad() dismissInteraction = GalleryDismissInteraction(viewController: self) view.backgroundColor = .black overrideUserInterfaceStyle = .dark dataSource = self delegate = self setViewControllers([makeItemVC(index: initialItemIndex)], direction: .forward, animated: false) } public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) if animated { // Wait until the transition is no longer in-progress, otherwise things will just get deferred again. DispatchQueue.main.async { self.presentationAnimationCompleted() } } } public override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if isBeingDismissed { currentItemViewController.content.galleryContentWillDisappear() } } private func makeItemVC(index: Int) -> GalleryItemViewController { let content = galleryDataSource.galleryContentViewController(forItemAt: index) return GalleryItemViewController(delegate: self, itemIndex: index, content: content) } func presentationAnimationCompleted() { for block in presentationAnimationCompletionHandlers { block() } currentItemViewController.content.galleryContentDidAppear() } } extension GalleryViewController: UIPageViewControllerDataSource { public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let viewController = viewController as? GalleryItemViewController else { preconditionFailure("VC must be GalleryItemViewController") } guard viewController.itemIndex > 0 else { return nil } return makeItemVC(index: viewController.itemIndex - 1) } public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let viewController = viewController as? GalleryItemViewController else { preconditionFailure("VC must be GalleryItemViewController") } guard viewController.itemIndex < itemsCount - 1 else { return nil } return makeItemVC(index: viewController.itemIndex + 1) } } extension GalleryViewController: UIPageViewControllerDelegate { public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { currentItemViewController.content.galleryContentWillDisappear() } public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { currentItemViewController.content.galleryContentDidAppear() } } extension GalleryViewController: GalleryItemViewControllerDelegate { func isGalleryBeingPresented() -> Bool { isBeingPresented } func addPresentationAnimationCompletion(_ block: @escaping () -> Void) { presentationAnimationCompletionHandlers.append(block) } func galleryItemClose(_ item: GalleryItemViewController) { dismiss(animated: true) } func galleryItemApplicationActivities(_ item: GalleryItemViewController) -> [UIActivity]? { galleryDataSource.galleryApplicationActivities(forItemAt: item.itemIndex) } } extension GalleryViewController: UIViewControllerTransitioningDelegate { public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { if let sourceView = galleryDataSource.galleryContentTransitionSourceView(forItemAt: initialItemIndex) { return GalleryPresentationAnimationController(sourceView: sourceView) } else { return nil } } public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { if let sourceView = galleryDataSource.galleryContentTransitionSourceView(forItemAt: currentItemViewController.itemIndex) { let translation: CGPoint? let velocity: CGPoint? if let dismissInteraction, dismissInteraction.isActive { translation = dismissInteraction.dismissTranslation velocity = dismissInteraction.dismissVelocity } else { translation = nil velocity = nil } return GalleryDismissAnimationController(sourceView: sourceView, interactiveTranslation: translation, interactiveVelocity: velocity) } else { return nil } } }