chore: remove Redundant codes

This commit is contained in:
sunxiaojian 2021-04-19 18:06:02 +08:00
parent bb03c10ef6
commit da19f8f641
6 changed files with 57 additions and 363 deletions

View File

@ -47,7 +47,7 @@ extension NotificationSection {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: NotificationStatusTableViewCell.self), for: indexPath) as! NotificationStatusTableViewCell
cell.delegate = delegate cell.delegate = delegate
let frame = CGRect(x: 0, y: 0, width: tableView.readableContentGuide.layoutFrame.width - NotificationStatusTableViewCell.statusPadding.left - NotificationStatusTableViewCell.statusPadding.right, height: tableView.readableContentGuide.layoutFrame.height) let frame = CGRect(x: 0, y: 0, width: tableView.readableContentGuide.layoutFrame.width - NotificationStatusTableViewCell.statusPadding.left - NotificationStatusTableViewCell.statusPadding.right, height: tableView.readableContentGuide.layoutFrame.height)
NotificationSection.configure(cell: cell, StatusSection.configure(cell: cell,
dependency: dependency, dependency: dependency,
readableLayoutFrame: frame, readableLayoutFrame: frame,
timestampUpdatePublisher: timestampUpdatePublisher, timestampUpdatePublisher: timestampUpdatePublisher,
@ -116,354 +116,3 @@ extension NotificationSection {
} }
} }
extension NotificationSection {
static func configure(
cell: NotificationStatusTableViewCell,
dependency: NeedsDependency,
readableLayoutFrame: CGRect?,
timestampUpdatePublisher: AnyPublisher<Date, Never>,
status: Status,
requestUserID: String,
statusItemAttribute: Item.StatusAttribute
) {
// setup attribute
statusItemAttribute.setupForStatus(status: status)
// set header
NotificationSection.configureHeader(cell: cell, status: status)
ManagedObjectObserver.observe(object: status)
.receive(on: DispatchQueue.main)
.sink { _ in
// do nothing
} receiveValue: { change in
guard case .update(let object) = change.changeType,
let newStatus = object as? Status else { return }
NotificationSection.configureHeader(cell: cell, status: newStatus)
}
.store(in: &cell.disposeBag)
// set name username
cell.statusView.nameLabel.text = {
let author = (status.reblog ?? status).author
return author.displayName.isEmpty ? author.username : author.displayName
}()
cell.statusView.usernameLabel.text = "@" + (status.reblog ?? status).author.acct
// set avatar
cell.statusView.avatarButton.isHidden = false
cell.statusView.avatarStackedContainerButton.isHidden = true
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: status.author.avatarImageURL()))
// set text
cell.statusView.activeTextLabel.configure(content: (status.reblog ?? status).content)
// set status text content warning
let isStatusTextSensitive = statusItemAttribute.isStatusTextSensitive ?? false
let spoilerText = (status.reblog ?? status).spoilerText ?? ""
cell.statusView.isStatusTextSensitive = isStatusTextSensitive
cell.statusView.updateContentWarningDisplay(isHidden: !isStatusTextSensitive)
cell.statusView.contentWarningTitle.text = {
if spoilerText.isEmpty {
return L10n.Common.Controls.Status.statusContentWarning
} else {
return L10n.Common.Controls.Status.statusContentWarning + ": \(spoilerText)"
}
}()
// prepare media attachments
let mediaAttachments = Array((status.reblog ?? status).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
// set image
let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments)
let imageViewMaxSize: CGSize = {
let maxWidth: CGFloat = {
// use timelinePostView width as container width
// that width follows readable width and keep constant width after rotate
let containerFrame = readableLayoutFrame ?? cell.statusView.frame
var containerWidth = containerFrame.width
containerWidth -= 10
containerWidth -= StatusView.avatarImageSize.width
return containerWidth
}()
let scale: CGFloat = {
switch mosiacImageViewModel.metas.count {
case 1: return 1.3
default: return 0.7
}
}()
return CGSize(width: maxWidth, height: maxWidth * scale)
}()
if mosiacImageViewModel.metas.count == 1 {
let meta = mosiacImageViewModel.metas[0]
let imageView = cell.statusView.statusMosaicImageViewContainer.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize)
imageView.af.setImage(
withURL: meta.url,
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
} else {
let imageViews = cell.statusView.statusMosaicImageViewContainer.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height)
for (i, imageView) in imageViews.enumerated() {
let meta = mosiacImageViewModel.metas[i]
imageView.af.setImage(
withURL: meta.url,
placeholderImage: UIImage.placeholder(color: .systemFill),
imageTransition: .crossDissolve(0.2)
)
}
}
cell.statusView.statusMosaicImageViewContainer.isHidden = mosiacImageViewModel.metas.isEmpty
let isStatusSensitive = statusItemAttribute.isStatusSensitive ?? false
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
cell.statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive
// set audio
if let _ = mediaAttachments.filter({ $0.type == .audio }).first {
cell.statusView.audioView.isHidden = false
cell.statusView.audioView.playButton.isSelected = false
cell.statusView.audioView.slider.isEnabled = false
cell.statusView.audioView.slider.setValue(0, animated: false)
} else {
cell.statusView.audioView.isHidden = true
}
// set GIF & video
let playerViewMaxSize: CGSize = {
let maxWidth: CGFloat = {
// use statusView width as container width
// that width follows readable width and keep constant width after rotate
let containerFrame = readableLayoutFrame ?? cell.statusView.frame
return containerFrame.width
}()
let scale: CGFloat = 1.3
return CGSize(width: maxWidth, height: maxWidth * scale)
}()
cell.statusView.playerContainerView.contentWarningOverlayView.blurVisualEffectView.effect = isStatusSensitive ? ContentWarningOverlayView.blurVisualEffect : nil
cell.statusView.playerContainerView.contentWarningOverlayView.vibrancyVisualEffectView.alpha = isStatusSensitive ? 1.0 : 0.0
cell.statusView.playerContainerView.contentWarningOverlayView.isUserInteractionEnabled = isStatusSensitive
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment)
{
let parent = cell.delegate?.parent()
let playerContainerView = cell.statusView.playerContainerView
let playerViewController = playerContainerView.setupPlayer(
aspectRatio: videoPlayerViewModel.videoSize,
maxSize: playerViewMaxSize,
parent: parent
)
playerViewController.player = videoPlayerViewModel.player
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind)
if videoPlayerViewModel.videoKind == .gif {
playerContainerView.setMediaIndicator(isHidden: false)
} else {
videoPlayerViewModel.timeControlStatus.sink { timeControlStatus in
UIView.animate(withDuration: 0.33) {
switch timeControlStatus {
case .playing:
playerContainerView.setMediaIndicator(isHidden: true)
case .paused, .waitingToPlayAtSpecifiedRate:
playerContainerView.setMediaIndicator(isHidden: false)
@unknown default:
assertionFailure()
}
}
}
.store(in: &cell.disposeBag)
}
playerContainerView.isHidden = false
} else {
cell.statusView.playerContainerView.playerViewController.player?.pause()
cell.statusView.playerContainerView.playerViewController.player = nil
}
// set poll
let poll = (status.reblog ?? status).poll
NotificationSection.configurePoll(
cell: cell,
poll: poll,
requestUserID: requestUserID,
updateProgressAnimated: false,
timestampUpdatePublisher: timestampUpdatePublisher
)
if let poll = poll {
ManagedObjectObserver.observe(object: poll)
.sink { _ in
// do nothing
} receiveValue: { change in
guard case .update(let object) = change.changeType,
let newPoll = object as? Poll else { return }
NotificationSection.configurePoll(
cell: cell,
poll: newPoll,
requestUserID: requestUserID,
updateProgressAnimated: true,
timestampUpdatePublisher: timestampUpdatePublisher
)
}
.store(in: &cell.disposeBag)
}
// set date
let createdAt = (status.reblog ?? status).createdAt
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
timestampUpdatePublisher
.sink { _ in
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
}
.store(in: &cell.disposeBag)
}
static func configureHeader(
cell: NotificationStatusTableViewCell,
status: Status
) {
if status.reblog != nil {
cell.statusView.headerContainerStackView.isHidden = false
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.reblogIconImage)
cell.statusView.headerInfoLabel.text = {
let author = status.author
let name = author.displayName.isEmpty ? author.username : author.displayName
return L10n.Common.Controls.Status.userReblogged(name)
}()
} else if let replyTo = status.replyTo {
cell.statusView.headerContainerStackView.isHidden = false
cell.statusView.headerIconLabel.attributedText = StatusView.iconAttributedString(image: StatusView.replyIconImage)
cell.statusView.headerInfoLabel.text = {
let author = replyTo.author
let name = author.displayName.isEmpty ? author.username : author.displayName
return L10n.Common.Controls.Status.userRepliedTo(name)
}()
} else {
cell.statusView.headerContainerStackView.isHidden = true
}
}
static func configurePoll(
cell: NotificationStatusTableViewCell,
poll: Poll?,
requestUserID: String,
updateProgressAnimated: Bool,
timestampUpdatePublisher: AnyPublisher<Date, Never>
) {
guard let poll = poll,
let managedObjectContext = poll.managedObjectContext
else {
cell.statusView.pollTableView.isHidden = true
cell.statusView.pollStatusStackView.isHidden = true
cell.statusView.pollVoteButton.isHidden = true
return
}
cell.statusView.pollTableView.isHidden = false
cell.statusView.pollStatusStackView.isHidden = false
cell.statusView.pollVoteCountLabel.text = {
if poll.multiple {
let count = poll.votersCount?.intValue ?? 0
if count > 1 {
return L10n.Common.Controls.Status.Poll.VoterCount.single(count)
} else {
return L10n.Common.Controls.Status.Poll.VoterCount.multiple(count)
}
} else {
let count = poll.votesCount.intValue
if count > 1 {
return L10n.Common.Controls.Status.Poll.VoteCount.single(count)
} else {
return L10n.Common.Controls.Status.Poll.VoteCount.multiple(count)
}
}
}()
if poll.expired {
cell.pollCountdownSubscription = nil
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.closed
} else if let expiresAt = poll.expiresAt {
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
cell.pollCountdownSubscription = timestampUpdatePublisher
.sink { _ in
cell.statusView.pollCountdownLabel.text = L10n.Common.Controls.Status.Poll.timeLeft(expiresAt.shortTimeAgoSinceNow)
}
} else {
// assertionFailure()
cell.pollCountdownSubscription = nil
cell.statusView.pollCountdownLabel.text = "-"
}
cell.statusView.pollTableView.allowsSelection = !poll.expired
let votedOptions = poll.options.filter { option in
(option.votedBy ?? Set()).map(\.id).contains(requestUserID)
}
let didVotedLocal = !votedOptions.isEmpty
let didVotedRemote = (poll.votedBy ?? Set()).map(\.id).contains(requestUserID)
cell.statusView.pollVoteButton.isEnabled = didVotedLocal
cell.statusView.pollVoteButton.isHidden = !poll.multiple ? true : (didVotedRemote || poll.expired)
cell.statusView.pollTableViewDataSource = PollSection.tableViewDiffableDataSource(
for: cell.statusView.pollTableView,
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 attribute: PollItem.Attribute = {
let selectState: PollItem.Attribute.SelectState = {
// check didVotedRemote later to make the local change possible
if !votedOptions.isEmpty {
return votedOptions.contains(option) ? .on : .off
} else if poll.expired {
return .none
} else if didVotedRemote, votedOptions.isEmpty {
return .none
} else {
return .off
}
}()
let voteState: PollItem.Attribute.VoteState = {
var needsReveal: Bool
if poll.expired {
needsReveal = true
} else if didVotedRemote {
needsReveal = true
} else {
needsReveal = false
}
guard needsReveal else { return .hidden }
let percentage: Double = {
guard poll.votesCount.intValue > 0 else { return 0.0 }
return Double(option.votesCount?.intValue ?? 0) / Double(poll.votesCount.intValue)
}()
let voted = votedOptions.isEmpty ? true : votedOptions.contains(option)
return .reveal(voted: voted, percentage: percentage, animated: updateProgressAnimated)
}()
return PollItem.Attribute(selectState: selectState, voteState: voteState)
}()
let option = PollItem.opion(objectID: option.objectID, attribute: attribute)
return option
}
snapshot.appendItems(pollItems, toSection: .main)
cell.statusView.pollTableViewDataSource?.apply(snapshot, animatingDifferences: false, completion: nil)
}
static func configureEmptyStateHeader(
cell: TimelineHeaderTableViewCell,
attribute: Item.EmptyStateHeaderAttribute
) {
cell.timelineHeaderView.iconImageView.image = attribute.reason.iconImage
cell.timelineHeaderView.messageLabel.text = attribute.reason.message
}
}
extension NotificationSection {
private static func formattedNumberTitleForActionButton(_ number: Int?) -> String {
guard let number = number, number > 0 else { return "" }
return String(number)
}
}

View File

@ -10,6 +10,12 @@ import CoreData
import CoreDataStack import CoreDataStack
import os.log import os.log
import UIKit import UIKit
import AVKit
protocol StatusCell : DisposeBagCollectable {
var statusView: StatusView { get }
var pollCountdownSubscription: AnyCancellable? { get set }
}
enum StatusSection: Equatable, Hashable { enum StatusSection: Equatable, Hashable {
case main case main
@ -127,7 +133,7 @@ extension StatusSection {
extension StatusSection { extension StatusSection {
static func configure( static func configure(
cell: StatusTableViewCell, cell: StatusCell,
dependency: NeedsDependency, dependency: NeedsDependency,
readableLayoutFrame: CGRect?, readableLayoutFrame: CGRect?,
timestampUpdatePublisher: AnyPublisher<Date, Never>, timestampUpdatePublisher: AnyPublisher<Date, Never>,
@ -260,14 +266,27 @@ extension StatusSection {
if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first, if let videoAttachment = mediaAttachments.filter({ $0.type == .gifv || $0.type == .video }).first,
let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment) let videoPlayerViewModel = dependency.context.videoPlaybackService.dequeueVideoPlayerViewModel(for: videoAttachment)
{ {
let parent = cell.delegate?.parent() var parent: UIViewController?
var playerViewControllerDelegate: AVPlayerViewControllerDelegate? = nil
switch cell {
case is StatusTableViewCell:
let statusTableViewCell = cell as! StatusTableViewCell
parent = statusTableViewCell.delegate?.parent()
playerViewControllerDelegate = statusTableViewCell.delegate?.playerViewControllerDelegate
case is NotificationTableViewCell:
let notificationTableViewCell = cell as! NotificationTableViewCell
parent = notificationTableViewCell.delegate?.parent()
default:
parent = nil
assertionFailure("unknown cell")
}
let playerContainerView = cell.statusView.playerContainerView let playerContainerView = cell.statusView.playerContainerView
let playerViewController = playerContainerView.setupPlayer( let playerViewController = playerContainerView.setupPlayer(
aspectRatio: videoPlayerViewModel.videoSize, aspectRatio: videoPlayerViewModel.videoSize,
maxSize: playerViewMaxSize, maxSize: playerViewMaxSize,
parent: parent parent: parent
) )
playerViewController.delegate = cell.delegate?.playerViewControllerDelegate playerViewController.delegate = playerViewControllerDelegate
playerViewController.player = videoPlayerViewModel.player playerViewController.player = videoPlayerViewModel.player
playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif playerViewController.showsPlaybackControls = videoPlayerViewModel.videoKind != .gif
playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind) playerContainerView.setMediaKind(kind: videoPlayerViewModel.videoKind)
@ -325,7 +344,9 @@ extension StatusSection {
StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID) StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID)
// separator line // separator line
cell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden if let statusTableViewCell = cell as? StatusTableViewCell {
statusTableViewCell.separatorLine.isHidden = statusItemAttribute.isSeparatorLineHidden
}
// set date // set date
let createdAt = (status.reblog ?? status).createdAt let createdAt = (status.reblog ?? status).createdAt
@ -388,7 +409,7 @@ extension StatusSection {
static func configureHeader( static func configureHeader(
cell: StatusTableViewCell, cell: StatusCell,
status: Status status: Status
) { ) {
if status.reblog != nil { if status.reblog != nil {
@ -416,7 +437,7 @@ extension StatusSection {
} }
static func configureActionToolBar( static func configureActionToolBar(
cell: StatusTableViewCell, cell: StatusCell,
status: Status, status: Status,
requestUserID: String requestUserID: String
) { ) {
@ -447,7 +468,7 @@ extension StatusSection {
} }
static func configurePoll( static func configurePoll(
cell: StatusTableViewCell, cell: StatusCell,
poll: Poll?, poll: Poll?,
requestUserID: String, requestUserID: String,
updateProgressAnimated: Bool, updateProgressAnimated: Bool,

View File

@ -131,9 +131,33 @@ extension NotificationViewController {
} }
} }
extension NotificationViewController {
func cacheTableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let diffableDataSource = viewModel.diffableDataSource else { return }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
let key = item.hashValue
let frame = cell.frame
viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key))
}
func handleTableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
guard let diffableDataSource = viewModel.diffableDataSource else { return UITableView.automaticDimension }
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return UITableView.automaticDimension }
guard let frame = viewModel.cellFrameCache.object(forKey: NSNumber(value: item.hashValue))?.cgRectValue else {
if case .bottomLoader = item {
return TimelineLoaderTableViewCell.cellHeight
} else {
return UITableView.automaticDimension
}
}
return ceil(frame.height)
}
}
// MARK: - UITableViewDelegate // MARK: - UITableViewDelegate
extension NotificationViewController: UITableViewDelegate { extension NotificationViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true) tableView.deselectRow(at: indexPath, animated: true)
guard let diffableDataSource = viewModel.diffableDataSource else { return } guard let diffableDataSource = viewModel.diffableDataSource else { return }

View File

@ -9,7 +9,7 @@ import Combine
import Foundation import Foundation
import UIKit import UIKit
final class NotificationStatusTableViewCell: UITableViewCell { final class NotificationStatusTableViewCell: UITableViewCell, StatusCell {
static let actionImageBorderWidth: CGFloat = 2 static let actionImageBorderWidth: CGFloat = 2
static let statusPadding = UIEdgeInsets(top: 50, left: 73, bottom: 24, right: 24) static let statusPadding = UIEdgeInsets(top: 50, left: 73, bottom: 24, right: 24)
var disposeBag = Set<AnyCancellable>() var disposeBag = Set<AnyCancellable>()

View File

@ -46,7 +46,7 @@ extension StatusTableViewCellDelegate {
} }
} }
final class StatusTableViewCell: UITableViewCell { final class StatusTableViewCell: UITableViewCell, StatusCell {
static let bottomPaddingHeight: CGFloat = 10 static let bottomPaddingHeight: CGFloat = 10

View File

@ -11,7 +11,7 @@ import UIKit
class AudioContainerViewModel { class AudioContainerViewModel {
static func configure( static func configure(
cell: StatusTableViewCell, cell: StatusCell,
audioAttachment: Attachment, audioAttachment: Attachment,
audioService: AudioPlaybackService audioService: AudioPlaybackService
) { ) {
@ -51,7 +51,7 @@ class AudioContainerViewModel {
} }
static func observePlayer( static func observePlayer(
cell: StatusTableViewCell, cell: StatusCell,
audioAttachment: Attachment, audioAttachment: Attachment,
audioService: AudioPlaybackService audioService: AudioPlaybackService
) { ) {