forked from shadowfacts/Tusker
Support dragging and dropping attachments in the compose view controller
Allos dragging in attachments from other apps and drag/dropping with the compose VC to reorder attachments
This commit is contained in:
parent
7117ce6320
commit
1ccb450477
|
@ -2,6 +2,17 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>space.vaccor.Tusker.composition-attachment</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
@ -49,7 +60,7 @@
|
|||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Post videos from the camera.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Save photos directly from other people's posts.</string>
|
||||
<string>Save photos directly from other people's posts.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Post photos from the photo library.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
|
|
|
@ -23,6 +23,7 @@ class ComposeAttachmentTableViewCell: UITableViewCell {
|
|||
@IBOutlet weak var assetImageView: UIImageView!
|
||||
@IBOutlet weak var descriptionTextView: UITextView!
|
||||
@IBOutlet weak var descriptionPlaceholderLabel: UILabel!
|
||||
@IBOutlet weak var removeButton: UIButton!
|
||||
|
||||
var attachment: CompositionAttachment!
|
||||
|
||||
|
@ -38,7 +39,7 @@ class ComposeAttachmentTableViewCell: UITableViewCell {
|
|||
func updateUI(for attachment: CompositionAttachment) {
|
||||
self.attachment = attachment
|
||||
|
||||
descriptionTextView.text = attachment.description
|
||||
descriptionTextView.text = attachment.attachmentDescription
|
||||
updateDescriptionPlaceholderLabel()
|
||||
|
||||
switch attachment.data {
|
||||
|
@ -63,6 +64,16 @@ class ComposeAttachmentTableViewCell: UITableViewCell {
|
|||
descriptionPlaceholderLabel.isHidden = !descriptionTextView.text.isEmpty
|
||||
}
|
||||
|
||||
func setEnabled(_ enabled: Bool) {
|
||||
descriptionTextView.isEditable = enabled
|
||||
removeButton.isEnabled = enabled
|
||||
}
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
assetImageView.image = nil
|
||||
}
|
||||
|
||||
@IBAction func removeButtonPressed(_ sender: Any) {
|
||||
delegate?.removeAttachment(self)
|
||||
}
|
||||
|
@ -72,7 +83,7 @@ class ComposeAttachmentTableViewCell: UITableViewCell {
|
|||
extension ComposeAttachmentTableViewCell: UITextViewDelegate {
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
delegate?.attachmentDescriptionChanged(self)
|
||||
attachment.description = textView.text
|
||||
attachment.attachmentDescription = textView.text
|
||||
updateDescriptionPlaceholderLabel()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="GLY-o8-47z">
|
||||
<rect key="frame" x="0.0" y="0.0" width="80" height="80"/>
|
||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="80" id="X6q-g9-dPN"/>
|
||||
<constraint firstAttribute="height" constant="80" id="xgQ-E3-0QI"/>
|
||||
|
@ -72,6 +73,7 @@
|
|||
<outlet property="assetImageView" destination="GLY-o8-47z" id="hZH-ur-m4z"/>
|
||||
<outlet property="descriptionPlaceholderLabel" destination="h6T-x4-yzl" id="jBe-R0-Sfn"/>
|
||||
<outlet property="descriptionTextView" destination="cwP-Eh-5dJ" id="pxJ-zF-GKC"/>
|
||||
<outlet property="removeButton" destination="Lvf-I9-aV3" id="3qk-Zr-je1"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="107" y="181"/>
|
||||
</tableViewCell>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
import UIKit
|
||||
import Pachyderm
|
||||
import MobileCoreServices
|
||||
|
||||
protocol ComposeAttachmentsViewControllerDelegate: class {
|
||||
func composeSelectedAttachmentsDidChange()
|
||||
|
@ -55,10 +56,20 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
tableView.register(UINib(nibName: "AddAttachmentTableViewCell", bundle: .main), forCellReuseIdentifier: "addAttachment")
|
||||
tableView.register(UINib(nibName: "ComposeAttachmentTableViewCell", bundle: .main), forCellReuseIdentifier: "composeAttachment")
|
||||
|
||||
heightConstraint = tableView.heightAnchor.constraint(equalToConstant: tableView.contentSize.height)
|
||||
// you would think the table view could handle this itself, but no, using a constraint on the table view's contentLayoutGuide doesn't work
|
||||
// add extra space, so when dropping items, the add attachment cell doesn't disappear
|
||||
heightConstraint = tableView.heightAnchor.constraint(equalToConstant: tableView.contentSize.height + 80)
|
||||
heightConstraint.isActive = true
|
||||
|
||||
pasteConfiguration = UIPasteConfiguration(forAccepting: UIImage.self)
|
||||
// prevents extra separator lines from appearing when the height of the table view is greater than the height of the content
|
||||
tableView.tableFooterView = UIView()
|
||||
|
||||
pasteConfiguration = UIPasteConfiguration(forAccepting: CompositionAttachment.self)
|
||||
|
||||
// enable dragging on iPhone to allow reordering
|
||||
tableView.dragInteractionEnabled = true
|
||||
tableView.dragDelegate = self
|
||||
tableView.dropDelegate = self
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
|
@ -77,7 +88,8 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
}
|
||||
|
||||
private func updateHeightConstraint() {
|
||||
heightConstraint.constant = tableView.contentSize.height
|
||||
// add extra space, so when dropping items, the add attachment cell doesn't disappear
|
||||
heightConstraint.constant = tableView.contentSize.height + 80
|
||||
}
|
||||
|
||||
private func isAddAttachmentsButtonEnabled() -> Bool {
|
||||
|
@ -90,7 +102,7 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
}
|
||||
|
||||
private func updateAddAttachmentsButtonEnabled() {
|
||||
let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 1)) as! AddAttachmentTableViewCell
|
||||
guard let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 1)) as? AddAttachmentTableViewCell else { return }
|
||||
cell.setEnabled(isAddAttachmentsButtonEnabled())
|
||||
}
|
||||
|
||||
|
@ -105,16 +117,15 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
|
||||
override func paste(itemProviders: [NSItemProvider]) {
|
||||
for provider in itemProviders {
|
||||
provider.loadObject(ofClass: UIImage.self) { (object, error) in
|
||||
provider.loadObject(ofClass: CompositionAttachment.self) { (object, error) in
|
||||
if let error = error {
|
||||
fatalError("Couldn't load image from NSItemProvider: \(error)")
|
||||
}
|
||||
guard let image = object as? UIImage else {
|
||||
fatalError("Couldn't convert object from NSItemProvider to UIImage")
|
||||
guard let attachment = object as? CompositionAttachment else {
|
||||
fatalError("Couldn't convert object from NSItemProvider to CompositionAttachment")
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let attachment = CompositionAttachment(data: .image(image))
|
||||
self.attachments.append(attachment)
|
||||
self.tableView.insertRows(at: [IndexPath(row: self.attachments.count - 1, section: 0)], with: .automatic)
|
||||
self.updateHeightConstraint()
|
||||
|
@ -219,6 +230,7 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
let cell = tableView.dequeueReusableCell(withIdentifier: "composeAttachment", for: indexPath) as! ComposeAttachmentTableViewCell
|
||||
cell.delegate = self
|
||||
cell.updateUI(for: attachment)
|
||||
cell.setEnabled(true)
|
||||
return cell
|
||||
case 1:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "addAttachment", for: indexPath) as! AddAttachmentTableViewCell
|
||||
|
@ -229,6 +241,12 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
|
||||
guard sourceIndexPath != destinationIndexPath, sourceIndexPath.section == 0, destinationIndexPath.section == 0 else { return }
|
||||
|
||||
attachments.insert(attachments.remove(at: sourceIndexPath.row), at: destinationIndexPath.row)
|
||||
}
|
||||
|
||||
// MARK: Table view delegate
|
||||
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
|
@ -254,6 +272,88 @@ class ComposeAttachmentsViewController: UITableViewController {
|
|||
|
||||
}
|
||||
|
||||
extension ComposeAttachmentsViewController: UITableViewDragDelegate {
|
||||
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
let attachment = attachments[indexPath.row]
|
||||
let provider = NSItemProvider(object: attachment)
|
||||
let dragItem = UIDragItem(itemProvider: provider)
|
||||
dragItem.localObject = attachment
|
||||
return [dragItem]
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
|
||||
let attachment = attachments[indexPath.row]
|
||||
let provider = NSItemProvider(object: attachment)
|
||||
let dragItem = UIDragItem(itemProvider: provider)
|
||||
dragItem.localObject = attachment
|
||||
return [dragItem]
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, dragPreviewParametersForRowAt indexPath: IndexPath) -> UIDragPreviewParameters? {
|
||||
let cell = tableView.cellForRow(at: indexPath) as! ComposeAttachmentTableViewCell
|
||||
let rect = cell.convert(cell.assetImageView.bounds, from: cell.assetImageView)
|
||||
let path = UIBezierPath(roundedRect: rect, cornerRadius: cell.assetImageView.layer.cornerRadius)
|
||||
let params = UIDragPreviewParameters()
|
||||
params.visiblePath = path
|
||||
return params
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeAttachmentsViewController: UITableViewDropDelegate {
|
||||
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
|
||||
return session.canLoadObjects(ofClass: CompositionAttachment.self)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
|
||||
// if items were dragged out of ourself, then the items are only being moved
|
||||
if tableView.hasActiveDrag {
|
||||
// todo: should moving multiple items actually be prohibited?
|
||||
if session.items.count > 1 {
|
||||
return UITableViewDropProposal(operation: .cancel)
|
||||
} else {
|
||||
return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
|
||||
}
|
||||
} else {
|
||||
return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
|
||||
}
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
|
||||
let destinationIndexPath = coordinator.destinationIndexPath ?? IndexPath(row: attachments.count, section: 0)
|
||||
|
||||
// we don't need to handle local items here, when the .move operation is used returned from the tableView(_:dropSessionDidUpdate:withDestinationIndexPath:) method,
|
||||
// the table view will handle animating and call the normal data source tableView(_:moveRowAt:to:)
|
||||
|
||||
for (index, item) in coordinator.items.enumerated() {
|
||||
let provider = item.dragItem.itemProvider
|
||||
|
||||
if provider.canLoadObject(ofClass: CompositionAttachment.self) {
|
||||
let indexPath = IndexPath(row: destinationIndexPath.row + index, section: 0)
|
||||
let placeholder = UITableViewDropPlaceholder(insertionIndexPath: indexPath, reuseIdentifier: "composeAttachment", rowHeight: 96)
|
||||
placeholder.cellUpdateHandler = { (cell) in
|
||||
let cell = cell as! ComposeAttachmentTableViewCell
|
||||
cell.setEnabled(false)
|
||||
}
|
||||
let placeholderContext = coordinator.drop(item.dragItem, to: placeholder)
|
||||
|
||||
provider.loadObject(ofClass: CompositionAttachment.self) { (object, error) in
|
||||
DispatchQueue.main.async {
|
||||
if let attachment = object as? CompositionAttachment {
|
||||
placeholderContext.commitInsertion { (insertionIndexPath) in
|
||||
self.attachments.insert(attachment, at: insertionIndexPath.row)
|
||||
}
|
||||
} else {
|
||||
placeholderContext.deletePlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateHeightConstraint()
|
||||
}
|
||||
}
|
||||
|
||||
extension ComposeAttachmentsViewController: AssetPickerViewControllerDelegate {
|
||||
func assetPicker(_ assetPicker: AssetPickerViewController, shouldAllowAssetOfType type: CompositionAttachmentData.AttachmentType) -> Bool {
|
||||
switch mastodonController.instance.instanceType {
|
||||
|
|
|
@ -7,19 +7,72 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
|
||||
final class CompositionAttachment: NSObject, Codable {
|
||||
static let typeIdentifier = "space.vaccor.Tusker.composition-attachment"
|
||||
|
||||
class CompositionAttachment: Codable {
|
||||
let data: CompositionAttachmentData
|
||||
var description: String
|
||||
var attachmentDescription: String
|
||||
|
||||
init(data: CompositionAttachmentData, description: String = "") {
|
||||
self.data = data
|
||||
self.description = description
|
||||
}
|
||||
self.attachmentDescription = description
|
||||
}
|
||||
|
||||
extension CompositionAttachment: Equatable {
|
||||
static func ==(lhs: CompositionAttachment, rhs: CompositionAttachment) -> Bool {
|
||||
return lhs.data == rhs.data
|
||||
}
|
||||
}
|
||||
|
||||
private let imageType = kUTTypeImage as String
|
||||
private let dataType = kUTTypeData as String
|
||||
|
||||
extension CompositionAttachment: NSItemProviderWriting {
|
||||
static var writableTypeIdentifiersForItemProvider: [String] {
|
||||
[typeIdentifier]
|
||||
}
|
||||
|
||||
func loadData(withTypeIdentifier typeIdentifier: String, forItemProviderCompletionHandler completionHandler: @escaping (Data?, Error?) -> Void) -> Progress? {
|
||||
if typeIdentifier == CompositionAttachment.typeIdentifier {
|
||||
do {
|
||||
completionHandler(try PropertyListEncoder().encode(self), nil)
|
||||
} catch {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(nil, ItemProviderError.incompatibleTypeIdentifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
enum ItemProviderError: Error {
|
||||
case incompatibleTypeIdentifier
|
||||
|
||||
var localizedDescription: String {
|
||||
switch self {
|
||||
case .incompatibleTypeIdentifier:
|
||||
return "Cannot provide data for given type"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CompositionAttachment: NSItemProviderReading {
|
||||
static var readableTypeIdentifiersForItemProvider: [String] {
|
||||
[typeIdentifier] + UIImage.readableTypeIdentifiersForItemProvider + NSURL.readableTypeIdentifiersForItemProvider
|
||||
}
|
||||
|
||||
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self {
|
||||
if typeIdentifier == CompositionAttachment.typeIdentifier {
|
||||
return try PropertyListDecoder().decode(Self.self, from: data)
|
||||
} else if UIImage.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let image = try? UIImage.object(withItemProviderData: data, typeIdentifier: typeIdentifier) {
|
||||
return CompositionAttachment(data: .image(image)) as! Self
|
||||
} else if NSURL.readableTypeIdentifiersForItemProvider.contains(typeIdentifier), let url = try? NSURL.object(withItemProviderData: data, typeIdentifier: typeIdentifier) as URL {
|
||||
return CompositionAttachment(data: .video(url)) as! Self
|
||||
} else {
|
||||
throw ItemProviderError.incompatibleTypeIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue