feat: make toot poll display

This commit is contained in:
CMK 2021-03-02 19:10:45 +08:00
parent 8b63c2fda1
commit aea2ddc078
9 changed files with 112 additions and 30 deletions

View File

@ -150,7 +150,7 @@
<relationship name="favouritedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
<relationship name="homeTimelineIndexes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="HomeTimelineIndex" inverseName="toot" inverseEntity="HomeTimelineIndex"/>
<relationship name="mediaAttachments" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Attachment" inverseName="toot" inverseEntity="Attachment"/>
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Mention" inverseName="toot" inverseEntity="Mention"/>
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="Mention" inverseName="toot" inverseEntity="Mention"/>
<relationship name="mutedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedToot" inverseEntity="MastodonUser"/>
<relationship name="poll" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Poll" inverseName="toot" inverseEntity="Poll"/>
@ -168,9 +168,9 @@
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="299"/>
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
<element name="Toot" positionX="0" positionY="0" width="128" height="539"/>
<element name="Poll" positionX="72" positionY="162" width="128" height="179"/>
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
<element name="Toot" positionX="0" positionY="0" width="128" height="539"/>
</elements>
</model>

View File

@ -153,7 +153,7 @@
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */; };
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF55C25C138B7002E6C99 /* UIViewController.swift */; };
DB8AF56825C13E2A002E6C99 /* HomeTimelineIndex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */; };
DB92CF7225E7BB98002C1017 /* PollTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB92CF7125E7BB98002C1017 /* PollTableViewCell.swift */; };
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */; };
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; };
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; };
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
@ -379,7 +379,7 @@
DB8AF54F25C13703002E6C99 /* MainTabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainTabBarController.swift; sourceTree = "<group>"; };
DB8AF55C25C138B7002E6C99 /* UIViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
DB8AF56725C13E2A002E6C99 /* HomeTimelineIndex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeTimelineIndex.swift; sourceTree = "<group>"; };
DB92CF7125E7BB98002C1017 /* PollTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableViewCell.swift; sourceTree = "<group>"; };
DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollOptionTableViewCell.swift; sourceTree = "<group>"; };
DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = "<group>"; };
DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = "<group>"; };
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = "<group>"; };
@ -684,7 +684,7 @@
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */,
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */,
DB92CF7125E7BB98002C1017 /* PollTableViewCell.swift */,
DB92CF7125E7BB98002C1017 /* PollOptionTableViewCell.swift */,
);
path = TableviewCell;
sourceTree = "<group>";
@ -1469,7 +1469,7 @@
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
0FAA101225E105390017CCDE /* PrimaryActionButton.swift in Sources */,
DB8AF53025C13561002E6C99 /* AppContext.swift in Sources */,
DB92CF7225E7BB98002C1017 /* PollTableViewCell.swift in Sources */,
DB92CF7225E7BB98002C1017 /* PollOptionTableViewCell.swift in Sources */,
DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */,
2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */,
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,

View File

