From 2eddf8c558f423bbcb20d5fa7ac7b79db4517349 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Sun, 17 Jan 2021 22:03:32 -0800 Subject: [PATCH] Save media --- Extensions/AVURLAsset+Extensions.swift | 54 +++++++++++++++++++ Localizations/Localizable.strings | 1 + Metatext.xcodeproj/project.pbxproj | 4 ++ Supporting Files/Info.plist | 12 +++-- .../ImagePageViewController.swift | 6 +++ View Controllers/ImageViewController.swift | 39 ++++++++++++++ 6 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 Extensions/AVURLAsset+Extensions.swift diff --git a/Extensions/AVURLAsset+Extensions.swift b/Extensions/AVURLAsset+Extensions.swift new file mode 100644 index 0000000..3261480 --- /dev/null +++ b/Extensions/AVURLAsset+Extensions.swift @@ -0,0 +1,54 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import AVFoundation + +enum AssetExportError: Error { + case exportSetup + case export +} + +extension AVURLAsset { + func exportWithoutAudioTrack(completion: @escaping ((Result) -> Void)) { + let composition = AVMutableComposition() + let exportDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + + guard let sourceVideoTrack = tracks(withMediaType: .video).first, + let compositionVideoTrack = composition.addMutableTrack( + withMediaType: .video, + preferredTrackID: kCMPersistentTrackID_Invalid), + case .success = Result(catching: { + try compositionVideoTrack.insertTimeRange( + CMTimeRange(start: .zero, duration: duration), + of: sourceVideoTrack, at: .zero) + }), + let exportSession = AVAssetExportSession( + asset: composition, + presetName: AVAssetExportPresetHighestQuality), + exportSession.supportedFileTypes.contains(.mp4), + case .success = Result(catching: { + try FileManager.default.createDirectory( + at: exportDirectory, + withIntermediateDirectories: false) + }) + else { + completion(.failure(.exportSetup)) + + return + } + + let exportURL = exportDirectory.appendingPathComponent(url.lastPathComponent) + + exportSession.outputFileType = AVFileType.mp4 + exportSession.outputURL = exportURL + exportSession.timeRange = CMTimeRange(start: .zero, duration: duration) + exportSession.exportAsynchronously { + guard exportSession.status == .completed else { + completion(.failure(.export)) + + return + } + + completion(.success(exportURL)) + } + } +} diff --git a/Localizations/Localizable.strings b/Localizations/Localizable.strings index 79e6ee3..6d76135 100644 --- a/Localizations/Localizable.strings +++ b/Localizations/Localizable.strings @@ -39,6 +39,7 @@ "attachment.edit.thumbnail.prompt" = "Drag the circle on the preview to choose the focal point which will always be in view on all thumbnails"; "attachment.sensitive-content" = "Sensitive content"; "attachment.media-hidden" = "Media hidden"; +"attachment.unable-to-export-media" = "Unable to export media"; "bookmarks" = "Bookmarks"; "camera-access.title" = "Camera access needed"; "camera-access.description" = "Open system settings to allow camera access"; diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index d831616..cb8ab2f 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -51,6 +51,7 @@ D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */; }; D059373F25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */; }; D059376125ABE2E800754FDF /* XMLUnescaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D059376025ABE2E800754FDF /* XMLUnescaper.swift */; }; + D05E688525B55AE8001FB2C6 /* AVURLAsset+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */; }; D0625E59250F092900502611 /* StatusListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E58250F092900502611 /* StatusListCell.swift */; }; D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */; }; D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06BC5E525202AD90079541D /* ProfileViewController.swift */; }; @@ -218,6 +219,7 @@ D059373225AAEA7000754FDF /* CompositionPollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollView.swift; sourceTree = ""; }; D059373D25AB8D5200754FDF /* CompositionPollOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionPollOptionView.swift; sourceTree = ""; }; D059376025ABE2E800754FDF /* XMLUnescaper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLUnescaper.swift; sourceTree = ""; }; + D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AVURLAsset+Extensions.swift"; sourceTree = ""; }; D0625E58250F092900502611 /* StatusListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusListCell.swift; sourceTree = ""; }; D0625E5C250F0B5C00502611 /* StatusContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentConfiguration.swift; sourceTree = ""; }; D0666A2124C677B400F3F04B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -569,6 +571,7 @@ isa = PBXGroup; children = ( D01C6FAB252024BD003D0300 /* Array+Extensions.swift */, + D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */, D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */, D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */, D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */, @@ -834,6 +837,7 @@ D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */, D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */, D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */, + D05E688525B55AE8001FB2C6 /* AVURLAsset+Extensions.swift in Sources */, D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */, D08B8D42253F92B600B1EBEF /* ImagePageViewController.swift in Sources */, D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */, diff --git a/Supporting Files/Info.plist b/Supporting Files/Info.plist index 5a2039d..ec8459d 100644 --- a/Supporting Files/Info.plist +++ b/Supporting Files/Info.plist @@ -2,10 +2,6 @@ - NSMicrophoneUsageDescription - Enables Metatext to take videos and add them to your posts. - NSCameraUsageDescription - Enables Metatext to take photos and videos and add them to your posts. CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable @@ -24,6 +20,14 @@ 1 LSRequiresIPhoneOS + NSCameraUsageDescription + Enables Metatext to take photos and videos and add them to your posts + NSMicrophoneUsageDescription + Enables Metatext to take videos and add them to your posts + NSPhotoLibraryAddUsageDescription + Enables Metatext to add items to your Photo Library + NSPhotoLibraryUsageDescription + Enables Metatext to access photos from your library and add them to your posts UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/View Controllers/ImagePageViewController.swift b/View Controllers/ImagePageViewController.swift index b654445..2b00a7b 100644 --- a/View Controllers/ImagePageViewController.swift +++ b/View Controllers/ImagePageViewController.swift @@ -48,6 +48,12 @@ final class ImagePageViewController: UIPageViewController { systemItem: .close, primaryAction: UIAction { [weak self] _ in self?.presentingViewController?.dismiss(animated: true) }) + navigationItem.rightBarButtonItem = .init( + systemItem: .action, + primaryAction: UIAction { [weak self] _ in + (self?.viewControllers?.first as? ImageViewController)?.presentActivityViewController() + }) + navigationController?.barHideOnTapGestureRecognizer.addTarget( self, action: #selector(toggleDescriptionVisibility)) diff --git a/View Controllers/ImageViewController.swift b/View Controllers/ImageViewController.swift index 2a04861..0324669 100644 --- a/View Controllers/ImageViewController.swift +++ b/View Controllers/ImageViewController.swift @@ -1,5 +1,6 @@ // Copyright © 2020 Metabolist. All rights reserved. +import AVFoundation import Kingfisher import UIKit import ViewModels @@ -146,6 +147,44 @@ extension ImageViewController { self.descriptionBackgroundView.alpha = self.descriptionBackgroundView.alpha > 0 ? 0 : 1 } } + + func presentActivityViewController() { + if let image = imageView.image { + let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: []) + + present(activityViewController, animated: true) + } else if let asset = playerView.player?.currentItem?.asset as? AVURLAsset { + asset.exportWithoutAudioTrack { result in + DispatchQueue.main.async { + switch result { + case let .success(url): + let activityViewController = UIActivityViewController( + activityItems: [url], + applicationActivities: []) + + activityViewController.completionWithItemsHandler = { _, _, _, _ in + try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) + } + + self.present(activityViewController, animated: true) + case .failure: + let alertController = UIAlertController( + title: nil, + message: NSLocalizedString("attachment.unable-to-export-media", comment: ""), + preferredStyle: .alert) + + let okAction = UIAlertAction( + title: NSLocalizedString("ok", comment: ""), + style: .default) { _ in } + + alertController.addAction(okAction) + + self.present(alertController, animated: true) + } + } + } + } + } } extension ImageViewController: UIScrollViewDelegate {