From 135e88c650e7c63fc4d0a5bd2a472bf66def2cdc Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 24 Mar 2021 15:46:40 +0800 Subject: [PATCH] feat: add poll option reorder supports --- ...lOptionAppendEntryCollectionViewCell.swift | 27 +++++++++++-- ...seStatusPollOptionCollectionViewCell.swift | 23 ++++++++++- .../Scene/Compose/ComposeViewController.swift | 31 ++++++++------ .../Compose/ComposeViewModel+Diffable.swift | 40 +++++++++---------- .../ComposeViewModel+PublishState.swift | 2 +- Mastodon/Scene/Compose/ComposeViewModel.swift | 14 +++---- 6 files changed, 93 insertions(+), 44 deletions(-) diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift index 9479575b..2c321f51 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionAppendEntryCollectionViewCell.swift @@ -13,11 +13,20 @@ protocol ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate: class { } final class ComposeStatusPollOptionAppendEntryCollectionViewCell: UICollectionViewCell { - + let pollOptionView = PollOptionView() + let reorderBarImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.image = UIImage(systemName: "line.horizontal.3")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)).withRenderingMode(.alwaysTemplate) + imageView.tintColor = Asset.Colors.Label.secondary.color + return imageView + }() let singleTagGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer + weak var delegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate? + override var isHighlighted: Bool { didSet { pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? Asset.Colors.Background.secondarySystemBackground.color : Asset.Colors.Background.systemBackground.color @@ -25,7 +34,9 @@ final class ComposeStatusPollOptionAppendEntryCollectionViewCell: UICollectionVi } } - weak var delegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate? + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return pollOptionView.frame.contains(point) + } override func prepareForReuse() { super.prepareForReuse() @@ -53,10 +64,18 @@ extension ComposeStatusPollOptionAppendEntryCollectionViewCell { NSLayoutConstraint.activate([ pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor), pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - pollOptionView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) + reorderBarImageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(reorderBarImageView) + NSLayoutConstraint.activate([ + reorderBarImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + reorderBarImageView.leadingAnchor.constraint(equalTo: pollOptionView.trailingAnchor, constant: ComposeStatusPollOptionCollectionViewCell.reorderHandlerImageLeadingMargin), + reorderBarImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + reorderBarImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + pollOptionView.checkmarkImageView.isHidden = true pollOptionView.checkmarkBackgroundView.isHidden = true pollOptionView.optionPercentageLabel.isHidden = true @@ -68,6 +87,8 @@ extension ComposeStatusPollOptionAppendEntryCollectionViewCell { pollOptionView.addGestureRecognizer(singleTagGestureRecognizer) singleTagGestureRecognizer.addTarget(self, action: #selector(ComposeStatusPollOptionAppendEntryCollectionViewCell.singleTagGestureRecognizerHandler(_:))) + + reorderBarImageView.isHidden = true } private func setupBorderColor() { diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift index 3d930682..8935d804 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollOptionCollectionViewCell.swift @@ -16,16 +16,29 @@ protocol ComposeStatusPollOptionCollectionViewCellDelegate: class { final class ComposeStatusPollOptionCollectionViewCell: UICollectionViewCell { + static let reorderHandlerImageLeadingMargin: CGFloat = 11 + var disposeBag = Set() weak var delegate: ComposeStatusPollOptionCollectionViewCellDelegate? let pollOptionView = PollOptionView() + let reorderBarImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.image = UIImage(systemName: "line.horizontal.3")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)).withRenderingMode(.alwaysTemplate) + imageView.tintColor = Asset.Colors.Label.secondary.color + return imageView + }() let singleTagGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer private var pollOptionSubscription: AnyCancellable? let pollOption = PassthroughSubject() + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + return pollOptionView.frame.contains(point) + } + override func prepareForReuse() { super.prepareForReuse() @@ -53,10 +66,18 @@ extension ComposeStatusPollOptionCollectionViewCell { NSLayoutConstraint.activate([ pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor), pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - pollOptionView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) + reorderBarImageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(reorderBarImageView) + NSLayoutConstraint.activate([ + reorderBarImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + reorderBarImageView.leadingAnchor.constraint(equalTo: pollOptionView.trailingAnchor, constant: ComposeStatusPollOptionCollectionViewCell.reorderHandlerImageLeadingMargin), + reorderBarImageView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + reorderBarImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + pollOptionView.checkmarkImageView.isHidden = true pollOptionView.optionPercentageLabel.isHidden = true pollOptionView.optionTextField.text = nil diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 95398ca1..19605ff3 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -150,9 +150,6 @@ extension ComposeViewController { ]) collectionView.delegate = self - // Note: do not allow reorder due to the images display order following the upload time - // let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:))) - // collectionView.addGestureRecognizer(longPressReorderGesture) viewModel.setupDiffableDataSource( for: collectionView, dependency: self, @@ -162,6 +159,8 @@ extension ComposeViewController { composeStatusNewPollOptionCollectionViewCellDelegate: self, composeStatusPollExpiresOptionCollectionViewCellDelegate: self ) + let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:))) + collectionView.addGestureRecognizer(longPressReorderGesture) // respond scrollView overlap change view.layoutIfNeeded() @@ -389,13 +388,20 @@ extension ComposeViewController { dismiss(animated: true, completion: nil) } - /* Do not allow reorder image due to image display order following the update time + // seealso: ComposeViewModel.setupDiffableDataSource(…) @objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) { switch(sender.state) { case .began: - guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)) else { + guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)), + let cell = collectionView.cellForItem(at: selectedIndexPath) as? ComposeStatusPollOptionCollectionViewCell else { break } + // check if pressing reorder bar no not + let locationInCell = sender.location(in: cell) + guard cell.reorderBarImageView.frame.contains(locationInCell) else { + return + } + collectionView.beginInteractiveMovementForItem(at: selectedIndexPath) case .changed: guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)), @@ -403,19 +409,20 @@ extension ComposeViewController { break } guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath), - case .attachment = item else { + case .pollOption = item else { collectionView.cancelInteractiveMovement() return } - collectionView.updateInteractiveMovementTargetPosition(sender.location(in: collectionView)) + var position = sender.location(in: collectionView) + position.x = collectionView.frame.width * 0.5 + collectionView.updateInteractiveMovementTargetPosition(position) case .ended: collectionView.endInteractiveMovement() default: collectionView.cancelInteractiveMovement() } } - */ } @@ -571,8 +578,8 @@ extension ComposeViewController: ComposeToolbarViewDelegate { viewModel.isPollComposing.value.toggle() // setup initial poll option if needs - if viewModel.isPollComposing.value, viewModel.pollAttributes.value.isEmpty { - viewModel.pollAttributes.value = [ComposeStatusItem.ComposePollOptionAttribute(), ComposeStatusItem.ComposePollOptionAttribute()] + if viewModel.isPollComposing.value, viewModel.pollOptionAttributes.value.isEmpty { + viewModel.pollOptionAttributes.value = [ComposeStatusItem.ComposePollOptionAttribute(), ComposeStatusItem.ComposePollOptionAttribute()] } if viewModel.isPollComposing.value { @@ -708,7 +715,7 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } guard case let .pollOption(attribute) = item else { return } - var pollAttributes = viewModel.pollAttributes.value + var pollAttributes = viewModel.pollOptionAttributes.value guard let index = pollAttributes.firstIndex(of: attribute) else { return } // mark previous (fallback to next) item of removed middle poll option become first responder @@ -741,7 +748,7 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega pollAttributes.remove(at: index) // update data source - viewModel.pollAttributes.value = pollAttributes + viewModel.pollOptionAttributes.value = pollAttributes } // handle keyboard return event for poll option input diff --git a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift index bfca4a39..3f9f3d3f 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+Diffable.swift @@ -31,26 +31,26 @@ extension ComposeViewModel { composeStatusPollExpiresOptionCollectionViewCellDelegate: composeStatusPollExpiresOptionCollectionViewCellDelegate ) - // Note: do not allow reorder due to the images display order following the upload time - // diffableDataSource.reorderingHandlers.canReorderItem = { item in - // switch item { - // case .attachment: return true - // default: return false - // } - // - // } - // diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in - // guard let self = self else { return } - // - // let items = transaction.finalSnapshot.itemIdentifiers - // var attachmentServices: [MastodonAttachmentService] = [] - // for item in items { - // guard case let .attachment(attachmentService) = item else { continue } - // attachmentServices.append(attachmentService) - // } - // self.attachmentServices.value = attachmentServices - // } - // + diffableDataSource.reorderingHandlers.canReorderItem = { item in + switch item { + case .pollOption: return true + default: return false + } + } + + // update reordered data source + diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in + guard let self = self else { return } + + let items = transaction.finalSnapshot.itemIdentifiers + var pollOptionAttributes: [ComposeStatusItem.ComposePollOptionAttribute] = [] + for item in items { + guard case let .pollOption(attribute) = item else { continue } + pollOptionAttributes.append(attribute) + } + self.pollOptionAttributes.value = pollOptionAttributes + } + self.diffableDataSource = diffableDataSource var snapshot = NSDiffableDataSourceSnapshot() diff --git a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift index 040ba26c..85ad4668 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift @@ -55,7 +55,7 @@ extension ComposeViewModel.PublishState { } let pollOptions: [String]? = { guard viewModel.isPollComposing.value else { return nil } - return viewModel.pollAttributes.value.map { attribute in attribute.option.value } + return viewModel.pollOptionAttributes.value.map { attribute in attribute.option.value } }() let pollExpiresIn: Int? = { guard viewModel.isPollComposing.value else { return nil } diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index e423312e..80cd6769 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -52,7 +52,7 @@ final class ComposeViewModel { let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([]) // polls - let pollAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollOptionAttribute], Never>([]) + let pollOptionAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollOptionAttribute], Never>([]) let pollExpiresOptionAttribute = ComposeStatusItem.ComposePollExpiresOptionAttribute() init( @@ -105,7 +105,7 @@ final class ComposeViewModel { .map { services in services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish } } - let isPollAttributeAllValid = pollAttributes + let isPollAttributeAllValid = pollOptionAttributes .map { pollAttributes in pollAttributes.allSatisfy { attribute -> Bool in !attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty @@ -177,7 +177,7 @@ final class ComposeViewModel { Publishers.CombineLatest3( attachmentServices.eraseToAnyPublisher(), isPollComposing.eraseToAnyPublisher(), - pollAttributes.eraseToAnyPublisher() + pollOptionAttributes.eraseToAnyPublisher() ) .receive(on: DispatchQueue.main) .sink { [weak self] attachmentServices, isPollComposing, pollAttributes in @@ -240,7 +240,7 @@ final class ComposeViewModel { } .store(in: &disposeBag) - pollAttributes + pollOptionAttributes .sink { [weak self] pollAttributes in guard let self = self else { return } pollAttributes.forEach { $0.delegate = self } @@ -268,10 +268,10 @@ final class ComposeViewModel { extension ComposeViewModel { func createNewPollOptionIfPossible() { - guard pollAttributes.value.count < 4 else { return } + guard pollOptionAttributes.value.count < 4 else { return } let attribute = ComposeStatusItem.ComposePollOptionAttribute() - pollAttributes.value = pollAttributes.value + [attribute] + pollOptionAttributes.value = pollOptionAttributes.value + [attribute] } } @@ -287,6 +287,6 @@ extension ComposeViewModel: MastodonAttachmentServiceDelegate { extension ComposeViewModel: ComposePollAttributeDelegate { func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) { // trigger update - pollAttributes.value = pollAttributes.value + pollOptionAttributes.value = pollOptionAttributes.value } }