diff --git a/Packages/ComposeUI/Package.swift b/Packages/ComposeUI/Package.swift index 0cb82d3e..3b34ad6f 100644 --- a/Packages/ComposeUI/Package.swift +++ b/Packages/ComposeUI/Package.swift @@ -22,13 +22,22 @@ let package = Package( .package(path: "../MatchedGeometryPresentation"), .package(path: "../TuskerPreferences"), .package(path: "../UserAccounts"), + .package(path: "../GalleryVC"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "ComposeUI", - dependencies: ["Pachyderm", "InstanceFeatures", "TuskerComponents", "MatchedGeometryPresentation", "TuskerPreferences", "UserAccounts"], + dependencies: [ + "Pachyderm", + "InstanceFeatures", + "TuskerComponents", + "MatchedGeometryPresentation", + "TuskerPreferences", + "UserAccounts", + "GalleryVC", + ], swiftSettings: [ .swiftLanguageMode(.v5) ]), diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentCollectionViewCell.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentCollectionViewCell.swift index fdc5e4d9..49991ce8 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentCollectionViewCell.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentCollectionViewCell.swift @@ -76,7 +76,7 @@ private struct AttachmentRemoveButton: View { } private struct AttachmentDescriptionLabel: View { - let attachment: DraftAttachment + @ObservedObject var attachment: DraftAttachment var body: some View { ZStack(alignment: .bottomLeading) { @@ -87,10 +87,8 @@ private struct AttachmentDescriptionLabel: View { ) label - .lineLimit(1) - .font(.callout) .foregroundStyle(.white) - .shadow(radius: 1) + .shadow(color: .black.opacity(0.5), radius: 1) .padding([.horizontal, .bottom], 4) } } @@ -100,8 +98,12 @@ private struct AttachmentDescriptionLabel: View { if attachment.attachmentDescription.isEmpty { Label("Add alt", systemImage: "pencil") .labelStyle(NarrowSpacingLabelStyle()) + .font(.callout) + .lineLimit(1) } else { Text(attachment.attachmentDescription) + .font(.caption) + .lineLimit(2) } } } diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentsGalleryDataSource.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentsGalleryDataSource.swift new file mode 100644 index 00000000..49e7fa93 --- /dev/null +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentsGalleryDataSource.swift @@ -0,0 +1,50 @@ +// +// AttachmentGalleryDataSource.swift +// ComposeUI +// +// Created by Shadowfacts on 11/21/24. +// + +import UIKit +import GalleryVC + +struct AttachmentsGalleryDataSource: GalleryDataSource { + let collectionView: UICollectionView + let attachmentAtIndex: (Int) -> DraftAttachment? + + func galleryItemsCount() -> Int { + collectionView.numberOfItems(inSection: 0) - 1 + } + + func galleryContentViewController(forItemAt index: Int) -> any GalleryVC.GalleryContentViewController { + let attachment = attachmentAtIndex(index)! + + let content: any GalleryContentViewController + switch attachment.data { + case .editing(_, _, _): + fatalError("TODO") + + case .asset(_): + fatalError("TODO") + + case .drawing(let drawing): + let image = drawing.imageInLightMode(from: drawing.bounds) + content = ImageGalleryContentViewController(image: image, caption: nil, gifController: nil) + + case .file(_, _): + fatalError("TODO") + + case .none: + return LoadingGalleryContentViewController(caption: nil) { + nil + } + } + + return EditAttachmentWrapperGalleryContentViewController(draftAttachment: attachment, wrapped: content) + } + + func galleryContentTransitionSourceView(forItemAt index: Int) -> UIView? { + collectionView.cellForItem(at: IndexPath(item: index, section: 0)) + } + +} diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentsSection.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentsSection.swift index e1aef7c8..15d2ab32 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentsSection.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/AttachmentsSection.swift @@ -8,6 +8,7 @@ import SwiftUI import PhotosUI import PencilKit +import GalleryVC struct AttachmentsSection: View { @ObservedObject var draft: Draft @@ -24,14 +25,49 @@ struct AttachmentsSection: View { } } -private struct WrappedCollectionView: UIViewRepresentable { +// Use a UIViewControllerRepresentable so we have something from which to present the gallery VC. +private struct WrappedCollectionView: UIViewControllerRepresentable { @ObservedObject var draft: Draft let spacing: CGFloat let minItemSize: CGFloat - func makeUIView(context: Context) -> some UIView { - let layout = UICollectionViewCompositionalLayout { section, environment in - let (itemSize, itemsPerRow) = itemSize(width: environment.container.contentSize.width) + func makeUIViewController(context: Context) -> WrappedCollectionViewController { + WrappedCollectionViewController(spacing: spacing, minItemSize: minItemSize) + } + + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { + uiViewController.draft = draft + uiViewController.addAttachment = { + DraftsPersistentContainer.shared.viewContext.insert($0) + $0.draft = draft + draft.attachments.add($0) + } + uiViewController.updateAttachments() + } +} + +private class WrappedCollectionViewController: UIViewController { + let spacing: CGFloat + let minItemSize: CGFloat + var draft: Draft! + private var dataSource: UICollectionViewDiffableDataSource! + fileprivate var currentInteractiveMoveStartOffsetInCell: CGPoint? + fileprivate var currentInteractiveMoveCell: AttachmentCollectionViewCell? + fileprivate var addAttachment: ((DraftAttachment) -> Void)? = nil + + init(spacing: CGFloat, minItemSize: CGFloat) { + self.spacing = spacing + self.minItemSize = minItemSize + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + let layout = UICollectionViewCompositionalLayout { [unowned self] section, environment in + let (itemSize, itemsPerRow) = self.itemSize(width: environment.container.contentSize.width) let item = NSCollectionLayoutItem(layoutSize: .init(widthDimension: .absolute(itemSize), heightDimension: .absolute(itemSize))) let group = NSCollectionLayoutGroup.horizontal(layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(itemSize)), repeatingSubitem: item, count: itemsPerRow) @@ -40,9 +76,23 @@ private struct WrappedCollectionView: UIViewRepresentable { section.interGroupSpacing = spacing return section } - let view = IntrinsicContentSizeCollectionView(frame: .zero, collectionViewLayout: layout) - let dataSource = UICollectionViewDiffableDataSource(collectionView: view) { collectionView, indexPath, itemIdentifier in - context.coordinator.makeCell(collectionView: collectionView, indexPath: indexPath, item: itemIdentifier) + let attachmentCell = UICollectionView.CellRegistration { cell, indexPath, attachment in + cell.updateUI(attachment: attachment) + } + let addButtonCell = UICollectionView.CellRegistration { [unowned self] cell, indexPath, item in + cell.contentConfiguration = UIHostingConfiguration(content: { + AddAttachmentButton(viewController: self, enabled: item) + }).margins(.all, .zero) + } + let collectionView = IntrinsicContentSizeCollectionView(frame: .zero, collectionViewLayout: layout) + self.view = collectionView + dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in + switch itemIdentifier { + case .attachment(let attachment): + return collectionView.dequeueConfiguredReusableCell(using: attachmentCell, for: indexPath, item: attachment) + case .addButton: + return collectionView.dequeueConfiguredReusableCell(using: addButtonCell, for: indexPath, item: true) + } } dataSource.reorderingHandlers.canReorderItem = { item in switch item { @@ -52,7 +102,7 @@ private struct WrappedCollectionView: UIViewRepresentable { false } } - dataSource.reorderingHandlers.didReorder = { transaction in + dataSource.reorderingHandlers.didReorder = { [unowned self] transaction in let attachmentChanges = transaction.difference.map { switch $0 { case .insert(let offset, let element, let associatedWith): @@ -67,35 +117,14 @@ private struct WrappedCollectionView: UIViewRepresentable { let array = draft.draftAttachments.applying(attachmentsDiff)! draft.attachments = NSMutableOrderedSet(array: array) } - context.coordinator.dataSource = dataSource - view.isScrollEnabled = false - view.clipsToBounds = false - view.delegate = context.coordinator + collectionView.isScrollEnabled = false + collectionView.clipsToBounds = false + collectionView.delegate = self - let longPressRecognizer = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(WrappedCollectionViewCoordinator.reorderingLongPressRecognized)) - longPressRecognizer.delegate = context.coordinator - view.addGestureRecognizer(longPressRecognizer) - - return view - } - - func updateUIView(_ uiView: UIViewType, context: Context) { - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.all]) - snapshot.appendItems(draft.draftAttachments.map { .attachment($0) }) - snapshot.appendItems([.addButton]) - context.coordinator.dataSource.apply(snapshot) - context.coordinator.addAttachment = { - DraftsPersistentContainer.shared.viewContext.insert($0) - $0.draft = draft - draft.attachments.add($0) - } - context.coordinator.draft = draft - } - - func makeCoordinator() -> WrappedCollectionViewCoordinator { - WrappedCollectionViewCoordinator() + let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(reorderingLongPressRecognized)) + longPressRecognizer.delegate = self + collectionView.addGestureRecognizer(longPressRecognizer) } private func itemSize(width: CGFloat) -> (CGFloat, Int) { @@ -118,42 +147,13 @@ private struct WrappedCollectionView: UIViewRepresentable { itemSize = itemSize + remainingSpace / fittingCount return (itemSize, Int(fittingCount)) } - - enum Section { - case all - } - - enum Item: Hashable { - case attachment(DraftAttachment) - case addButton - } -} -private class WrappedCollectionViewCoordinator: NSObject { - var draft: Draft! - var dataSource: UICollectionViewDiffableDataSource! - var currentInteractiveMoveStartOffsetInCell: CGPoint? - var currentInteractiveMoveCell: AttachmentCollectionViewCell? - var addAttachment: ((DraftAttachment) -> Void)? = nil - - private let attachmentCell = UICollectionView.CellRegistration { cell, indexPath, attachment in - cell.updateUI(attachment: attachment) - } - - private let addButtonCell = UICollectionView.CellRegistration { cell, indexPath, item in - let (coordinator, enabled) = item - cell.contentConfiguration = UIHostingConfiguration(content: { - AddAttachmentButton(coordinator: coordinator, enabled: enabled) - }).margins(.all, .zero) - } - - func makeCell(collectionView: UICollectionView, indexPath: IndexPath, item: WrappedCollectionView.Item) -> UICollectionViewCell { - switch item { - case .attachment(let attachment): - return collectionView.dequeueConfiguredReusableCell(using: attachmentCell, for: indexPath, item: attachment) - case .addButton: - return collectionView.dequeueConfiguredReusableCell(using: addButtonCell, for: indexPath, item: (self, true)) - } + func updateAttachments() { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.all]) + snapshot.appendItems(draft.draftAttachments.map { .attachment($0) }) + snapshot.appendItems([.addButton]) + dataSource.apply(snapshot) } @objc func reorderingLongPressRecognized(_ recognizer: UILongPressGestureRecognizer) { @@ -186,9 +186,18 @@ private class WrappedCollectionViewCoordinator: NSObject { break } } + + enum Section { + case all + } + + enum Item: Hashable { + case attachment(DraftAttachment) + case addButton + } } -extension WrappedCollectionViewCoordinator: UIGestureRecognizerDelegate { +extension WrappedCollectionViewController: UIGestureRecognizerDelegate { func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { let collectionView = gestureRecognizer.view as! UICollectionView let location = gestureRecognizer.location(in: collectionView) @@ -207,7 +216,7 @@ extension WrappedCollectionViewCoordinator: UIGestureRecognizerDelegate { } } -extension WrappedCollectionViewCoordinator: UICollectionViewDelegate { +extension WrappedCollectionViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveOfItemFromOriginalIndexPath originalIndexPath: IndexPath, atCurrentIndexPath currentIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath { let snapshot = dataSource.snapshot() let items = snapshot.itemIdentifiers(inSection: .all).count @@ -217,6 +226,24 @@ extension WrappedCollectionViewCoordinator: UICollectionViewDelegate { return proposedIndexPath } } + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard case .attachment(_) = dataSource.itemIdentifier(for: indexPath) else { + return + } + let dataSource = AttachmentsGalleryDataSource(collectionView: collectionView) { [dataSource] in + let item = dataSource?.itemIdentifier(for: IndexPath(item: $0, section: 0)) + switch item { + case .attachment(let attachment): + return attachment + default: + return nil + } + } + let galleryVC = GalleryViewController(dataSource: dataSource, initialItemIndex: indexPath.item) + galleryVC.showShareButton = false + present(galleryVC, animated: true) + } } private final class IntrinsicContentSizeCollectionView: UICollectionView { @@ -237,7 +264,7 @@ private final class IntrinsicContentSizeCollectionView: UICollectionView { } private struct AddAttachmentButton: View { - let coordinator: WrappedCollectionViewCoordinator + unowned let viewController: WrappedCollectionViewController let enabled: Bool @Environment(\.composeUIConfig.presentAssetPicker) private var presentAssetPicker @Environment(\.composeUIConfig.presentDrawing) private var presentDrawing @@ -247,7 +274,7 @@ private struct AddAttachmentButton: View { if let presentAssetPicker { Button("Add photo or video", systemImage: "photo") { presentAssetPicker { - let draft = coordinator.draft! + let draft = viewController.draft! AttachmentsListSection.insertAttachments(in: draft, at: draft.attachments.count, itemProviders: $0.map(\.itemProvider)) } } @@ -255,7 +282,7 @@ private struct AddAttachmentButton: View { if let presentDrawing { Button("Draw something", systemImage: "hand.draw") { presentDrawing(PKDrawing()) { drawing in - let draft = coordinator.draft! + let draft = viewController.draft! let attachment = DraftAttachment(context: DraftsPersistentContainer.shared.viewContext) attachment.id = UUID() attachment.drawing = drawing diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/EditAttachmentWrapperGalleryContentViewController.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/EditAttachmentWrapperGalleryContentViewController.swift new file mode 100644 index 00000000..86fe600b --- /dev/null +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/Attachments/EditAttachmentWrapperGalleryContentViewController.swift @@ -0,0 +1,198 @@ +// +// EditAttachmentWrapperGalleryContentViewController.swift +// ComposeUI +// +// Created by Shadowfacts on 11/22/24. +// + +import UIKit +import GalleryVC + +class EditAttachmentWrapperGalleryContentViewController: UIViewController, GalleryContentViewController { + let draftAttachment: DraftAttachment + let wrapped: any GalleryContentViewController + + var container: (any GalleryContentViewControllerContainer)? + + var contentSize: CGSize { + wrapped.contentSize + } + + var activityItemsForSharing: [Any] { + wrapped.activityItemsForSharing + } + + var caption: String? { + wrapped.caption + } + + private lazy var editDescriptionViewController: EditAttachmentDescriptionViewController = EditAttachmentDescriptionViewController(draftAttachment: draftAttachment, wrapped: wrapped.bottomControlsAccessoryViewController) + + var bottomControlsAccessoryViewController: UIViewController? { + editDescriptionViewController + } + + var insetBottomControlsAccessoryViewControllerToSafeArea: Bool { + false + } + + var canAnimateFromSourceView: Bool { + wrapped.canAnimateFromSourceView + } + + var hideControlsOnZoom: Bool { + false + } + + init(draftAttachment: DraftAttachment, wrapped: any GalleryContentViewController) { + self.draftAttachment = draftAttachment + self.wrapped = wrapped + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + wrapped.container = container + addChild(wrapped) + wrapped.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(wrapped.view) + NSLayoutConstraint.activate([ + wrapped.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + wrapped.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + wrapped.view.topAnchor.constraint(equalTo: view.topAnchor), + wrapped.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + wrapped.didMove(toParent: self) + } + + func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { + wrapped.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction) + if !visible { + editDescriptionViewController.textView?.resignFirstResponder() + } + } + + func galleryContentDidAppear() { + wrapped.galleryContentDidAppear() + } + + func galleryContentWillDisappear() { + wrapped.galleryContentWillDisappear() + } + + func shouldHideControls() -> Bool { + if editDescriptionViewController.textView.isFirstResponder { + editDescriptionViewController.textView.resignFirstResponder() + return false + } else { + return true + } + } +} + +private class EditAttachmentDescriptionViewController: UIViewController { + private let draftAttachment: DraftAttachment + private let wrapped: UIViewController? + + private(set) var textView: UITextView! + private var isShowingPlaceholder = false + + private var descriptionObservation: NSKeyValueObservation? + + init(draftAttachment: DraftAttachment, wrapped: UIViewController?) { + self.draftAttachment = draftAttachment + self.wrapped = wrapped + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + view.overrideUserInterfaceStyle = .dark + view.backgroundColor = .secondarySystemFill + + let stack = UIStackView() + stack.axis = .vertical + stack.distribution = .fill + stack.spacing = 0 + stack.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(stack) + NSLayoutConstraint.activate([ + stack.leadingAnchor.constraint(equalTo: view.leadingAnchor), + stack.trailingAnchor.constraint(equalTo: view.trailingAnchor), + stack.topAnchor.constraint(equalTo: view.topAnchor), + stack.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor), + ]) + + if let wrapped { + stack.addArrangedSubview(wrapped.view) + } + + textView = UITextView() + textView.backgroundColor = nil + textView.font = .preferredFont(forTextStyle: .body) + textView.adjustsFontForContentSizeCategory = true + if draftAttachment.attachmentDescription.isEmpty { + showPlaceholder() + } else { + removePlaceholder() + textView.text = draftAttachment.attachmentDescription + } + textView.delegate = self + stack.addArrangedSubview(textView) + textView.heightAnchor.constraint(equalToConstant: 150).isActive = true + + descriptionObservation = draftAttachment.observe(\.attachmentDescription) { [unowned self] _, _ in + let desc = self.draftAttachment.attachmentDescription + if desc.isEmpty { + if !isShowingPlaceholder { + showPlaceholder() + } + } else { + if isShowingPlaceholder { + removePlaceholder() + } + self.textView.text = desc + } + } + } + + fileprivate func showPlaceholder() { + isShowingPlaceholder = true + textView.text = "Describe for the visually impaired" + textView.textColor = .secondaryLabel + } + + fileprivate func removePlaceholder() { + isShowingPlaceholder = false + textView.text = "" + textView.textColor = .label + } +} + +extension EditAttachmentDescriptionViewController: UITextViewDelegate { + func textViewDidBeginEditing(_ textView: UITextView) { + if isShowingPlaceholder { + removePlaceholder() + } + } + + func textViewDidEndEditing(_ textView: UITextView) { + draftAttachment.attachmentDescription = textView.text + + if textView.text.isEmpty { + showPlaceholder() + } + } +} diff --git a/Packages/GalleryVC/Sources/GalleryVC/Content/VideoGalleryContentViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/Content/VideoGalleryContentViewController.swift index 09f2751d..11fc0820 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/Content/VideoGalleryContentViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/Content/VideoGalleryContentViewController.swift @@ -153,7 +153,6 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi } open var activityItemsForSharing: [Any] { -// [VideoActivityItemSource(asset: item.asset, url: url)] [] } diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift index e2bbbfa5..980e720d 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryContentViewController.swift @@ -15,8 +15,11 @@ public protocol GalleryContentViewController: UIViewController { var caption: String? { get } var contentOverlayAccessoryViewController: UIViewController? { get } var bottomControlsAccessoryViewController: UIViewController? { get } + var insetBottomControlsAccessoryViewControllerToSafeArea: Bool { get } var canAnimateFromSourceView: Bool { get } + var hideControlsOnZoom: Bool { get } + func shouldHideControls() -> Bool func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) func galleryContentDidAppear() func galleryContentWillDisappear() @@ -31,10 +34,22 @@ public extension GalleryContentViewController { nil } + var insetBottomControlsAccessoryViewControllerToSafeArea: Bool { + true + } + var canAnimateFromSourceView: Bool { true } + var hideControlsOnZoom: Bool { + true + } + + func shouldHideControls() -> Bool { + true + } + func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { } diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift index b41f7397..36491852 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryItemViewController.swift @@ -45,6 +45,12 @@ class GalleryItemViewController: UIViewController { private var scrollViewSizeForLastZoomScaleUpdate: CGSize? + var showShareButton: Bool = true { + didSet { + shareButton?.isHidden = !showShareButton + } + } + override var prefersHomeIndicatorAutoHidden: Bool { return !controlsVisible } @@ -66,6 +72,10 @@ class GalleryItemViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() + // Need to use the keyboard layout guide in some way in this VC, + // otherwise the keyboardLayoutGuide inside the bottom controls accessory view doesn't animate + _ = view.keyboardLayoutGuide + scrollView = UIScrollView() scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.delegate = self @@ -105,6 +115,7 @@ class GalleryItemViewController: UIViewController { return UIPointerStyle(effect: .highlight(effect.preview), shape: .roundedRect(button.frame)) } shareButton.preferredBehavioralStyle = .pad + shareButton.isHidden = !showShareButton shareButton.translatesAutoresizingMaskIntoConstraints = false updateShareButton() topControlsView.addSubview(shareButton) @@ -137,12 +148,14 @@ class GalleryItemViewController: UIViewController { bottomControlsView.addArrangedSubview(controlsAccessory.view) controlsAccessory.didMove(toParent: self) - // Make sure the controls accessory is within the safe area. - let spacer = UIView() - bottomControlsView.addArrangedSubview(spacer) - let spacerTopConstraint = spacer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) - spacerTopConstraint.priority = .init(999) - spacerTopConstraint.isActive = true + if content.insetBottomControlsAccessoryViewControllerToSafeArea { + // Make sure the controls accessory is within the safe area. + let spacer = UIView() + bottomControlsView.addArrangedSubview(spacer) + let spacerTopConstraint = spacer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) + spacerTopConstraint.priority = .init(999) + spacerTopConstraint.isActive = true + } } captionTextView = UITextView() @@ -206,6 +219,14 @@ class GalleryItemViewController: UIViewController { singleTap.require(toFail: doubleTap) view.addGestureRecognizer(singleTap) view.addGestureRecognizer(doubleTap) + + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillUpdate), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillUpdate), name: UIResponder.keyboardWillHideNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillUpdate), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + } + + @objc private func keyboardWillUpdate() { + updateZoomScale(resetZoom: true) } override func viewSafeAreaInsetsDidChange() { @@ -328,7 +349,7 @@ class GalleryItemViewController: UIViewController { return } - let heightScale = view.bounds.height / content.contentSize.height + let heightScale = (view.bounds.height - view.keyboardLayoutGuide.layoutFrame.height) / content.contentSize.height let widthScale = view.bounds.width / content.contentSize.width let minScale = min(widthScale, heightScale) let maxScale = minScale >= 1 ? minScale + 2 : 2 @@ -351,7 +372,7 @@ class GalleryItemViewController: UIViewController { // Note: use frame for the content.view, because that's in the coordinate space of the scroll view // which means it's already been scaled by the zoom factor. - let yOffset = max(0, (view.bounds.height - content.view.frame.height) / 2) + let yOffset = max(0, (view.bounds.height - view.keyboardLayoutGuide.layoutFrame.height - content.view.frame.height) / 2) contentViewTopConstraint!.constant = yOffset let xOffset = max(0, (view.bounds.width - content.view.frame.width) / 2) @@ -428,7 +449,9 @@ class GalleryItemViewController: UIViewController { scrollView.zoomScale > scrollView.minimumZoomScale { animateZoomOut() } else { - setControlsVisible(!controlsVisible, animated: true, dueToUserInteraction: true) + if content.shouldHideControls() { + setControlsVisible(!controlsVisible, animated: true, dueToUserInteraction: true) + } } } @@ -547,7 +570,9 @@ extension GalleryItemViewController: UIScrollViewDelegate { if scrollView.zoomScale <= scrollView.minimumZoomScale { setControlsVisible(true, animated: true, dueToUserInteraction: true) } else { - setControlsVisible(false, animated: true, dueToUserInteraction: true) + if content.hideControlsOnZoom { + setControlsVisible(false, animated: true, dueToUserInteraction: true) + } } centerContent() diff --git a/Packages/GalleryVC/Sources/GalleryVC/GalleryViewController.swift b/Packages/GalleryVC/Sources/GalleryVC/GalleryViewController.swift index 8013c3fa..69131aa8 100644 --- a/Packages/GalleryVC/Sources/GalleryVC/GalleryViewController.swift +++ b/Packages/GalleryVC/Sources/GalleryVC/GalleryViewController.swift @@ -26,6 +26,14 @@ public class GalleryViewController: UIPageViewController { private var dismissInteraction: GalleryDismissInteraction! private var presentationAnimationCompletionHandlers: [() -> Void] = [] + public var showShareButton: Bool = true { + didSet { + if viewControllers?.isEmpty == false { + currentItemViewController.showShareButton = showShareButton + } + } + } + override public var prefersStatusBarHidden: Bool { true } @@ -89,7 +97,9 @@ public class GalleryViewController: UIPageViewController { private func makeItemVC(index: Int) -> GalleryItemViewController { let content = galleryDataSource.galleryContentViewController(forItemAt: index) - return GalleryItemViewController(delegate: self, itemIndex: index, content: content) + let itemVC = GalleryItemViewController(delegate: self, itemIndex: index, content: content) + itemVC.showShareButton = showShareButton + return itemVC } func presentationAnimationCompleted() {