feat: add save photo action for image preview scene

This commit is contained in:
CMK 2021-04-29 19:49:46 +08:00
parent df2a73d96c
commit aace886401
7 changed files with 172 additions and 7 deletions

View File

@ -380,6 +380,7 @@
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; }; DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */; };
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; }; DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; }; DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
DBA5E7A3263AD0A3004598BB /* PhotoLibraryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */; };
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; }; DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F672615DD60004B8251 /* UserProvider.swift */; }; DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F672615DD60004B8251 /* UserProvider.swift */; };
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */; }; DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */; };
@ -935,6 +936,7 @@
DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = "<group>"; }; DB9E0D6E25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIInterpolatingMotionEffect.swift; sourceTree = "<group>"; };
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; }; DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; };
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; }; DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryService.swift; sourceTree = "<group>"; };
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; }; DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
DBAE3F672615DD60004B8251 /* UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProvider.swift; sourceTree = "<group>"; }; DBAE3F672615DD60004B8251 /* UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProvider.swift; sourceTree = "<group>"; };
DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewController+UserProvider.swift"; sourceTree = "<group>"; }; DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewController+UserProvider.swift"; sourceTree = "<group>"; };
@ -1289,6 +1291,7 @@
DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */, DBC7A67B260DFADE00E57475 /* StatusPublishService.swift */,
DB4924E126312AB200E9DB22 /* NotificationService.swift */, DB4924E126312AB200E9DB22 /* NotificationService.swift */,
DB6D9F6226357848008423CD /* SettingService.swift */, DB6D9F6226357848008423CD /* SettingService.swift */,
DBA5E7A2263AD0A3004598BB /* PhotoLibraryService.swift */,
); );
path = Service; path = Service;
sourceTree = "<group>"; sourceTree = "<group>";
@ -2864,6 +2867,7 @@
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */, 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */, DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */,
DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */, DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */,
DBA5E7A3263AD0A3004598BB /* PhotoLibraryService.swift in Sources */,
5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */, 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */,
DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */, DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */,
2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */, 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */,

View File

@ -12,7 +12,7 @@
<key>CoreDataStack.xcscheme_^#shared#^_</key> <key>CoreDataStack.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>14</integer> <integer>18</integer>
</dict> </dict>
<key>Mastodon - RTL.xcscheme_^#shared#^_</key> <key>Mastodon - RTL.xcscheme_^#shared#^_</key>
<dict> <dict>
@ -32,7 +32,7 @@
<key>NotificationService.xcscheme_^#shared#^_</key> <key>NotificationService.xcscheme_^#shared#^_</key>
<dict> <dict>
<key>orderHint</key> <key>orderHint</key>
<integer>15</integer> <integer>17</integer>
</dict> </dict>
</dict> </dict>
<key>SuppressBuildableAutocreation</key> <key>SuppressBuildableAutocreation</key>

View File

@ -191,11 +191,38 @@ extension MediaPreviewViewController: PageboyViewControllerDelegate {
extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate { extension MediaPreviewViewController: MediaPreviewImageViewControllerDelegate {
func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer) { func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer) {
// do nothing
} }
func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, longPressGestureRecognizerDidTrigger longPressGestureRecognizer: UILongPressGestureRecognizer) { func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, longPressGestureRecognizerDidTrigger longPressGestureRecognizer: UILongPressGestureRecognizer) {
// delegate?.mediaPreviewViewController(self, longPressGestureRecognizerTriggered: longPressGestureRecognizer) // do nothing
}
func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, contextMenuActionPerform action: MediaPreviewImageViewController.ContextMenuAction) {
switch action {
case .savePhoto:
switch viewController.viewModel.item {
case .status(let meta):
context.photoLibraryService.saveImage(url: meta.url).sink { _ in
// do nothing
} receiveValue: { _ in
// do nothing
}
.store(in: &context.disposeBag)
case .local(let meta):
context.photoLibraryService.save(image: meta.image, withNotificationFeedback: true)
}
case .share:
let applicationActivities: [UIActivity] = [
SafariActivity(sceneCoordinator: self.coordinator)
]
let activityViewController = UIActivityViewController(
activityItems: viewController.viewModel.item.activityItems,
applicationActivities: applicationActivities
)
activityViewController.popoverPresentationController?.sourceView = viewController.previewImageView.imageView
self.present(activityViewController, animated: true, completion: nil)
}
} }
} }

View File

