Add drawing attachments using PencilKit

This commit is contained in:
Shadowfacts 2020-05-09 22:14:48 -04:00
parent 9f75106706
commit 4fdafa893e
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
8 changed files with 332 additions and 9 deletions

View File

@ -157,6 +157,7 @@
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7B2157E01900721E32 /* XCBManager.swift */; }; D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7B2157E01900721E32 /* XCBManager.swift */; };
D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */; }; D6757A7E2157E02600721E32 /* XCBRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */; };
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; }; D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; };
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */; };
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; }; D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; }; D67C57AD21E265FC00C3118B /* LargeAccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */; };
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; }; D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
@ -164,6 +165,7 @@
D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; }; D67C57B421E2910700C3118B /* ComposeStatusReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */; };
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; }; D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; }; D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; }; D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */; };
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; }; D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; }; D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
@ -451,6 +453,7 @@
D6757A7B2157E01900721E32 /* XCBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBManager.swift; sourceTree = "<group>"; }; D6757A7B2157E01900721E32 /* XCBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBManager.swift; sourceTree = "<group>"; };
D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequestSpec.swift; sourceTree = "<group>"; }; D6757A7D2157E02600721E32 /* XCBRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBRequestSpec.swift; sourceTree = "<group>"; };
D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = "<group>"; }; D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = "<group>"; };
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PKDrawing+Render.swift"; sourceTree = "<group>"; };
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; }; D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; }; D67C57AC21E265FC00C3118B /* LargeAccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeAccountDetailView.swift; sourceTree = "<group>"; };
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; }; D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
@ -458,6 +461,7 @@
D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; }; D67C57B321E2910700C3118B /* ComposeStatusReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusReplyView.swift; sourceTree = "<group>"; };
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; }; D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; }; D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; }; D68FEC4E232C5BC300C84F23 /* SegmentedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedPageViewController.swift; sourceTree = "<group>"; };
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; }; D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; }; D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
@ -910,6 +914,7 @@
D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */, D6A5FAF0217B7E05003DB2D9 /* ComposeViewController.xib */,
D66362702136338600C9CBA2 /* ComposeViewController.swift */, D66362702136338600C9CBA2 /* ComposeViewController.swift */,
D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */, D60309B42419D4F100A465FF /* ComposeAttachmentsViewController.swift */,
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */,
); );
path = Compose; path = Compose;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1025,6 +1030,7 @@
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */, D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */,
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */, D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */,
D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */, D6969E9D240C81B9002843CE /* NSTextAttachment+Emoji.swift */,
D67895BB24671E6D00D4CD9E /* PKDrawing+Render.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1758,11 +1764,13 @@
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */, D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */, D6A3BC7C232195C600FD64D5 /* ActionNotificationGroupTableViewCell.swift in Sources */,
D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */, D6F953EC212519E700CF0F2B /* TimelineTableViewController.swift in Sources */,
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */,
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */, 04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */,
D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */, D620483223D2A6A3008A63EF /* CompositionState.swift in Sources */,
D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */, D6BC9DB5232D4CE3002CA326 /* NotificationsMode.swift in Sources */,
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */, D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */,
D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */, D667E5EB21349EF80057A976 /* ProfileHeaderTableViewCell.swift in Sources */,
D67895BC24671E6D00D4CD9E /* PKDrawing+Render.swift in Sources */,
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */, 04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */, D641C773213CAA25004B4513 /* NotificationsTableViewController.swift in Sources */,
D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */, D64BC18A23C16487000D0238 /* UnpinStatusActivity.swift in Sources */,

View File

@ -0,0 +1,33 @@
//
// PKDrawing+Render.swift
// Tusker
//
// Created by Shadowfacts on 5/9/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import PencilKit
extension PKDrawing {
func imageInLightMode(from rect: CGRect, scale: CGFloat = UIScreen.main.scale) -> UIImage {
let lightTraitCollection = UITraitCollection(userInterfaceStyle: .light)
var drawingImage: UIImage!
lightTraitCollection.performAsCurrent {
drawingImage = self.image(from: rect, scale: scale)
}
let imageRect = CGRect(origin: .zero, size: rect.size)
let format = UIGraphicsImageRendererFormat()
format.opaque = false
format.scale = scale
let renderer = UIGraphicsImageRenderer(size: rect.size, format: format)
return renderer.image { (context) in
UIColor.white.setFill()
context.fill(imageRect)
drawingImage.draw(in: imageRect)
}
}
}

View File

@ -9,11 +9,13 @@
import UIKit import UIKit
import Photos import Photos
import MobileCoreServices import MobileCoreServices
import PencilKit
enum CompositionAttachmentData { enum CompositionAttachmentData {
case asset(PHAsset) case asset(PHAsset)
case image(UIImage) case image(UIImage)
case video(URL) case video(URL)
case drawing(PKDrawing)
var type: AttachmentType { var type: AttachmentType {
switch self { switch self {
@ -23,6 +25,8 @@ enum CompositionAttachmentData {
return .image return .image
case .video(_): case .video(_):
return .video return .video
case .drawing(_):
return .image
} }
} }
@ -44,7 +48,7 @@ enum CompositionAttachmentData {
} }
} }
func getData(completion: @escaping (Data, String) -> Void) { func getData(completion: @escaping (_ data: Data, _ mimeType: String) -> Void) {
switch self { switch self {
case let .image(image): case let .image(image):
completion(image.pngData()!, "image/png") completion(image.pngData()!, "image/png")
@ -90,6 +94,10 @@ enum CompositionAttachmentData {
fatalError("failed to create export session") fatalError("failed to create export session")
} }
CompositionAttachmentData.exportVideoData(session: session, completion: completion) CompositionAttachmentData.exportVideoData(session: session, completion: completion)
case let .drawing(drawing):
let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1)
completion(image.pngData()!, "image/png")
} }
} }
@ -138,6 +146,10 @@ extension CompositionAttachmentData: Codable {
try container.encode(image.pngData()!, forKey: .imageData) try container.encode(image.pngData()!, forKey: .imageData)
case .video(_): case .video(_):
throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: [], debugDescription: "video CompositionAttachments cannot be encoded")) throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: [], debugDescription: "video CompositionAttachments cannot be encoded"))
case let .drawing(drawing):
try container.encode("drawing", forKey: .type)
let drawingData = drawing.dataRepresentation()
try container.encode(drawingData, forKey: .drawing)
} }
} }
@ -156,6 +168,10 @@ extension CompositionAttachmentData: Codable {
throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "Could not decode UIImage from image data") throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "Could not decode UIImage from image data")
} }
self = .image(image) self = .image(image)
case "drawing":
let drawingData = try container.decode(Data.self, forKey: .drawing)
let drawing = try PKDrawing(data: drawingData)
self = .drawing(drawing)
default: default:
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of 'image' or 'asset'") throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of 'image' or 'asset'")
} }
@ -166,6 +182,8 @@ extension CompositionAttachmentData: Codable {
case imageData case imageData
/// The local identifier of the PHAsset for this attachment /// The local identifier of the PHAsset for this attachment
case assetIdentifier case assetIdentifier
/// The PKDrawing object for this attachment.
case drawing
} }
} }
@ -178,6 +196,8 @@ extension CompositionAttachmentData: Equatable {
return a == b return a == b
case let (.video(a), .video(b)): case let (.video(a), .video(b)):
return a == b return a == b
case let (.drawing(a), .drawing(b)):
return a == b
default: default:
return false return false
} }

View File

@ -52,6 +52,9 @@ class AssetPreviewViewController: UIViewController {
default: default:
fatalError("asset mediaType must be image or video") fatalError("asset mediaType must be image or video")
} }
case let .drawing(drawing):
let image = drawing.imageInLightMode(from: drawing.bounds)
showImage(image)
} }
} }

View File

@ -9,6 +9,7 @@
import UIKit import UIKit
import Pachyderm import Pachyderm
import MobileCoreServices import MobileCoreServices
import PencilKit
protocol ComposeAttachmentsViewControllerDelegate: class { protocol ComposeAttachmentsViewControllerDelegate: class {
func composeSelectedAttachmentsDidChange() func composeSelectedAttachmentsDidChange()
@ -38,6 +39,8 @@ class ComposeAttachmentsViewController: UITableViewController {
} }
} }
private var currentlyEditedDrawingIndex: Int?
init(attachments: [CompositionAttachment], mastodonController: MastodonController) { init(attachments: [CompositionAttachment], mastodonController: MastodonController) {
self.attachments = attachments self.attachments = attachments
self.mastodonController = mastodonController self.mastodonController = mastodonController
@ -139,6 +142,23 @@ class ComposeAttachmentsViewController: UITableViewController {
} }
} }
func presentComposeDrawingViewController(editingAttachmentAt attachmentIndex: Int? = nil) {
let drawingVC: ComposeDrawingViewController
if let index = attachmentIndex,
case let .drawing(drawing) = attachments[index].data {
drawingVC = ComposeDrawingViewController(editing: drawing)
currentlyEditedDrawingIndex = index
} else {
drawingVC = ComposeDrawingViewController()
}
drawingVC.delegate = self
let nav = UINavigationController(rootViewController: drawingVC)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: true)
}
func uploadAll(stepProgress: @escaping () -> Void, completion: @escaping (_ success: Bool, _ uploadedAttachments: [Attachment]) -> Void) { func uploadAll(stepProgress: @escaping () -> Void, completion: @escaping (_ success: Bool, _ uploadedAttachments: [Attachment]) -> Void) {
let group = DispatchGroup() let group = DispatchGroup()
@ -270,19 +290,49 @@ class ComposeAttachmentsViewController: UITableViewController {
} }
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard indexPath.section == 0 else { return nil } if indexPath.section == 0 {
let attachment = attachments[indexPath.row] let attachment = attachments[indexPath.row]
// cast to NSIndexPath because identifier needs to conform to NSCopying // cast to NSIndexPath because identifier needs to conform to NSCopying
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
return AssetPreviewViewController(attachment: attachment.data) return AssetPreviewViewController(attachment: attachment.data)
}) { (_) -> UIMenu? in }) { (_) -> UIMenu? in
var actions = [UIAction]()
switch attachment.data {
case .drawing(_):
actions.append(UIAction(title: "Edit Drawing", image: UIImage(systemName: "hand.draw"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
self.presentComposeDrawingViewController(editingAttachmentAt: indexPath.row)
}))
default:
break
}
if actions.isEmpty {
return nil
} else {
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: actions)
}
}
} else if indexPath.section == 1 {
guard isAddAttachmentsButtonEnabled() else {
return nil
}
// show context menu for drawing/file uploads
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) -> UIMenu? in
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
UIAction(title: "Draw Something", image: UIImage(systemName: "hand.draw"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off, handler: { (_) in
self.presentComposeDrawingViewController()
})
])
}
} else {
return nil return nil
} }
} }
private func targetedPreview(forConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { private func targetedPreview(forConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?, if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?,
indexPath.section == 0,
let cell = tableView.cellForRow(at: indexPath) as? ComposeAttachmentTableViewCell { let cell = tableView.cellForRow(at: indexPath) as? ComposeAttachmentTableViewCell {
let parameters = UIPreviewParameters() let parameters = UIPreviewParameters()
parameters.backgroundColor = .black parameters.backgroundColor = .black
@ -452,3 +502,26 @@ extension ComposeAttachmentsViewController: ComposeAttachmentTableViewCellDelega
delegate?.composeRequiresAttachmentDescriptionsDidChange() delegate?.composeRequiresAttachmentDescriptionsDidChange()
} }
} }
extension ComposeAttachmentsViewController: ComposeDrawingViewControllerDelegate {
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController) {
dismiss(animated: true)
currentlyEditedDrawingIndex = nil
}
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing) {
let newAttachment = CompositionAttachment(data: .drawing(drawing))
if let currentlyEditedDrawingIndex = currentlyEditedDrawingIndex {
attachments[currentlyEditedDrawingIndex] = newAttachment
tableView.reloadRows(at: [IndexPath(row: currentlyEditedDrawingIndex, section: 0)], with: .automatic)
} else {
attachments.append(newAttachment)
tableView.insertRows(at: [IndexPath(row: self.attachments.count - 1, section: 0)], with: .automatic)
updateHeightConstraint()
}
dismiss(animated: true)
currentlyEditedDrawingIndex = nil
}
}

