Use gallery VC for editing attachment descriptions

This commit is contained in:
Shadowfacts 2024-11-23 10:47:58 -05:00
parent 8cc9849b36
commit ec9673f6c0
9 changed files with 427 additions and 92 deletions

View File

@ -22,13 +22,22 @@ let package = Package(
.package(path: "../MatchedGeometryPresentation"), .package(path: "../MatchedGeometryPresentation"),
.package(path: "../TuskerPreferences"), .package(path: "../TuskerPreferences"),
.package(path: "../UserAccounts"), .package(path: "../UserAccounts"),
.package(path: "../GalleryVC"),
], ],
targets: [ targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite. // 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. // Targets can depend on other targets in this package, and on products in packages this package depends on.
.target( .target(
name: "ComposeUI", name: "ComposeUI",
dependencies: ["Pachyderm", "InstanceFeatures", "TuskerComponents", "MatchedGeometryPresentation", "TuskerPreferences", "UserAccounts"], dependencies: [
"Pachyderm",
"InstanceFeatures",
"TuskerComponents",
"MatchedGeometryPresentation",
"TuskerPreferences",
"UserAccounts",
"GalleryVC",
],
swiftSettings: [ swiftSettings: [
.swiftLanguageMode(.v5) .swiftLanguageMode(.v5)
]), ]),

View File

@ -76,7 +76,7 @@ private struct AttachmentRemoveButton: View {
} }
private struct AttachmentDescriptionLabel: View { private struct AttachmentDescriptionLabel: View {
let attachment: DraftAttachment @ObservedObject var attachment: DraftAttachment
var body: some View { var body: some View {
ZStack(alignment: .bottomLeading) { ZStack(alignment: .bottomLeading) {
@ -87,10 +87,8 @@ private struct AttachmentDescriptionLabel: View {
) )
label label
.lineLimit(1)
.font(.callout)
.foregroundStyle(.white) .foregroundStyle(.white)
.shadow(radius: 1) .shadow(color: .black.opacity(0.5), radius: 1)
.padding([.horizontal, .bottom], 4) .padding([.horizontal, .bottom], 4)
} }
} }
@ -100,8 +98,12 @@ private struct AttachmentDescriptionLabel: View {
if attachment.attachmentDescription.isEmpty { if attachment.attachmentDescription.isEmpty {
Label("Add alt", systemImage: "pencil") Label("Add alt", systemImage: "pencil")
.labelStyle(NarrowSpacingLabelStyle()) .labelStyle(NarrowSpacingLabelStyle())
.font(.callout)
.lineLimit(1)
} else { } else {
Text(attachment.attachmentDescription) Text(attachment.attachmentDescription)
.font(.caption)
.lineLimit(2)
} }
} }
} }

View File

@ -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))
}
}

View File

