feat: add poll option reorder supports

This commit is contained in:
CMK 2021-03-24 15:46:40 +08:00
parent 0e84b4c164
commit 135e88c650
6 changed files with 93 additions and 44 deletions

View File

@ -13,11 +13,20 @@ protocol ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate: class {
} }
final class ComposeStatusPollOptionAppendEntryCollectionViewCell: UICollectionViewCell { final class ComposeStatusPollOptionAppendEntryCollectionViewCell: UICollectionViewCell {
let pollOptionView = PollOptionView() 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 let singleTagGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
weak var delegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate?
override var isHighlighted: Bool { override var isHighlighted: Bool {
didSet { didSet {
pollOptionView.roundedBackgroundView.backgroundColor = isHighlighted ? Asset.Colors.Background.secondarySystemBackground.color : Asset.Colors.Background.systemBackground.color 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() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
@ -53,10 +64,18 @@ extension ComposeStatusPollOptionAppendEntryCollectionViewCell {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor), pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor),
pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
pollOptionView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 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.checkmarkImageView.isHidden = true
pollOptionView.checkmarkBackgroundView.isHidden = true pollOptionView.checkmarkBackgroundView.isHidden = true
pollOptionView.optionPercentageLabel.isHidden = true pollOptionView.optionPercentageLabel.isHidden = true
@ -68,6 +87,8 @@ extension ComposeStatusPollOptionAppendEntryCollectionViewCell {
pollOptionView.addGestureRecognizer(singleTagGestureRecognizer) pollOptionView.addGestureRecognizer(singleTagGestureRecognizer)
singleTagGestureRecognizer.addTarget(self, action: #selector(ComposeStatusPollOptionAppendEntryCollectionViewCell.singleTagGestureRecognizerHandler(_:))) singleTagGestureRecognizer.addTarget(self, action: #selector(ComposeStatusPollOptionAppendEntryCollectionViewCell.singleTagGestureRecognizerHandler(_:)))
reorderBarImageView.isHidden = true
} }
private func setupBorderColor() { private func setupBorderColor() {

View File

@ -16,16 +16,29 @@ protocol ComposeStatusPollOptionCollectionViewCellDelegate: class {
final class ComposeStatusPollOptionCollectionViewCell: UICollectionViewCell { final class ComposeStatusPollOptionCollectionViewCell: UICollectionViewCell {
static let reorderHandlerImageLeadingMargin: CGFloat = 11
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()
weak var delegate: ComposeStatusPollOptionCollectionViewCellDelegate? weak var delegate: ComposeStatusPollOptionCollectionViewCellDelegate?
let pollOptionView = PollOptionView() 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 let singleTagGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer
private var pollOptionSubscription: AnyCancellable? private var pollOptionSubscription: AnyCancellable?
let pollOption = PassthroughSubject<String, Never>() let pollOption = PassthroughSubject<String, Never>()
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return pollOptionView.frame.contains(point)
}
override func prepareForReuse() { override func prepareForReuse() {
super.prepareForReuse() super.prepareForReuse()
@ -53,10 +66,18 @@ extension ComposeStatusPollOptionCollectionViewCell {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor), pollOptionView.topAnchor.constraint(equalTo: contentView.topAnchor),
pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), pollOptionView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
pollOptionView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
pollOptionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 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.checkmarkImageView.isHidden = true
pollOptionView.optionPercentageLabel.isHidden = true pollOptionView.optionPercentageLabel.isHidden = true
pollOptionView.optionTextField.text = nil pollOptionView.optionTextField.text = nil

View File

@ -150,9 +150,6 @@ extension ComposeViewController {
]) ])
collectionView.delegate = self 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( viewModel.setupDiffableDataSource(
for: collectionView, for: collectionView,
dependency: self, dependency: self,
@ -162,6 +159,8 @@ extension ComposeViewController {
composeStatusNewPollOptionCollectionViewCellDelegate: self, composeStatusNewPollOptionCollectionViewCellDelegate: self,
composeStatusPollExpiresOptionCollectionViewCellDelegate: self composeStatusPollExpiresOptionCollectionViewCellDelegate: self
) )
let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ComposeViewController.longPressReorderGestureHandler(_:)))
collectionView.addGestureRecognizer(longPressReorderGesture)
// respond scrollView overlap change // respond scrollView overlap change
view.layoutIfNeeded() view.layoutIfNeeded()
@ -389,13 +388,20 @@ extension ComposeViewController {
dismiss(animated: true, completion: nil) 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) { @objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) {
switch(sender.state) { switch(sender.state) {
case .began: 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 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) collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed: case .changed:
guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)), guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)),
@ -403,19 +409,20 @@ extension ComposeViewController {
break break
} }
guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath), guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath),
case .attachment = item else { case .pollOption = item else {
collectionView.cancelInteractiveMovement() collectionView.cancelInteractiveMovement()
return 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: case .ended:
collectionView.endInteractiveMovement() collectionView.endInteractiveMovement()
default: default:
collectionView.cancelInteractiveMovement() collectionView.cancelInteractiveMovement()
} }
} }
*/
} }
@ -571,8 +578,8 @@ extension ComposeViewController: ComposeToolbarViewDelegate {
viewModel.isPollComposing.value.toggle() viewModel.isPollComposing.value.toggle()
// setup initial poll option if needs // setup initial poll option if needs
if viewModel.isPollComposing.value, viewModel.pollAttributes.value.isEmpty { if viewModel.isPollComposing.value, viewModel.pollOptionAttributes.value.isEmpty {
viewModel.pollAttributes.value = [ComposeStatusItem.ComposePollOptionAttribute(), ComposeStatusItem.ComposePollOptionAttribute()] viewModel.pollOptionAttributes.value = [ComposeStatusItem.ComposePollOptionAttribute(), ComposeStatusItem.ComposePollOptionAttribute()]
} }
if viewModel.isPollComposing.value { if viewModel.isPollComposing.value {
@ -708,7 +715,7 @@ extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelega
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
guard case let .pollOption(attribute) = item 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 } guard let index = pollAttributes.firstIndex(of: attribute) else { return }
// mark previous (fallback to next) item of removed middle poll option become first responder // 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) pollAttributes.remove(at: index)
// update data source // update data source
viewModel.pollAttributes.value = pollAttributes viewModel.pollOptionAttributes.value = pollAttributes
} }
// handle keyboard return event for poll option input // handle keyboard return event for poll option input