@ -9,9 +9,10 @@ import os.log
import UIKit import UIKit
import Combine import Combine
protocol MediaPreviewImageViewControllerDelegate: class { protocol MediaPreviewImageViewControllerDelegate: AnyObject {
func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer) func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, tapGestureRecognizerDidTrigger tapGestureRecognizer: UITapGestureRecognizer)
func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, longPressGestureRecognizerDidTrigger longPressGestureRecognizer: UILongPressGestureRecognizer) func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, longPressGestureRecognizerDidTrigger longPressGestureRecognizer: UILongPressGestureRecognizer)
func mediaPreviewImageViewController(_ viewController: MediaPreviewImageViewController, contextMenuActionPerform action: MediaPreviewImageViewController.ContextMenuAction)
} }
final class MediaPreviewImageViewController: UIViewController { final class MediaPreviewImageViewController: UIViewController {
@ -63,6 +64,9 @@ extension MediaPreviewImageViewController {
previewImageView.addGestureRecognizer(tapGestureRecognizer) previewImageView.addGestureRecognizer(tapGestureRecognizer)
previewImageView.addGestureRecognizer(longPressGestureRecognizer) previewImageView.addGestureRecognizer(longPressGestureRecognizer)
let previewImageViewContextMenuInteraction = UIContextMenuInteraction(delegate: self)
previewImageView.addInteraction(previewImageViewContextMenuInteraction)
switch viewModel.item { switch viewModel.item {
case .status(let meta): case .status(let meta):
// progressBarView.isHidden = meta.thumbnail != nil // progressBarView.isHidden = meta.thumbnail != nil
@ -113,3 +117,50 @@ extension MediaPreviewImageViewController {
} }
} }
// MARK: - UIContextMenuInteractionDelegate
extension MediaPreviewImageViewController: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
let previewProvider: UIContextMenuContentPreviewProvider = { () -> UIViewController? in
return nil
}
let saveAction = UIAction(
title: L10n.Common.Controls.Actions.savePhoto, image: UIImage(systemName: "square.and.arrow.down")!, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: save photo", ((#file as NSString).lastPathComponent), #line, #function)
guard let self = self else { return }
self.delegate?.mediaPreviewImageViewController(self, contextMenuActionPerform: .savePhoto)
}
let shareAction = UIAction(
title: L10n.Common.Controls.Actions.share, image: UIImage(systemName: "square.and.arrow.up")!, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] _ in
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: share", ((#file as NSString).lastPathComponent), #line, #function)
guard let self = self else { return }
self.delegate?.mediaPreviewImageViewController(self, contextMenuActionPerform: .share)
}
let actionProvider: UIContextMenuActionProvider = { elements -> UIMenu? in
return UIMenu(title: "", image: nil, identifier: nil, options: [], children: [
saveAction,
shareAction
])
}
return UIContextMenuConfiguration(identifier: nil, previewProvider: previewProvider, actionProvider: actionProvider)
}
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
// set preview view
return UITargetedPreview(view: previewImageView.imageView)
}
}
extension MediaPreviewImageViewController {
enum ContextMenuAction {
case savePhoto
case share
}
}

View File

@ -27,6 +27,19 @@ extension MediaPreviewImageViewModel {
enum ImagePreviewItem { enum ImagePreviewItem {
case status(RemoteImagePreviewMeta) case status(RemoteImagePreviewMeta)
case local(LocalImagePreviewMeta) case local(LocalImagePreviewMeta)
var activityItems: [Any] {
var items: [Any] = []
switch self {
case .status(let meta):
items.append(meta.url)
case .local(let meta):
items.append(meta.image)
}
return items
}
} }
struct RemoteImagePreviewMeta { struct RemoteImagePreviewMeta {

View File

@ -0,0 +1,69 @@
//
// PhotoLibraryService.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-4-29.
//
import os.log
import UIKit
import Combine
import AlamofireImage
final class PhotoLibraryService: NSObject {
}
extension PhotoLibraryService {
func saveImage(url: URL) -> AnyPublisher<UIImage, Error> {
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
return Future<UIImage, Error> { promise in
ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in
guard let self = self else { return }
switch response.result {
case .failure(let error):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription)
promise(.failure(error))
case .success(let image):
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription)
self.save(image: image)
promise(.success(image))
}
})
}
.handleEvents(receiveSubscription: { _ in
impactFeedbackGenerator.impactOccurred()
}, receiveCompletion: { completion in
switch completion {
case .failure:
notificationFeedbackGenerator.notificationOccurred(.error)
case .finished:
notificationFeedbackGenerator.notificationOccurred(.success)
}
})
.eraseToAnyPublisher()
}
func save(image: UIImage, withNotificationFeedback: Bool = false) {
UIImageWriteToSavedPhotosAlbum(
image,
self,
#selector(PhotoLibraryService.image(_:didFinishSavingWithError:contextInfo:)),
nil
)
// assert no error
if withNotificationFeedback {
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
notificationFeedbackGenerator.notificationOccurred(.success)
}
}
@objc private func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
// TODO: notify banner
}
}

View File

@ -30,6 +30,7 @@ class AppContext: ObservableObject {
let statusPublishService = StatusPublishService() let statusPublishService = StatusPublishService()
let notificationService: NotificationService let notificationService: NotificationService
let settingService: SettingService let settingService: SettingService
let photoLibraryService = PhotoLibraryService()
let documentStore: DocumentStore let documentStore: DocumentStore
private var documentStoreSubscription: AnyCancellable! private var documentStoreSubscription: AnyCancellable!