@ -9,7 +9,7 @@ import Foundation
import CoreData
enum PollItem {
case pollOpion(objectID: NSManagedObjectID, attribute: Attribute)
case opion(objectID: NSManagedObjectID, attribute: Attribute)
}
@ -17,6 +17,10 @@ extension PollItem {
class Attribute: Hashable {
var voted: Bool = false
init(voted: Bool = false) {
self.voted = voted
}
static func == (lhs: PollItem.Attribute, rhs: PollItem.Attribute) -> Bool {
return lhs.voted == rhs.voted
}
@ -30,10 +34,8 @@ extension PollItem {
extension PollItem: Equatable {
static func == (lhs: PollItem, rhs: PollItem) -> Bool {
switch (lhs, rhs) {
case (.pollOpion(let objectIDLeft, _), .pollOpion(let objectIDRight, _)):
case (.opion(let objectIDLeft, _), .opion(let objectIDRight, _)):
return objectIDLeft == objectIDRight
default:
return false
}
}
}
@ -42,7 +44,7 @@ extension PollItem: Equatable {
extension PollItem: Hashable {
func hash(into hasher: inout Hasher) {
switch self {
case .pollOpion(let objectID, _):
case .opion(let objectID, _):
hasher.combine(objectID)
}
}

View File

@ -9,7 +9,7 @@ import UIKit
import CoreData
import CoreDataStack
enum PollSection {
enum PollSection: Equatable, Hashable {
case main
}
@ -19,7 +19,25 @@ extension PollSection {
managedObjectContext: NSManagedObjectContext
) -> UITableViewDiffableDataSource<PollSection, PollItem> {
return UITableViewDiffableDataSource<PollSection, PollItem>(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
return nil
switch item {
case .opion(let objectID, let attribute):
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PollOptionTableViewCell.self), for: indexPath) as! PollOptionTableViewCell
managedObjectContext.performAndWait {
let option = managedObjectContext.object(with: objectID) as! PollOption
PollSection.configure(cell: cell, pollOption: option, itemAttribute: attribute)
}
return cell
}
}
}
}
extension PollSection {
static func configure(
cell: PollOptionTableViewCell,
pollOption: PollOption,
itemAttribute: PollItem.Attribute
) {
cell.optionLabel.text = pollOption.title
}
}

View File

@ -155,7 +155,31 @@ extension StatusSection {
cell.statusView.statusMosaicImageView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
// set poll
if let poll = (toot.reblog ?? toot).poll {
cell.statusView.statusPollTableView.isHidden = false
let managedObjectContext = toot.managedObjectContext!
cell.statusView.statusPollTableViewDataSource = PollSection.tableViewDiffableDataSource(
for: cell.statusView.statusPollTableView,
managedObjectContext: managedObjectContext
)
var snapshot = NSDiffableDataSourceSnapshot<PollSection, PollItem>()
snapshot.appendSections([.main])
let pollItems = poll.options
.sorted(by: { $0.index.intValue < $1.index.intValue })
.map { option -> PollItem in
let isVoted = (option.votedBy ?? Set()).map { $0.id }.contains(requestUserID)
let attribute = PollItem.Attribute(voted: isVoted)
let option = PollItem.opion(objectID: option.objectID, attribute: attribute)
return option
}
snapshot.appendItems(pollItems, toSection: .main)
cell.statusView.statusPollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
// cell.statusView.statusPollTableView.layoutIfNeeded()
} else {
cell.statusView.statusPollTableView.isHidden = true
}
// toolbar
let replyCountTitle: String = {

View File

@ -152,6 +152,10 @@ extension HomeTimelineViewController {
self.context.apiService.backgroundManagedObjectContext.delete(toot)
}
}
.sink { _ in
// do nothing
}
.store(in: &self.disposeBag)
case .failure(let error):
assertionFailure(error.localizedDescription)
}

View File

@ -17,6 +17,8 @@ protocol StatusViewDelegate: class {
final class StatusView: UIView {
var statusPollTableViewHeightObservation: NSKeyValueObservation?
static let avatarImageSize = CGSize(width: 42, height: 42)
static let avatarImageCornerRadius: CGFloat = 4
static let contentWarningBlurRadius: CGFloat = 12
@ -24,6 +26,7 @@ final class StatusView: UIView {
weak var delegate: StatusViewDelegate?
var isStatusTextSensitive = false
var statusPollTableViewDataSource: UITableViewDiffableDataSource<PollSection, PollItem>?
var statusPollTableViewHeightLaoutConstraint: NSLayoutConstraint!
let headerContainerStackView = UIStackView()
@ -103,9 +106,11 @@ final class StatusView: UIView {
let statusMosaicImageView = MosaicImageViewContainer()
let statusPollTableView: UITableView = {
let tableView = UITableView()
tableView.register(PollTableViewCell.self, forCellReuseIdentifier: String(describing: PollTableViewCell.self))
let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
tableView.register(PollOptionTableViewCell.self, forCellReuseIdentifier: String(describing: PollOptionTableViewCell.self))
tableView.isScrollEnabled = false
tableView.separatorStyle = .none
tableView.backgroundColor = .clear
return tableView
}()
@ -145,6 +150,10 @@ final class StatusView: UIView {
}
}
deinit {
statusPollTableViewHeightObservation = nil
}
}
extension StatusView {
@ -265,8 +274,23 @@ extension StatusView {
])
statusContentWarningContainerStackView.addArrangedSubview(contentWarningTitle)
statusContentWarningContainerStackView.addArrangedSubview(contentWarningActionButton)
statusContainerStackView.addArrangedSubview(statusMosaicImageView)
statusPollTableView.translatesAutoresizingMaskIntoConstraints = false
statusContainerStackView.addArrangedSubview(statusPollTableView)
statusPollTableViewHeightLaoutConstraint = statusPollTableView.heightAnchor.constraint(equalToConstant: 44.0).priority(.required - 1)
NSLayoutConstraint.activate([
statusPollTableViewHeightLaoutConstraint,
])
statusPollTableViewHeightObservation = statusPollTableView.observe(\.contentSize, options: .new, changeHandler: { [weak self] tableView, _ in
guard let self = self else { return }
guard self.statusPollTableView.contentSize.height != .zero else {
self.statusPollTableViewHeightLaoutConstraint.constant = 44
return
}
self.statusPollTableViewHeightLaoutConstraint.constant = self.statusPollTableView.contentSize.height
})
// action toolbar container
containerStackView.addArrangedSubview(actionToolbarContainer)
@ -322,14 +346,13 @@ extension StatusView {
}
}
// MARK: - AvatarConfigurableView
extension StatusView: AvatarConfigurableView {
static var configurableAvatarImageSize: CGSize { return Self.avatarImageSize }
static var configurableAvatarImageCornerRadius: CGFloat { return 4 }
var configurableAvatarImageView: UIImageView? { return nil }
var configurableAvatarButton: UIButton? { return avatarButton }
var configurableVerifiedBadgeImageView: UIImageView? { nil }
}
#if canImport(SwiftUI) && DEBUG

View File

@ -1,5 +1,5 @@
//
// PollTableViewCell.swift
// PollOptionTableViewCell.swift
// Mastodon
//
// Created by MainasuK Cirno on 2021-2-25.
@ -7,8 +7,11 @@
import UIKit
final class PollTableViewCell: UITableViewCell {
final class PollOptionTableViewCell: UITableViewCell {
static let height: CGFloat = optionHeight + 2 * verticalMargin
static let optionHeight: CGFloat = 44
static let verticalMargin: CGFloat = 5
static let checkmarkImageSize = CGSize(width: 26, height: 26)
let roundedBackgroundView = UIView()
@ -57,9 +60,11 @@ final class PollTableViewCell: UITableViewCell {
}
extension PollTableViewCell {
extension PollOptionTableViewCell {
private func _init() {
selectionStyle = .none
backgroundColor = .clear
roundedBackgroundView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
roundedBackgroundView.translatesAutoresizingMaskIntoConstraints = false
@ -69,6 +74,7 @@ extension PollTableViewCell {
roundedBackgroundView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
roundedBackgroundView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: roundedBackgroundView.bottomAnchor, constant: 5),
roundedBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.optionHeight).priority(.defaultHigh),
])
checkmarkBackgroundView.translatesAutoresizingMaskIntoConstraints = false
@ -77,8 +83,8 @@ extension PollTableViewCell {
checkmarkBackgroundView.topAnchor.constraint(equalTo: roundedBackgroundView.topAnchor, constant: 9),
checkmarkBackgroundView.leadingAnchor.constraint(equalTo: roundedBackgroundView.leadingAnchor, constant: 9),
roundedBackgroundView.bottomAnchor.constraint(equalTo: checkmarkBackgroundView.bottomAnchor, constant: 9),
checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: PollTableViewCell.checkmarkImageSize.width).priority(.defaultHigh),
checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: PollTableViewCell.checkmarkImageSize.height).priority(.defaultHigh),
checkmarkBackgroundView.widthAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.width).priority(.defaultHigh),
checkmarkBackgroundView.heightAnchor.constraint(equalToConstant: PollOptionTableViewCell.checkmarkImageSize.height).priority(.defaultHigh),
])
checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
@ -104,6 +110,8 @@ extension PollTableViewCell {
roundedBackgroundView.trailingAnchor.constraint(equalTo: optionPercentageLabel.trailingAnchor, constant: 18),
optionPercentageLabel.centerYAnchor.constraint(equalTo: roundedBackgroundView.centerYAnchor),
])
optionPercentageLabel.setContentHuggingPriority(.required - 1, for: .horizontal)
optionPercentageLabel.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
configureCheckmark(state: .none)
}
@ -111,8 +119,12 @@ extension PollTableViewCell {
override func layoutSubviews() {
super.layoutSubviews()
updateCornerRadius()
}
private func updateCornerRadius() {
roundedBackgroundView.layer.masksToBounds = true
roundedBackgroundView.layer.cornerRadius = roundedBackgroundView.bounds.height * 0.5
roundedBackgroundView.layer.cornerRadius = PollOptionTableViewCell.optionHeight * 0.5
roundedBackgroundView.layer.cornerCurve = .circular
checkmarkBackgroundView.layer.masksToBounds = true
@ -122,7 +134,7 @@ extension PollTableViewCell {
}
extension PollTableViewCell {
extension PollOptionTableViewCell {
enum CheckmarkState {
case none
@ -168,17 +180,17 @@ struct PollTableViewCell_Previews: PreviewProvider {
static var controls: some View {
Group {
UIViewPreview() {
PollTableViewCell()
PollOptionTableViewCell()
}
.previewLayout(.fixed(width: 375, height: 44 + 10))
UIViewPreview() {
let cell = PollTableViewCell()
let cell = PollOptionTableViewCell()
cell.configureCheckmark(state: .off)
return cell
}
.previewLayout(.fixed(width: 375, height: 44 + 10))
UIViewPreview() {
let cell = PollTableViewCell()
let cell = PollOptionTableViewCell()
cell.configureCheckmark(state: .on)
return cell
}

View File

@ -10,7 +10,6 @@ import UIKit
import AVKit
import Combine
protocol StatusTableViewCellDelegate: class {
func statusTableViewCell(_ cell: StatusTableViewCell, actionToolbarContainer: ActionToolbarContainer, likeButtonDidPressed sender: UIButton)
func statusTableViewCell(_ cell: StatusTableViewCell, statusView: StatusView, contentWarningActionButtonPressed button: UIButton)