From e15058a8fc7dcc5e5078529291306b381a3e60cc Mon Sep 17 00:00:00 2001 From: Brandon Titus Date: Tue, 9 Feb 2021 21:48:58 -0700 Subject: [PATCH 1/6] Add archiving --- Classes/Camera/CameraController.swift | 199 +++++++++++++----- Classes/Camera/MediaArchiver.swift | 30 ++- Classes/Editor/EditorView.swift | 25 ++- Classes/Editor/EditorViewController.swift | 90 +++++--- .../Media/Stickers/StylableImageView.swift | 47 ++++- Classes/Editor/MovableViews/MovableView.swift | 52 ++++- .../MovableViews/MovableViewCanvas.swift | 105 +++++++-- .../MovableViewInnerElement.swift | 8 +- .../MovableViews/ViewTransformations.swift | 16 +- .../MultiEditorViewController.swift | 158 +++++++++++--- Classes/Editor/Text/MainTextView.swift | 8 +- Classes/Editor/Text/StylableTextView.swift | 108 +++++++++- .../Preview/CameraPreviewViewController.swift | 44 ++-- Classes/Recording/CameraSegmentHandler.swift | 79 ++++--- Classes/Rendering/MediaPlayer.swift | 48 ++--- 15 files changed, 777 insertions(+), 240 deletions(-) diff --git a/Classes/Camera/CameraController.swift b/Classes/Camera/CameraController.swift index 96ada4983..ff92ec60d 100644 --- a/Classes/Camera/CameraController.swift +++ b/Classes/Camera/CameraController.swift @@ -7,6 +7,9 @@ import AVFoundation import Foundation import UIKit +import MobileCoreServices +import Photos +import Combine // Media wrapper for media generated from the CameraController public struct KanvasMedia { @@ -14,34 +17,38 @@ public struct KanvasMedia { public let output: URL public let info: MediaInfo public let size: CGSize + public let archive: URL? public let type: MediaType init(unmodified: URL?, output: URL, info: MediaInfo, size: CGSize, + archive: URL?, type: MediaType) { self.unmodified = unmodified self.output = output self.info = info self.size = size + self.archive = archive self.type = type } - init(asset: AVURLAsset, original: URL?, info: MediaInfo) { + init(asset: AVURLAsset, original: URL?, info: MediaInfo, archive: URL?) { self.init(unmodified: original, output: asset.url, info: info, size: asset.videoScreenSize ?? .zero, - type: MediaType.video - ) + archive: archive, + type: MediaType.video) } - init(image: UIImage, url: URL, original: URL?, info: MediaInfo) { + init(image: UIImage, url: URL, original: URL?, info: MediaInfo, archive: URL?) { self.init(unmodified: original, output: url, info: info, size: image.size, + archive: archive, type: MediaType.image ) } @@ -87,7 +94,7 @@ public protocol CameraControllerDelegate: class { func tagButtonPressed() /// Called when the editor is dismissed - func editorDismissed() + func editorDismissed(_ cameraController: CameraController) /// Called after the welcome tooltip is dismissed func didDismissWelcomeTooltip() @@ -132,11 +139,77 @@ public protocol CameraControllerDelegate: class { func getBlogSwitcher() -> UIView } +class Archive: NSObject, NSSecureCoding { + static var supportsSecureCoding: Bool = true + + let image: UIImage? + let video: URL? + let data: Data? + + init(image: UIImage, data: Data?) { + self.image = image + self.data = data + self.video = nil + } + + init(video: URL, data: Data?) { + self.video = video + self.data = data + self.image = nil + } + + func encode(with coder: NSCoder) { + coder.encode(image, forKey: "image") + coder.encode(video?.absoluteString, forKey: "video") + coder.encode(data?.base64EncodedString(), forKey: "data") + } + + required init?(coder: NSCoder) { + image = coder.decodeObject(of: UIImage.self, forKey: "image") + if let urlString = coder.decodeObject(of: NSString.self, forKey: "video") as? String { + video = URL(string: urlString) + } else { + video = nil + } + if let dataString = coder.decodeObject(of: NSString.self, forKey: "data") as? String { + data = Data(base64Encoded: dataString) + } else { + data = nil + } + } +} + // A controller that contains and layouts all camera handling views and controllers (mode selector, input, etc). -public class CameraController: UIViewController, MediaClipsEditorDelegate, CameraPreviewControllerDelegate, EditorControllerDelegate, CameraZoomHandlerDelegate, OptionsControllerDelegate, ModeSelectorAndShootControllerDelegate, CameraViewDelegate, CameraInputControllerDelegate, FilterSettingsControllerDelegate, CameraPermissionsViewControllerDelegate, KanvasMediaPickerViewControllerDelegate, MediaPickerThumbnailFetcherDelegate, MultiEditorComposerDelegate { +open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraPreviewControllerDelegate, EditorControllerDelegate, CameraZoomHandlerDelegate, OptionsControllerDelegate, ModeSelectorAndShootControllerDelegate, CameraViewDelegate, CameraInputControllerDelegate, FilterSettingsControllerDelegate, CameraPermissionsViewControllerDelegate, KanvasMediaPickerViewControllerDelegate, MediaPickerThumbnailFetcherDelegate, MultiEditorComposerDelegate { + + enum ArchiveErrors: Error { + case unknownMedia + } + + public static func unarchive(_ url: URL) throws -> (CameraSegment, Data?) { + let data = try Data(contentsOf: url) + let archive = try NSKeyedUnarchiver.unarchivedObject(ofClass: Archive.self, from: data) + let segment: CameraSegment + if let image = archive?.image { + let info: MediaInfo + if let imageData = image.jpegData(compressionQuality: 1.0), let mInfo = MediaInfo(fromImageData: imageData) { + info = mInfo + } else { + info = MediaInfo(source: .kanvas_camera) + } + segment = CameraSegment.image(image, nil, nil, info) + } else if let video = archive?.video { + segment = CameraSegment.video(video, MediaInfo(fromVideoURL: video)) + } else { + throw ArchiveErrors.unknownMedia + } + return (segment, archive?.data) + } + public func show(media: [(CameraSegment, Data?)]) { showPreview = true self.segments = media.map({ return $0.0 }) + self.edits = media.map({ return $0.1 }) if view.superview != nil { showPreviewWithSegments(segments, selected: segments.startIndex, edits: nil, animated: false) @@ -173,6 +246,8 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer controller.delegate = self return controller }() + + private var clips = [MediaClip]() private lazy var cameraInputController: CameraInputController = { let controller = CameraInputController(settings: self.settings, recorderClass: self.recorderClass, segmentsHandler: self.segmentsHandler, delegate: self) @@ -354,7 +429,7 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer return } if segments.isEmpty == false && showPreview { - showPreviewWithSegments(segments, selected: segments.startIndex, animated: false) + showPreviewWithSegments(segments, selected: segments.startIndex, edits: edits, animated: false) showPreview = false } if delegate?.cameraShouldShowWelcomeTooltip() == true && cameraPermissionsViewController.hasFullAccess() { @@ -363,7 +438,9 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer } // MARK: - navigation + private var segments: [CameraSegment] = [] + private var edits: [Data?]? private var showPreview: Bool = false private func showPreviewWithSegments(_ segments: [CameraSegment], selected: Array.Index, edits: [Data?]? = nil, animated: Bool = true) { @@ -390,7 +467,8 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer multiEditorViewController = controller as? MultiEditorViewController } else if settings.features.editor { - controller = createEditorViewController(segments, selected: selected) + let existing = existingEditor + controller = existing ?? createEditorViewController(segments, selected: selected) } else { controller = createPreviewViewController(segments) @@ -400,7 +478,7 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer return controller } - private func createEditorViewController(_ segments: [CameraSegment], selected: Array.Index) -> EditorViewController { + private func createEditorViewController(_ segments: [CameraSegment], selected: Array.Index, canvas: MovableViewCanvas? = nil, drawing: IgnoreTouchesView? = nil, cache: NSCache? = nil) -> EditorViewController { let controller = EditorViewController(settings: settings, segments: segments, assetsHandler: segmentsHandler, @@ -410,6 +488,8 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer stickerProvider: stickerProvider, analyticsProvider: analyticsProvider, quickBlogSelectorCoordinator: quickBlogSelectorCoordinator, + canvas: canvas, + drawingView: drawing, tagCollection: tagCollection) controller.delegate = self return controller @@ -419,7 +499,8 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer let controller = MultiEditorViewController(settings: settings, segments: segments, delegate: self, - selected: selected) + selected: selected, + edits: edits) return controller } @@ -454,27 +535,13 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer } // MARK: - Media Content Creation - - class func save(image: UIImage?, info: MediaInfo) -> URL? { - do { - guard let image = image, let jpgImageData = image.jpegData(compressionQuality: 1.0) else { - return nil - } - let fileURL = try save(data: jpgImageData, to: "kanvas-image", ext: "jpg") - info.write(toImage: fileURL) - return fileURL - } catch { - print("Failed to save to file. \(error)") - return nil - } - } class func save(data: Data, to filename: String, ext fileExtension: String) throws -> URL { let fileURL = try URL(filename: filename, fileExtension: fileExtension, unique: false, removeExisting: true) try data.write(to: fileURL, options: .atomic) return fileURL } - + private func durationStringForAssetAtURL(_ url: URL?) -> String { var text = "" if let url = url { @@ -605,7 +672,7 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer if isRecording { modeAndShootController.hideModeButton() } - else if !isRecording && !clipsController.hasClips && settings.enabledModes.count > 1 { + else if !isRecording && !clipsController.hasClips && !clips.isEmpty && settings.enabledModes.count > 1 { modeAndShootController.showModeButton() } } @@ -665,7 +732,7 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer // Let's prompt for losing clips if they have clips and it's the "x" button, rather than the ">" button. if clipsController.hasClips && !settings.topButtonsSwapped { showDismissTooltip() - } else if clipsController.hasClips && multiEditorViewController != nil { + } else if clips.isEmpty == false && multiEditorViewController != nil { showPreviewWithSegments([], selected: multiEditorViewController?.selected ?? 0) } else { @@ -875,18 +942,20 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer dismiss(animated: false, completion: nil) } - func editor(segment: CameraSegment) -> EditorViewController { + func editor(segment: CameraSegment, canvas: MovableViewCanvas?, drawingView: IgnoreTouchesView?) -> EditorViewController { let segments = [segment] - return createEditorViewController(segments, selected: segments.startIndex) + return createEditorViewController(segments, selected: segments.startIndex, canvas: canvas, drawing: drawingView) } + + // MARK: - CameraPreviewControllerDelegate & EditorControllerDelegate & StoryComposerDelegate func didFinishExportingVideo(url: URL?) { - didFinishExportingVideo(url: url, info: MediaInfo(source: .kanvas_camera), action: .previewConfirm, mediaChanged: true) + didFinishExportingVideo(url: url, info: MediaInfo(source: .kanvas_camera), archive: Data(), action: .previewConfirm, mediaChanged: true) } func didFinishExportingImage(image: UIImage?) { - didFinishExportingImage(image: image, info: MediaInfo(source: .kanvas_camera), action: .previewConfirm, mediaChanged: true) + didFinishExportingImage(image: image, info: MediaInfo(source: .kanvas_camera), archive: Data(), action: .previewConfirm, mediaChanged: true) } func didFinishExportingFrames(url: URL?) { @@ -894,10 +963,10 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer if let url = url { size = GIFDecoderFactory.main().size(of: url) } - didFinishExportingFrames(url: url, size: size, info: MediaInfo(source: .kanvas_camera), action: .previewConfirm, mediaChanged: true) + didFinishExportingFrames(url: url, size: size, info: MediaInfo(source: .kanvas_camera), archive: Data(), action: .previewConfirm, mediaChanged: true) } - public func didFinishExportingVideo(url: URL?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) { + public func didFinishExportingVideo(url: URL?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { guard settings.features.multipleExports == false else { return } let asset: AVURLAsset? if let url = url { @@ -907,8 +976,18 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer asset = nil } + let fileName = url?.deletingPathExtension().lastPathComponent ?? UUID().uuidString + if let asset = asset, let info = info { - let media = KanvasMedia(asset: asset, original: url, info: info) + + let archiveURL: URL? + if let saveDirectory = saveDirectory { + archiveURL = try! archive?.save(to: fileName, in: saveDirectory, ext: "") + } else { + archiveURL = nil + } + + let media = KanvasMedia(asset: asset, original: url, info: info, archive: archiveURL) logMediaCreation(action: action, clipsCount: cameraInputController.segments().count, length: CMTimeGetSeconds(asset.duration)) performUIUpdate { [weak self] in if let self = self { @@ -920,16 +999,24 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer performUIUpdate { [weak self] in if let self = self { self.handleCloseSoon(action: action) - self.delegate?.didCreateMedia(self, media: [], exportAction: action) + self.delegate?.didCreateMedia(self, media: [.failure(CameraControllerError.exportFailure)], exportAction: action) } } } } - public func didFinishExportingImage(image: UIImage?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) { + public func didFinishExportingImage(image: UIImage?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { guard settings.features.multipleExports == false else { return } if let image = image, let info = info, let url = image.save(info: info) { - let media = KanvasMedia(image: image, url: url, original: url, info: info) + + let archiveURL: URL? + if let saveDirectory = saveDirectory { + archiveURL = try! archive?.save(to: UUID().uuidString, in: saveDirectory, ext: "") + } else { + archiveURL = nil + } + + let media = KanvasMedia(image: image, url: url, original: url, info: info, archive: archiveURL) logMediaCreation(action: action, clipsCount: 1, length: 0) performUIUpdate { [weak self] in if let self = self { @@ -942,23 +1029,30 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer performUIUpdate { [weak self] in if let self = self { self.handleCloseSoon(action: action) - self.delegate?.didCreateMedia(self, media: [], exportAction: action) + self.delegate?.didCreateMedia(self, media: [.failure(CameraControllerError.exportFailure)], exportAction: action) } } } } - public func didFinishExportingFrames(url: URL?, size: CGSize?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) { + public func didFinishExportingFrames(url: URL?, size: CGSize?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { + guard settings.features.multipleExports == false else { return } guard let url = url, let info = info, let size = size, size != .zero else { performUIUpdate { self.handleCloseSoon(action: action) - self.delegate?.didCreateMedia(self, media: [], exportAction: action) + self.delegate?.didCreateMedia(self, media: [.failure(CameraControllerError.exportFailure)], exportAction: action) } return } + let archiveURL: URL? + if let saveDirectory = saveDirectory { + archiveURL = try! archive?.save(to: UUID().uuidString, in: saveDirectory, ext: "") + } else { + archiveURL = nil + } performUIUpdate { self.handleCloseSoon(action: action) - let media = KanvasMedia(unmodified: url, output: url, info: info, size: size, type: .frames) + let media = KanvasMedia(unmodified: url, output: url, info: info, size: size, archive: archiveURL, type: .frames) self.delegate?.didCreateMedia(self, media: [.success(media)], exportAction: action) } } @@ -971,17 +1065,16 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer let archiver = MediaArchiver(saveDirectory: saveDirectory) - let exports: [EditorViewController.ExportResult?] = result.map { result in - switch result { - case .success(let export): - return export - case .failure(_): - return nil - } - } - queue.async { [weak self] in guard let self = self else { return } + let exports: [EditorViewController.ExportResult?] = result.map { result in + switch result { + case .success(let export): + return export + case .failure(let error): + return nil + } + } let publishers = archiver.handle(exports: exports) self.exportCancellable = publishers.receive(on: DispatchQueue.main).sink { completion in @@ -1013,14 +1106,14 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer else { analyticsProvider?.logPreviewDismissed() } - if settings.features.multipleExports && clipsController.hasClips { + if settings.features.multipleExports && !clips.isEmpty { showPreviewWithSegments([], selected: multiEditorViewController?.selected ?? 0) } else { performUIUpdate { [weak self] in self?.dismiss(animated: true) } } - delegate?.editorDismissed() + delegate?.editorDismissed(self) } public func tagButtonPressed() { @@ -1125,7 +1218,7 @@ public class CameraController: UIViewController, MediaClipsEditorDelegate, Camer modeAndShootController.showMediaPickerButton(basedOn: currentMode, animated: animated) } else { - modeAndShootController.toggleMediaPickerButton(false, animated: animated) + modeAndShootController.toggleMediaPickerButton(settings.features.cameraFilters == false, animated: animated) } } else { diff --git a/Classes/Camera/MediaArchiver.swift b/Classes/Camera/MediaArchiver.swift index 007ad99c4..f36591874 100644 --- a/Classes/Camera/MediaArchiver.swift +++ b/Classes/Camera/MediaArchiver.swift @@ -59,21 +59,45 @@ class MediaArchiver { } else { originalURL = nil } - return KanvasMedia(image: image, url: url, original: originalURL, info: export.info) + let archiveURL = self.archive(media: .image(original), archive: export.archive, to: url.deletingPathExtension().lastPathComponent) + return KanvasMedia(image: image, url: url, original: originalURL, info: export.info, archive: archiveURL) } else { return nil } case (.video(let url), .video(let original)): print("Original video URL: \(original)") + let archiveURL = self.archive(media: .video(original), archive: export.archive, to: url.deletingPathExtension().lastPathComponent) let asset = AVURLAsset(url: url) - return KanvasMedia(asset: asset, original: original, info: export.info) + return KanvasMedia(asset: asset, original: original, info: export.info, archive: archiveURL) default: return nil } } + + private func archive(media: EditorViewController.Media, archive data: Data, to path: String) -> URL? { + + let archive: Archive + + switch media { + case .image(let image): + archive = Archive(image: image, data: data) + case .video(let url): + archive = Archive(video: url, data: data) + } + + let archiveURL: URL? + if let saveDirectory = saveDirectory { + let data = try! NSKeyedArchiver.archivedData(withRootObject: archive, requiringSecureCoding: true) + archiveURL = try! data.save(to: path, in: saveDirectory, ext: "") + } else { + archiveURL = nil + } + + return archiveURL + } } -private extension Data { +extension Data { func save(to filename: String, in directory: URL, ext fileExtension: String) throws -> URL { let fileURL = directory.appendingPathComponent(filename).appendingPathExtension(fileExtension) try write(to: fileURL, options: .atomic) diff --git a/Classes/Editor/EditorView.swift b/Classes/Editor/EditorView.swift index 0be35f77c..a36857be0 100644 --- a/Classes/Editor/EditorView.swift +++ b/Classes/Editor/EditorView.swift @@ -116,8 +116,9 @@ private struct EditorViewConstants { final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelegate { func didRenderRectChange(rect: CGRect) { - drawingCanvasConstraints.update(with: rect) - movableViewCanvasConstraints.update(with: rect) + let newRect = rect.intersection(UIScreen.main.bounds) + drawingCanvasConstraints.update(with: newRect) + movableViewCanvasConstraints.update(with: newRect) delegate?.didRenderRectChange(rect: rect) } @@ -159,7 +160,7 @@ final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelega private let quickBlogSelectorCoordinator: KanvasQuickBlogSelectorCoordinating? private let tagCollection: UIView? - let drawingCanvas = IgnoreTouchesView() + var drawingCanvas: IgnoreTouchesView private lazy var drawingCanvasConstraints: FullViewConstraints = { return FullViewConstraints( @@ -171,11 +172,7 @@ final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelega ) }() - lazy var movableViewCanvas: MovableViewCanvas = { - let canvas = MovableViewCanvas() - canvas.delegate = self - return canvas - }() + var movableViewCanvas: MovableViewCanvas private lazy var movableViewCanvasConstraints = { return FullViewConstraints( @@ -219,7 +216,9 @@ final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelega showBlogSwitcher: Bool, quickBlogSelectorCoordinator: KanvasQuickBlogSelectorCoordinating?, tagCollection: UIView?, - metalContext: MetalContext?) { + metalContext: MetalContext?, + movableViewCanvas: MovableViewCanvas?, + drawingCanvas: IgnoreTouchesView?) { self.delegate = delegate self.mainActionMode = mainActionMode self.showSaveButton = showSaveButton @@ -232,7 +231,11 @@ final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelega self.quickBlogSelectorCoordinator = quickBlogSelectorCoordinator self.tagCollection = tagCollection self.metalContext = metalContext + self.movableViewCanvas = movableViewCanvas ?? MovableViewCanvas() + self.drawingCanvas = drawingCanvas ?? IgnoreTouchesView() + super.init(frame: .zero) + self.movableViewCanvas.delegate = self setupViews() } @@ -277,7 +280,7 @@ final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelega setupOverlay() setupOverlayLabel() } - + // MARK: - views private func setupPlayer() { @@ -299,7 +302,7 @@ final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelega addSubview(movableViewCanvas) movableViewCanvasConstraints.activate() } - + /// Container that holds the back button and the bottom menu private func setupNavigationContainer() { navigationContainer.accessibilityIdentifier = "Navigation Container" diff --git a/Classes/Editor/EditorViewController.swift b/Classes/Editor/EditorViewController.swift index 8e67df6cf..8af9ce8fd 100644 --- a/Classes/Editor/EditorViewController.swift +++ b/Classes/Editor/EditorViewController.swift @@ -12,13 +12,13 @@ import UIKit public protocol EditorControllerDelegate: class { /// callback when finished exporting video clips. - func didFinishExportingVideo(url: URL?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) + func didFinishExportingVideo(url: URL?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) /// callback when finished exporting image - func didFinishExportingImage(image: UIImage?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) + func didFinishExportingImage(image: UIImage?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) /// callback when finished exporting frames - func didFinishExportingFrames(url: URL?, size: CGSize?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) + func didFinishExportingFrames(url: URL?, size: CGSize?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) /// callback when dismissing controller without exporting func dismissButtonPressed() @@ -76,9 +76,10 @@ public final class EditorViewController: UIViewController, MediaPlayerController } public struct ExportResult { - let original: Media? - let result: Media - let info: MediaInfo + let original: Media? + let result: Media + let info: MediaInfo + let archive: Data } var editorView: EditorView @@ -153,6 +154,8 @@ public final class EditorViewController: UIViewController, MediaPlayerController private let cameraMode: CameraMode? private var openedMenu: EditionOption? private var selectedCell: KanvasEditorMenuCollectionCell? + + var shouldExportSound: Bool = true private let metalContext = MetalContext.createContext() private var shouldExportMediaAsGIF: Bool { @@ -182,12 +185,14 @@ public final class EditorViewController: UIViewController, MediaPlayerController private var editingNewText: Bool = true public weak var delegate: EditorControllerDelegate? - + private var exportCompletion: ((Result) -> Void)? private static func editor(delegate: EditorViewDelegate?, settings: CameraSettings, + canvas: MovableViewCanvas?, quickBlogSelectorCoordinator: KanvasQuickBlogSelectorCoordinating?, + drawingView: IgnoreTouchesView?, tagCollection: UIView?, metalContext: MetalContext?) -> EditorView { var mainActionMode: EditorView.MainActionMode = .confirm @@ -209,7 +214,9 @@ public final class EditorViewController: UIViewController, MediaPlayerController showBlogSwitcher: settings.showBlogSwitcherInEditor, quickBlogSelectorCoordinator: quickBlogSelectorCoordinator, tagCollection: tagCollection, - metalContext: metalContext) + metalContext: metalContext, + movableViewCanvas: canvas, + drawingCanvas: drawingView) return editorView } @@ -302,6 +309,8 @@ public final class EditorViewController: UIViewController, MediaPlayerController stickerProvider: StickerProvider?, analyticsProvider: KanvasAnalyticsProvider?, quickBlogSelectorCoordinator: KanvasQuickBlogSelectorCoordinating?, + canvas: MovableViewCanvas? = nil, + drawingView: IgnoreTouchesView? = nil, tagCollection: UIView?) { self.settings = settings self.originalSegments = segments @@ -318,7 +327,9 @@ public final class EditorViewController: UIViewController, MediaPlayerController self.player = MediaPlayer(renderer: Renderer(settings: settings, metalContext: metalContext)) self.editorView = EditorViewController.editor(delegate: nil, settings: settings, + canvas: canvas, quickBlogSelectorCoordinator: quickBlogSelectorCoordinator, + drawingView: drawingView, tagCollection: tagCollection, metalContext: metalContext) super.init(nibName: .none, bundle: .none) @@ -386,7 +397,7 @@ public final class EditorViewController: UIViewController, MediaPlayerController private func addCarouselDefaultColors(_ image: UIImage) { let dominantColors = image.getDominantColors(count: 3) drawingController.addColorsForCarousel(colors: dominantColors) - + if let mostDominantColor = dominantColors.first { textController.addColorsForCarousel(colors: [mostDominantColor, .white, .black]) } @@ -496,14 +507,14 @@ public final class EditorViewController: UIViewController, MediaPlayerController } // More than one segment, or one video-only segment, enable it. - if segments.count > 1 || segments.first?.image == nil { + if segments.count > 1 || segments.first?.isVideo == true { return true } // A single segment that has both an image and a video (live photo), enabled it. if segments.count == 1, let firstSegment = segments.first, - firstSegment.image != nil, + firstSegment.isVideo == false, firstSegment.videoURL != nil { return true } @@ -650,15 +661,15 @@ public final class EditorViewController: UIViewController, MediaPlayerController player.stop() delegate?.dismissButtonPressed() } - - func getQuickPostButton() -> UIView { + + func getBlogSwitcher() -> UIView { guard let delegate = delegate else { return UIView() } - return delegate.getQuickPostButton() + return delegate.getBlogSwitcher() } - func getBlogSwitcher() -> UIView { + func getQuickPostButton() -> UIView { guard let delegate = delegate else { return UIView() } - return delegate.getBlogSwitcher() + return delegate.getQuickPostButton() } func restartPlayback() { @@ -679,14 +690,11 @@ public final class EditorViewController: UIViewController, MediaPlayerController private func startExporting(action: KanvasExportAction) { player.stop() showLoading() - if segments.count == 1, let firstSegment = segments.first, let image = firstSegment.image { + if segments.count == 1, let firstSegment = segments.first, case CameraSegment.image(let image, _, _, _) = firstSegment { // If the camera mode is .stopMotion, .normal or .stitch (.video) and the `exportStopMotionPhotoAsVideo` is true, // then single photos from that mode should still export as video. - if let cameraMode = cameraMode, cameraMode.group == .video && settings.exportStopMotionPhotoAsVideo { - assetsHandler.ensureAllImagesHaveVideo(segments: segments) { segments in - guard let videoURL = segments.first?.videoURL else { return } - self.createFinalVideo(videoURL: videoURL, mediaInfo: firstSegment.mediaInfo, exportAction: action) - } + if let cameraMode = cameraMode, cameraMode.group == .video && settings.exportStopMotionPhotoAsVideo, let videoURL = firstSegment.videoURL { + createFinalVideo(videoURL: videoURL, mediaInfo: firstSegment.mediaInfo, exportAction: action) } else { createFinalImage(image: image, mediaInfo: firstSegment.mediaInfo, exportAction: action) @@ -733,12 +741,17 @@ public final class EditorViewController: UIViewController, MediaPlayerController startExporting(action: .post) } + var archive: Data { + return try! NSKeyedArchiver.archivedData(withRootObject: editorView.movableViewCanvas, requiringSecureCoding: true) + } + private func createFinalGIF(segments: [CameraSegment], mediaInfo: MediaInfo, exportAction: KanvasExportAction) { let exporter = exporterClass.init(settings: settings) exporter.filterType = filterType ?? .passthrough exporter.imageOverlays = imageOverlays() let segments = gifMakerHandler.trimmedSegments(segments) let frames = segments.compactMap { $0.mediaFrame(defaultTimeInterval: getDefaultTimeIntervalForImageSegments()) } + let archive = self.archive exporter.export(frames: frames) { orderedFrames in let playbackFrames = self.gifMakerHandler.framesForPlayback(orderedFrames) self.gifEncoderClass.init().encode(frames: playbackFrames, loopCount: 0) { gifURL in @@ -746,7 +759,9 @@ public final class EditorViewController: UIViewController, MediaPlayerController if let gifURL = gifURL { size = GIFDecoderFactory.main().size(of: gifURL) } - self.delegate?.didFinishExportingFrames(url: gifURL, size: size, info: mediaInfo, action: exportAction, mediaChanged: self.mediaChanged) + let result = ExportResult(original: nil, result: .video(gifURL!), info: mediaInfo, archive: archive) + self.exportCompletion?(.success(result)) + self.delegate?.didFinishExportingFrames(url: gifURL, size: size, info: mediaInfo, archive: archive, action: exportAction, mediaChanged: self.mediaChanged) performUIUpdate { self.hideLoading() } @@ -758,6 +773,7 @@ public final class EditorViewController: UIViewController, MediaPlayerController let exporter = exporterClass.init(settings: settings) exporter.filterType = filterType ?? .passthrough exporter.imageOverlays = imageOverlays() + let archive = self.archive exporter.export(video: videoURL, mediaInfo: mediaInfo) { (exportedVideoURL, _) in guard let exportedVideoURL = exportedVideoURL else { performUIUpdate { @@ -776,9 +792,9 @@ public final class EditorViewController: UIViewController, MediaPlayerController return } let size = GIFDecoderFactory.main().size(of: gifURL) - let result = ExportResult(original: nil, result: .video(gifURL), info: mediaInfo) + let result = ExportResult(original: nil, result: .video(gifURL), info: mediaInfo, archive: archive) self.exportCompletion?(.success(result)) - self.delegate?.didFinishExportingFrames(url: gifURL, size: size, info: mediaInfo, action: exportAction, mediaChanged: self.mediaChanged) + self.delegate?.didFinishExportingFrames(url: gifURL, size: size, info: mediaInfo, archive: archive, action: exportAction, mediaChanged: self.mediaChanged) performUIUpdate { self.hideLoading() } @@ -788,8 +804,8 @@ public final class EditorViewController: UIViewController, MediaPlayerController private func createFinalVideo(videoURL: URL, mediaInfo: MediaInfo, exportAction: KanvasExportAction) { let exporter = exporterClass.init(settings: settings) - exporter.filterType = filterType ?? .passthrough exporter.imageOverlays = imageOverlays() + let archive = self.archive exporter.export(video: videoURL, mediaInfo: mediaInfo) { (exportedVideoURL, error) in performUIUpdate { guard let url = exportedVideoURL else { @@ -801,9 +817,9 @@ public final class EditorViewController: UIViewController, MediaPlayerController } return } - let result = ExportResult(original: .video(videoURL), result: .video(url), info: mediaInfo) + let result = ExportResult(original: .video(videoURL), result: .video(url), info: mediaInfo, archive: archive) self.exportCompletion?(.success(result)) - self.delegate?.didFinishExportingVideo(url: url, info: mediaInfo, action: exportAction, mediaChanged: self.mediaChanged) + self.delegate?.didFinishExportingVideo(url: url, info: mediaInfo, archive: archive, action: exportAction, mediaChanged: self.mediaChanged) self.hideLoading() } } @@ -813,7 +829,11 @@ public final class EditorViewController: UIViewController, MediaPlayerController let exporter = exporterClass.init(settings: settings) exporter.filterType = filterType ?? .passthrough exporter.imageOverlays = imageOverlays() - exporter.export(image: image, time: player.lastStillFilterTime) { (exportedImage, error) in +// exporter.dimensions = UIScreen.main.bounds.size + let archive = self.archive + exporter.export(image: image, time: player.lastStillFilterTime) { [weak self] (exportedImage, error) in + guard let self = self else { return } + let originalImage = image performUIUpdate { guard let unwrappedImage = exportedImage else { self.hideLoading() @@ -824,9 +844,9 @@ public final class EditorViewController: UIViewController, MediaPlayerController } return } - let result = ExportResult(original: .image(image), result: .image(unwrappedImage), info: mediaInfo) + let result = ExportResult(original: .image(originalImage), result: .image(unwrappedImage), info: mediaInfo, archive: archive) self.exportCompletion?(.success(result)) - self.delegate?.didFinishExportingImage(image: unwrappedImage, info: mediaInfo, action: exportAction, mediaChanged: self.mediaChanged) + self.delegate?.didFinishExportingImage(image: unwrappedImage, info: mediaInfo, archive: archive, action: exportAction, mediaChanged: self.mediaChanged) self.hideLoading() } } @@ -958,7 +978,7 @@ public final class EditorViewController: UIViewController, MediaPlayerController func getDefaultTimeIntervalForImageSegments() -> TimeInterval { return CameraSegment.defaultTimeInterval(segments: segments) } - + // MARK: - GifMakerHandlerDelegate func didConfirmGif() { @@ -987,7 +1007,7 @@ public final class EditorViewController: UIViewController, MediaPlayerController func unsetMediaPlayerFrame() { player.cancelPlayingSingleFrame() } - + // MARK: - EditorFilterControllerDelegate func didConfirmFilters() { @@ -1019,7 +1039,7 @@ public final class EditorViewController: UIViewController, MediaPlayerController func didConfirmText(textView: StylableTextView, transformations: ViewTransformations, location: CGPoint, size: CGSize) { if !textView.text.isEmpty { - editorView.movableViewCanvas.addView(view: textView, transformations: transformations, location: location, size: size) + editorView.movableViewCanvas.addView(view: textView, transformations: transformations, location: location, size: size, animated: true) if let font = textView.options.font, let alignment = KanvasTextAlignment.from(alignment: textView.options.alignment) { analyticsProvider?.logEditorTextConfirm(isNew: editingNewText, font: font, alignment: alignment, highlighted: textView.options.highlightColor != nil) } @@ -1095,7 +1115,7 @@ public final class EditorViewController: UIViewController, MediaPlayerController func didSelectSticker(imageView: StylableImageView, size: CGSize) { analyticsProvider?.logEditorStickerAdd(stickerId: imageView.id) editorView.movableViewCanvas.addView(view: imageView, transformations: ViewTransformations(), - location: editorView.movableViewCanvas.bounds.center, size: size) + location: editorView.movableViewCanvas.bounds.center, size: size, animated: true) } func didSelectStickerType(_ stickerType: StickerType) { diff --git a/Classes/Editor/Media/Stickers/StylableImageView.swift b/Classes/Editor/Media/Stickers/StylableImageView.swift index e7997a1ba..e29291583 100644 --- a/Classes/Editor/Media/Stickers/StylableImageView.swift +++ b/Classes/Editor/Media/Stickers/StylableImageView.swift @@ -8,7 +8,9 @@ import Foundation import UIKit /// Image view that increases its image quality when its contentScaleFactor is modified -final class StylableImageView: UIImageView, MovableViewInnerElement { +@objc final class StylableImageView: UIImageView, MovableViewInnerElement, Codable, NSSecureCoding { + + static var supportsSecureCoding: Bool { return true } let id: String @@ -17,6 +19,10 @@ final class StylableImageView: UIImageView, MovableViewInnerElement { setScaleFactor(newValue) } } + + var viewSize: CGSize = .zero + + var viewCenter: CGPoint = .zero // MARK: - Initializers @@ -25,10 +31,43 @@ final class StylableImageView: UIImageView, MovableViewInnerElement { super.init(image: image) } - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + required convenience init?(coder: NSCoder) { + let id = String(coder.decodeObject(of: NSString.self, forKey: CodingKeys.id.rawValue) ?? "") + let image = coder.decodeObject(of: UIImage.self, forKey: CodingKeys.image.rawValue) + self.init(id: id, image: image) + viewSize = coder.decodeCGSize(forKey: CodingKeys.size.rawValue) + viewCenter = coder.decodeCGPoint(forKey: CodingKeys.center.rawValue) } - + + enum CodingKeys: String, CodingKey { + case id + case size + case center + case image + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.viewSize = try container.decode(CGSize.self, forKey: .size) + self.viewCenter = try container.decode(CGPoint.self, forKey: .center) + super.init(image: nil) + } + + func encode(to encoder: Encoder) throws { + var container = try encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(viewSize, forKey: .size) + try container.encode(viewCenter, forKey: .center) + } + + override func encode(with coder: NSCoder) { + coder.encode(id, forKey: CodingKeys.id.rawValue) + coder.encode(viewSize, forKey: CodingKeys.size.rawValue) + coder.encode(viewCenter, forKey: CodingKeys.center.rawValue) + coder.encode(image, forKey: CodingKeys.image.rawValue) + } + // MARK: - Scale factor /// Sets a new scale factor to update the quality of the inner image. This value represents how content in the view is mapped diff --git a/Classes/Editor/MovableViews/MovableView.swift b/Classes/Editor/MovableViews/MovableView.swift index 60ae65f19..98ec16fa4 100644 --- a/Classes/Editor/MovableViews/MovableView.swift +++ b/Classes/Editor/MovableViews/MovableView.swift @@ -50,10 +50,12 @@ private struct Constants { } /// A wrapper for UIViews that can be rotated, moved and scaled -final class MovableView: UIView { +final class MovableView: UIView, NSSecureCoding { + + static var supportsSecureCoding: Bool { return true } weak var delegate: MovableViewDelegate? - private let innerView: MovableViewInnerElement + let innerView: MovableViewInnerElement /// Current rotation angle var rotation: CGFloat { @@ -75,6 +77,10 @@ final class MovableView: UIView { applyTransform() } } + + var originLocation: CGPoint = .zero + + var size: CGSize = .zero var transformations: ViewTransformations { return ViewTransformations(position: position, scale: scale, rotation: rotation) @@ -91,7 +97,46 @@ final class MovableView: UIView { } required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + position = aDecoder.decodeCGPoint(forKey: "position") + scale = CGFloat(aDecoder.decodeFloat(forKey: "scale")) + rotation = CGFloat(aDecoder.decodeFloat(forKey: "rotation")) + let view = aDecoder.decodeObject(of: [StylableTextView.self, StylableImageView.self], forKey: "innerView") + + switch view { + case let imageView as StylableImageView: + innerView = imageView + case let textView as StylableTextView: + innerView = textView + default: + innerView = StylableTextView() + } + originLocation = aDecoder.decodeCGPoint(forKey: "origin") + + super.init(frame: .zero) + + setupInnerView() + } + + override func encode(with coder: NSCoder) { + super.encode(with: coder) + coder.encode(position, forKey: "position") + coder.encode(Float(scale), forKey: "scale") + coder.encode(Float(rotation), forKey: "rotation") + coder.encode(innerView, forKey: "innerView") + coder.encode(originLocation, forKey: "origin") + } + + enum ViewType { + case text + case image + } + + var type: ViewType { + if innerView is StylableTextView { + return .text + } else { + return .image + } } // MARK: - Layout @@ -170,6 +215,7 @@ final class MovableView: UIView { // Called when the view is moved func onMove() { + innerView.viewCenter = position if let _ = innerView as? StylableTextView { delegate?.didMoveTextView() } diff --git a/Classes/Editor/MovableViews/MovableViewCanvas.swift b/Classes/Editor/MovableViews/MovableViewCanvas.swift index e60ff3366..2479fffc5 100644 --- a/Classes/Editor/MovableViews/MovableViewCanvas.swift +++ b/Classes/Editor/MovableViews/MovableViewCanvas.swift @@ -47,8 +47,10 @@ private struct Constants { } /// View that contains the collection of movable views -final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, MovableViewDelegate { - +final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, MovableViewDelegate, Codable, NSSecureCoding { + + static var supportsSecureCoding: Bool { return true } + weak var delegate: MovableViewCanvasDelegate? // View that has been tapped @@ -66,9 +68,15 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M // Values from which the different gestures start private var originTransformations: ViewTransformations + + private var innerViews: [MovableViewInnerElement] = [] var isEmpty: Bool { - return subviews.compactMap{ $0 as? MovableView }.count == 0 + return movableViews.isEmpty + } + + var movableViews: [MovableView] { + return subviews.compactMap { $0 as? MovableView } } init() { @@ -77,12 +85,66 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M originTransformations = ViewTransformations() super.init(frame: .zero) setUpViews() + } + + enum CodingKeys: String, CodingKey { + case originTransformations + case textViews + case imageViews + case movableViews } - - required init?(coder aDecoder: NSCoder) { - fatalError("init(coder:) has not been implemented") + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + overlay = UIView() + trashView = TrashView() + originTransformations = try values.decode(ViewTransformations.self, forKey: .originTransformations) + + super.init(frame: .zero) + + let textViews = try values.decode([StylableTextView].self, forKey: .textViews) + let imageViews = try values.decode([StylableImageView].self, forKey: .imageViews) + let innerViews: [MovableViewInnerElement] = textViews + imageViews + innerViews.forEach({ view in + addView(view: view, transformations: originTransformations, location: view.viewCenter, size: view.viewSize, animated: false) + }) } - + + required init?(coder: NSCoder) { + + overlay = UIView() + trashView = TrashView() + + originTransformations = ViewTransformations() + + super.init(frame: .zero) + + let innerViews = coder.decodeObject(of: [NSArray.self, MovableView.self], forKey: CodingKeys.movableViews.rawValue) as? [MovableView] + innerViews?.forEach({ view in + addView(view: view.innerView, transformations: view.transformations, location: view.innerView.viewCenter, origin: view.originLocation, size: view.innerView.viewSize, animated: false) + }) + setUpViews() + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(originTransformations, forKey: .originTransformations) + + let textViews = innerViews.compactMap { $0 as? StylableTextView } as [StylableTextView] + let imageViews = innerViews.compactMap { $0 as? StylableImageView } as [StylableImageView] + + try container.encode(textViews, forKey: .textViews) + try container.encode(imageViews, forKey: .imageViews) + } + + override func encode(with coder: NSCoder) { + coder.encode(originTransformations, forKey: CodingKeys.originTransformations.rawValue) + + let movableViews = subviews.compactMap({ $0 as? MovableView }) + coder.encode(movableViews, forKey: CodingKeys.movableViews.rawValue) + } + // MARK: - Layout private func setUpViews() { @@ -93,7 +155,6 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M /// Sets up the trash bin used during deletion private func setUpTrashView() { trashView.accessibilityIdentifier = "Editor Movable View Canvas Trash View" - trashView.layer.zPosition = 1 trashView.translatesAutoresizingMaskIntoConstraints = false addSubview(trashView) @@ -121,6 +182,13 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M overlay.alpha = 0 } + + override func layoutSubviews() { + super.layoutSubviews() + subviews.compactMap({ return $0 as? MovableView }).forEach({ view in + view.moveToDefinedPosition() + }) + } // MARK: - Public interface @@ -130,9 +198,12 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M /// - transformations: transformations for the view /// - location: location of the view before transformations /// - size: size of the view - func addView(view: MovableViewInnerElement, transformations: ViewTransformations, location: CGPoint, size: CGSize) { + func addView(view: MovableViewInnerElement, transformations: ViewTransformations, location: CGPoint, origin: CGPoint? = nil, size: CGSize, animated: Bool) { let movableView = MovableView(view: view, transformations: transformations) + movableView.originLocation = origin ?? location movableView.delegate = self + view.viewSize = size + view.viewCenter = location movableView.isUserInteractionEnabled = true movableView.isExclusiveTouch = true movableView.isMultipleTouchEnabled = true @@ -142,8 +213,8 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M NSLayoutConstraint.activate([ movableView.heightAnchor.constraint(equalToConstant: size.height), movableView.widthAnchor.constraint(equalToConstant: size.width), - movableView.topAnchor.constraint(equalTo: topAnchor, constant: location.y - (size.height / 2)), - movableView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: location.x - (size.width / 2)) + movableView.centerXAnchor.constraint(equalTo: leadingAnchor, constant: movableView.originLocation.x), + movableView.centerYAnchor.constraint(equalTo: topAnchor, constant: movableView.originLocation.y) ]) let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(movableViewTapped(recognizer:))) @@ -163,10 +234,18 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M movableView.addGestureRecognizer(pinchRecognizer) movableView.addGestureRecognizer(panRecognizer) movableView.addGestureRecognizer(longPressRecognizer) - - UIView.animate(withDuration: Constants.animationDuration) { + + let move: () -> Void = { movableView.moveToDefinedPosition() } + if animated { + UIView.animate(withDuration: Constants.animationDuration) { + move() + } + } else { + move() + } + innerViews.append(view) } /// Removes the tapped view from the canvas diff --git a/Classes/Editor/MovableViews/MovableViewInnerElement.swift b/Classes/Editor/MovableViews/MovableViewInnerElement.swift index c129293fa..7fe6c3632 100644 --- a/Classes/Editor/MovableViews/MovableViewInnerElement.swift +++ b/Classes/Editor/MovableViews/MovableViewInnerElement.swift @@ -7,11 +7,17 @@ import Foundation /// Protocol for the view inside MovableView -protocol MovableViewInnerElement: UIView { +protocol MovableViewInnerElement: UIView, Codable, NSSecureCoding { /// Checks whether the hit is done inside the shape of the view /// /// - Parameter point: location where the view was touched /// - Returns: true if the touch was inside, false if not func hitInsideShape(point: CGPoint) -> Bool + + var viewSize: CGSize { get set } + + var viewCenter: CGPoint { get set } + + } diff --git a/Classes/Editor/MovableViews/ViewTransformations.swift b/Classes/Editor/MovableViews/ViewTransformations.swift index 6e3150e32..dcc363dc2 100644 --- a/Classes/Editor/MovableViews/ViewTransformations.swift +++ b/Classes/Editor/MovableViews/ViewTransformations.swift @@ -7,7 +7,9 @@ import Foundation import UIKit -final class ViewTransformations { +final class ViewTransformations: NSObject, Codable, NSSecureCoding { + + static var supportsSecureCoding: Bool { return true } static let defaultPosition: CGPoint = .zero static let defaultScale: CGFloat = 1.0 @@ -25,4 +27,16 @@ final class ViewTransformations { self.scale = scale self.rotation = rotation } + + init?(coder: NSCoder) { + position = coder.decodeCGPoint(forKey: "position") + scale = CGFloat(coder.decodeFloat(forKey: "scale")) + rotation = CGFloat(coder.decodeFloat(forKey: "rotation")) + } + + func encode(with coder: NSCoder) { + coder.encode(position, forKey: "position") + coder.encode(Float(scale), forKey: "scale") + coder.encode(Float(rotation), forKey: "rotation") + } } diff --git a/Classes/Editor/MultiEditor/MultiEditorViewController.swift b/Classes/Editor/MultiEditor/MultiEditorViewController.swift index 2fe725b05..12ce0cab3 100644 --- a/Classes/Editor/MultiEditor/MultiEditorViewController.swift +++ b/Classes/Editor/MultiEditor/MultiEditorViewController.swift @@ -9,7 +9,7 @@ import Foundation protocol MultiEditorComposerDelegate: EditorControllerDelegate { func didFinishExporting(media: [Result]) func addButtonWasPressed() - func editor(segment: CameraSegment) -> EditorViewController + func editor(segment: CameraSegment, canvas: MovableViewCanvas?, drawingView: IgnoreTouchesView?) -> EditorViewController func dismissButtonPressed() } @@ -30,6 +30,7 @@ class MultiEditorViewController: UIViewController { struct Frame { let segment: CameraSegment + let edit: Edit? } private var frames: [Frame] @@ -46,6 +47,9 @@ class MultiEditorViewController: UIViewController { guard newValue != selected && migratedIndex != newValue else { return } + if let old = selected { + try! archive(index: old)//migratedIndex ?? old) + } if let new = newValue { // If the new index is the same as the old just keep the current editor loadEditor(for: new) } else { @@ -55,7 +59,10 @@ class MultiEditorViewController: UIViewController { } func addSegment(_ segment: CameraSegment) { - frames.append(Frame(segment: segment)) + +// let newEditor = editor(for: segment) + + frames.append(Frame(segment: segment, edit: nil)) let clip = MediaClip(representativeFrame: segment.lastFrame, overlayText: nil, @@ -68,6 +75,13 @@ class MultiEditorViewController: UIViewController { private let settings: CameraSettings +// private var edits: [[View]] = [] + + struct Edit { + let data: Data? + let canvasView: IgnoreTouchesView? + let options: EditOptions + } private var exportingEditors: [EditorViewController]? @@ -76,14 +90,21 @@ class MultiEditorViewController: UIViewController { init(settings: CameraSettings, segments: [CameraSegment], delegate: MultiEditorComposerDelegate, - selected: Array.Index?) { + selected: Array.Index?, + edits: [Data?]?) { self.settings = settings self.delegate = delegate - frames = segments.map({ segment in - return Frame(segment: segment) - }) + if let edits = edits { + frames = zip(segments, edits).map { (segment, data) in + return Frame(segment: segment, edit: Edit(data: data, canvasView: nil, options: EditOptions(soundEnabled: true))) + } + } else { + frames = segments.map({ segment in + return Frame(segment: segment, edit: nil) + }) + } self.exportHandler = MultiEditorExportHandler({ [weak delegate] result in delegate?.didFinishExporting(media: result) @@ -122,7 +143,8 @@ class MultiEditorViewController: UIViewController { } func loadEditor(for index: Int) { - if let editor = delegate?.editor(segment: frames[index].segment) { + let views = edits(for: index) + if let editor = delegate?.editor(segment: frames[index].segment, canvas: views?.0, drawingView: views?.1) { currentEditor?.stopPlayback() currentEditor?.unloadFromParentViewController() let additionalPadding: CGFloat = 10 // Extra padding for devices that don't have safe areas (which provide some padding by default). @@ -134,6 +156,7 @@ class MultiEditorViewController: UIViewController { } editor.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: bottom, right: 0) editor.delegate = self + unarchive(editor: editor, index: index) load(childViewController: editor, into: editorContainer) currentEditor = editor } @@ -159,6 +182,7 @@ class MultiEditorViewController: UIViewController { editorContainer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), editorContainer.topAnchor.constraint(equalTo: view.topAnchor), editorContainer.bottomAnchor.constraint(equalTo: clipsContainer.bottomAnchor), +// editorContainer.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: clipsContainer.topAnchor) ]) } @@ -186,19 +210,22 @@ extension MultiEditorViewController: MediaPlayerController { } extension MultiEditorViewController: MediaClipsEditorDelegate { + func mediaClipStartedMoving() { - // No-op for the moment. UI is coming in a future commit. + } func mediaClipFinishedMoving() { - // No-op for the moment. UI is coming in a future commit. + } - + func addButtonWasPressed() { delegate?.addButtonWasPressed() } func mediaClipWasDeleted(at index: Int) { + var clips = self.clipsController.getClips() + if frames.indices.contains(index) { frames.remove(at: index) } @@ -244,6 +271,9 @@ extension MultiEditorViewController: MediaClipsEditorDelegate { } func mediaClipWasMoved(from originIndex: Int, to destinationIndex: Int) { + if let selected = selected { + try! archive(index: selected) + } frames.move(from: originIndex, to: destinationIndex) let newIndex: Int @@ -264,7 +294,7 @@ extension MultiEditorViewController: MediaClipsEditorDelegate { } @objc func nextButtonWasPressed() { - } + } } extension MultiEditorViewController: EditorControllerDelegate { @@ -277,16 +307,21 @@ extension MultiEditorViewController: EditorControllerDelegate { return UIView() } - func didFinishExportingVideo(url: URL?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) { - // No-op for the moment. API is coming in future commit. + func didFinishExportingVideo(url: URL?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { } - func didFinishExportingImage(image: UIImage?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) { - // No-op for the moment. API is coming in future commit. + func didFinishExportingImage(image: UIImage?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { +// if let image = image { +// var clips = clipsController.getClips() +// if let selected = selected { +// let selectedClip = editors.distance(from: editors.startIndex, to: selected) +// clips[selectedClip] = MediaClip(representativeFrame: image, overlayText: nil, lastFrame: image) +// } +// clipsController.replace(clips: clips) +// } } - func didFinishExportingFrames(url: URL?, size: CGSize?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) { - // No-op for the moment. API is coming in future commit. + func didFinishExportingFrames(url: URL?, size: CGSize?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { } func dismissButtonPressed() { @@ -294,29 +329,77 @@ extension MultiEditorViewController: EditorControllerDelegate { } func didDismissColorSelectorTooltip() { - delegate?.didDismissColorSelectorTooltip() + } func editorShouldShowColorSelectorTooltip() -> Bool { - return delegate?.editorShouldShowColorSelectorTooltip() == true + return true } func didEndStrokeSelectorAnimation() { - delegate?.didEndStrokeSelectorAnimation() + } func editorShouldShowStrokeSelectorAnimation() -> Bool { - return delegate?.editorShouldShowStrokeSelectorAnimation() == true + return true } func tagButtonPressed() { - delegate?.tagButtonPressed() + } struct EditOptions { let soundEnabled: Bool } + func archive(index: Int) throws { + guard let currentEditor = currentEditor else { + return + } + let currentCanvas = try NSKeyedArchiver.archivedData(withRootObject: currentEditor.editorView.movableViewCanvas, requiringSecureCoding: true) + let drawingLayer = currentEditor.editorView.drawingCanvas + let options = EditOptions(soundEnabled: currentEditor.shouldExportSound ?? true) + if frames.indices ~= index { + let frame = frames[index] + frames[index] = Frame(segment: frame.segment, edit: Edit(data: currentCanvas, canvasView: drawingLayer, options: options)) + } else { + print("Invalid frame index") +// edits.insert((currentCanvas, drawingLayer, options), at: min(index, edits.endIndex)) + } + } + + func edits(for index: Int) -> (MovableViewCanvas?, IgnoreTouchesView?, EditOptions)? { + if frames.indices ~= index, let edit = frames[index].edit { + let canvas: MovableViewCanvas? + if let edit = edit.data { + canvas = try! NSKeyedUnarchiver.unarchivedObject(ofClass: MovableViewCanvas.self, from: edit) + } else { + canvas = nil + } + let drawing = edit.canvasView + let options = edit.options + return (canvas, drawing, options) + } else { + return nil + } + } + + func unarchive(editor: EditorViewController, index: Int) { + if frames.indices ~= index { + let canvas: MovableViewCanvas? + let drawing: IgnoreTouchesView? + if let edits = edits(for: index) { + canvas = edits.0 + drawing = edits.1 + } else { + canvas = nil + drawing = nil + } + + canvas?.delegate = editor.editorView + } + } + func showLoading() { currentEditor?.showLoading() clipsContainer.alpha = 0.5 @@ -334,21 +417,40 @@ extension MultiEditorViewController: EditorControllerDelegate { showLoading() + if let selected = selected { + try! archive(index: selected) + } + exportHandler.startWaiting(for: frames.count) guard let delegate = delegate else { return true } frames.enumerated().forEach({ (idx, frame) in autoreleasepool { - let editor = delegate.editor(segment: frame.segment) - DispatchQueue.main.async { - editor.export { [weak self, editor] result in - let _ = editor // strong reference until the export completes - self?.exportHandler.handleExport(result, for: idx) - } + let canvas: MovableViewCanvas? + if let edit = frame.edit?.data { + canvas = try! NSKeyedUnarchiver.unarchivedObject(ofClass: MovableViewCanvas.self, from: edit) + } else { + canvas = nil + } + let editor = delegate.editor(segment: frame.segment, canvas: canvas, drawingView: frame.edit?.canvasView) + editor.shouldExportSound = frame.edit?.options.soundEnabled ?? true + + unarchive(editor: editor, index: idx) + editor.export { [weak self, editor] result in + let _ = editor // strong reference until the export completes + self?.exportHandler.handleExport(result, for: idx) } } }) return false } + + func archive(editor: EditorViewController) { + + } + + func addButtonPressed() { + dismiss(animated: true, completion: nil) + } } diff --git a/Classes/Editor/Text/MainTextView.swift b/Classes/Editor/Text/MainTextView.swift index de4f7de9c..8eb04d303 100644 --- a/Classes/Editor/Text/MainTextView.swift +++ b/Classes/Editor/Text/MainTextView.swift @@ -43,9 +43,13 @@ final class MainTextView: StylableTextView { } required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) + fatalError("init(from:) has not been implemented") } - + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) + } + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { return false } diff --git a/Classes/Editor/Text/StylableTextView.swift b/Classes/Editor/Text/StylableTextView.swift index 57ec5f081..ef7b84b70 100644 --- a/Classes/Editor/Text/StylableTextView.swift +++ b/Classes/Editor/Text/StylableTextView.swift @@ -13,7 +13,9 @@ private struct Constants { } /// TextView that can be customized with TextOptions -class StylableTextView: UITextView, UITextViewDelegate, MovableViewInnerElement { +@objc class StylableTextView: UITextView, UITextViewDelegate, MovableViewInnerElement, Codable, NSSecureCoding { + + static var supportsSecureCoding: Bool { return true } // Color rectangles behind the text private var highlightViews: [UIView] @@ -41,7 +43,11 @@ class StylableTextView: UITextView, UITextViewDelegate, MovableViewInnerElement setScaleFactor(newValue) } } - + + var viewSize: CGSize = .zero + + var viewCenter: CGPoint = .zero + // MARK: - Initializers init() { @@ -65,11 +71,105 @@ class StylableTextView: UITextView, UITextViewDelegate, MovableViewInnerElement backgroundColor = .clear } - required init?(coder aDecoder: NSCoder) { + required init?(coder: NSCoder) { highlightViews = [] - super.init(coder: aDecoder) + + let size = coder.decodeCGSize(forKey: CodingKeys.size.rawValue) + let center = coder.decodeCGPoint(forKey: CodingKeys.center.rawValue) + + super.init(frame: CGRect(origin: .zero, size: size), textContainer: nil) delegate = self + backgroundColor = .clear + + textAlignment = NSTextAlignment(rawValue: coder.decodeInteger(forKey: CodingKeys.textAlignment.rawValue))! + contentScaleFactor = CGFloat(coder.decodeFloat(forKey: CodingKeys.contentScaleFactor.rawValue)) + text = String(coder.decodeObject(of: NSString.self, forKey: CodingKeys.text.rawValue) ?? "") + + viewSize = coder.decodeCGSize(forKey: CodingKeys.size.rawValue) + viewCenter = coder.decodeCGPoint(forKey: CodingKeys.center.rawValue) + textColor = coder.decodeObject(of: UIColor.self, forKey: CodingKeys.textColor.rawValue) + highlightColor = coder.decodeObject(of: UIColor.self, forKey: CodingKeys.highlightColor.rawValue) + +// if let descriptor = coder.decodeObject(of: UIFontDescriptor.self, forKey: CodingKeys.font.rawValue) { +// font = UIFont(descriptor: descriptor, size: 12) +// } + let fontName = String(coder.decodeObject(of: NSString.self, forKey: FontKeys.name.rawValue) ?? "") + let fontSize = CGFloat(coder.decodeFloat(forKey: FontKeys.fontSize.rawValue)) + font = UIFont(name: fontName, size: fontSize) } + + enum CodingKeys: String, CodingKey { + case textAlignment + case contentScaleFactor + case font + case text + case size + case center + case textColor + case highlightColor + } + + enum FontKeys: String, CodingKey { + case name + case fontSize + } + + required init(from decoder: Decoder) throws { + highlightViews = [] + super.init(frame: .zero, textContainer: nil) + delegate = self + backgroundColor = .clear + let values = try decoder.container(keyedBy: CodingKeys.self) + textAlignment = NSTextAlignment(rawValue: try values.decode(Int.self, forKey: .textAlignment))! + contentScaleFactor = try values.decode(CGFloat.self, forKey: .contentScaleFactor) + + text = try values.decode(String.self, forKey: .text) +// textColor = try values.decode(UIColor.self, forKey: .textColor) + viewSize = try values.decode(CGSize.self, forKey: .size) + viewCenter = try values.decode(CGPoint.self, forKey: .center) + + let fontInfo = try values.nestedContainer(keyedBy: FontKeys.self, forKey: .font) + let name = try fontInfo.decode(String.self, forKey: .name) + let size = try fontInfo.decode(CGFloat.self, forKey: .fontSize) + let descriptor = UIFontDescriptor(name: font!.fontName, size: font!.pointSize) +// font = UIFont(descriptor: descriptor, size: size) + font = UIFont(name: name, size: size) + } + + func encode(to encoder: Encoder) throws { + var container = try encoder.container(keyedBy: CodingKeys.self) + try container.encode(textAlignment.rawValue, forKey: .textAlignment) + try container.encode(contentScaleFactor, forKey: .contentScaleFactor) + + try container.encode(text, forKey: .text) + try container.encode(viewSize, forKey: .size) + try container.encode(viewCenter, forKey: .center) + +// try container.encode(textColor, forKey: .textColor) + + + var fontInfo = container.nestedContainer(keyedBy: FontKeys.self, forKey: .font) + print("Attributes: \(font!.fontDescriptor.fontAttributes)") + try fontInfo.encode(font?.fontName, forKey: .name) + try fontInfo.encode(font?.pointSize, forKey: .fontSize) + } + + override func encode(with coder: NSCoder) { + + coder.encode(textAlignment.rawValue, forKey: CodingKeys.textAlignment.rawValue) + coder.encode(Float(contentScaleFactor), forKey: CodingKeys.contentScaleFactor.rawValue) + + coder.encode(text, forKey: CodingKeys.text.rawValue) + coder.encode(viewSize, forKey: CodingKeys.size.rawValue) + coder.encode(viewCenter, forKey: CodingKeys.center.rawValue) + coder.encode(textColor, forKey: CodingKeys.textColor.rawValue) + coder.encode(highlightColor, forKey: CodingKeys.highlightColor.rawValue) + + coder.encode(font!.fontName, forKey: FontKeys.name.rawValue) + coder.encode(Float(font!.pointSize), forKey: FontKeys.fontSize.rawValue) +// coder.encode(font!.fontDescriptor, forKey: CodingKeys.font.rawValue) + } + override func layoutSubviews() { super.layoutSubviews() diff --git a/Classes/Preview/CameraPreviewViewController.swift b/Classes/Preview/CameraPreviewViewController.swift index 2bf1e69c5..7b2ba9267 100644 --- a/Classes/Preview/CameraPreviewViewController.swift +++ b/Classes/Preview/CameraPreviewViewController.swift @@ -251,29 +251,29 @@ extension CameraPreviewViewController: CameraPreviewViewDelegate { func confirmButtonPressed() { stopPlayback() showLoading() - if segments.count == 1, let firstSegment = segments.first, let image = firstSegment.image { - // If the camera mode is .stopMotion, .normal or .stitch (.video) and the `exportStopMotionPhotoAsVideo` is true, - // then single photos from that mode should still export as video. - if let cameraMode = cameraMode, cameraMode.group == .video && settings.exportStopMotionPhotoAsVideo, let videoURL = firstSegment.videoURL { - performUIUpdate { - self.delegate?.didFinishExportingVideo(url: videoURL) - self.hideLoading() - } - } - else { - performUIUpdate { - self.delegate?.didFinishExportingImage(image: image) - self.hideLoading() + if segments.count == 1, let firstSegment = segments.first { + switch firstSegment { + case .image(let image, let videoURL, _, _): + if let cameraMode = cameraMode, cameraMode.group == .video && settings.exportStopMotionPhotoAsVideo, let videoURL = videoURL { + performUIUpdate { + self.delegate?.didFinishExportingVideo(url: videoURL) + self.hideLoading() + } + } else { + performUIUpdate { + self.delegate?.didFinishExportingImage(image: image) + self.hideLoading() + } } - } - } - else if settings.features.gifs, - let group = cameraMode?.group, group == .gif, segments.count == 1, let segment = segments.first, let url = segment.videoURL { - // If one GIF/Loop video was captured, export it as a GIF - GIFEncoderImageIO().encode(video: url, loopCount: 0, framesPerSecond: KanvasTimes.gifPreferredFramesPerSecond) { gifURL in - performUIUpdate { - self.delegate?.didFinishExportingFrames(url: gifURL) - self.hideLoading() + case .video(let videoURL, _): + // If the camera mode is .stopMotion, .normal or .stitch (.video) and the `exportStopMotionPhotoAsVideo` is true, + // then single photos from that mode should still export as video. + if settings.features.gifs, + let group = cameraMode?.group, group == .gif, let url = firstSegment.videoURL { + performUIUpdate { + self.delegate?.didFinishExportingVideo(url: videoURL) + self.hideLoading() + } } } } diff --git a/Classes/Recording/CameraSegmentHandler.swift b/Classes/Recording/CameraSegmentHandler.swift index 63045e08c..6063e6b87 100644 --- a/Classes/Recording/CameraSegmentHandler.swift +++ b/Classes/Recording/CameraSegmentHandler.swift @@ -4,8 +4,10 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. // +import UIKit import AVFoundation import Foundation +import CoreServices /// A container for segments public enum CameraSegment { @@ -20,6 +22,15 @@ public enum CameraSegment { } } + var isVideo: Bool { + switch self { + case .video: + return true + case .image: + return false + } + } + var videoURL: URL? { switch self { case .image(_, let url, _, _): return url @@ -45,9 +56,9 @@ public enum CameraSegment { static func defaultTimeInterval(segments: [CameraSegment]) -> TimeInterval { for media in segments { switch media { - case .image(_, _, _, _): + case .image: break - case .video(_, _): + case .video: return KanvasTimes.stopMotionFrameTimeInterval } } @@ -71,10 +82,10 @@ public enum CameraSegment { } func mediaFrame(defaultTimeInterval: TimeInterval) -> MediaFrame? { - if let image = self.image { - return (image: image, interval: self.timeInterval ?? defaultTimeInterval) - } - else { + switch self { + case .image(let image, _, let timeInterval, _): + return (image: image, interval: timeInterval ?? defaultTimeInterval) + case .video: return nil } } @@ -108,12 +119,13 @@ extension AssetsHandlerType { /// - segments: the CameraSegments /// - Returns: true if all images, false otherwise func containsOnlyImages(segments: [CameraSegment]) -> Bool { - for segment in segments { - if segment.image == nil { + return segments.contains(where: { segment in + if case let .video = segment { + return true + } else { return false } - } - return true + }) == false } } @@ -312,13 +324,13 @@ final class CameraSegmentHandler: SegmentsHandlerType { var totalDuration: CMTime = CMTime.zero let allImages = containsOnlyImages(segments: segments) for segment in segments { - if let segmentURL = segment.videoURL { - let asset = AVURLAsset(url: segmentURL) + switch segment { + case .video(let url, _): + let asset = AVURLAsset(url: url) totalDuration = CMTimeAdd(totalDuration, asset.duration) - } - else if segment.image != nil { + case .image(_, _, let timeInterval, _): let duration: CMTime = { - if let timeInterval = segment.timeInterval { + if let timeInterval = timeInterval { return CMTime(seconds: timeInterval, preferredTimescale: KanvasTimes.stopMotionFrameTimescale) } else if allImages { @@ -431,23 +443,24 @@ final class CameraSegmentHandler: SegmentsHandlerType { for segment in segments { if segment.videoURL != nil { newSegments.append(segment) - continue } - guard let segmentImage = segment.image else { - assertionFailure("No video and no image?") - continue - } - dispatchGroup.enter() - self.videoQueue.async { - self.createVideoFromImage(image: segmentImage, duration: segment.timeInterval) { url in - guard let url = url else { - dispatchGroup.leave() - return - } - DispatchQueue.main.async { - newSegments.append(.image(segmentImage, url, segment.timeInterval, segment.mediaInfo)) - dispatchGroup.leave() + switch segment { + case .video: + assertionFailure("Video without a video URL") + case .image(let imageURL, _, let timeInterval, let mediaInfo): + dispatchGroup.enter() + + self.videoQueue.async { + self.createVideoFromImage(image: imageURL, duration: segment.timeInterval) { url in + guard let url = url else { + dispatchGroup.leave() + return + } + DispatchQueue.main.async { + newSegments.append(.image(imageURL, url, segment.timeInterval, segment.mediaInfo)) + dispatchGroup.leave() + } } } } @@ -459,7 +472,7 @@ final class CameraSegmentHandler: SegmentsHandlerType { private func allImagesHaveVideo(segments: [CameraSegment]) -> Bool { for segment in segments { - if segment.image != nil && segment.videoURL == nil { + if case let .image = segment, segment.videoURL == nil { return false } } @@ -540,7 +553,9 @@ final class CameraSegmentHandler: SegmentsHandlerType { assetExport.shouldOptimizeForNetworkUse = true assetExport.exportAsynchronously() { - completion(assetExport.status == .completed ? finalURL : nil, mediaInfo) + DispatchQueue.main.async { + completion(assetExport.status == .completed ? finalURL : nil, mediaInfo) + } } } diff --git a/Classes/Rendering/MediaPlayer.swift b/Classes/Rendering/MediaPlayer.swift index dfe3e31b2..cf30f8857 100644 --- a/Classes/Rendering/MediaPlayer.swift +++ b/Classes/Rendering/MediaPlayer.swift @@ -52,32 +52,35 @@ enum MediaPlayerPlaybackMode { final class MediaPlayerView: UIView, GLPixelBufferViewDelegate { weak var pixelBufferView: PixelBufferView? - - var mediaTransform: GLKMatrix4? { - didSet { - pixelBufferView?.mediaTransform = mediaTransform - } - } - - var isPortrait: Bool = true { - didSet { - pixelBufferView?.isPortrait = isPortrait - } - } weak var delegate: MediaPlayerViewDelegate? - init(metalContext: MetalContext?) { + var mediaTransform: GLKMatrix4? { + didSet { + if let metalPixelBufferView = pixelBufferView as? MetalPixelBufferView { + metalPixelBufferView.mediaTransform = mediaTransform + } + } + } + + var isPortrait: Bool = true { + didSet { + if let metalPixelBufferView = pixelBufferView as? MetalPixelBufferView { + metalPixelBufferView.isPortrait = isPortrait + } + } + } + + init(metalContext: MetalContext?=nil) { super.init(frame: .zero) let pixelBufferView: PixelBufferView & UIView + if let metalContext = metalContext { pixelBufferView = MetalPixelBufferView(context: metalContext) + } else { + pixelBufferView = GLPixelBufferView(delegate: self, mediaContentMode: .scaleAspectFill) } - else { - pixelBufferView = GLPixelBufferView(delegate: self, mediaContentMode: .scaleAspectFit) - } - pixelBufferView.add(into: self) self.pixelBufferView = pixelBufferView } @@ -199,17 +202,6 @@ final class MediaPlayer { } } - func getFrame(at index: Int) -> UIImage? { - guard index >= 0 && index < playableMedia.count else { - return nil - } - switch playableMedia[index] { - case .image(let image, _, _): - return image - case .video(_, _, _): - return nil - } - } /// Default initializer /// - Parameter renderer: Rendering instance for this player to use. From b91f6cbd28d6388c93d015cb73856150167bc536 Mon Sep 17 00:00:00 2001 From: Brandon Titus Date: Thu, 11 Feb 2021 16:46:22 -0700 Subject: [PATCH 2/6] Code cleanup and error handling --- Classes/Camera/CameraController.swift | 6 +-- Classes/Camera/MediaArchiver.swift | 9 +++- Classes/Editor/EditorViewController.swift | 40 +++++++------- .../Media/Stickers/StylableImageView.swift | 2 +- .../MultiEditorViewController.swift | 50 ++++++++++------- Classes/Editor/Text/StylableTextView.swift | 53 ++----------------- Classes/Recording/CameraSegmentHandler.swift | 6 +-- 7 files changed, 71 insertions(+), 95 deletions(-) diff --git a/Classes/Camera/CameraController.swift b/Classes/Camera/CameraController.swift index f5b322819..af1948281 100644 --- a/Classes/Camera/CameraController.swift +++ b/Classes/Camera/CameraController.swift @@ -167,12 +167,12 @@ class Archive: NSObject, NSSecureCoding { required init?(coder: NSCoder) { image = coder.decodeObject(of: UIImage.self, forKey: "image") - if let urlString = coder.decodeObject(of: NSString.self, forKey: "video") as? String { + if let urlString = coder.decodeObject(of: NSString.self, forKey: "video") as String? { video = URL(string: urlString) } else { video = nil } - if let dataString = coder.decodeObject(of: NSString.self, forKey: "data") as? String { + if let dataString = coder.decodeObject(of: NSString.self, forKey: "data") as String? { data = Data(base64Encoded: dataString) } else { data = nil @@ -1050,7 +1050,7 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP switch result { case .success(let export): return export - case .failure(let error): + case .failure(_): return nil } } diff --git a/Classes/Camera/MediaArchiver.swift b/Classes/Camera/MediaArchiver.swift index 60cbea386..77503f001 100644 --- a/Classes/Camera/MediaArchiver.swift +++ b/Classes/Camera/MediaArchiver.swift @@ -89,8 +89,13 @@ class MediaArchiver { let archiveURL: URL? if let saveDirectory = saveDirectory { - let data = try! NSKeyedArchiver.archivedData(withRootObject: archive, requiringSecureCoding: true) - archiveURL = try! data.save(to: path, in: saveDirectory, ext: "") + do { + let data = try NSKeyedArchiver.archivedData(withRootObject: archive, requiringSecureCoding: true) + archiveURL = try data.save(to: path, in: saveDirectory, ext: "") + } catch let error { + archiveURL = nil + print("Failed to archive \(error)") + } } else { archiveURL = nil } diff --git a/Classes/Editor/EditorViewController.swift b/Classes/Editor/EditorViewController.swift index ae09b354c..140d054ce 100644 --- a/Classes/Editor/EditorViewController.swift +++ b/Classes/Editor/EditorViewController.swift @@ -696,6 +696,13 @@ public final class EditorViewController: UIViewController, MediaPlayerController private func startExporting(action: KanvasExportAction) { player.stop() showLoading() + let archive: Data + do { + archive = try self.archive() + } catch { + handleExportError() + return + } if segments.count == 1, let firstSegment = segments.first, case CameraSegment.image(let image, _, _, _) = firstSegment { // If the camera mode is .stopMotion, .normal or .stitch (.video) and the `exportStopMotionPhotoAsVideo` is true, // then single photos from that mode should still export as video. @@ -703,20 +710,20 @@ public final class EditorViewController: UIViewController, MediaPlayerController assetsHandler.ensureAllImagesHaveVideo(segments: segments) { segments in guard let videoURL = segments.first?.videoURL else { return } DispatchQueue.main.async { - self.createFinalVideo(videoURL: videoURL, mediaInfo: firstSegment.mediaInfo, exportAction: action) + self.createFinalVideo(videoURL: videoURL, mediaInfo: firstSegment.mediaInfo, archive: archive, exportAction: action) } } } else { - createFinalImage(image: image, mediaInfo: firstSegment.mediaInfo, exportAction: action) + createFinalImage(image: image, mediaInfo: firstSegment.mediaInfo, archive: archive, exportAction: action) } } else if shouldExportMediaAsGIF { if segments.count == 1, let segment = segments.first, let url = segment.videoURL { - self.createFinalGIF(videoURL: url, framesPerSecond: KanvasTimes.gifPreferredFramesPerSecond, mediaInfo: segment.mediaInfo, exportAction: action) + self.createFinalGIF(videoURL: url, framesPerSecond: KanvasTimes.gifPreferredFramesPerSecond, mediaInfo: segment.mediaInfo, archive: archive, exportAction: action) } else if assetsHandler.containsOnlyImages(segments: segments) { - self.createFinalGIF(segments: segments, mediaInfo: segments.first?.mediaInfo ?? MediaInfo(source: .kanvas_camera), exportAction: action) + self.createFinalGIF(segments: segments, mediaInfo: segments.first?.mediaInfo ?? MediaInfo(source: .kanvas_camera), archive: archive, exportAction: action) } else { // Segments are not all frames, so we need to generate a full video first, and then convert that to a GIF. @@ -732,7 +739,7 @@ public final class EditorViewController: UIViewController, MediaPlayerController } let fps = Int(CMTime(seconds: 1.0, preferredTimescale: KanvasTimes.stopMotionFrameTimescale).seconds / KanvasTimes.onlyImagesFrameTime.seconds) DispatchQueue.main.async { - self.createFinalGIF(videoURL: url, framesPerSecond: fps, mediaInfo: mediaInfo, exportAction: action) + self.createFinalGIF(videoURL: url, framesPerSecond: fps, mediaInfo: mediaInfo, archive: archive, exportAction: action) } } } @@ -745,7 +752,7 @@ public final class EditorViewController: UIViewController, MediaPlayerController return } DispatchQueue.main.async { - self?.createFinalVideo(videoURL: url, mediaInfo: mediaInfo ?? MediaInfo(source: .media_library), exportAction: action) + self?.createFinalVideo(videoURL: url, mediaInfo: mediaInfo ?? MediaInfo(source: .media_library), archive: archive, exportAction: action) } } } @@ -756,23 +763,24 @@ public final class EditorViewController: UIViewController, MediaPlayerController startExporting(action: .post) } - var archive: Data { - return try! NSKeyedArchiver.archivedData(withRootObject: editorView.movableViewCanvas, requiringSecureCoding: true) + private func archive() throws -> Data { + return try NSKeyedArchiver.archivedData(withRootObject: editorView.movableViewCanvas, requiringSecureCoding: true) } - private func createFinalGIF(segments: [CameraSegment], mediaInfo: MediaInfo, exportAction: KanvasExportAction) { + private func createFinalGIF(segments: [CameraSegment], mediaInfo: MediaInfo, archive: Data, exportAction: KanvasExportAction) { let exporter = exporterClass.init(settings: settings) exporter.filterType = filterType ?? .passthrough exporter.imageOverlays = imageOverlays() let segments = gifMakerHandler.trimmedSegments(segments) let frames = segments.compactMap { $0.mediaFrame(defaultTimeInterval: getDefaultTimeIntervalForImageSegments()) } - let archive = self.archive exporter.export(frames: frames) { orderedFrames in let playbackFrames = self.gifMakerHandler.framesForPlayback(orderedFrames) self.gifEncoderClass.init().encode(frames: playbackFrames, loopCount: 0) { gifURL in - var size: CGSize? = nil + let size: CGSize? if let gifURL = gifURL { size = GIFDecoderFactory.main().size(of: gifURL) + } else { + size = nil } let result = ExportResult(original: nil, result: .video(gifURL!), info: mediaInfo, archive: archive) self.exportCompletion?(.success(result)) @@ -784,11 +792,10 @@ public final class EditorViewController: UIViewController, MediaPlayerController } } - private func createFinalGIF(videoURL: URL, framesPerSecond: Int, mediaInfo: MediaInfo, exportAction: KanvasExportAction) { + private func createFinalGIF(videoURL: URL, framesPerSecond: Int, mediaInfo: MediaInfo, archive: Data, exportAction: KanvasExportAction) { let exporter = exporterClass.init(settings: settings) exporter.filterType = filterType ?? .passthrough exporter.imageOverlays = imageOverlays() - let archive = self.archive exporter.export(video: videoURL, mediaInfo: mediaInfo) { (exportedVideoURL, _) in guard let exportedVideoURL = exportedVideoURL else { performUIUpdate { @@ -817,10 +824,9 @@ public final class EditorViewController: UIViewController, MediaPlayerController } } - private func createFinalVideo(videoURL: URL, mediaInfo: MediaInfo, exportAction: KanvasExportAction) { + private func createFinalVideo(videoURL: URL, mediaInfo: MediaInfo, archive: Data, exportAction: KanvasExportAction) { let exporter = exporterClass.init(settings: settings) exporter.imageOverlays = imageOverlays() - let archive = self.archive exporter.export(video: videoURL, mediaInfo: mediaInfo) { (exportedVideoURL, error) in performUIUpdate { guard let url = exportedVideoURL else { @@ -840,12 +846,10 @@ public final class EditorViewController: UIViewController, MediaPlayerController } } - private func createFinalImage(image: UIImage, mediaInfo: MediaInfo, exportAction: KanvasExportAction) { + private func createFinalImage(image: UIImage, mediaInfo: MediaInfo, archive: Data, exportAction: KanvasExportAction) { let exporter = exporterClass.init(settings: settings) exporter.filterType = filterType ?? .passthrough exporter.imageOverlays = imageOverlays() -// exporter.dimensions = UIScreen.main.bounds.size - let archive = self.archive exporter.export(image: image, time: player.lastStillFilterTime) { [weak self] (exportedImage, error) in guard let self = self else { return } let originalImage = image diff --git a/Classes/Editor/Media/Stickers/StylableImageView.swift b/Classes/Editor/Media/Stickers/StylableImageView.swift index e29291583..c7f1af3c2 100644 --- a/Classes/Editor/Media/Stickers/StylableImageView.swift +++ b/Classes/Editor/Media/Stickers/StylableImageView.swift @@ -55,7 +55,7 @@ import UIKit } func encode(to encoder: Encoder) throws { - var container = try encoder.container(keyedBy: CodingKeys.self) + var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(viewSize, forKey: .size) try container.encode(viewCenter, forKey: .center) diff --git a/Classes/Editor/MultiEditor/MultiEditorViewController.swift b/Classes/Editor/MultiEditor/MultiEditorViewController.swift index 42a42ea2f..66dd8fc53 100644 --- a/Classes/Editor/MultiEditor/MultiEditorViewController.swift +++ b/Classes/Editor/MultiEditor/MultiEditorViewController.swift @@ -48,7 +48,11 @@ class MultiEditorViewController: UIViewController { return } if let old = selected { - try! archive(index: old)//migratedIndex ?? old) + do { + try archive(index: old) + } catch let error { + print("Failed to archive current edits \(error)") + } } if let new = newValue { // If the new index is the same as the old just keep the current editor loadEditor(for: new) @@ -223,8 +227,6 @@ extension MultiEditorViewController: MediaClipsEditorDelegate { } func mediaClipWasDeleted(at index: Int) { - var clips = self.clipsController.getClips() - if frames.indices.contains(index) { frames.remove(at: index) } @@ -271,7 +273,11 @@ extension MultiEditorViewController: MediaClipsEditorDelegate { func mediaClipWasMoved(from originIndex: Int, to destinationIndex: Int) { if let selected = selected { - try! archive(index: selected) + do { + try archive(index: selected) + } catch let error { + print("Failed to archive current edits: \(error)") + } } frames.move(from: originIndex, to: destinationIndex) @@ -310,14 +316,6 @@ extension MultiEditorViewController: EditorControllerDelegate { } func didFinishExportingImage(image: UIImage?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { -// if let image = image { -// var clips = clipsController.getClips() -// if let selected = selected { -// let selectedClip = editors.distance(from: editors.startIndex, to: selected) -// clips[selectedClip] = MediaClip(representativeFrame: image, overlayText: nil, lastFrame: image) -// } -// clipsController.replace(clips: clips) -// } } func didFinishExportingFrames(url: URL?, size: CGSize?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { @@ -357,13 +355,12 @@ extension MultiEditorViewController: EditorControllerDelegate { } let currentCanvas = try NSKeyedArchiver.archivedData(withRootObject: currentEditor.editorView.movableViewCanvas, requiringSecureCoding: true) let drawingLayer = currentEditor.editorView.drawingCanvas - let options = EditOptions(soundEnabled: currentEditor.shouldExportSound ?? true) + let options = EditOptions(soundEnabled: currentEditor.shouldExportSound) if frames.indices ~= index { let frame = frames[index] frames[index] = Frame(segment: frame.segment, edit: Edit(data: currentCanvas, canvasView: drawingLayer, options: options)) } else { print("Invalid frame index") -// edits.insert((currentCanvas, drawingLayer, options), at: min(index, edits.endIndex)) } } @@ -371,7 +368,13 @@ extension MultiEditorViewController: EditorControllerDelegate { if frames.indices ~= index, let edit = frames[index].edit { let canvas: MovableViewCanvas? if let edit = edit.data { - canvas = try! NSKeyedUnarchiver.unarchivedObject(ofClass: MovableViewCanvas.self, from: edit) + do { + canvas = try NSKeyedUnarchiver.unarchivedObject(ofClass: MovableViewCanvas.self, from: edit) + } catch let error { + print("Failed to unarchive edits for \(index): \(error)") + assertionFailure("Failed to unarchive edits for \(index): \(error)") + canvas = nil + } } else { canvas = nil } @@ -386,13 +389,10 @@ extension MultiEditorViewController: EditorControllerDelegate { func unarchive(editor: EditorViewController, index: Int) { if frames.indices ~= index { let canvas: MovableViewCanvas? - let drawing: IgnoreTouchesView? if let edits = edits(for: index) { canvas = edits.0 - drawing = edits.1 } else { canvas = nil - drawing = nil } canvas?.delegate = editor.editorView @@ -417,7 +417,11 @@ extension MultiEditorViewController: EditorControllerDelegate { showLoading() if let selected = selected { - try! archive(index: selected) + do { + try archive(index: selected) + } catch let error { + print("Failed to archive current edits on export \(error)") + } } exportHandler.startWaiting(for: frames.count) @@ -428,7 +432,13 @@ extension MultiEditorViewController: EditorControllerDelegate { autoreleasepool { let canvas: MovableViewCanvas? if let edit = frame.edit?.data { - canvas = try! NSKeyedUnarchiver.unarchivedObject(ofClass: MovableViewCanvas.self, from: edit) + do { + canvas = try NSKeyedUnarchiver.unarchivedObject(ofClass: MovableViewCanvas.self, from: edit) + } catch let error { + print("Failed to unarchive edits on export for \(idx): \(error)") + assertionFailure("Failed to unarchive edits on export for \(idx): \(error)") + canvas = nil + } } else { canvas = nil } diff --git a/Classes/Editor/Text/StylableTextView.swift b/Classes/Editor/Text/StylableTextView.swift index ef7b84b70..008ac9dfb 100644 --- a/Classes/Editor/Text/StylableTextView.swift +++ b/Classes/Editor/Text/StylableTextView.swift @@ -75,13 +75,12 @@ private struct Constants { highlightViews = [] let size = coder.decodeCGSize(forKey: CodingKeys.size.rawValue) - let center = coder.decodeCGPoint(forKey: CodingKeys.center.rawValue) super.init(frame: CGRect(origin: .zero, size: size), textContainer: nil) delegate = self backgroundColor = .clear - textAlignment = NSTextAlignment(rawValue: coder.decodeInteger(forKey: CodingKeys.textAlignment.rawValue))! + textAlignment = NSTextAlignment(rawValue: coder.decodeInteger(forKey: CodingKeys.textAlignment.rawValue)) ?? .left contentScaleFactor = CGFloat(coder.decodeFloat(forKey: CodingKeys.contentScaleFactor.rawValue)) text = String(coder.decodeObject(of: NSString.self, forKey: CodingKeys.text.rawValue) ?? "") @@ -90,9 +89,6 @@ private struct Constants { textColor = coder.decodeObject(of: UIColor.self, forKey: CodingKeys.textColor.rawValue) highlightColor = coder.decodeObject(of: UIColor.self, forKey: CodingKeys.highlightColor.rawValue) -// if let descriptor = coder.decodeObject(of: UIFontDescriptor.self, forKey: CodingKeys.font.rawValue) { -// font = UIFont(descriptor: descriptor, size: 12) -// } let fontName = String(coder.decodeObject(of: NSString.self, forKey: FontKeys.name.rawValue) ?? "") let fontSize = CGFloat(coder.decodeFloat(forKey: FontKeys.fontSize.rawValue)) font = UIFont(name: fontName, size: fontSize) @@ -114,46 +110,6 @@ private struct Constants { case fontSize } - required init(from decoder: Decoder) throws { - highlightViews = [] - super.init(frame: .zero, textContainer: nil) - delegate = self - backgroundColor = .clear - let values = try decoder.container(keyedBy: CodingKeys.self) - textAlignment = NSTextAlignment(rawValue: try values.decode(Int.self, forKey: .textAlignment))! - contentScaleFactor = try values.decode(CGFloat.self, forKey: .contentScaleFactor) - - text = try values.decode(String.self, forKey: .text) -// textColor = try values.decode(UIColor.self, forKey: .textColor) - viewSize = try values.decode(CGSize.self, forKey: .size) - viewCenter = try values.decode(CGPoint.self, forKey: .center) - - let fontInfo = try values.nestedContainer(keyedBy: FontKeys.self, forKey: .font) - let name = try fontInfo.decode(String.self, forKey: .name) - let size = try fontInfo.decode(CGFloat.self, forKey: .fontSize) - let descriptor = UIFontDescriptor(name: font!.fontName, size: font!.pointSize) -// font = UIFont(descriptor: descriptor, size: size) - font = UIFont(name: name, size: size) - } - - func encode(to encoder: Encoder) throws { - var container = try encoder.container(keyedBy: CodingKeys.self) - try container.encode(textAlignment.rawValue, forKey: .textAlignment) - try container.encode(contentScaleFactor, forKey: .contentScaleFactor) - - try container.encode(text, forKey: .text) - try container.encode(viewSize, forKey: .size) - try container.encode(viewCenter, forKey: .center) - -// try container.encode(textColor, forKey: .textColor) - - - var fontInfo = container.nestedContainer(keyedBy: FontKeys.self, forKey: .font) - print("Attributes: \(font!.fontDescriptor.fontAttributes)") - try fontInfo.encode(font?.fontName, forKey: .name) - try fontInfo.encode(font?.pointSize, forKey: .fontSize) - } - override func encode(with coder: NSCoder) { coder.encode(textAlignment.rawValue, forKey: CodingKeys.textAlignment.rawValue) @@ -165,9 +121,10 @@ private struct Constants { coder.encode(textColor, forKey: CodingKeys.textColor.rawValue) coder.encode(highlightColor, forKey: CodingKeys.highlightColor.rawValue) - coder.encode(font!.fontName, forKey: FontKeys.name.rawValue) - coder.encode(Float(font!.pointSize), forKey: FontKeys.fontSize.rawValue) -// coder.encode(font!.fontDescriptor, forKey: CodingKeys.font.rawValue) + if let font = font { + coder.encode(font.fontName, forKey: FontKeys.name.rawValue) + coder.encode(Float(font.pointSize), forKey: FontKeys.fontSize.rawValue) + } } diff --git a/Classes/Recording/CameraSegmentHandler.swift b/Classes/Recording/CameraSegmentHandler.swift index 6063e6b87..8ba20f525 100644 --- a/Classes/Recording/CameraSegmentHandler.swift +++ b/Classes/Recording/CameraSegmentHandler.swift @@ -120,7 +120,7 @@ extension AssetsHandlerType { /// - Returns: true if all images, false otherwise func containsOnlyImages(segments: [CameraSegment]) -> Bool { return segments.contains(where: { segment in - if case let .video = segment { + if case .video = segment { return true } else { return false @@ -448,7 +448,7 @@ final class CameraSegmentHandler: SegmentsHandlerType { switch segment { case .video: assertionFailure("Video without a video URL") - case .image(let imageURL, _, let timeInterval, let mediaInfo): + case .image(let imageURL, _, _, _): dispatchGroup.enter() self.videoQueue.async { @@ -472,7 +472,7 @@ final class CameraSegmentHandler: SegmentsHandlerType { private func allImagesHaveVideo(segments: [CameraSegment]) -> Bool { for segment in segments { - if case let .image = segment, segment.videoURL == nil { + if case .image = segment, segment.videoURL == nil { return false } } From 782180e54395188b5f9a6278127feae8a3b692a7 Mon Sep 17 00:00:00 2001 From: Brandon Titus Date: Thu, 11 Feb 2021 16:51:21 -0700 Subject: [PATCH 3/6] Remove archived Drawing views + Codable conformances --- Classes/Camera/CameraController.swift | 5 ++-- Classes/Editor/EditorView.swift | 6 ++-- Classes/Editor/EditorViewController.swift | 6 +--- .../MovableViews/MovableViewCanvas.swift | 30 +------------------ .../MovableViewInnerElement.swift | 2 +- .../MultiEditorViewController.swift | 17 +++++------ Classes/Editor/Text/MainTextView.swift | 4 --- Classes/Editor/Text/StylableTextView.swift | 2 +- 8 files changed, 16 insertions(+), 56 deletions(-) diff --git a/Classes/Camera/CameraController.swift b/Classes/Camera/CameraController.swift index af1948281..d70fec715 100644 --- a/Classes/Camera/CameraController.swift +++ b/Classes/Camera/CameraController.swift @@ -466,7 +466,6 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP analyticsProvider: analyticsProvider, quickBlogSelectorCoordinator: quickBlogSelectorCoordinator, canvas: canvas, - drawingView: drawing, tagCollection: tagCollection) controller.delegate = self return controller @@ -921,10 +920,10 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP dismiss(animated: false, completion: nil) } - func editor(segment: CameraSegment, canvas: MovableViewCanvas?, drawingView: IgnoreTouchesView?) -> EditorViewController { + func editor(segment: CameraSegment, canvas: MovableViewCanvas?) -> EditorViewController { let segments = [segment] - return createEditorViewController(segments, selected: segments.startIndex, canvas: canvas, drawing: drawingView) + return createEditorViewController(segments, selected: segments.startIndex, canvas: canvas) } // MARK: - CameraPreviewControllerDelegate & EditorControllerDelegate & StoryComposerDelegate diff --git a/Classes/Editor/EditorView.swift b/Classes/Editor/EditorView.swift index a36857be0..38deb35c7 100644 --- a/Classes/Editor/EditorView.swift +++ b/Classes/Editor/EditorView.swift @@ -217,8 +217,7 @@ final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelega quickBlogSelectorCoordinator: KanvasQuickBlogSelectorCoordinating?, tagCollection: UIView?, metalContext: MetalContext?, - movableViewCanvas: MovableViewCanvas?, - drawingCanvas: IgnoreTouchesView?) { + movableViewCanvas: MovableViewCanvas?) { self.delegate = delegate self.mainActionMode = mainActionMode self.showSaveButton = showSaveButton @@ -232,7 +231,8 @@ final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelega self.tagCollection = tagCollection self.metalContext = metalContext self.movableViewCanvas = movableViewCanvas ?? MovableViewCanvas() - self.drawingCanvas = drawingCanvas ?? IgnoreTouchesView() + + self.drawingCanvas = IgnoreTouchesView() super.init(frame: .zero) self.movableViewCanvas.delegate = self diff --git a/Classes/Editor/EditorViewController.swift b/Classes/Editor/EditorViewController.swift index 140d054ce..71e077331 100644 --- a/Classes/Editor/EditorViewController.swift +++ b/Classes/Editor/EditorViewController.swift @@ -192,7 +192,6 @@ public final class EditorViewController: UIViewController, MediaPlayerController settings: CameraSettings, canvas: MovableViewCanvas?, quickBlogSelectorCoordinator: KanvasQuickBlogSelectorCoordinating?, - drawingView: IgnoreTouchesView?, tagCollection: UIView?, metalContext: MetalContext?) -> EditorView { var mainActionMode: EditorView.MainActionMode = .confirm @@ -215,8 +214,7 @@ public final class EditorViewController: UIViewController, MediaPlayerController quickBlogSelectorCoordinator: quickBlogSelectorCoordinator, tagCollection: tagCollection, metalContext: metalContext, - movableViewCanvas: canvas, - drawingCanvas: drawingView) + movableViewCanvas: canvas) return editorView } @@ -310,7 +308,6 @@ public final class EditorViewController: UIViewController, MediaPlayerController analyticsProvider: KanvasAnalyticsProvider?, quickBlogSelectorCoordinator: KanvasQuickBlogSelectorCoordinating?, canvas: MovableViewCanvas? = nil, - drawingView: IgnoreTouchesView? = nil, tagCollection: UIView?) { self.settings = settings self.originalSegments = segments @@ -329,7 +326,6 @@ public final class EditorViewController: UIViewController, MediaPlayerController settings: settings, canvas: canvas, quickBlogSelectorCoordinator: quickBlogSelectorCoordinator, - drawingView: drawingView, tagCollection: tagCollection, metalContext: metalContext) super.init(nibName: .none, bundle: .none) diff --git a/Classes/Editor/MovableViews/MovableViewCanvas.swift b/Classes/Editor/MovableViews/MovableViewCanvas.swift index 2479fffc5..1194041f8 100644 --- a/Classes/Editor/MovableViews/MovableViewCanvas.swift +++ b/Classes/Editor/MovableViews/MovableViewCanvas.swift @@ -47,7 +47,7 @@ private struct Constants { } /// View that contains the collection of movable views -final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, MovableViewDelegate, Codable, NSSecureCoding { +final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, MovableViewDelegate, NSSecureCoding { static var supportsSecureCoding: Bool { return true } @@ -94,22 +94,6 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M case movableViews } - init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - overlay = UIView() - trashView = TrashView() - originTransformations = try values.decode(ViewTransformations.self, forKey: .originTransformations) - - super.init(frame: .zero) - - let textViews = try values.decode([StylableTextView].self, forKey: .textViews) - let imageViews = try values.decode([StylableImageView].self, forKey: .imageViews) - let innerViews: [MovableViewInnerElement] = textViews + imageViews - innerViews.forEach({ view in - addView(view: view, transformations: originTransformations, location: view.viewCenter, size: view.viewSize, animated: false) - }) - } - required init?(coder: NSCoder) { overlay = UIView() @@ -126,18 +110,6 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M setUpViews() } - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(originTransformations, forKey: .originTransformations) - - let textViews = innerViews.compactMap { $0 as? StylableTextView } as [StylableTextView] - let imageViews = innerViews.compactMap { $0 as? StylableImageView } as [StylableImageView] - - try container.encode(textViews, forKey: .textViews) - try container.encode(imageViews, forKey: .imageViews) - } - override func encode(with coder: NSCoder) { coder.encode(originTransformations, forKey: CodingKeys.originTransformations.rawValue) diff --git a/Classes/Editor/MovableViews/MovableViewInnerElement.swift b/Classes/Editor/MovableViews/MovableViewInnerElement.swift index 7fe6c3632..9096105f0 100644 --- a/Classes/Editor/MovableViews/MovableViewInnerElement.swift +++ b/Classes/Editor/MovableViews/MovableViewInnerElement.swift @@ -7,7 +7,7 @@ import Foundation /// Protocol for the view inside MovableView -protocol MovableViewInnerElement: UIView, Codable, NSSecureCoding { +protocol MovableViewInnerElement: UIView, NSSecureCoding { /// Checks whether the hit is done inside the shape of the view /// diff --git a/Classes/Editor/MultiEditor/MultiEditorViewController.swift b/Classes/Editor/MultiEditor/MultiEditorViewController.swift index 66dd8fc53..498cb4973 100644 --- a/Classes/Editor/MultiEditor/MultiEditorViewController.swift +++ b/Classes/Editor/MultiEditor/MultiEditorViewController.swift @@ -9,7 +9,7 @@ import Foundation protocol MultiEditorComposerDelegate: EditorControllerDelegate { func didFinishExporting(media: [Result]) func addButtonWasPressed() - func editor(segment: CameraSegment, canvas: MovableViewCanvas?, drawingView: IgnoreTouchesView?) -> EditorViewController + func editor(segment: CameraSegment, canvas: MovableViewCanvas?) -> EditorViewController func dismissButtonPressed() } @@ -83,7 +83,6 @@ class MultiEditorViewController: UIViewController { struct Edit { let data: Data? - let canvasView: IgnoreTouchesView? let options: EditOptions } @@ -102,7 +101,7 @@ class MultiEditorViewController: UIViewController { if let edits = edits { frames = zip(segments, edits).map { (segment, data) in - return Frame(segment: segment, edit: Edit(data: data, canvasView: nil, options: EditOptions(soundEnabled: true))) + return Frame(segment: segment, edit: Edit(data: data, options: EditOptions(soundEnabled: true))) } } else { frames = segments.map({ segment in @@ -147,7 +146,7 @@ class MultiEditorViewController: UIViewController { func loadEditor(for index: Int) { let views = edits(for: index) - if let editor = delegate?.editor(segment: frames[index].segment, canvas: views?.0, drawingView: views?.1) { + if let editor = delegate?.editor(segment: frames[index].segment, canvas: views?.0) { currentEditor?.stopPlayback() currentEditor?.unloadFromParentViewController() let additionalPadding: CGFloat = 10 // Extra padding for devices that don't have safe areas (which provide some padding by default). @@ -354,17 +353,16 @@ extension MultiEditorViewController: EditorControllerDelegate { return } let currentCanvas = try NSKeyedArchiver.archivedData(withRootObject: currentEditor.editorView.movableViewCanvas, requiringSecureCoding: true) - let drawingLayer = currentEditor.editorView.drawingCanvas let options = EditOptions(soundEnabled: currentEditor.shouldExportSound) if frames.indices ~= index { let frame = frames[index] - frames[index] = Frame(segment: frame.segment, edit: Edit(data: currentCanvas, canvasView: drawingLayer, options: options)) + frames[index] = Frame(segment: frame.segment, edit: Edit(data: currentCanvas, options: options)) } else { print("Invalid frame index") } } - func edits(for index: Int) -> (MovableViewCanvas?, IgnoreTouchesView?, EditOptions)? { + func edits(for index: Int) -> (MovableViewCanvas?, EditOptions)? { if frames.indices ~= index, let edit = frames[index].edit { let canvas: MovableViewCanvas? if let edit = edit.data { @@ -378,9 +376,8 @@ extension MultiEditorViewController: EditorControllerDelegate { } else { canvas = nil } - let drawing = edit.canvasView let options = edit.options - return (canvas, drawing, options) + return (canvas, options) } else { return nil } @@ -442,7 +439,7 @@ extension MultiEditorViewController: EditorControllerDelegate { } else { canvas = nil } - let editor = delegate.editor(segment: frame.segment, canvas: canvas, drawingView: frame.edit?.canvasView) + let editor = delegate.editor(segment: frame.segment, canvas: canvas) editor.shouldExportSound = frame.edit?.options.soundEnabled ?? true unarchive(editor: editor, index: idx) diff --git a/Classes/Editor/Text/MainTextView.swift b/Classes/Editor/Text/MainTextView.swift index 8eb04d303..0d6564014 100644 --- a/Classes/Editor/Text/MainTextView.swift +++ b/Classes/Editor/Text/MainTextView.swift @@ -46,10 +46,6 @@ final class MainTextView: StylableTextView { fatalError("init(from:) has not been implemented") } - required init(from decoder: Decoder) throws { - try super.init(from: decoder) - } - override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { return false } diff --git a/Classes/Editor/Text/StylableTextView.swift b/Classes/Editor/Text/StylableTextView.swift index 008ac9dfb..cd8e57cce 100644 --- a/Classes/Editor/Text/StylableTextView.swift +++ b/Classes/Editor/Text/StylableTextView.swift @@ -13,7 +13,7 @@ private struct Constants { } /// TextView that can be customized with TextOptions -@objc class StylableTextView: UITextView, UITextViewDelegate, MovableViewInnerElement, Codable, NSSecureCoding { +@objc class StylableTextView: UITextView, UITextViewDelegate, MovableViewInnerElement, NSSecureCoding { static var supportsSecureCoding: Bool { return true } From 228dac6588ddf57fd5143d44eeaf90a310d32c31 Mon Sep 17 00:00:00 2001 From: Brandon Titus Date: Thu, 11 Feb 2021 17:03:22 -0700 Subject: [PATCH 4/6] Fix unit tests --- .../KanvasExampleTests/Camera/CameraControllerTests.swift | 4 ++++ .../KanvasExampleTests/Editor/EditorControllerTests.swift | 6 +++--- .../KanvasExampleTests/Editor/EditorViewTests.swift | 3 ++- .../Editor/MovableViews/MovableViewCanvasTests.swift | 2 +- .../KanvasExampleTests/Utility/MediaMetadataTests.swift | 5 ++++- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/KanvasExample/KanvasExampleTests/Camera/CameraControllerTests.swift b/KanvasExample/KanvasExampleTests/Camera/CameraControllerTests.swift index 80fa97953..660860acb 100644 --- a/KanvasExample/KanvasExampleTests/Camera/CameraControllerTests.swift +++ b/KanvasExample/KanvasExampleTests/Camera/CameraControllerTests.swift @@ -329,6 +329,10 @@ final class CameraControllerDelegateStub: CameraControllerDelegate { func cameraShouldShowWelcomeTooltip() -> Bool { return false } + + func editorDismissed(_ cameraController: CameraController) { + + } func didDismissColorSelectorTooltip() { diff --git a/KanvasExample/KanvasExampleTests/Editor/EditorControllerTests.swift b/KanvasExample/KanvasExampleTests/Editor/EditorControllerTests.swift index f950a5a63..410a6603b 100644 --- a/KanvasExample/KanvasExampleTests/Editor/EditorControllerTests.swift +++ b/KanvasExample/KanvasExampleTests/Editor/EditorControllerTests.swift @@ -339,19 +339,19 @@ final class EditorControllerDelegateStub: EditorControllerDelegate { var imageExportCompletion: (() -> Void)? var framesExportCompletion: (() -> Void)? - func didFinishExportingVideo(url: URL?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) { + func didFinishExportingVideo(url: URL?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { XCTAssertNotNil(url) videoExportCalled = true videoExportCompletion?() } - func didFinishExportingImage(image: UIImage?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) { + func didFinishExportingImage(image: UIImage?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { XCTAssertNotNil(image) imageExportCalled = true imageExportCompletion?() } - func didFinishExportingFrames(url: URL?, size: CGSize?, info: MediaInfo?, action: KanvasExportAction, mediaChanged: Bool) { + func didFinishExportingFrames(url: URL?, size: CGSize?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { XCTAssertNotNil(url) framesExportCalled = true framesExportCompletion?() diff --git a/KanvasExample/KanvasExampleTests/Editor/EditorViewTests.swift b/KanvasExample/KanvasExampleTests/Editor/EditorViewTests.swift index d2fce4d00..d6ef451c6 100644 --- a/KanvasExample/KanvasExampleTests/Editor/EditorViewTests.swift +++ b/KanvasExample/KanvasExampleTests/Editor/EditorViewTests.swift @@ -32,7 +32,8 @@ final class EditorViewTests: FBSnapshotTestCase { showBlogSwitcher: false, quickBlogSelectorCoordinator: nil, tagCollection: nil, - metalContext: nil) + metalContext: nil, + movableViewCanvas: nil) view.frame = CGRect(x: 0, y: 0, width: 320, height: 480) return view } diff --git a/KanvasExample/KanvasExampleTests/Editor/MovableViews/MovableViewCanvasTests.swift b/KanvasExample/KanvasExampleTests/Editor/MovableViews/MovableViewCanvasTests.swift index 988b0190e..994ce283c 100644 --- a/KanvasExample/KanvasExampleTests/Editor/MovableViews/MovableViewCanvasTests.swift +++ b/KanvasExample/KanvasExampleTests/Editor/MovableViews/MovableViewCanvasTests.swift @@ -32,7 +32,7 @@ final class MovableViewCanvasTests: FBSnapshotTestCase { textView.options = TextOptions(text: "Example", font: .fairwater(fontSize: 48)) let location = view.center let transformations = ViewTransformations() - view.addView(view: textView, transformations: transformations, location: location, size: view.frame.size) + view.addView(view: textView, transformations: transformations, location: location, size: view.frame.size, animated: true) FBSnapshotVerifyView(view) } diff --git a/KanvasExample/KanvasExampleTests/Utility/MediaMetadataTests.swift b/KanvasExample/KanvasExampleTests/Utility/MediaMetadataTests.swift index cfaae9385..17345917c 100644 --- a/KanvasExample/KanvasExampleTests/Utility/MediaMetadataTests.swift +++ b/KanvasExample/KanvasExampleTests/Utility/MediaMetadataTests.swift @@ -35,10 +35,13 @@ class MediaMetadataTests: XCTestCase { let segmentsHandler = CameraSegmentHandler() let recorder = CameraRecorderStub(size: CGSize(width: 300, height: 300), photoOutput: nil, videoOutput: nil, audioOutput: nil, recordingDelegate: nil, segmentsHandler: segmentsHandler, settings: settings) recorder.takePhoto(on: .photo) { image in - guard let url = CameraController.save(image: image, info: .init(source: .kanvas_camera)) else { + let info = MediaInfo(source: .kanvas_camera) + guard let data = image?.jpegData(compressionQuality: 1), + let url = try? CameraController.save(data: data, to: "kanvas-image", ext: "jpg") else { XCTFail() return } + info.write(toImage: url) let mediaInfo = MediaInfo(fromImage: url) XCTAssertEqual(mediaInfo?.source, .kanvas_camera) expectation.fulfill() From 3e3d4fae2a46dfae6446d74267f0db86418e5c96 Mon Sep 17 00:00:00 2001 From: Brandon Titus Date: Thu, 11 Feb 2021 21:50:20 -0700 Subject: [PATCH 5/6] Code cleanup - Remove drawing canvas changes - Move Archive - Add CodingKeys - Move Frames --- Classes/Archival/Archive.swift | 52 +++++++ Classes/Camera/CameraController.swift | 141 ++++++++---------- Classes/Editor/EditorView.swift | 9 +- Classes/Editor/EditorViewController.swift | 14 +- .../Media/Stickers/StylableImageView.swift | 19 +-- Classes/Editor/MovableViews/MovableView.swift | 28 ++-- .../MovableViews/MovableViewCanvas.swift | 9 +- .../MovableViews/ViewTransformations.swift | 20 ++- .../MultiEditorViewController.swift | 137 ++++++----------- Classes/Editor/Text/StylableTextView.swift | 4 +- 10 files changed, 210 insertions(+), 223 deletions(-) create mode 100644 Classes/Archival/Archive.swift diff --git a/Classes/Archival/Archive.swift b/Classes/Archival/Archive.swift new file mode 100644 index 000000000..2c145d127 --- /dev/null +++ b/Classes/Archival/Archive.swift @@ -0,0 +1,52 @@ +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. +// + +/// The class containing an image or video and associated data (an encoded representation of the edits). +class Archive: NSObject, NSSecureCoding { + static var supportsSecureCoding: Bool = true + + let image: UIImage? + let video: URL? + let data: Data? + + init(image: UIImage, data: Data?) { + self.image = image + self.data = data + self.video = nil + } + + init(video: URL, data: Data?) { + self.video = video + self.data = data + self.image = nil + } + + private enum CodingKeys: String { + case image + case video + case data + } + + func encode(with coder: NSCoder) { + coder.encode(image, forKey: CodingKeys.image.rawValue) + coder.encode(video?.absoluteString, forKey: CodingKeys.video.rawValue) + coder.encode(data?.base64EncodedString(), forKey: CodingKeys.data.rawValue) + } + + required init?(coder: NSCoder) { + image = coder.decodeObject(of: UIImage.self, forKey: CodingKeys.image.rawValue) + if let urlString = coder.decodeObject(of: NSString.self, forKey: CodingKeys.video.rawValue) as String? { + video = URL(string: urlString) + } else { + video = nil + } + if let dataString = coder.decodeObject(of: NSString.self, forKey: CodingKeys.data.rawValue) as String? { + data = Data(base64Encoded: dataString) + } else { + data = nil + } + } +} diff --git a/Classes/Camera/CameraController.swift b/Classes/Camera/CameraController.swift index d70fec715..3ebec155e 100644 --- a/Classes/Camera/CameraController.swift +++ b/Classes/Camera/CameraController.swift @@ -7,9 +7,6 @@ import AVFoundation import Foundation import UIKit -import MobileCoreServices -import Photos -import Combine // Media wrapper for media generated from the CameraController public struct KanvasMedia { @@ -140,46 +137,6 @@ public protocol CameraControllerDelegate: class { func getBlogSwitcher() -> UIView } -class Archive: NSObject, NSSecureCoding { - static var supportsSecureCoding: Bool = true - - let image: UIImage? - let video: URL? - let data: Data? - - init(image: UIImage, data: Data?) { - self.image = image - self.data = data - self.video = nil - } - - init(video: URL, data: Data?) { - self.video = video - self.data = data - self.image = nil - } - - func encode(with coder: NSCoder) { - coder.encode(image, forKey: "image") - coder.encode(video?.absoluteString, forKey: "video") - coder.encode(data?.base64EncodedString(), forKey: "data") - } - - required init?(coder: NSCoder) { - image = coder.decodeObject(of: UIImage.self, forKey: "image") - if let urlString = coder.decodeObject(of: NSString.self, forKey: "video") as String? { - video = URL(string: urlString) - } else { - video = nil - } - if let dataString = coder.decodeObject(of: NSString.self, forKey: "data") as String? { - data = Data(base64Encoded: dataString) - } else { - data = nil - } - } -} - // A controller that contains and layouts all camera handling views and controllers (mode selector, input, etc). open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraPreviewControllerDelegate, EditorControllerDelegate, CameraZoomHandlerDelegate, OptionsControllerDelegate, ModeSelectorAndShootControllerDelegate, CameraViewDelegate, CameraInputControllerDelegate, FilterSettingsControllerDelegate, CameraPermissionsViewControllerDelegate, KanvasMediaPickerViewControllerDelegate, MediaPickerThumbnailFetcherDelegate, MultiEditorComposerDelegate { @@ -187,36 +144,6 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP case unknownMedia } - public static func unarchive(_ url: URL) throws -> (CameraSegment, Data?) { - let data = try Data(contentsOf: url) - let archive = try NSKeyedUnarchiver.unarchivedObject(ofClass: Archive.self, from: data) - let segment: CameraSegment - if let image = archive?.image { - let info: MediaInfo - if let imageData = image.jpegData(compressionQuality: 1.0), let mInfo = MediaInfo(fromImageData: imageData) { - info = mInfo - } else { - info = MediaInfo(source: .kanvas_camera) - } - segment = CameraSegment.image(image, nil, nil, info) - } else if let video = archive?.video { - segment = CameraSegment.video(video, MediaInfo(fromVideoURL: video)) - } else { - throw ArchiveErrors.unknownMedia - } - return (segment, archive?.data) - } - - public func show(media: [(CameraSegment, Data?)]) { - showPreview = true - self.segments = media.map({ return $0.0 }) - self.edits = media.map({ return $0.1 }) - - if view.superview != nil { - showPreviewWithSegments(segments, selected: segments.startIndex, edits: nil, animated: false) - } - } - public func hideLoading() { multiEditorViewController?.hideLoading() } @@ -455,7 +382,7 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP return controller } - private func createEditorViewController(_ segments: [CameraSegment], selected: Array.Index, canvas: MovableViewCanvas? = nil, drawing: IgnoreTouchesView? = nil, cache: NSCache? = nil) -> EditorViewController { + private func createEditorViewController(_ segments: [CameraSegment], selected: Array.Index, canvas: MovableViewCanvas? = nil, drawing: IgnoreTouchesView? = nil) -> EditorViewController { let controller = EditorViewController(settings: settings, segments: segments, assetsHandler: segmentsHandler, @@ -468,15 +395,28 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP canvas: canvas, tagCollection: tagCollection) controller.delegate = self + canvas?.delegate = controller.editorView return controller } + private func frames(segments: [CameraSegment], edits: [Data?]?) -> [MultiEditorViewController.Frame] { + if let edits = edits { + return zip(segments, edits).map { (segment, data) in + return MultiEditorViewController.Frame(segment: segment, edit: MultiEditorViewController.Edit(data: data)) + } + } else { + return segments.map({ segment in + return MultiEditorViewController.Frame(segment: segment, edit: nil) + }) + } + } + private func createStoryViewController(_ segments: [CameraSegment], selected: Int, edits: [Data?]?) -> MultiEditorViewController { + let controller = MultiEditorViewController(settings: settings, - segments: segments, + frames: frames(segments: segments, edits: edits), delegate: self, - selected: selected, - edits: edits) + selected: selected) return controller } @@ -929,11 +869,11 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP // MARK: - CameraPreviewControllerDelegate & EditorControllerDelegate & StoryComposerDelegate func didFinishExportingVideo(url: URL?) { - didFinishExportingVideo(url: url, info: MediaInfo(source: .kanvas_camera), archive: Data(), action: .previewConfirm, mediaChanged: true) + didFinishExportingVideo(url: url, info: MediaInfo(source: .kanvas_camera), archive: nil, action: .previewConfirm, mediaChanged: true) } func didFinishExportingImage(image: UIImage?) { - didFinishExportingImage(image: image, info: MediaInfo(source: .kanvas_camera), archive: Data(), action: .previewConfirm, mediaChanged: true) + didFinishExportingImage(image: image, info: MediaInfo(source: .kanvas_camera), archive: nil, action: .previewConfirm, mediaChanged: true) } func didFinishExportingFrames(url: URL?) { @@ -941,7 +881,7 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP if let url = url { size = GIFDecoderFactory.main().size(of: url) } - didFinishExportingFrames(url: url, size: size, info: MediaInfo(source: .kanvas_camera), archive: Data(), action: .previewConfirm, mediaChanged: true) + didFinishExportingFrames(url: url, size: size, info: MediaInfo(source: .kanvas_camera), archive: nil, action: .previewConfirm, mediaChanged: true) } public func didFinishExportingVideo(url: URL?, info: MediaInfo?, archive: Data?, action: KanvasExportAction, mediaChanged: Bool) { @@ -960,7 +900,12 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP let archiveURL: URL? if let saveDirectory = saveDirectory { - archiveURL = try! archive?.save(to: fileName, in: saveDirectory, ext: "") + do { + archiveURL = try archive?.save(to: fileName, in: saveDirectory, ext: "") + } catch let error { + print("Failed to save archive on video export: \(error)") + archiveURL = nil + } } else { archiveURL = nil } @@ -1329,3 +1274,37 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP mediaPlayerController?.onQuickPostOptionsSelected(selected: selected, hintText: hintText, view: view) } } + +//MARK: Archival + +extension CameraController { + public static func unarchive(_ url: URL) throws -> (CameraSegment, Data?) { + let data = try Data(contentsOf: url) + let archive = try NSKeyedUnarchiver.unarchivedObject(ofClass: Archive.self, from: data) + let segment: CameraSegment + if let image = archive?.image { + let info: MediaInfo + if let imageData = image.jpegData(compressionQuality: 1.0), let mInfo = MediaInfo(fromImageData: imageData) { + info = mInfo + } else { + info = MediaInfo(source: .kanvas_camera) + } + segment = CameraSegment.image(image, nil, nil, info) + } else if let video = archive?.video { + segment = CameraSegment.video(video, MediaInfo(fromVideoURL: video)) + } else { + throw ArchiveErrors.unknownMedia + } + return (segment, archive?.data) + } + + public func show(media: [(CameraSegment, Data?)]) { + showPreview = true + self.segments = media.map({ return $0.0 }) + self.edits = media.map({ return $0.1 }) + + if view.superview != nil { + showPreviewWithSegments(segments, selected: segments.startIndex, edits: nil, animated: false) + } + } +} diff --git a/Classes/Editor/EditorView.swift b/Classes/Editor/EditorView.swift index 38deb35c7..b8f7b4362 100644 --- a/Classes/Editor/EditorView.swift +++ b/Classes/Editor/EditorView.swift @@ -116,9 +116,8 @@ private struct EditorViewConstants { final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelegate { func didRenderRectChange(rect: CGRect) { - let newRect = rect.intersection(UIScreen.main.bounds) - drawingCanvasConstraints.update(with: newRect) - movableViewCanvasConstraints.update(with: newRect) + drawingCanvasConstraints.update(with: rect) + movableViewCanvasConstraints.update(with: rect) delegate?.didRenderRectChange(rect: rect) } @@ -160,7 +159,7 @@ final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelega private let quickBlogSelectorCoordinator: KanvasQuickBlogSelectorCoordinating? private let tagCollection: UIView? - var drawingCanvas: IgnoreTouchesView + let drawingCanvas = IgnoreTouchesView() private lazy var drawingCanvasConstraints: FullViewConstraints = { return FullViewConstraints( @@ -232,8 +231,6 @@ final class EditorView: UIView, MovableViewCanvasDelegate, MediaPlayerViewDelega self.metalContext = metalContext self.movableViewCanvas = movableViewCanvas ?? MovableViewCanvas() - self.drawingCanvas = IgnoreTouchesView() - super.init(frame: .zero) self.movableViewCanvas.delegate = self setupViews() diff --git a/Classes/Editor/EditorViewController.swift b/Classes/Editor/EditorViewController.swift index 71e077331..dd3b1ee5c 100644 --- a/Classes/Editor/EditorViewController.swift +++ b/Classes/Editor/EditorViewController.swift @@ -772,13 +772,15 @@ public final class EditorViewController: UIViewController, MediaPlayerController exporter.export(frames: frames) { orderedFrames in let playbackFrames = self.gifMakerHandler.framesForPlayback(orderedFrames) self.gifEncoderClass.init().encode(frames: playbackFrames, loopCount: 0) { gifURL in - let size: CGSize? - if let gifURL = gifURL { - size = GIFDecoderFactory.main().size(of: gifURL) - } else { - size = nil + guard let gifURL = gifURL else { + performUIUpdate { + self.hideLoading() + self.handleExportError() + } + return } - let result = ExportResult(original: nil, result: .video(gifURL!), info: mediaInfo, archive: archive) + let size = GIFDecoderFactory.main().size(of: gifURL) + let result = ExportResult(original: nil, result: .video(gifURL), info: mediaInfo, archive: archive) self.exportCompletion?(.success(result)) self.delegate?.didFinishExportingFrames(url: gifURL, size: size, info: mediaInfo, archive: archive, action: exportAction, mediaChanged: self.mediaChanged) performUIUpdate { diff --git a/Classes/Editor/Media/Stickers/StylableImageView.swift b/Classes/Editor/Media/Stickers/StylableImageView.swift index c7f1af3c2..410c607e0 100644 --- a/Classes/Editor/Media/Stickers/StylableImageView.swift +++ b/Classes/Editor/Media/Stickers/StylableImageView.swift @@ -8,7 +8,7 @@ import Foundation import UIKit /// Image view that increases its image quality when its contentScaleFactor is modified -@objc final class StylableImageView: UIImageView, MovableViewInnerElement, Codable, NSSecureCoding { +@objc final class StylableImageView: UIImageView, MovableViewInnerElement, NSSecureCoding { static var supportsSecureCoding: Bool { return true } @@ -39,28 +39,13 @@ import UIKit viewCenter = coder.decodeCGPoint(forKey: CodingKeys.center.rawValue) } - enum CodingKeys: String, CodingKey { + private enum CodingKeys: String { case id case size case center case image } - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.id = try container.decode(String.self, forKey: .id) - self.viewSize = try container.decode(CGSize.self, forKey: .size) - self.viewCenter = try container.decode(CGPoint.self, forKey: .center) - super.init(image: nil) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encode(viewSize, forKey: .size) - try container.encode(viewCenter, forKey: .center) - } - override func encode(with coder: NSCoder) { coder.encode(id, forKey: CodingKeys.id.rawValue) coder.encode(viewSize, forKey: CodingKeys.size.rawValue) diff --git a/Classes/Editor/MovableViews/MovableView.swift b/Classes/Editor/MovableViews/MovableView.swift index 98ec16fa4..e1d917366 100644 --- a/Classes/Editor/MovableViews/MovableView.swift +++ b/Classes/Editor/MovableViews/MovableView.swift @@ -95,12 +95,20 @@ final class MovableView: UIView, NSSecureCoding { setupInnerView() } + + private enum CodingKeys: String { + case position + case scale + case rotation + case innerView + case origin + } required init?(coder aDecoder: NSCoder) { - position = aDecoder.decodeCGPoint(forKey: "position") - scale = CGFloat(aDecoder.decodeFloat(forKey: "scale")) - rotation = CGFloat(aDecoder.decodeFloat(forKey: "rotation")) - let view = aDecoder.decodeObject(of: [StylableTextView.self, StylableImageView.self], forKey: "innerView") + position = aDecoder.decodeCGPoint(forKey: CodingKeys.position.rawValue) + scale = CGFloat(aDecoder.decodeFloat(forKey: CodingKeys.scale.rawValue)) + rotation = CGFloat(aDecoder.decodeFloat(forKey: CodingKeys.rotation.rawValue)) + let view = aDecoder.decodeObject(of: [StylableTextView.self, StylableImageView.self], forKey: CodingKeys.innerView.rawValue) switch view { case let imageView as StylableImageView: @@ -110,7 +118,7 @@ final class MovableView: UIView, NSSecureCoding { default: innerView = StylableTextView() } - originLocation = aDecoder.decodeCGPoint(forKey: "origin") + originLocation = aDecoder.decodeCGPoint(forKey: CodingKeys.origin.rawValue) super.init(frame: .zero) @@ -119,11 +127,11 @@ final class MovableView: UIView, NSSecureCoding { override func encode(with coder: NSCoder) { super.encode(with: coder) - coder.encode(position, forKey: "position") - coder.encode(Float(scale), forKey: "scale") - coder.encode(Float(rotation), forKey: "rotation") - coder.encode(innerView, forKey: "innerView") - coder.encode(originLocation, forKey: "origin") + coder.encode(position, forKey: CodingKeys.position.rawValue) + coder.encode(Float(scale), forKey: CodingKeys.scale.rawValue) + coder.encode(Float(rotation), forKey: CodingKeys.rotation.rawValue) + coder.encode(innerView, forKey: CodingKeys.innerView.rawValue) + coder.encode(originLocation, forKey: CodingKeys.origin.rawValue) } enum ViewType { diff --git a/Classes/Editor/MovableViews/MovableViewCanvas.swift b/Classes/Editor/MovableViews/MovableViewCanvas.swift index 1194041f8..b700b37aa 100644 --- a/Classes/Editor/MovableViews/MovableViewCanvas.swift +++ b/Classes/Editor/MovableViews/MovableViewCanvas.swift @@ -68,8 +68,6 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M // Values from which the different gestures start private var originTransformations: ViewTransformations - - private var innerViews: [MovableViewInnerElement] = [] var isEmpty: Bool { return movableViews.isEmpty @@ -87,7 +85,7 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M setUpViews() } - enum CodingKeys: String, CodingKey { + private enum CodingKeys: String, CodingKey { case originTransformations case textViews case imageViews @@ -103,8 +101,8 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M super.init(frame: .zero) - let innerViews = coder.decodeObject(of: [NSArray.self, MovableView.self], forKey: CodingKeys.movableViews.rawValue) as? [MovableView] - innerViews?.forEach({ view in + let movableViews = coder.decodeObject(of: [NSArray.self, MovableView.self], forKey: CodingKeys.movableViews.rawValue) as? [MovableView] + movableViews?.forEach({ view in addView(view: view.innerView, transformations: view.transformations, location: view.innerView.viewCenter, origin: view.originLocation, size: view.innerView.viewSize, animated: false) }) setUpViews() @@ -217,7 +215,6 @@ final class MovableViewCanvas: IgnoreTouchesView, UIGestureRecognizerDelegate, M } else { move() } - innerViews.append(view) } /// Removes the tapped view from the canvas diff --git a/Classes/Editor/MovableViews/ViewTransformations.swift b/Classes/Editor/MovableViews/ViewTransformations.swift index dcc363dc2..3cd5f83f0 100644 --- a/Classes/Editor/MovableViews/ViewTransformations.swift +++ b/Classes/Editor/MovableViews/ViewTransformations.swift @@ -7,7 +7,7 @@ import Foundation import UIKit -final class ViewTransformations: NSObject, Codable, NSSecureCoding { +final class ViewTransformations: NSObject, NSSecureCoding { static var supportsSecureCoding: Bool { return true } @@ -28,15 +28,21 @@ final class ViewTransformations: NSObject, Codable, NSSecureCoding { self.rotation = rotation } + private enum CodingKeys: String { + case position + case scale + case rotation + } + init?(coder: NSCoder) { - position = coder.decodeCGPoint(forKey: "position") - scale = CGFloat(coder.decodeFloat(forKey: "scale")) - rotation = CGFloat(coder.decodeFloat(forKey: "rotation")) + position = coder.decodeCGPoint(forKey: CodingKeys.position.rawValue) + scale = CGFloat(coder.decodeFloat(forKey: CodingKeys.scale.rawValue)) + rotation = CGFloat(coder.decodeFloat(forKey: CodingKeys.rotation.rawValue)) } func encode(with coder: NSCoder) { - coder.encode(position, forKey: "position") - coder.encode(Float(scale), forKey: "scale") - coder.encode(Float(rotation), forKey: "rotation") + coder.encode(position, forKey: CodingKeys.position.rawValue) + coder.encode(Float(scale), forKey: CodingKeys.scale.rawValue) + coder.encode(Float(rotation), forKey: CodingKeys.rotation.rawValue) } } diff --git a/Classes/Editor/MultiEditor/MultiEditorViewController.swift b/Classes/Editor/MultiEditor/MultiEditorViewController.swift index 498cb4973..db0755e6f 100644 --- a/Classes/Editor/MultiEditor/MultiEditorViewController.swift +++ b/Classes/Editor/MultiEditor/MultiEditorViewController.swift @@ -64,8 +64,6 @@ class MultiEditorViewController: UIViewController { func addSegment(_ segment: CameraSegment) { -// let newEditor = editor(for: segment) - frames.append(Frame(segment: segment, edit: nil)) let clip = MediaClip(representativeFrame: segment.lastFrame, @@ -79,11 +77,8 @@ class MultiEditorViewController: UIViewController { private let settings: CameraSettings -// private var edits: [[View]] = [] - struct Edit { let data: Data? - let options: EditOptions } private var exportingEditors: [EditorViewController]? @@ -91,34 +86,24 @@ class MultiEditorViewController: UIViewController { private weak var currentEditor: EditorViewController? init(settings: CameraSettings, - segments: [CameraSegment], + frames: [Frame], delegate: MultiEditorComposerDelegate, - selected: Array.Index?, - edits: [Data?]?) { + selected: Array.Index?) { self.settings = settings self.delegate = delegate - - if let edits = edits { - frames = zip(segments, edits).map { (segment, data) in - return Frame(segment: segment, edit: Edit(data: data, options: EditOptions(soundEnabled: true))) - } - } else { - frames = segments.map({ segment in - return Frame(segment: segment, edit: nil) - }) - } + self.frames = frames self.exportHandler = MultiEditorExportHandler({ [weak delegate] result in delegate?.didFinishExporting(media: result) }) self.selected = selected super.init(nibName: nil, bundle: nil) - let clips = segments.map { segment in + let clips = frames.map { frame in return MediaClip(representativeFrame: - segment.lastFrame, + frame.segment.lastFrame, overlayText: nil, - lastFrame: segment.lastFrame) + lastFrame: frame.segment.lastFrame) } clipsController.replace(clips: clips) } @@ -145,8 +130,9 @@ class MultiEditorViewController: UIViewController { } func loadEditor(for index: Int) { - let views = edits(for: index) - if let editor = delegate?.editor(segment: frames[index].segment, canvas: views?.0) { + let canvas = edits(for: index) + let frame = frames[index] + if let editor = delegate?.editor(segment: frame.segment, canvas: canvas) { currentEditor?.stopPlayback() currentEditor?.unloadFromParentViewController() let additionalPadding: CGFloat = 10 // Extra padding for devices that don't have safe areas (which provide some padding by default). @@ -158,7 +144,6 @@ class MultiEditorViewController: UIViewController { } editor.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: bottom, right: 0) editor.delegate = self - unarchive(editor: editor, index: index) load(childViewController: editor, into: editorContainer) currentEditor = editor } @@ -184,7 +169,6 @@ class MultiEditorViewController: UIViewController { editorContainer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), editorContainer.topAnchor.constraint(equalTo: view.topAnchor), editorContainer.bottomAnchor.constraint(equalTo: clipsContainer.bottomAnchor), -// editorContainer.safeAreaLayoutGuide.bottomAnchor.constraint(equalTo: clipsContainer.topAnchor) ]) } @@ -212,15 +196,14 @@ extension MultiEditorViewController: MediaPlayerController { } extension MultiEditorViewController: MediaClipsEditorDelegate { - func mediaClipStartedMoving() { - + // No-op for the moment. UI is coming in a future commit. } func mediaClipFinishedMoving() { - + // No-op for the moment. UI is coming in a future commit. } - + func addButtonWasPressed() { delegate?.addButtonWasPressed() } @@ -344,58 +327,6 @@ extension MultiEditorViewController: EditorControllerDelegate { } - struct EditOptions { - let soundEnabled: Bool - } - - func archive(index: Int) throws { - guard let currentEditor = currentEditor else { - return - } - let currentCanvas = try NSKeyedArchiver.archivedData(withRootObject: currentEditor.editorView.movableViewCanvas, requiringSecureCoding: true) - let options = EditOptions(soundEnabled: currentEditor.shouldExportSound) - if frames.indices ~= index { - let frame = frames[index] - frames[index] = Frame(segment: frame.segment, edit: Edit(data: currentCanvas, options: options)) - } else { - print("Invalid frame index") - } - } - - func edits(for index: Int) -> (MovableViewCanvas?, EditOptions)? { - if frames.indices ~= index, let edit = frames[index].edit { - let canvas: MovableViewCanvas? - if let edit = edit.data { - do { - canvas = try NSKeyedUnarchiver.unarchivedObject(ofClass: MovableViewCanvas.self, from: edit) - } catch let error { - print("Failed to unarchive edits for \(index): \(error)") - assertionFailure("Failed to unarchive edits for \(index): \(error)") - canvas = nil - } - } else { - canvas = nil - } - let options = edit.options - return (canvas, options) - } else { - return nil - } - } - - func unarchive(editor: EditorViewController, index: Int) { - if frames.indices ~= index { - let canvas: MovableViewCanvas? - if let edits = edits(for: index) { - canvas = edits.0 - } else { - canvas = nil - } - - canvas?.delegate = editor.editorView - } - } - func showLoading() { currentEditor?.showLoading() clipsContainer.alpha = 0.5 @@ -440,9 +371,6 @@ extension MultiEditorViewController: EditorControllerDelegate { canvas = nil } let editor = delegate.editor(segment: frame.segment, canvas: canvas) - editor.shouldExportSound = frame.edit?.options.soundEnabled ?? true - - unarchive(editor: editor, index: idx) editor.export { [weak self, editor] result in let _ = editor // strong reference until the export completes self?.exportHandler.handleExport(result, for: idx) @@ -452,11 +380,44 @@ extension MultiEditorViewController: EditorControllerDelegate { return false } - func archive(editor: EditorViewController) { - - } - func addButtonPressed() { dismiss(animated: true, completion: nil) } } + +//MARK: Edit + Archive + +extension MultiEditorViewController { + func archive(index: Int) throws { + guard let currentEditor = currentEditor else { + return + } + let currentCanvas = try NSKeyedArchiver.archivedData(withRootObject: currentEditor.editorView.movableViewCanvas, requiringSecureCoding: true) + if frames.indices ~= index { + let frame = frames[index] + frames[index] = Frame(segment: frame.segment, edit: Edit(data: currentCanvas)) + } else { + print("Invalid frame index") + } + } + + func edits(for index: Int) -> MovableViewCanvas? { + if frames.indices ~= index, let edit = frames[index].edit { + let canvas: MovableViewCanvas? + if let edit = edit.data { + do { + canvas = try NSKeyedUnarchiver.unarchivedObject(ofClass: MovableViewCanvas.self, from: edit) + } catch let error { + print("Failed to unarchive edits for \(index): \(error)") + assertionFailure("Failed to unarchive edits for \(index): \(error)") + canvas = nil + } + } else { + canvas = nil + } + return canvas + } else { + return nil + } + } +} diff --git a/Classes/Editor/Text/StylableTextView.swift b/Classes/Editor/Text/StylableTextView.swift index cd8e57cce..5c0d0415f 100644 --- a/Classes/Editor/Text/StylableTextView.swift +++ b/Classes/Editor/Text/StylableTextView.swift @@ -94,7 +94,7 @@ private struct Constants { font = UIFont(name: fontName, size: fontSize) } - enum CodingKeys: String, CodingKey { + private enum CodingKeys: String { case textAlignment case contentScaleFactor case font @@ -105,7 +105,7 @@ private struct Constants { case highlightColor } - enum FontKeys: String, CodingKey { + private enum FontKeys: String { case name case fontSize } From e7d083d8db88a3fee2bfa60c8d2c7c4bc004df83 Mon Sep 17 00:00:00 2001 From: Brandon Titus Date: Fri, 12 Feb 2021 10:17:41 -0700 Subject: [PATCH 6/6] Add NSCache for storing expensive frame data Reduces the impact of `EditorViewController.addCarouselDefaultColors` on time when switching frames. --- Classes/Camera/CameraController.swift | 14 +++---- Classes/Editor/EditorViewController.swift | 37 ++++++++++++++++--- .../MultiEditorViewController.swift | 9 +++-- 3 files changed, 44 insertions(+), 16 deletions(-) diff --git a/Classes/Camera/CameraController.swift b/Classes/Camera/CameraController.swift index 3ebec155e..5d7ba51c8 100644 --- a/Classes/Camera/CameraController.swift +++ b/Classes/Camera/CameraController.swift @@ -372,7 +372,7 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP } else if settings.features.editor { let existing = existingEditor - controller = existing ?? createEditorViewController(segments, selected: selected) + controller = existing ?? createEditorViewController(segments, selected: selected, cache: nil) } else { controller = createPreviewViewController(segments) @@ -382,7 +382,7 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP return controller } - private func createEditorViewController(_ segments: [CameraSegment], selected: Array.Index, canvas: MovableViewCanvas? = nil, drawing: IgnoreTouchesView? = nil) -> EditorViewController { + private func createEditorViewController(_ segments: [CameraSegment], selected: Array.Index, canvas: MovableViewCanvas? = nil, drawing: IgnoreTouchesView? = nil, cache: NSCache?) -> EditorViewController { let controller = EditorViewController(settings: settings, segments: segments, assetsHandler: segmentsHandler, @@ -393,7 +393,8 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP analyticsProvider: analyticsProvider, quickBlogSelectorCoordinator: quickBlogSelectorCoordinator, canvas: canvas, - tagCollection: tagCollection) + tagCollection: tagCollection, + cache: cache) controller.delegate = self canvas?.delegate = controller.editorView return controller @@ -402,7 +403,7 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP private func frames(segments: [CameraSegment], edits: [Data?]?) -> [MultiEditorViewController.Frame] { if let edits = edits { return zip(segments, edits).map { (segment, data) in - return MultiEditorViewController.Frame(segment: segment, edit: MultiEditorViewController.Edit(data: data)) + return MultiEditorViewController.Frame(segment: segment, edit: MultiEditorViewController.Edit(data: data, cache: nil)) } } else { return segments.map({ segment in @@ -860,10 +861,9 @@ open class CameraController: UIViewController, MediaClipsEditorDelegate, CameraP dismiss(animated: false, completion: nil) } - func editor(segment: CameraSegment, canvas: MovableViewCanvas?) -> EditorViewController { + func editor(segment: CameraSegment, canvas: MovableViewCanvas?, cache: NSCache?) -> EditorViewController { let segments = [segment] - - return createEditorViewController(segments, selected: segments.startIndex, canvas: canvas) + return createEditorViewController(segments, selected: segments.startIndex, canvas: canvas, cache: cache) } // MARK: - CameraPreviewControllerDelegate & EditorControllerDelegate & StoryComposerDelegate diff --git a/Classes/Editor/EditorViewController.swift b/Classes/Editor/EditorViewController.swift index dd3b1ee5c..0e80958a7 100644 --- a/Classes/Editor/EditorViewController.swift +++ b/Classes/Editor/EditorViewController.swift @@ -158,6 +158,8 @@ public final class EditorViewController: UIViewController, MediaPlayerController var shouldExportSound: Bool = true private let metalContext = MetalContext.createContext() + let cache: NSCache + private var shouldExportMediaAsGIF: Bool { get { return collectionController.shouldExportMediaAsGIF @@ -241,7 +243,8 @@ public final class EditorViewController: UIViewController, MediaPlayerController stickerProvider: stickerProvider, analyticsProvider: analyticsProvider, quickBlogSelectorCoordinator: nil, - tagCollection: nil) + tagCollection: nil, + cache: nil) } public static func createEditor(for videoURL: URL, settings: CameraSettings, stickerProvider: StickerProvider) -> EditorViewController { @@ -254,7 +257,8 @@ public final class EditorViewController: UIViewController, MediaPlayerController stickerProvider: stickerProvider, analyticsProvider: nil, quickBlogSelectorCoordinator: nil, - tagCollection: nil) + tagCollection: nil, + cache: nil) } public static func createEditor(forGIF url: URL, @@ -273,6 +277,13 @@ public final class EditorViewController: UIViewController, MediaPlayerController } } + private static func freshCache() -> NSCache { + let cache = NSCache() + cache.name = "Kanvas Editor Cache" + cache.totalCostLimit = 50_000_000 + return cache + } + convenience init(settings: CameraSettings, segments: [CameraSegment], stickerProvider: StickerProvider, @@ -286,7 +297,8 @@ public final class EditorViewController: UIViewController, MediaPlayerController stickerProvider: stickerProvider, analyticsProvider: analyticsProvider, quickBlogSelectorCoordinator: nil, - tagCollection: nil) + tagCollection: nil, + cache: nil) } /// The designated initializer for the editor controller @@ -308,7 +320,8 @@ public final class EditorViewController: UIViewController, MediaPlayerController analyticsProvider: KanvasAnalyticsProvider?, quickBlogSelectorCoordinator: KanvasQuickBlogSelectorCoordinating?, canvas: MovableViewCanvas? = nil, - tagCollection: UIView?) { + tagCollection: UIView?, + cache: NSCache?) { self.settings = settings self.originalSegments = segments self.assetsHandler = assetsHandler @@ -328,6 +341,7 @@ public final class EditorViewController: UIViewController, MediaPlayerController quickBlogSelectorCoordinator: quickBlogSelectorCoordinator, tagCollection: tagCollection, metalContext: metalContext) + self.cache = cache ?? EditorViewController.freshCache() super.init(nibName: .none, bundle: .none) self.editorView.delegate = self @@ -391,12 +405,25 @@ public final class EditorViewController: UIViewController, MediaPlayerController /// Sets up the color carousels of both drawing and text tools private func addCarouselDefaultColors(_ image: UIImage) { - let dominantColors = image.getDominantColors(count: 3) + + let cacheKey = "dominantColors" + + let dominantColors: [UIColor] + if let cached = cache.object(forKey: cacheKey as NSString) { + let colors = try! NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, UIColor.self], from: cached as Data) + dominantColors = colors as! [UIColor] + } else { + dominantColors = image.getDominantColors(count: 3) + } + drawingController.addColorsForCarousel(colors: dominantColors) if let mostDominantColor = dominantColors.first { textController.addColorsForCarousel(colors: [mostDominantColor, .white, .black]) } + + let archivedColors = try! NSKeyedArchiver.archivedData(withRootObject: dominantColors as NSArray, requiringSecureCoding: true) + cache.setObject(archivedColors as NSData, forKey: cacheKey as NSString) } // MARK: - Media Player diff --git a/Classes/Editor/MultiEditor/MultiEditorViewController.swift b/Classes/Editor/MultiEditor/MultiEditorViewController.swift index db0755e6f..6542c5665 100644 --- a/Classes/Editor/MultiEditor/MultiEditorViewController.swift +++ b/Classes/Editor/MultiEditor/MultiEditorViewController.swift @@ -9,7 +9,7 @@ import Foundation protocol MultiEditorComposerDelegate: EditorControllerDelegate { func didFinishExporting(media: [Result]) func addButtonWasPressed() - func editor(segment: CameraSegment, canvas: MovableViewCanvas?) -> EditorViewController + func editor(segment: CameraSegment, canvas: MovableViewCanvas?, cache: NSCache?) -> EditorViewController func dismissButtonPressed() } @@ -79,6 +79,7 @@ class MultiEditorViewController: UIViewController { struct Edit { let data: Data? + let cache: NSCache? } private var exportingEditors: [EditorViewController]? @@ -132,7 +133,7 @@ class MultiEditorViewController: UIViewController { func loadEditor(for index: Int) { let canvas = edits(for: index) let frame = frames[index] - if let editor = delegate?.editor(segment: frame.segment, canvas: canvas) { + if let editor = delegate?.editor(segment: frame.segment, canvas: canvas, cache: frame.edit?.cache) { currentEditor?.stopPlayback() currentEditor?.unloadFromParentViewController() let additionalPadding: CGFloat = 10 // Extra padding for devices that don't have safe areas (which provide some padding by default). @@ -370,7 +371,7 @@ extension MultiEditorViewController: EditorControllerDelegate { } else { canvas = nil } - let editor = delegate.editor(segment: frame.segment, canvas: canvas) + let editor = delegate.editor(segment: frame.segment, canvas: canvas, cache: frame.edit?.cache) editor.export { [weak self, editor] result in let _ = editor // strong reference until the export completes self?.exportHandler.handleExport(result, for: idx) @@ -395,7 +396,7 @@ extension MultiEditorViewController { let currentCanvas = try NSKeyedArchiver.archivedData(withRootObject: currentEditor.editorView.movableViewCanvas, requiringSecureCoding: true) if frames.indices ~= index { let frame = frames[index] - frames[index] = Frame(segment: frame.segment, edit: Edit(data: currentCanvas)) + frames[index] = Frame(segment: frame.segment, edit: Edit(data: currentCanvas, cache: currentEditor.cache)) } else { print("Invalid frame index") }