Use gallery VC for editing attachment descriptions
This commit is contained in:
parent
8cc9849b36
commit
ec9673f6c0
|
@ -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)
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
@ -118,42 +147,13 @@ private struct WrappedCollectionView: UIViewRepresentable {
|
||||||
itemSize = itemSize + remainingSpace / fittingCount
|
itemSize = itemSize + remainingSpace / fittingCount
|
||||||
return (itemSize, Int(fittingCount))
|
return (itemSize, Int(fittingCount))
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Section {
|
|
||||||
case all
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Item: Hashable {
|
|
||||||
case attachment(DraftAttachment)
|
|
||||||
case addButton
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class WrappedCollectionViewCoordinator: NSObject {
|
func updateAttachments() {
|
||||||
var draft: Draft!
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||||
var dataSource: UICollectionViewDiffableDataSource<WrappedCollectionView.Section, WrappedCollectionView.Item>!
|
snapshot.appendSections([.all])
|
||||||
var currentInteractiveMoveStartOffsetInCell: CGPoint?
|
snapshot.appendItems(draft.draftAttachments.map { .attachment($0) })
|
||||||
var currentInteractiveMoveCell: AttachmentCollectionViewCell?
|
snapshot.appendItems([.addButton])
|
||||||
var addAttachment: ((DraftAttachment) -> Void)? = nil
|
dataSource.apply(snapshot)
|
||||||
|
|
||||||
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
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -153,7 +153,6 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
|
||||||
}
|
}
|
||||||
|
|
||||||
open var activityItemsForSharing: [Any] {
|
open var activityItemsForSharing: [Any] {
|
||||||
// [VideoActivityItemSource(asset: item.asset, url: url)]
|
|
||||||
[]
|
[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
Loading…
Reference in New Issue