@ -8,6 +8,7 @@
import SwiftUI import SwiftUI
import PhotosUI import PhotosUI
import PencilKit import PencilKit
import GalleryVC
struct AttachmentsSection: View { struct AttachmentsSection: View {
@ObservedObject var draft: Draft @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 @ObservedObject var draft: Draft
let spacing: CGFloat let spacing: CGFloat
let minItemSize: CGFloat let minItemSize: CGFloat
func makeUIView(context: Context) -> some UIView { func makeUIViewController(context: Context) -> WrappedCollectionViewController {
let layout = UICollectionViewCompositionalLayout { section, environment in WrappedCollectionViewController(spacing: spacing, minItemSize: minItemSize)
let (itemSize, itemsPerRow) = itemSize(width: environment.container.contentSize.width) }
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<Section, Item>!
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 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) 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 section.interGroupSpacing = spacing
return section return section
} }
let view = IntrinsicContentSizeCollectionView(frame: .zero, collectionViewLayout: layout) let attachmentCell = UICollectionView.CellRegistration<AttachmentCollectionViewCell, DraftAttachment> { cell, indexPath, attachment in
let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: view) { collectionView, indexPath, itemIdentifier in cell.updateUI(attachment: attachment)
context.coordinator.makeCell(collectionView: collectionView, indexPath: indexPath, item: itemIdentifier) }
let addButtonCell = UICollectionView.CellRegistration<UICollectionViewCell, Bool> { [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<Section, Item>(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 dataSource.reorderingHandlers.canReorderItem = { item in
switch item { switch item {
@ -52,7 +102,7 @@ private struct WrappedCollectionView: UIViewRepresentable {
false false
} }
} }
dataSource.reorderingHandlers.didReorder = { transaction in dataSource.reorderingHandlers.didReorder = { [unowned self] transaction in
let attachmentChanges = transaction.difference.map { let attachmentChanges = transaction.difference.map {
switch $0 { switch $0 {
case .insert(let offset, let element, let associatedWith): case .insert(let offset, let element, let associatedWith):
@ -67,35 +117,14 @@ private struct WrappedCollectionView: UIViewRepresentable {
let array = draft.draftAttachments.applying(attachmentsDiff)! let array = draft.draftAttachments.applying(attachmentsDiff)!
draft.attachments = NSMutableOrderedSet(array: array) draft.attachments = NSMutableOrderedSet(array: array)
} }
context.coordinator.dataSource = dataSource
view.isScrollEnabled = false collectionView.isScrollEnabled = false
view.clipsToBounds = false collectionView.clipsToBounds = false
view.delegate = context.coordinator collectionView.delegate = self
let longPressRecognizer = UILongPressGestureRecognizer(target: context.coordinator, action: #selector(WrappedCollectionViewCoordinator.reorderingLongPressRecognized)) let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(reorderingLongPressRecognized))
longPressRecognizer.delegate = context.coordinator longPressRecognizer.delegate = self
view.addGestureRecognizer(longPressRecognizer) collectionView.addGestureRecognizer(longPressRecognizer)
return view
}
func updateUIView(_ uiView: UIViewType, context: Context) {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
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()
} }
private func itemSize(width: CGFloat) -> (CGFloat, Int) { private func itemSize(width: CGFloat) -> (CGFloat, Int) {
@ -119,41 +148,12 @@ private struct WrappedCollectionView: UIViewRepresentable {
return (itemSize, Int(fittingCount)) return (itemSize, Int(fittingCount))
} }
enum Section { func updateAttachments() {
case all var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
} snapshot.appendSections([.all])
snapshot.appendItems(draft.draftAttachments.map { .attachment($0) })
enum Item: Hashable { snapshot.appendItems([.addButton])
case attachment(DraftAttachment) dataSource.apply(snapshot)
case addButton
}
}
private class WrappedCollectionViewCoordinator: NSObject {
var draft: Draft!
var dataSource: UICollectionViewDiffableDataSource<WrappedCollectionView.Section, WrappedCollectionView.Item>!
var currentInteractiveMoveStartOffsetInCell: CGPoint?
var currentInteractiveMoveCell: AttachmentCollectionViewCell?
var addAttachment: ((DraftAttachment) -> Void)? = nil
private let attachmentCell = UICollectionView.CellRegistration<AttachmentCollectionViewCell, DraftAttachment> { cell, indexPath, attachment in
cell.updateUI(attachment: attachment)
}
private let addButtonCell = UICollectionView.CellRegistration<UICollectionViewCell, (WrappedCollectionViewCoordinator, Bool)> { 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))
}
} }
@objc func reorderingLongPressRecognized(_ recognizer: UILongPressGestureRecognizer) { @objc func reorderingLongPressRecognized(_ recognizer: UILongPressGestureRecognizer) {
@ -186,9 +186,18 @@ private class WrappedCollectionViewCoordinator: NSObject {
break break
} }
} }
enum Section {
case all
}
enum Item: Hashable {
case attachment(DraftAttachment)
case addButton
}
} }
extension WrappedCollectionViewCoordinator: UIGestureRecognizerDelegate { extension WrappedCollectionViewController: UIGestureRecognizerDelegate {
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let collectionView = gestureRecognizer.view as! UICollectionView let collectionView = gestureRecognizer.view as! UICollectionView
let location = gestureRecognizer.location(in: collectionView) 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 { func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveOfItemFromOriginalIndexPath originalIndexPath: IndexPath, atCurrentIndexPath currentIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath {
let snapshot = dataSource.snapshot() let snapshot = dataSource.snapshot()
let items = snapshot.itemIdentifiers(inSection: .all).count let items = snapshot.itemIdentifiers(inSection: .all).count
@ -217,6 +226,24 @@ extension WrappedCollectionViewCoordinator: UICollectionViewDelegate {
return proposedIndexPath 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 { private final class IntrinsicContentSizeCollectionView: UICollectionView {
@ -237,7 +264,7 @@ private final class IntrinsicContentSizeCollectionView: UICollectionView {
} }
private struct AddAttachmentButton: View { private struct AddAttachmentButton: View {
let coordinator: WrappedCollectionViewCoordinator unowned let viewController: WrappedCollectionViewController
let enabled: Bool let enabled: Bool
@Environment(\.composeUIConfig.presentAssetPicker) private var presentAssetPicker @Environment(\.composeUIConfig.presentAssetPicker) private var presentAssetPicker
@Environment(\.composeUIConfig.presentDrawing) private var presentDrawing @Environment(\.composeUIConfig.presentDrawing) private var presentDrawing
@ -247,7 +274,7 @@ private struct AddAttachmentButton: View {
if let presentAssetPicker { if let presentAssetPicker {
Button("Add photo or video", systemImage: "photo") { Button("Add photo or video", systemImage: "photo") {
presentAssetPicker { presentAssetPicker {
let draft = coordinator.draft! let draft = viewController.draft!
AttachmentsListSection.insertAttachments(in: draft, at: draft.attachments.count, itemProviders: $0.map(\.itemProvider)) AttachmentsListSection.insertAttachments(in: draft, at: draft.attachments.count, itemProviders: $0.map(\.itemProvider))
} }
} }
@ -255,7 +282,7 @@ private struct AddAttachmentButton: View {
if let presentDrawing { if let presentDrawing {
Button("Draw something", systemImage: "hand.draw") { Button("Draw something", systemImage: "hand.draw") {
presentDrawing(PKDrawing()) { drawing in presentDrawing(PKDrawing()) { drawing in
let draft = coordinator.draft! let draft = viewController.draft!
let attachment = DraftAttachment(context: DraftsPersistentContainer.shared.viewContext) let attachment = DraftAttachment(context: DraftsPersistentContainer.shared.viewContext)
attachment.id = UUID() attachment.id = UUID()
attachment.drawing = drawing attachment.drawing = drawing

View File

@ -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()
}
}
}

View File

@ -153,7 +153,6 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
} }
open var activityItemsForSharing: [Any] { open var activityItemsForSharing: [Any] {
// [VideoActivityItemSource(asset: item.asset, url: url)]
[] []
} }

View File

@ -15,8 +15,11 @@ public protocol GalleryContentViewController: UIViewController {
var caption: String? { get } var caption: String? { get }
var contentOverlayAccessoryViewController: UIViewController? { get } var contentOverlayAccessoryViewController: UIViewController? { get }
var bottomControlsAccessoryViewController: UIViewController? { get } var bottomControlsAccessoryViewController: UIViewController? { get }
var insetBottomControlsAccessoryViewControllerToSafeArea: Bool { get }
var canAnimateFromSourceView: Bool { get } var canAnimateFromSourceView: Bool { get }
var hideControlsOnZoom: Bool { get }
func shouldHideControls() -> Bool
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool)
func galleryContentDidAppear() func galleryContentDidAppear()
func galleryContentWillDisappear() func galleryContentWillDisappear()
@ -31,10 +34,22 @@ public extension GalleryContentViewController {
nil nil
} }
var insetBottomControlsAccessoryViewControllerToSafeArea: Bool {
true
}
var canAnimateFromSourceView: Bool { var canAnimateFromSourceView: Bool {
true true
} }
var hideControlsOnZoom: Bool {
true
}
func shouldHideControls() -> Bool {
true
}
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) { func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
} }

View File

@ -45,6 +45,12 @@ class GalleryItemViewController: UIViewController {
private var scrollViewSizeForLastZoomScaleUpdate: CGSize? private var scrollViewSizeForLastZoomScaleUpdate: CGSize?
var showShareButton: Bool = true {
didSet {
shareButton?.isHidden = !showShareButton
}
}
override var prefersHomeIndicatorAutoHidden: Bool { override var prefersHomeIndicatorAutoHidden: Bool {
return !controlsVisible return !controlsVisible
} }
@ -66,6 +72,10 @@ class GalleryItemViewController: UIViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.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 = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.delegate = self scrollView.delegate = self
@ -105,6 +115,7 @@ class GalleryItemViewController: UIViewController {
return UIPointerStyle(effect: .highlight(effect.preview), shape: .roundedRect(button.frame)) return UIPointerStyle(effect: .highlight(effect.preview), shape: .roundedRect(button.frame))
} }
shareButton.preferredBehavioralStyle = .pad shareButton.preferredBehavioralStyle = .pad
shareButton.isHidden = !showShareButton
shareButton.translatesAutoresizingMaskIntoConstraints = false shareButton.translatesAutoresizingMaskIntoConstraints = false
updateShareButton() updateShareButton()
topControlsView.addSubview(shareButton) topControlsView.addSubview(shareButton)
@ -137,12 +148,14 @@ class GalleryItemViewController: UIViewController {
bottomControlsView.addArrangedSubview(controlsAccessory.view) bottomControlsView.addArrangedSubview(controlsAccessory.view)
controlsAccessory.didMove(toParent: self) controlsAccessory.didMove(toParent: self)
// Make sure the controls accessory is within the safe area. if content.insetBottomControlsAccessoryViewControllerToSafeArea {
let spacer = UIView() // Make sure the controls accessory is within the safe area.
bottomControlsView.addArrangedSubview(spacer) let spacer = UIView()
let spacerTopConstraint = spacer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) bottomControlsView.addArrangedSubview(spacer)
spacerTopConstraint.priority = .init(999) let spacerTopConstraint = spacer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
spacerTopConstraint.isActive = true spacerTopConstraint.priority = .init(999)
spacerTopConstraint.isActive = true
}
} }
captionTextView = UITextView() captionTextView = UITextView()
@ -206,6 +219,14 @@ class GalleryItemViewController: UIViewController {
singleTap.require(toFail: doubleTap) singleTap.require(toFail: doubleTap)
view.addGestureRecognizer(singleTap) view.addGestureRecognizer(singleTap)
view.addGestureRecognizer(doubleTap) 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() { override func viewSafeAreaInsetsDidChange() {
@ -328,7 +349,7 @@ class GalleryItemViewController: UIViewController {
return 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 widthScale = view.bounds.width / content.contentSize.width
let minScale = min(widthScale, heightScale) let minScale = min(widthScale, heightScale)
let maxScale = minScale >= 1 ? minScale + 2 : 2 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 // 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. // 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 contentViewTopConstraint!.constant = yOffset
let xOffset = max(0, (view.bounds.width - content.view.frame.width) / 2) let xOffset = max(0, (view.bounds.width - content.view.frame.width) / 2)
@ -428,7 +449,9 @@ class GalleryItemViewController: UIViewController {
scrollView.zoomScale > scrollView.minimumZoomScale { scrollView.zoomScale > scrollView.minimumZoomScale {
animateZoomOut() animateZoomOut()
} else { } 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 { if scrollView.zoomScale <= scrollView.minimumZoomScale {
setControlsVisible(true, animated: true, dueToUserInteraction: true) setControlsVisible(true, animated: true, dueToUserInteraction: true)
} else { } else {
setControlsVisible(false, animated: true, dueToUserInteraction: true) if content.hideControlsOnZoom {
setControlsVisible(false, animated: true, dueToUserInteraction: true)
}
} }
centerContent() centerContent()

View File

@ -26,6 +26,14 @@ public class GalleryViewController: UIPageViewController {
private var dismissInteraction: GalleryDismissInteraction! private var dismissInteraction: GalleryDismissInteraction!
private var presentationAnimationCompletionHandlers: [() -> Void] = [] private var presentationAnimationCompletionHandlers: [() -> Void] = []
public var showShareButton: Bool = true {
didSet {
if viewControllers?.isEmpty == false {
currentItemViewController.showShareButton = showShareButton
}
}
}
override public var prefersStatusBarHidden: Bool { override public var prefersStatusBarHidden: Bool {
true true
} }
@ -89,7 +97,9 @@ public class GalleryViewController: UIPageViewController {
private func makeItemVC(index: Int) -> GalleryItemViewController { private func makeItemVC(index: Int) -> GalleryItemViewController {
let content = galleryDataSource.galleryContentViewController(forItemAt: index) 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() { func presentationAnimationCompleted() {