Add drawing attachments using PencilKit
This commit is contained in:
parent
9f75106706
commit
4fdafa893e
|
@ -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 */,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue