feat: add PollSection and PollItem for diffable data source

This commit is contained in:
CMK 2021-03-02 16:27:11 +08:00
parent 80954b0492
commit 8b63c2fda1
9 changed files with 235 additions and 9 deletions

View File

@ -109,6 +109,9 @@
DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44384E25E8C1FA008912A2 /* CALayer.swift */; };
DB4481AD25EE155900BEFB67 /* Poll.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481AC25EE155900BEFB67 /* Poll.swift */; };
DB4481B325EE16D000BEFB67 /* PollOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B225EE16D000BEFB67 /* PollOption.swift */; };
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B825EE289600BEFB67 /* UITableView.swift */; };
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481C525EE2ADA00BEFB67 /* PollSection.swift */; };
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */; };
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */; };
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */; };
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */; };
@ -331,6 +334,9 @@
DB44384E25E8C1FA008912A2 /* CALayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CALayer.swift; sourceTree = "<group>"; };
DB4481AC25EE155900BEFB67 /* Poll.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poll.swift; sourceTree = "<group>"; };
DB4481B225EE16D000BEFB67 /* PollOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOption.swift; sourceTree = "<group>"; };
DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = "<group>"; };
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollSection.swift; sourceTree = "<group>"; };
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollItem.swift; sourceTree = "<group>"; };
DB4563BC25E11A24004DA0B9 /* KeyboardResponderService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardResponderService.swift; sourceTree = "<group>"; };
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIAlertController.swift; sourceTree = "<group>"; };
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIBarButtonItem.swift; sourceTree = "<group>"; };
@ -634,8 +640,8 @@
2D76319C25C151DE00929FB9 /* Diffiable */ = {
isa = PBXGroup;
children = (
2D7631B125C159E700929FB9 /* Item */,
2D76319D25C151F600929FB9 /* Section */,
2D7631B125C159E700929FB9 /* Item */,
);
path = Diffiable;
sourceTree = "<group>";
@ -644,6 +650,7 @@
isa = PBXGroup;
children = (
2D76319E25C1521200929FB9 /* StatusSection.swift */,
DB4481C525EE2ADA00BEFB67 /* PollSection.swift */,
);
path = Section;
sourceTree = "<group>";
@ -686,6 +693,7 @@
isa = PBXGroup;
children = (
2D7631B225C159F700929FB9 /* Item.swift */,
DB4481CB25EE2AFE00BEFB67 /* PollItem.swift */,
);
path = Item;
sourceTree = "<group>";
@ -1021,6 +1029,7 @@
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
2D42FF8E25C8228A004A627A /* UIButton.swift */,
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */,
DB4481B825EE289600BEFB67 /* UITableView.swift */,
2D32EAB925CB9B0500C9ED86 /* UIView.swift */,
0FAA101B25E10E760017CCDE /* UIFont.swift */,
2D650FAA25ECDC9300851B58 /* Mastodon+Entidy+ErrorDetailReason.swift */,
@ -1476,7 +1485,9 @@
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Toot.swift in Sources */,
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
@ -1501,6 +1512,7 @@
2D46976425C2A71500CF4AA9 /* UIIamge.swift in Sources */,
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
DB4563BD25E11A24004DA0B9 /* KeyboardResponderService.swift in Sources */,
DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */,
2DF75BA125D0E29D00694EC8 /* StatusProvider+TimelinePostTableViewCellDelegate.swift in Sources */,

View File

@ -0,0 +1,49 @@
//
// PollItem.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-2.
//
import Foundation
import CoreData
enum PollItem {
case pollOpion(objectID: NSManagedObjectID, attribute: Attribute)
}
extension PollItem {
class Attribute: Hashable {
var voted: Bool = false
static func == (lhs: PollItem.Attribute, rhs: PollItem.Attribute) -> Bool {
return lhs.voted == rhs.voted
}
func hash(into hasher: inout Hasher) {
hasher.combine(voted)
}
}
}
extension PollItem: Equatable {
static func == (lhs: PollItem, rhs: PollItem) -> Bool {
switch (lhs, rhs) {
case (.pollOpion(let objectIDLeft, _), .pollOpion(let objectIDRight, _)):
return objectIDLeft == objectIDRight
default:
return false
}
}
}
extension PollItem: Hashable {
func hash(into hasher: inout Hasher) {
switch self {
case .pollOpion(let objectID, _):
hasher.combine(objectID)
}
}
}

View File

@ -0,0 +1,25 @@
//
// PollSection.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-3-2.
//
import UIKit
import CoreData
import CoreDataStack
enum PollSection {
case main
}
extension PollSection {
static func tableViewDiffableDataSource(
for tableView: UITableView,
managedObjectContext: NSManagedObjectContext
) -> UITableViewDiffableDataSource<PollSection, PollItem> {
return UITableViewDiffableDataSource<PollSection, PollItem>(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
return nil
}
}
}

View File

@ -154,6 +154,9 @@ extension StatusSection {
cell.statusView.statusMosaicImageView.blurVisualEffectView.effect = isStatusSensitive ? MosaicImageViewContainer.blurVisualEffect : nil
cell.statusView.statusMosaicImageView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
// set poll
// toolbar
let replyCountTitle: String = {
let count = (toot.reblog ?? toot).repliesCount?.intValue ?? 0

View File

@ -0,0 +1,55 @@
//
// UITableView.swift
// Mastodon
//
// Created by Cirno MainasuK on 2021-3-2.
//
import UIKit
extension UITableView {
// static let groupedTableViewPaddingHeaderViewHeight: CGFloat = 16
// static var groupedTableViewPaddingHeaderView: UIView {
// return UIView(frame: CGRect(x: 0, y: 0, width: 100, height: groupedTableViewPaddingHeaderViewHeight))
// }
}
extension UITableView {
func deselectRow(with transitionCoordinator: UIViewControllerTransitionCoordinator?, animated: Bool) {
guard let indexPathForSelectedRow = indexPathForSelectedRow else { return }
guard let transitionCoordinator = transitionCoordinator else {
deselectRow(at: indexPathForSelectedRow, animated: animated)
return
}
transitionCoordinator.animate(alongsideTransition: { _ in
self.deselectRow(at: indexPathForSelectedRow, animated: animated)
}, completion: { context in
if context.isCancelled {
self.selectRow(at: indexPathForSelectedRow, animated: animated, scrollPosition: .none)
}
})
}
func blinkRow(at indexPath: IndexPath) {
DispatchQueue.main.asyncAfter(wallDeadline: .now() + 1) { [weak self] in
guard let self = self else { return }
guard let cell = self.cellForRow(at: indexPath) else { return }
let backgroundColor = cell.backgroundColor
UIView.animate(withDuration: 0.3) {
cell.backgroundColor = Asset.Colors.Label.highlight.color.withAlphaComponent(0.5)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
UIView.animate(withDuration: 0.3) {
cell.backgroundColor = backgroundColor
}
}
}
}
}
}

View File

@ -19,6 +19,7 @@ extension HomeTimelineViewController {
identifier: nil,
options: .displayInline,
children: [
moveMenu,
dropMenu,
UIAction(title: "Show Public Timeline", image: UIImage(systemName: "list.dash"), attributes: []) { [weak self] action in
guard let self = self else { return }
@ -33,6 +34,41 @@ extension HomeTimelineViewController {
return menu
}
var moveMenu: UIMenu {
return UIMenu(
title: "Move to…",
image: UIImage(systemName: "arrow.forward.circle"),
identifier: nil,
options: [],
children: [
UIAction(title: "First Gap", image: nil, attributes: [], handler: { [weak self] action in
guard let self = self else { return }
self.moveToTopGapAction(action)
}),
UIAction(title: "First Poll Toot", image: nil, attributes: [], handler: { [weak self] action in
guard let self = self else { return }
self.moveToFirstPollToot(action)
}),
// UIAction(title: "First Reply Toot", image: nil, attributes: [], handler: { [weak self] action in
// guard let self = self else { return }
// self.moveToFirstReplyToot(action)
// }),
// UIAction(title: "First Reply Reblog", image: nil, attributes: [], handler: { [weak self] action in
// guard let self = self else { return }
// self.moveToFirstReplyReblog(action)
// }),
// UIAction(title: "First Video Toot", image: nil, attributes: [], handler: { [weak self] action in
// guard let self = self else { return }
// self.moveToFirstVideoToot(action)
// }),
// UIAction(title: "First GIF Toot", image: nil, attributes: [], handler: { [weak self] action in
// guard let self = self else { return }
// self.moveToFirstGIFToot(action)
// }),
]
)
}
var dropMenu: UIMenu {
return UIMenu(
title: "Drop…",
@ -40,9 +76,9 @@ extension HomeTimelineViewController {
identifier: nil,
options: [],
children: [50, 100, 150, 200, 250, 300].map { count in
UIAction(title: "Drop Recent \(count) Tweets", image: nil, attributes: [], handler: { [weak self] action in
UIAction(title: "Drop Recent \(count) Toots", image: nil, attributes: [], handler: { [weak self] action in
guard let self = self else { return }
self.dropRecentTweetsAction(action, count: count)
self.dropRecentTootsAction(action, count: count)
})
}
)
@ -51,7 +87,42 @@ extension HomeTimelineViewController {
extension HomeTimelineViewController {
@objc private func dropRecentTweetsAction(_ sender: UIAction, count: Int) {
@objc private func moveToTopGapAction(_ sender: UIAction) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
let snapshotTransitioning = diffableDataSource.snapshot()
let item = snapshotTransitioning.itemIdentifiers.first(where: { item in
switch item {
case .homeMiddleLoader: return true
default: return false
}
})
if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) {
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
}
}
@objc private func moveToFirstPollToot(_ sender: UIAction) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
let snapshotTransitioning = diffableDataSource.snapshot()
let item = snapshotTransitioning.itemIdentifiers.first(where: { item in
switch item {
case .homeTimelineIndex(let objectID, _):
let homeTimelineIndex = viewModel.fetchedResultsController.managedObjectContext.object(with: objectID) as! HomeTimelineIndex
let toot = homeTimelineIndex.toot.reblog ?? homeTimelineIndex.toot
return toot.poll != nil
default:
return false
}
})
if let targetItem = item, let index = snapshotTransitioning.indexOfItem(targetItem) {
tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true)
tableView.blinkRow(at: IndexPath(row: index, section: 0))
} else {
print("Not found poll toot")
}
}
@objc private func dropRecentTootsAction(_ sender: UIAction, count: Int) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
let snapshotTransitioning = diffableDataSource.snapshot()

View File

@ -110,10 +110,10 @@ final class HomeTimelineViewModel: NSObject {
context.authenticationService.activeMastodonAuthentication
.sink { [weak self] activeMastodonAuthentication in
guard let self = self else { return }
guard let twitterAuthentication = activeMastodonAuthentication else { return }
let activeTwitterUserID = twitterAuthentication.userID
guard let mastodonAuthentication = activeMastodonAuthentication else { return }
let activeMastodonUserID = mastodonAuthentication.userID
let predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
HomeTimelineIndex.predicate(userID: activeTwitterUserID),
HomeTimelineIndex.predicate(userID: activeMastodonUserID),
HomeTimelineIndex.notDeleted()
])
self.timelinePredicate.value = predicate

View File

@ -23,6 +23,7 @@ final class StatusView: UIView {
weak var delegate: StatusViewDelegate?
var isStatusTextSensitive = false
var statusPollTableViewDataSource: UITableViewDiffableDataSource<PollSection, PollItem>?
let headerContainerStackView = UIStackView()
@ -101,6 +102,13 @@ final class StatusView: UIView {
}()
let statusMosaicImageView = MosaicImageViewContainer()
let statusPollTableView: UITableView = {
let tableView = UITableView()
tableView.register(PollTableViewCell.self, forCellReuseIdentifier: String(describing: PollTableViewCell.self))
tableView.isScrollEnabled = false
return tableView
}()
// do not use visual effect view due to we blur text only without background
let contentWarningBlurContentImageView: UIImageView = {
let imageView = UIImageView()
@ -222,7 +230,7 @@ extension StatusView {
subtitleContainerStackView.axis = .horizontal
subtitleContainerStackView.addArrangedSubview(usernameLabel)
// status container: [status | image / video | audio]
// status container: [status | image / video | audio | poll]
containerStackView.addArrangedSubview(statusContainerStackView)
statusContainerStackView.axis = .vertical
statusContainerStackView.spacing = 10
@ -258,7 +266,7 @@ extension StatusView {
statusContentWarningContainerStackView.addArrangedSubview(contentWarningTitle)
statusContentWarningContainerStackView.addArrangedSubview(contentWarningActionButton)
statusContainerStackView.addArrangedSubview(statusMosaicImageView)
statusContainerStackView.addArrangedSubview(statusPollTableView)
// action toolbar container
containerStackView.addArrangedSubview(actionToolbarContainer)
@ -266,6 +274,8 @@ extension StatusView {
headerContainerStackView.isHidden = true
statusMosaicImageView.isHidden = true
statusPollTableView.isHidden = true
contentWarningBlurContentImageView.isHidden = true
statusContentWarningContainerStackView.isHidden = true
statusContentWarningContainerStackViewBottomLayoutConstraint.isActive = false

View File

@ -33,6 +33,7 @@ final class StatusTableViewCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
statusView.isStatusTextSensitive = false
statusView.statusPollTableView.dataSource = nil
statusView.cleanUpContentWarning()
disposeBag.removeAll()
observations.removeAll()