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
}
}
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 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)
}

View file

@ -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<Event, Never>
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 }

View file

@ -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)