This commit is contained in:
Justin Mazzocchi 2021-01-01 12:18:10 -08:00
parent 86b9e4c903
commit 3b0f0bf82f
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
4 changed files with 88 additions and 52 deletions

View file

@ -17,3 +17,9 @@ extension Array where Element: Sequence, Element.Element: Hashable {
return snapshot return snapshot
} }
} }
extension Array where Element: Hashable {
func snapshot() -> NSDiffableDataSourceSnapshot<Int, Element> {
[self].snapshot()
}
}

View file

@ -10,6 +10,7 @@ final class NewStatusViewController: UIViewController {
private let viewModel: NewStatusViewModel private let viewModel: NewStatusViewModel
private let scrollView = UIScrollView() private let scrollView = UIScrollView()
private let stackView = UIStackView() private let stackView = UIStackView()
private let activityIndicatorView = UIActivityIndicatorView(style: .large)
private let postButton = UIBarButtonItem( private let postButton = UIBarButtonItem(
title: NSLocalizedString("post", comment: ""), title: NSLocalizedString("post", comment: ""),
style: .done, style: .done,
@ -42,6 +43,10 @@ final class NewStatusViewController: UIViewController {
stackView.axis = .vertical stackView.axis = .vertical
stackView.distribution = .equalSpacing stackView.distribution = .equalSpacing
scrollView.addSubview(activityIndicatorView)
activityIndicatorView.translatesAutoresizingMaskIntoConstraints = false
activityIndicatorView.hidesWhenStopped = true
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.topAnchor.constraint(equalTo: view.topAnchor), scrollView.topAnchor.constraint(equalTo: view.topAnchor),
@ -51,7 +56,9 @@ final class NewStatusViewController: UIViewController {
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), stackView.topAnchor.constraint(equalTo: scrollView.topAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), 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 postButton.primaryAction = UIAction(title: NSLocalizedString("post", comment: "")) { [weak self] _ in
@ -83,36 +90,37 @@ private extension NewStatusViewController {
} }
} }
func dismiss() { func apply(postingState: NewStatusViewModel.PostingState) {
if let extensionContext = extensionContext { switch postingState {
extensionContext.completeRequest(returningItems: nil) case .composing:
} else { activityIndicatorView.stopAnimating()
presentingViewController?.dismiss(animated: true) stackView.isUserInteractionEnabled = true
stackView.alpha = 1
case .posting:
activityIndicatorView.startAnimating()
stackView.isUserInteractionEnabled = false
stackView.alpha = 0.5
case .done:
dismiss()
} }
} }
func setupViewModelBindings() { func set(compositionViewModels: [CompositionViewModel]) {
viewModel.events.sink { [weak self] in self?.handle(event: $0) }.store(in: &cancellables) let diff = compositionViewModels.map(\.id).snapshot().itemIdentifiers.difference(
from: stackView.arrangedSubviews.compactMap { ($0 as? CompositionView)?.id }
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) .snapshot().itemIdentifiers)
for insertion in diff.insertions { for insertion in diff.insertions {
guard case let .insert(index, id, _) = insertion, guard case let .insert(index, id, _) = insertion,
let compositionViewModel = $0.first(where: { $0.id == id }) let compositionViewModel = compositionViewModels.first(where: { $0.id == id })
else { continue } else { continue }
let compositionView = CompositionView( let compositionView = CompositionView(
viewModel: compositionViewModel, viewModel: compositionViewModel,
parentViewModel: self.viewModel) parentViewModel: viewModel)
self.stackView.insertArrangedSubview(compositionView, at: index) stackView.insertArrangedSubview(compositionView, at: index)
compositionView.textView.becomeFirstResponder() compositionView.textView.becomeFirstResponder()
DispatchQueue.main.async { DispatchQueue.main.async {
self.scrollView.scrollRectToVisible( self.scrollView.scrollRectToVisible(
self.scrollView.convert(compositionView.frame, from: self.stackView), self.scrollView.convert(compositionView.frame, from: self.stackView),
@ -123,22 +131,36 @@ private extension NewStatusViewController {
for removal in diff.removals { for removal in diff.removals {
guard case let .remove(_, id, _) = removal else { continue } guard case let .remove(_, id, _) = removal else { continue }
self.stackView.arrangedSubviews.first { ($0 as? CompositionView)?.id == id }?.removeFromSuperview() stackView.arrangedSubviews.first { ($0 as? CompositionView)?.id == id }?.removeFromSuperview()
} }
} }
.store(in: &cancellables)
func dismiss() {
if let extensionContext = extensionContext {
extensionContext.completeRequest(returningItems: nil)
} else {
presentingViewController?.dismiss(animated: true)
}
}
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 self?.set(compositionViewModels: $0) }
.store(in: &cancellables)
viewModel.$identification viewModel.$identification
.sink { [weak self] in .sink { [weak self] in self?.setupBarButtonItems(identification: $0) }
guard let self = self else { return } .store(in: &cancellables)
viewModel.$postingState
self.setupBarButtonItems(identification: $0) .sink { [weak self] in self?.apply(postingState: $0) }
}
.store(in: &cancellables) .store(in: &cancellables)
viewModel.$alertItem viewModel.$alertItem
.compactMap { $0 } .compactMap { $0 }
.receive(on: DispatchQueue.main)
.sink { [weak self] in self?.present(alertItem: $0) } .sink { [weak self] in self?.present(alertItem: $0) }
.store(in: &cancellables) .store(in: &cancellables)
} }

View file

@ -13,7 +13,7 @@ public final class NewStatusViewModel: ObservableObject {
@Published public var canPost = false @Published public var canPost = false
@Published public var canChangeIdentity = true @Published public var canChangeIdentity = true
@Published public var alertItem: AlertItem? @Published public var alertItem: AlertItem?
@Published public private(set) var loading = false @Published public private(set) var postingState = PostingState.composing
public let events: AnyPublisher<Event, Never> public let events: AnyPublisher<Event, Never>
private let allIdentitiesService: AllIdentitiesService private let allIdentitiesService: AllIdentitiesService
@ -36,8 +36,8 @@ public final class NewStatusViewModel: ObservableObject {
$compositionViewModels.flatMap { Publishers.MergeMany($0.map(\.$isPostable)) } $compositionViewModels.flatMap { Publishers.MergeMany($0.map(\.$isPostable)) }
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring .receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
.compactMap { [weak self] _ in self?.compositionViewModels.allSatisfy(\.isPostable) } .compactMap { [weak self] _ in self?.compositionViewModels.allSatisfy(\.isPostable) }
.combineLatest($loading) .combineLatest($postingState)
.map { $0 && !$1 } .map { $0 && $1 == .composing }
.assign(to: &$canPost) .assign(to: &$canPost)
} }
} }
@ -47,6 +47,12 @@ public extension NewStatusViewModel {
case presentMediaPicker(CompositionViewModel) case presentMediaPicker(CompositionViewModel)
} }
enum PostingState {
case composing
case posting
case done
}
func setIdentity(_ identity: Identity) { func setIdentity(_ identity: Identity) {
let identityService: IdentityService let identityService: IdentityService
@ -103,7 +109,7 @@ public extension NewStatusViewModel {
private extension NewStatusViewModel { private extension NewStatusViewModel {
func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) { func post(viewModel: CompositionViewModel, inReplyToId: Status.Id?) {
loading = true postingState = .posting
identification.service.post(statusComponents: viewModel.components( identification.service.post(statusComponents: viewModel.components(
inReplyToId: inReplyToId, inReplyToId: inReplyToId,
visibility: visibility)) visibility: visibility))
@ -113,10 +119,12 @@ private extension NewStatusViewModel {
switch $0 { switch $0 {
case .finished: case .finished:
self.loading = self.compositionViewModels.allSatisfy(\.isPosted) if self.compositionViewModels.allSatisfy(\.isPosted) {
self.postingState = .done
}
case let .failure(error): case let .failure(error):
self.alertItem = AlertItem(error: error) self.alertItem = AlertItem(error: error)
self.loading = false self.postingState = .composing
} }
} receiveValue: { [weak self] in } receiveValue: { [weak self] in
guard let self = self else { return } guard let self = self else { return }

View file

@ -141,7 +141,7 @@ private extension CompositionView {
viewModel.$attachmentViewModels viewModel.$attachmentViewModels
.receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring .receive(on: DispatchQueue.main) // hack to punt to next run loop, consider refactoring
.sink { [weak self] in .sink { [weak self] in
self?.attachmentsDataSource.apply([$0.map(\.attachment)].snapshot()) self?.attachmentsDataSource.apply($0.map(\.attachment).snapshot())
self?.attachmentsCollectionView.isHidden = $0.isEmpty self?.attachmentsCollectionView.isHidden = $0.isEmpty
} }
.store(in: &cancellables) .store(in: &cancellables)