View File

@ -31,26 +31,26 @@ extension ComposeViewModel {
composeStatusPollExpiresOptionCollectionViewCellDelegate: composeStatusPollExpiresOptionCollectionViewCellDelegate composeStatusPollExpiresOptionCollectionViewCellDelegate: composeStatusPollExpiresOptionCollectionViewCellDelegate
) )
// Note: do not allow reorder due to the images display order following the upload time diffableDataSource.reorderingHandlers.canReorderItem = { item in
// diffableDataSource.reorderingHandlers.canReorderItem = { item in switch item {
// switch item { case .pollOption: return true
// case .attachment: return true default: return false
// default: return false }
// } }
//
// } // update reordered data source
// diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in
// guard let self = self else { return } guard let self = self else { return }
//
// let items = transaction.finalSnapshot.itemIdentifiers let items = transaction.finalSnapshot.itemIdentifiers
// var attachmentServices: [MastodonAttachmentService] = [] var pollOptionAttributes: [ComposeStatusItem.ComposePollOptionAttribute] = []
// for item in items { for item in items {
// guard case let .attachment(attachmentService) = item else { continue } guard case let .pollOption(attribute) = item else { continue }
// attachmentServices.append(attachmentService) pollOptionAttributes.append(attribute)
// } }
// self.attachmentServices.value = attachmentServices self.pollOptionAttributes.value = pollOptionAttributes
// } }
//
self.diffableDataSource = diffableDataSource self.diffableDataSource = diffableDataSource
var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>() var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusSection, ComposeStatusItem>()

View File

@ -55,7 +55,7 @@ extension ComposeViewModel.PublishState {
} }
let pollOptions: [String]? = { let pollOptions: [String]? = {
guard viewModel.isPollComposing.value else { return nil } 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? = { let pollExpiresIn: Int? = {
guard viewModel.isPollComposing.value else { return nil } guard viewModel.isPollComposing.value else { return nil }

View File

@ -52,7 +52,7 @@ final class ComposeViewModel {
let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([]) let attachmentServices = CurrentValueSubject<[MastodonAttachmentService], Never>([])
// polls // polls
let pollAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollOptionAttribute], Never>([]) let pollOptionAttributes = CurrentValueSubject<[ComposeStatusItem.ComposePollOptionAttribute], Never>([])
let pollExpiresOptionAttribute = ComposeStatusItem.ComposePollExpiresOptionAttribute() let pollExpiresOptionAttribute = ComposeStatusItem.ComposePollExpiresOptionAttribute()
init( init(
@ -105,7 +105,7 @@ final class ComposeViewModel {
.map { services in .map { services in
services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish } services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish }
} }
let isPollAttributeAllValid = pollAttributes let isPollAttributeAllValid = pollOptionAttributes
.map { pollAttributes in .map { pollAttributes in
pollAttributes.allSatisfy { attribute -> Bool in pollAttributes.allSatisfy { attribute -> Bool in
!attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty !attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
@ -177,7 +177,7 @@ final class ComposeViewModel {
Publishers.CombineLatest3( Publishers.CombineLatest3(
attachmentServices.eraseToAnyPublisher(), attachmentServices.eraseToAnyPublisher(),
isPollComposing.eraseToAnyPublisher(), isPollComposing.eraseToAnyPublisher(),
pollAttributes.eraseToAnyPublisher() pollOptionAttributes.eraseToAnyPublisher()
) )
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak self] attachmentServices, isPollComposing, pollAttributes in .sink { [weak self] attachmentServices, isPollComposing, pollAttributes in
@ -240,7 +240,7 @@ final class ComposeViewModel {
} }
.store(in: &disposeBag) .store(in: &disposeBag)
pollAttributes pollOptionAttributes
.sink { [weak self] pollAttributes in .sink { [weak self] pollAttributes in
guard let self = self else { return } guard let self = self else { return }
pollAttributes.forEach { $0.delegate = self } pollAttributes.forEach { $0.delegate = self }
@ -268,10 +268,10 @@ final class ComposeViewModel {
extension ComposeViewModel { extension ComposeViewModel {
func createNewPollOptionIfPossible() { func createNewPollOptionIfPossible() {
guard pollAttributes.value.count < 4 else { return } guard pollOptionAttributes.value.count < 4 else { return }
let attribute = ComposeStatusItem.ComposePollOptionAttribute() let attribute = ComposeStatusItem.ComposePollOptionAttribute()
pollAttributes.value = pollAttributes.value + [attribute] pollOptionAttributes.value = pollOptionAttributes.value + [attribute]
} }
} }
@ -287,6 +287,6 @@ extension ComposeViewModel: MastodonAttachmentServiceDelegate {
extension ComposeViewModel: ComposePollAttributeDelegate { extension ComposeViewModel: ComposePollAttributeDelegate {
func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) { func composePollAttribute(_ attribute: ComposeStatusItem.ComposePollOptionAttribute, pollOptionDidChange: String?) {
// trigger update // trigger update
pollAttributes.value = pollAttributes.value pollOptionAttributes.value = pollOptionAttributes.value
} }
} }