View File

@ -0,0 +1,174 @@
//
// ComposeDrawingViewController.swift
// Tusker
//
// Created by Shadowfacts on 5/7/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import PencilKit
protocol ComposeDrawingViewControllerDelegate: class {
func composeDrawingViewControllerClose(_ drawingController: ComposeDrawingViewController)
func composeDrawingViewController(_ drawingController: ComposeDrawingViewController, saveDrawing drawing: PKDrawing)
}
class ComposeDrawingViewController: UIViewController {
weak var delegate: ComposeDrawingViewControllerDelegate?
private(set) var canvasView: PKCanvasView!
private(set) var cancelBarButtonItem: UIBarButtonItem!
private(set) var undoBarButtonItem: UIBarButtonItem!
private(set) var redoBarButtonItem: UIBarButtonItem!
private var initialDrawing: PKDrawing?
init() {
super.init(nibName: nil, bundle: nil)
}
convenience init(editing initialDrawing: PKDrawing) {
self.init()
self.initialDrawing = initialDrawing
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .light
navigationItem.title = NSLocalizedString("Draw", comment: "compose drawing screen title")
cancelBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelPressed))
undoBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "arrow.uturn.left.circle"), style: .plain, target: self, action: #selector(undoPressed))
undoBarButtonItem.isEnabled = false
redoBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "arrow.uturn.right.circle"), style: .plain, target: self, action: #selector(redoPressed))
redoBarButtonItem.isEnabled = false
navigationItem.leftBarButtonItems = [cancelBarButtonItem]
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(savePressed))
canvasView = PKCanvasView()
if let initialDrawing = initialDrawing {
canvasView.drawing = initialDrawing
}
canvasView.delegate = self
canvasView.allowsFingerDrawing = true
canvasView.minimumZoomScale = 0.5
canvasView.maximumZoomScale = 2
canvasView.backgroundColor = .systemBackground
canvasView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(canvasView)
NSLayoutConstraint.activate([
canvasView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
canvasView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
canvasView.topAnchor.constraint(equalTo: view.topAnchor),
canvasView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let window = parent?.view.window, let toolPicker = PKToolPicker.shared(for: window) {
toolPicker.setVisible(true, forFirstResponder: canvasView)
toolPicker.addObserver(canvasView)
toolPicker.addObserver(self)
updateLayout(for: toolPicker)
canvasView.becomeFirstResponder()
// wait until the next run loop iteration so that the canvas view has become first responder and it's undo manager exists
DispatchQueue.main.async {
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidUndoChange, object: self.undoManager!)
NotificationCenter.default.addObserver(self, selector: #selector(self.updateUndoRedoButtonState), name: .NSUndoManagerDidRedoChange, object: self.undoManager!)
}
}
}
func updateLayout(for toolPicker: PKToolPicker) {
let obscuredFrame = toolPicker.frameObscured(in: view)
// if there is no obscured frame, the tool picker is floating
// which means we don't need to change inset or show undo/redo
if obscuredFrame.isNull {
navigationItem.leftBarButtonItems = [cancelBarButtonItem]
canvasView.contentInset = .zero
canvasView.automaticallyAdjustsScrollIndicatorInsets = true
} else {
navigationItem.leftBarButtonItems = [cancelBarButtonItem, undoBarButtonItem, redoBarButtonItem]
canvasView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: obscuredFrame.height, right: 0)
canvasView.automaticallyAdjustsScrollIndicatorInsets = false
// we don't care about the botom safe area inset becaused the tool picker obscured frame includes the safe area
canvasView.scrollIndicatorInsets = UIEdgeInsets(top: view.safeAreaInsets.top, left: view.safeAreaInsets.left, bottom: obscuredFrame.height, right: view.safeAreaInsets.right)
}
}
func updateContentSizeForDrawing() {
let overscrollAmount: CGFloat = 100
let drawingBounds = canvasView.drawing.bounds
let newSize: CGSize
if !drawingBounds.isNull {
let width = max(canvasView.bounds.width, (drawingBounds.maxX + overscrollAmount) * canvasView.zoomScale)
let height = max(canvasView.bounds.height, (drawingBounds.maxY + overscrollAmount) * canvasView.zoomScale)
newSize = CGSize(width: width, height: height)
} else {
newSize = canvasView.bounds.size
}
canvasView.contentSize = newSize
}
@objc func updateUndoRedoButtonState() {
undoBarButtonItem.isEnabled = undoManager!.canUndo
redoBarButtonItem.isEnabled = undoManager!.canRedo
}
// MARK: Interaction
@objc func cancelPressed() {
delegate?.composeDrawingViewControllerClose(self)
}
@objc func savePressed() {
delegate?.composeDrawingViewController(self, saveDrawing: canvasView.drawing)
}
@objc func undoPressed() {
undoManager!.undo()
}
@objc func redoPressed() {
undoManager!.redo()
}
}
extension ComposeDrawingViewController: PKCanvasViewDelegate {
func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) {
if !undoManager!.isUndoing && !undoManager!.isRedoing {
updateUndoRedoButtonState()
}
updateContentSizeForDrawing()
}
}
extension ComposeDrawingViewController: PKToolPickerObserver {
func toolPickerFramesObscuredDidChange(_ toolPicker: PKToolPicker) {
updateLayout(for: toolPicker)
}
func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) {
updateLayout(for: toolPicker)
}
}
extension ComposeDrawingViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return otherGestureRecognizer == canvasView.drawingGestureRecognizer
}
}

