From 3b0f0bf82ff22073ef6bd5aee7084849f1e779a6 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Fri, 1 Jan 2021 12:18:10 -0800 Subject: [PATCH] wip --- Extensions/Array+Extensions.swift | 6 + .../NewStatusViewController.swift | 112 +++++++++++------- .../ViewModels/NewStatusViewModel.swift | 20 +++- Views/CompositionView.swift | 2 +- 4 files changed, 88 insertions(+), 52 deletions(-) diff --git a/Extensions/Array+Extensions.swift b/Extensions/Array+Extensions.swift index 386a7de..b89d441 100644 --- a/Extensions/Array+Extensions.swift +++ b/Extensions/Array+Extensions.swift @@ -17,3 +17,9 @@ extension Array where Element: Sequence, Element.Element: Hashable { return snapshot } } + +extension Array where Element: Hashable { + func snapshot() -> NSDiffableDataSourceSnapshot { + [self].snapshot() + } +} diff --git a/View Controllers/NewStatusViewController.swift b/View Controllers/NewStatusViewController.swift index 5de4216..66dcfb0 100644 --- a/View Controllers/NewStatusViewController.swift +++ b/View Controllers/NewStatusViewController.swift @@ -10,6 +10,7 @@ final class NewStatusViewController: UIViewController { private let viewModel: NewStatusViewModel private let scrollView = UIScrollView() private let stackView = UIStackView() + private let activityIndicatorView = UIActivityIndicatorView(style: .large) private let postButton = UIBarButtonItem( title: NSLocalizedString("post", comment: ""), style: .done, @@ -42,6 +43,10 @@ final class NewStatusViewController: UIViewController { stackView.axis = .vertical stackView.distribution = .equalSpacing + scrollView.addSubview(activityIndicatorView) + activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false + activityIndicatorView.hidesWhenStopped = true + NSLayoutConstraint.activate([ scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.topAnchor.constraint(equalTo: view.topAnchor), @@ -51,7 +56,9 @@ final class NewStatusViewController: UIViewController { stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), - stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor) + stackView.widthAnchor.constraint(equalTo: scrollView.widthAnchor), + activityIndicatorView.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor), + activityIndicatorView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor) ]) postButton.primaryAction = UIAction(title: NSLocalizedString("post", comment: "")) { [weak self] _ in @@ -83,6 +90,51 @@ private extension NewStatusViewController { } } + func apply(postingState: NewStatusViewModel.PostingState) { + switch postingState { + case .composing: + activityIndicatorView.stopAnimating() + stackView.isUserInteractionEnabled = true + stackView.alpha = 1 + case .posting: + activityIndicatorView.startAnimating() + stackView.isUserInteractionEnabled = false + stackView.alpha = 0.5 + case .done: + dismiss() + } + } + + func set(compositionViewModels: [CompositionViewModel]) { + let diff = compositionViewModels.map(\.id).snapshot().itemIdentifiers.difference( + from: stackView.arrangedSubviews.compactMap { ($0 as? CompositionView)?.id } + .snapshot().itemIdentifiers) + + for insertion in diff.insertions { + guard case let .insert(index, id, _) = insertion, + let compositionViewModel = compositionViewModels.first(where: { $0.id == id }) + else { continue } + + let compositionView = CompositionView( + viewModel: compositionViewModel, + parentViewModel: viewModel) + stackView.insertArrangedSubview(compositionView, at: index) + compositionView.textView.becomeFirstResponder() + + DispatchQueue.main.async { + self.scrollView.scrollRectToVisible( + self.scrollView.convert(compositionView.frame, from: self.stackView), + animated: true) + } + } + + for removal in diff.removals { + guard case let .remove(_, id, _) = removal else { continue } + + stackView.arrangedSubviews.first { ($0 as? CompositionView)?.id == id }?.removeFromSuperview() + } + } + func dismiss() { if let extensionContext = extensionContext { extensionContext.completeRequest(returningItems: nil) @@ -92,53 +144,23 @@ private extension NewStatusViewController { } func setupViewModelBindings() { - viewModel.events.sink { [weak self] in self?.handle(event: $0) }.store(in: &cancellables) - - viewModel.$canPost.sink { [weak self] in self?.postButton.isEnabled = $0 }.store(in: &cancellables) - - viewModel.$compositionViewModels.sink { [weak self] in - guard let self = self else { return } - - let diff = [$0.map(\.id)].snapshot().itemIdentifiers.difference( - from: [self.stackView.arrangedSubviews.compactMap { ($0 as? CompositionView)?.id }] - .snapshot().itemIdentifiers) - - for insertion in diff.insertions { - guard case let .insert(index, id, _) = insertion, - let compositionViewModel = $0.first(where: { $0.id == id }) - else { continue } - - let compositionView = CompositionView( - viewModel: compositionViewModel, - parentViewModel: self.viewModel) - self.stackView.insertArrangedSubview(compositionView, at: index) - compositionView.textView.becomeFirstResponder() - DispatchQueue.main.async { - self.scrollView.scrollRectToVisible( - self.scrollView.convert(compositionView.frame, from: self.stackView), - animated: true) - } - } - - for removal in diff.removals { - guard case let .remove(_, id, _) = removal else { continue } - - self.stackView.arrangedSubviews.first { ($0 as? CompositionView)?.id == id }?.removeFromSuperview() - } - } - .store(in: &cancellables) - - viewModel.$identification - .sink { [weak self] in - guard let self = self else { return } - - self.setupBarButtonItems(identification: $0) - } + viewModel.events + .sink { [weak self] in self?.handle(event: $0) } + .store(in: &cancellables) + viewModel.$canPost + .sink { [weak self] in self?.postButton.isEnabled = $0 } + .store(in: &cancellables) + viewModel.$compositionViewModels + .sink { [weak self] in self?.set(compositionViewModels: $0) } + .store(in: &cancellables) + viewModel.$identification + .sink { [weak self] in self?.setupBarButtonItems(identification: $0) } + .store(in: &cancellables) + viewModel.$postingState + .sink { [weak self] in self?.apply(postingState: $0) } .store(in: &cancellables) - viewModel.$alertItem .compactMap { $0 } - .receive(on: DispatchQueue.main) .sink { [weak self] in self?.present(alertItem: $0) } .store(in: &cancellables) } diff --git a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift index b75eee7..11bdc30 100644 --- a/ViewModels/Sources/ViewModels/NewStatusViewModel.swift +++ b/ViewModels/Sources/ViewModels/NewStatusViewModel.swift @@ -13,7 +13,7 @@ public final class NewStatusViewModel: ObservableObject { @Published public var canPost = false @Published public var canChangeIdentity = true @Published public var alertItem: AlertItem? - @Published public private(set) var loading = false + @Published public private(set) var postingState = PostingState.composing public let events: AnyPublisher private let allIdentitiesService: AllIdentitiesService @@ -36,8 +36,8 @@ public final class NewStatusViewModel: ObservableObject { $compositionViewModels.flatMap { Publishers.MergeMany($0.map(\.$isPostable)) } .receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring .compactMap { [weak self] _ in self?.compositionViewModels.allSatisfy(\.isPostable) } - .combineLatest($loading) - .map { $0 && !$1 } + .combineLatest($postingState) + .map { $0 && $1 == .composing } .assign(to: &$canPost) } } @@ -47,6 +47,12 @@ public extension NewStatusViewModel { case presentMediaPicker(CompositionViewModel) } + enum PostingState { + case composing + case posting + case done + } + func setIdentity(_ identity: Identity) { let identityService: IdentityService @@ -103,7 +109,7 @@ public extension NewStatusViewModel { private extension NewStatusViewModel { func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) { - loading = true + postingState = .posting identification.service.post(statusComponents: viewModel.components( inReplyToId: inReplyToId, visibility: visibility)) @@ -113,10 +119,12 @@ private extension NewStatusViewModel { switch $0 { case .finished: - self.loading = self.compositionViewModels.allSatisfy(\.isPosted) + if self.compositionViewModels.allSatisfy(\.isPosted) { + self.postingState = .done + } case let .failure(error): self.alertItem = AlertItem(error: error) - self.loading = false + self.postingState = .composing } } receiveValue: { [weak self] in guard let self = self else { return } diff --git a/Views/CompositionView.swift b/Views/CompositionView.swift index 8a4b301..f22c845 100644 --- a/Views/CompositionView.swift +++ b/Views/CompositionView.swift @@ -141,7 +141,7 @@ private extension CompositionView { viewModel.$attachmentViewModels .receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring .sink { [weak self] in - self?.attachmentsDataSource.apply([$0.map(\.attachment)].snapshot()) + self?.attachmentsDataSource.apply($0.map(\.attachment).snapshot()) self?.attachmentsCollectionView.isHidden = $0.isEmpty } .store(in: &cancellables)