diff --git a/HTTP/Sources/HTTP/URLSession+Extensions.swift b/HTTP/Sources/HTTP/URLSession+Extensions.swift index c8766de..5ee5e7e 100644 --- a/HTTP/Sources/HTTP/URLSession+Extensions.swift +++ b/HTTP/Sources/HTTP/URLSession+Extensions.swift @@ -7,6 +7,8 @@ extension URLSession { func dataTaskPublisher(for request: URLRequest, progress: Progress?) -> AnyPublisher { if let progress = progress { + var dataTaskReference: URLSessionDataTask? + return Deferred { Future { promise in let dataTask = self.dataTask(with: request) { data, response, error in @@ -19,7 +21,11 @@ extension URLSession { progress.addChild(dataTask.progress, withPendingUnitCount: 1) dataTask.resume() + dataTaskReference = dataTask } + .handleEvents(receiveCancel: { + dataTaskReference?.cancel() + }) } .eraseToAnyPublisher() } else { diff --git a/ViewModels/Sources/ViewModels/CompositionViewModel.swift b/ViewModels/Sources/ViewModels/CompositionViewModel.swift index 474b291..4345fcb 100644 --- a/ViewModels/Sources/ViewModels/CompositionViewModel.swift +++ b/ViewModels/Sources/ViewModels/CompositionViewModel.swift @@ -17,7 +17,7 @@ public final class CompositionViewModel: ObservableObject, Identifiable { @Published public private(set) var canAddAttachment = true @Published public private(set) var canAddNonImageAttachment = true - private var cancellables = Set() + private var attachmentUploadCancellable: AnyCancellable? init() { $text.map { !$0.isEmpty } @@ -56,11 +56,15 @@ public extension CompositionViewModel { func remove(attachmentViewModel: CompositionAttachmentViewModel) { attachmentViewModels.removeAll { $0 === attachmentViewModel } } + + func cancelUpload() { + attachmentUploadCancellable?.cancel() + } } extension CompositionViewModel { - func attach(itemProvider: NSItemProvider, service: IdentityService) -> AnyPublisher { - MediaProcessingService.dataAndMimeType(itemProvider: itemProvider) + func attach(itemProvider: NSItemProvider, parentViewModel: NewStatusViewModel) { + attachmentUploadCancellable = MediaProcessingService.dataAndMimeType(itemProvider: itemProvider) .flatMap { [weak self] data, mimeType -> AnyPublisher in guard let self = self else { return Empty().eraseToAnyPublisher() } @@ -70,16 +74,19 @@ extension CompositionViewModel { self.attachmentUpload = AttachmentUpload(progress: progress, data: data, mimeType: mimeType) } - return service.uploadAttachment(data: data, mimeType: mimeType, progress: progress) + return parentViewModel.identification.service.uploadAttachment( + data: data, + mimeType: mimeType, + progress: progress) } .receive(on: DispatchQueue.main) - .handleEvents( - receiveOutput: { [weak self] in - self?.attachmentViewModels.append(CompositionAttachmentViewModel(attachment: $0)) - }, - receiveCompletion: { [weak self] _ in self?.attachmentUpload = nil }) - .ignoreOutput() - .eraseToAnyPublisher() + .assignErrorsToAlertItem(to: \.alertItem, on: parentViewModel) + .handleEvents(receiveCancel: { [weak self] in self?.attachmentUpload = nil }) + .sink { [weak self] _ in + self?.attachmentUpload = nil + } receiveValue: { [weak self] in + self?.attachmentViewModels.append(CompositionAttachmentViewModel(attachment: $0)) + } } } diff --git a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift index 68d73a5..7a02411 100644 --- a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift @@ -98,11 +98,7 @@ public extension NewStatusViewModel { } func attach(itemProvider: NSItemProvider, to compositionViewModel: CompositionViewModel) { - compositionViewModel.attach(itemProvider: itemProvider, service: identification.service) - .receive(on: DispatchQueue.main) - .assignErrorsToAlertItem(to: \.alertItem, on: self) - .sink { _ in } - .store(in: &cancellables) + compositionViewModel.attach(itemProvider: itemProvider, parentViewModel: self) } func post() { diff --git a/Views/AttachmentUploadView.swift b/Views/AttachmentUploadView.swift index 7c49ac2..d7ba78f 100644 --- a/Views/AttachmentUploadView.swift +++ b/Views/AttachmentUploadView.swift @@ -6,23 +6,16 @@ import ViewModels final class AttachmentUploadView: UIView { let label = UILabel() + let cancelButton = UIButton(type: .system) let progressView = UIProgressView(progressViewStyle: .default) + + private let viewModel: CompositionViewModel private var progressCancellable: AnyCancellable? + private var cancellables = Set() - var attachmentUpload: AttachmentUpload? { - didSet { - if let attachmentUpload = attachmentUpload { - progressCancellable = attachmentUpload.progress.publisher(for: \.fractionCompleted) - .receive(on: DispatchQueue.main) - .sink { [weak self] in self?.progressView.progress = Float($0) } - isHidden = false - } else { - isHidden = true - } - } - } + init(viewModel: CompositionViewModel) { + self.viewModel = viewModel - init() { super.init(frame: .zero) addSubview(label) @@ -34,18 +27,42 @@ final class AttachmentUploadView: UIView { label.textColor = .secondaryLabel label.numberOfLines = 0 + addSubview(cancelButton) + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.titleLabel?.adjustsFontForContentSizeCategory = true + cancelButton.titleLabel?.font = .preferredFont(forTextStyle: .callout) + cancelButton.setTitle(NSLocalizedString("cancel", comment: ""), for: .normal) + cancelButton.addAction(UIAction { _ in viewModel.cancelUpload() }, for: .touchUpInside) + addSubview(progressView) progressView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ label.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), - label.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + label.trailingAnchor.constraint(equalTo: cancelButton.leadingAnchor, constant: .defaultSpacing), + cancelButton.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor), + cancelButton.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), + cancelButton.bottomAnchor.constraint(equalTo: label.bottomAnchor), progressView.topAnchor.constraint(equalTo: label.bottomAnchor, constant: .defaultSpacing), progressView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor), progressView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor), progressView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor) ]) + + viewModel.$attachmentUpload.sink { [weak self] in + guard let self = self else { return } + + if let attachmentUpload = $0 { + self.progressCancellable = attachmentUpload.progress.publisher(for: \.fractionCompleted) + .receive(on: DispatchQueue.main) + .sink { self.progressView.progress = Float($0) } + self.isHidden = false + } else { + self.isHidden = true + } + } + .store(in: &cancellables) } @available(*, unavailable) diff --git a/Views/CompositionView.swift b/Views/CompositionView.swift index 9b84a74..2afdf31 100644 --- a/Views/CompositionView.swift +++ b/Views/CompositionView.swift @@ -10,8 +10,8 @@ final class CompositionView: UIView { let spoilerTextField = UITextField() let textView = UITextView() let textViewPlaceholder = UILabel() - let attachmentUploadView = AttachmentUploadView() let attachmentsCollectionView: UICollectionView + let attachmentUploadView: AttachmentUploadView private let viewModel: CompositionViewModel private let parentViewModel: NewStatusViewModel @@ -50,6 +50,7 @@ final class CompositionView: UIView { let attachmentsLayout = UICollectionViewCompositionalLayout(section: section, configuration: configuration) attachmentsCollectionView = UICollectionView(frame: .zero, collectionViewLayout: attachmentsLayout) + attachmentUploadView = AttachmentUploadView(viewModel: viewModel) super.init(frame: .zero) @@ -168,10 +169,6 @@ private extension CompositionView { } .store(in: &cancellables) - viewModel.$attachmentUpload - .sink { [weak self] in self?.attachmentUploadView.attachmentUpload = $0 } - .store(in: &cancellables) - let guide = UIDevice.current.userInterfaceIdiom == .pad ? readableContentGuide : layoutMarginsGuide let constraints = [ avatarImageView.heightAnchor.constraint(equalToConstant: .avatarDimension),