View File

@ -7,7 +7,6 @@
// //
import UIKit import UIKit
//import Combine
import Photos import Photos
import AVFoundation import AVFoundation
@ -42,6 +41,8 @@ class ComposeAttachmentTableViewCell: UITableViewCell {
descriptionTextView.text = attachment.attachmentDescription descriptionTextView.text = attachment.attachmentDescription
updateDescriptionPlaceholderLabel() updateDescriptionPlaceholderLabel()
assetImageView.contentMode = .scaleAspectFill
assetImageView.backgroundColor = .secondarySystemBackground
switch attachment.data { switch attachment.data {
case let .image(image): case let .image(image):
assetImageView.image = image assetImageView.image = image
@ -57,6 +58,10 @@ class ComposeAttachmentTableViewCell: UITableViewCell {
if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) { if let cgImage = try? imageGenerator.copyCGImage(at: .zero, actualTime: nil) {
assetImageView.image = UIImage(cgImage: cgImage) assetImageView.image = UIImage(cgImage: cgImage)
} }
case let .drawing(drawing):
assetImageView.image = drawing.imageInLightMode(from: drawing.bounds)
assetImageView.contentMode = .scaleAspectFit
assetImageView.backgroundColor = .white
} }
} }

View File

@ -34,6 +34,9 @@ class DraftTableViewCell: UITableViewCell {
attachmentsStackView.addArrangedSubview(imageView) attachmentsStackView.addArrangedSubview(imageView)
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
imageView.backgroundColor = .secondarySystemBackground
imageView.contentMode = .scaleAspectFill
switch attachment.data { switch attachment.data {
case let .asset(asset): case let .asset(asset):
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: nil) { (image, _) in
@ -44,6 +47,10 @@ class DraftTableViewCell: UITableViewCell {
case .video(_): case .video(_):
// videos aren't saved to drafts, so this is unreachable // videos aren't saved to drafts, so this is unreachable
return return
case let .drawing(drawing):
imageView.image = drawing.imageInLightMode(from: drawing.bounds)
imageView.backgroundColor = .white
imageView.contentMode = .scaleAspectFit
} }
} }
} }