feat: add MiddleLoader
This commit is contained in:
parent
dee84f5ec6
commit
6ff1a0c88e
|
@ -11,6 +11,9 @@
|
|||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
||||
2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* TimelinePostView.swift */; };
|
||||
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.swift */; };
|
||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; };
|
||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; };
|
||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; };
|
||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; };
|
||||
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; };
|
||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
||||
|
@ -155,6 +158,9 @@
|
|||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
||||
2D152A8B25C295CC009AA50C /* TimelinePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinePostView.swift; sourceTree = "<group>"; };
|
||||
2D152A9125C2980C009AA50C /* UIFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIFont.swift; sourceTree = "<group>"; };
|
||||
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = "<group>"; };
|
||||
2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = "<group>"; };
|
||||
2D42FF6A25C817D2004A627A /* MastodonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonContent.swift; sourceTree = "<group>"; };
|
||||
2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionToolBarContainer.swift; sourceTree = "<group>"; };
|
||||
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
|
||||
|
@ -375,6 +381,7 @@
|
|||
2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */,
|
||||
2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */,
|
||||
2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */,
|
||||
2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */,
|
||||
2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */,
|
||||
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */,
|
||||
);
|
||||
|
@ -423,6 +430,7 @@
|
|||
2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */,
|
||||
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */,
|
||||
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
|
||||
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */,
|
||||
);
|
||||
path = TableviewCell;
|
||||
sourceTree = "<group>";
|
||||
|
@ -698,6 +706,7 @@
|
|||
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
|
||||
2D42FF8E25C8228A004A627A /* UIButton.swift */,
|
||||
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */,
|
||||
2D32EAB925CB9B0500C9ED86 /* UIView.swift */,
|
||||
);
|
||||
path = Extension;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1092,6 +1101,7 @@
|
|||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */,
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */,
|
||||
|
@ -1101,6 +1111,7 @@
|
|||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
||||
|
@ -1117,6 +1128,7 @@
|
|||
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */,
|
||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
|
@ -15,6 +15,8 @@ enum Item {
|
|||
// normal list
|
||||
case toot(objectID: NSManagedObjectID)
|
||||
|
||||
// loader
|
||||
case middleLoader(tootID: String)
|
||||
case bottomLoader
|
||||
}
|
||||
|
||||
|
@ -25,6 +27,8 @@ extension Item: Equatable {
|
|||
return objectIDLeft == objectIDRight
|
||||
case (.bottomLoader, .bottomLoader):
|
||||
return true
|
||||
case (.middleLoader(let upperLeft), .middleLoader(let upperRight)):
|
||||
return upperLeft == upperRight
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
@ -36,6 +40,9 @@ extension Item: Hashable {
|
|||
switch self {
|
||||
case .toot(let objectID):
|
||||
hasher.combine(objectID)
|
||||
case .middleLoader(let upper):
|
||||
hasher.combine(String(describing: Item.middleLoader.self))
|
||||
hasher.combine(upper)
|
||||
case .bottomLoader:
|
||||
hasher.combine(String(describing: Item.bottomLoader.self))
|
||||
}
|
||||
|
|
|
@ -21,9 +21,10 @@ extension TimelineSection {
|
|||
dependency: NeedsDependency,
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||
timelinePostTableViewCellDelegate: TimelinePostTableViewCellDelegate
|
||||
timelinePostTableViewCellDelegate: TimelinePostTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||
) -> UITableViewDiffableDataSource<TimelineSection, Item> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [weak timelinePostTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [weak timelinePostTableViewCellDelegate, weak timelineMiddleLoaderTableViewCellDelegate] tableView, indexPath, item -> UITableViewCell? in
|
||||
guard let timelinePostTableViewCellDelegate = timelinePostTableViewCellDelegate else { return UITableViewCell() }
|
||||
|
||||
switch item {
|
||||
|
@ -33,10 +34,15 @@ extension TimelineSection {
|
|||
// configure cell
|
||||
managedObjectContext.performAndWait {
|
||||
let toot = managedObjectContext.object(with: objectID) as! Toot
|
||||
TimelineSection.configure(cell: cell,timestampUpdatePublisher: timestampUpdatePublisher, toot: toot)
|
||||
TimelineSection.configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot)
|
||||
}
|
||||
cell.delegate = timelinePostTableViewCellDelegate
|
||||
return cell
|
||||
case .middleLoader(let upperTimelineTootID):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self), for: indexPath) as! TimelineMiddleLoaderTableViewCell
|
||||
cell.delegate = timelineMiddleLoaderTableViewCellDelegate
|
||||
timelineMiddleLoaderTableViewCellDelegate?.configure(cell: cell, upperTimelineTootID: upperTimelineTootID)
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.activityIndicatorView.startAnimating()
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// UIView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/2/4.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIView {
|
||||
|
||||
static var separatorLine: UIView {
|
||||
let line = UIView()
|
||||
line.backgroundColor = .separator
|
||||
return line
|
||||
}
|
||||
|
||||
static func separatorLineHeight(of view: UIView) -> CGFloat {
|
||||
return 1.0 / view.traitCollection.displayScale
|
||||
}
|
||||
|
||||
static var floatyButtonBottomMargin: CGFloat {
|
||||
return 16
|
||||
}
|
||||
|
||||
}
|
|
@ -22,6 +22,9 @@ internal typealias AssetImageTypeAlias = ImageAsset.Image
|
|||
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
|
||||
internal enum Asset {
|
||||
internal static let accentColor = ColorAsset(name: "AccentColor")
|
||||
internal enum Arrows {
|
||||
internal static let arrowTriangle2Circlepath = ImageAsset(name: "Arrows/arrow.triangle.2.circlepath")
|
||||
}
|
||||
internal enum Colors {
|
||||
internal static let tootDark = ColorAsset(name: "Colors/Toot.Dark")
|
||||
internal static let tootGray = ColorAsset(name: "Colors/Toot.Gray")
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
21
Mastodon/Resources/Assets.xcassets/Arrows/arrow.triangle.2.circlepath.imageset/Contents.json
vendored
Normal file
21
Mastodon/Resources/Assets.xcassets/Arrows/arrow.triangle.2.circlepath.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "arrow.triangle.2.circlepath.pdf",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 4.000000 10.752930 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
15.009519 2.109471 m
|
||||
15.085540 1.562444 15.590621 1.180617 16.137648 1.256639 c
|
||||
16.684677 1.332660 17.066502 1.837741 16.990480 2.384768 c
|
||||
15.009519 2.109471 l
|
||||
h
|
||||
-0.423099 4.631682 m
|
||||
-0.635487 4.121869 -0.394376 3.536408 0.115438 3.324021 c
|
||||
0.625251 3.111633 1.210711 3.352744 1.423099 3.862558 c
|
||||
-0.423099 4.631682 l
|
||||
h
|
||||
1.000000 8.247120 m
|
||||
1.000000 8.799404 0.552285 9.247120 0.000000 9.247120 c
|
||||
-0.552285 9.247120 -1.000000 8.799404 -1.000000 8.247120 c
|
||||
1.000000 8.247120 l
|
||||
h
|
||||
0.000000 4.247120 m
|
||||
-1.000000 4.247120 l
|
||||
-1.000000 3.694835 -0.552285 3.247120 0.000000 3.247120 c
|
||||
0.000000 4.247120 l
|
||||
h
|
||||
4.000000 3.247120 m
|
||||
4.552285 3.247120 5.000000 3.694835 5.000000 4.247120 c
|
||||
5.000000 4.799405 4.552285 5.247120 4.000000 5.247120 c
|
||||
4.000000 3.247120 l
|
||||
h
|
||||
16.990480 2.384768 m
|
||||
16.715729 4.361807 15.798570 6.193669 14.380284 7.598174 c
|
||||
12.972991 6.177073 l
|
||||
14.079566 5.081251 14.795152 3.651996 15.009519 2.109471 c
|
||||
16.990480 2.384768 l
|
||||
h
|
||||
14.380284 7.598174 m
|
||||
12.961998 9.002679 11.121269 9.901910 9.141643 10.157345 c
|
||||
8.885699 8.173789 l
|
||||
10.430243 7.974494 11.866417 7.272897 12.972991 6.177073 c
|
||||
14.380284 7.598174 l
|
||||
h
|
||||
9.141643 10.157345 m
|
||||
7.162015 10.412781 5.153316 10.010252 3.424967 9.011765 c
|
||||
4.425436 7.279984 l
|
||||
5.773929 8.059025 7.341156 8.373085 8.885699 8.173789 c
|
||||
9.141643 10.157345 l
|
||||
h
|
||||
3.424967 9.011765 m
|
||||
1.696617 8.013276 0.344502 6.474223 -0.423099 4.631682 c
|
||||
1.423099 3.862558 l
|
||||
2.021996 5.300145 3.076944 6.500945 4.425436 7.279984 c
|
||||
3.424967 9.011765 l
|
||||
h
|
||||
-1.000000 8.247120 m
|
||||
-1.000000 4.247120 l
|
||||
1.000000 4.247120 l
|
||||
1.000000 8.247120 l
|
||||
-1.000000 8.247120 l
|
||||
h
|
||||
0.000000 3.247120 m
|
||||
4.000000 3.247120 l
|
||||
4.000000 5.247120 l
|
||||
0.000000 5.247120 l
|
||||
0.000000 3.247120 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 4.000000 1.767822 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
0.990481 9.369826 m
|
||||
0.914460 9.916854 0.409379 10.298680 -0.137649 10.222659 c
|
||||
-0.684676 10.146638 -1.066502 9.641557 -0.990481 9.094529 c
|
||||
0.990481 9.369826 l
|
||||
h
|
||||
16.423100 6.847616 m
|
||||
16.635487 7.357429 16.394375 7.942889 15.884562 8.155277 c
|
||||
15.374748 8.367664 14.789289 8.126554 14.576900 7.616740 c
|
||||
16.423100 6.847616 l
|
||||
h
|
||||
15.000000 3.232178 m
|
||||
15.000000 2.679893 15.447715 2.232178 16.000000 2.232178 c
|
||||
16.552284 2.232178 17.000000 2.679893 17.000000 3.232178 c
|
||||
15.000000 3.232178 l
|
||||
h
|
||||
16.000000 7.232178 m
|
||||
17.000000 7.232178 l
|
||||
17.000000 7.784462 16.552284 8.232178 16.000000 8.232178 c
|
||||
16.000000 7.232178 l
|
||||
h
|
||||
12.000000 8.232178 m
|
||||
11.447715 8.232178 11.000000 7.784462 11.000000 7.232178 c
|
||||
11.000000 6.679893 11.447715 6.232178 12.000000 6.232178 c
|
||||
12.000000 8.232178 l
|
||||
h
|
||||
-0.990481 9.094529 m
|
||||
-0.715729 7.117491 0.201429 5.285628 1.619715 3.881123 c
|
||||
3.027008 5.302223 l
|
||||
1.920433 6.398046 1.204848 7.827302 0.990481 9.369826 c
|
||||
-0.990481 9.094529 l
|
||||
h
|
||||
1.619715 3.881123 m
|
||||
3.038001 2.476617 4.878731 1.577388 6.858358 1.321952 c
|
||||
7.114300 3.305508 l
|
||||
5.569757 3.504804 4.133582 4.206400 3.027008 5.302223 c
|
||||
1.619715 3.881123 l
|
||||
h
|
||||
6.858358 1.321952 m
|
||||
8.837985 1.066517 10.846684 1.469046 12.575033 2.467534 c
|
||||
11.574564 4.199314 l
|
||||
10.226071 3.420273 8.658844 3.106212 7.114300 3.305508 c
|
||||
6.858358 1.321952 l
|
||||
h
|
||||
12.575033 2.467534 m
|
||||
14.303383 3.466022 15.655499 5.005074 16.423100 6.847616 c
|
||||
14.576900 7.616740 l
|
||||
13.978004 6.179152 12.923057 4.978354 11.574564 4.199314 c
|
||||
12.575033 2.467534 l
|
||||
h
|
||||
17.000000 3.232178 m
|
||||
17.000000 7.232178 l
|
||||
15.000000 7.232178 l
|
||||
15.000000 3.232178 l
|
||||
17.000000 3.232178 l
|
||||
h
|
||||
16.000000 8.232178 m
|
||||
12.000000 8.232178 l
|
||||
12.000000 6.232178 l
|
||||
16.000000 6.232178 l
|
||||
16.000000 8.232178 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
3597
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Type /Catalog
|
||||
/Pages 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000003687 00000 n
|
||||
0000003710 00000 n
|
||||
0000003883 00000 n
|
||||
0000003957 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
4016
|
||||
%%EOF
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import AVKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
import os.log
|
||||
|
@ -24,6 +25,7 @@ final class PublicTimelineViewController: UIViewController, NeedsDependency, Tim
|
|||
lazy var tableView: UITableView = {
|
||||
let tableView = UITableView()
|
||||
tableView.register(TimelinePostTableViewCell.self, forCellReuseIdentifier: String(describing: TimelinePostTableViewCell.self))
|
||||
tableView.register(TimelineMiddleLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineMiddleLoaderTableViewCell.self))
|
||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
|
@ -72,7 +74,8 @@ extension PublicTimelineViewController {
|
|||
viewModel.setupDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: self,
|
||||
timelinePostTableViewCellDelegate: self
|
||||
timelinePostTableViewCellDelegate: self,
|
||||
timelineMiddleLoaderTableViewCellDelegate: self
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -127,3 +130,66 @@ extension PublicTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
|||
var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine }
|
||||
}
|
||||
|
||||
// MARK: - TimelineMiddleLoaderTableViewCellDelegate
|
||||
extension PublicTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate {
|
||||
|
||||
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String) {
|
||||
viewModel.loadMiddleSateMachineList
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] ids in
|
||||
guard let _ = self else { return }
|
||||
if let stateMachine = ids[upperTimelineTootID] {
|
||||
guard let state = stateMachine.currentState else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
// make success state same as loading due to snapshot updating delay
|
||||
let isLoading = state is PublicTimelineViewModel.LoadMiddleState.Loading || state is PublicTimelineViewModel.LoadMiddleState.Success
|
||||
cell.loadMoreButton.isHidden = isLoading
|
||||
if isLoading {
|
||||
cell.activityIndicatorView.startAnimating()
|
||||
} else {
|
||||
cell.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
} else {
|
||||
cell.loadMoreButton.isHidden = false
|
||||
cell.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
var dict = viewModel.loadMiddleSateMachineList.value
|
||||
if let _ = dict[upperTimelineTootID] {
|
||||
// do nothing
|
||||
} else {
|
||||
let stateMachine = GKStateMachine(states: [
|
||||
PublicTimelineViewModel.LoadMiddleState.Initial(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
|
||||
PublicTimelineViewModel.LoadMiddleState.Loading(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
|
||||
PublicTimelineViewModel.LoadMiddleState.Fail(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
|
||||
PublicTimelineViewModel.LoadMiddleState.Success(viewModel: viewModel, upperTimelineTootID: upperTimelineTootID),
|
||||
])
|
||||
stateMachine.enter(PublicTimelineViewModel.LoadMiddleState.Initial.self)
|
||||
dict[upperTimelineTootID] = stateMachine
|
||||
viewModel.loadMiddleSateMachineList.value = dict
|
||||
}
|
||||
}
|
||||
|
||||
func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
guard let indexPath = tableView.indexPath(for: cell) else { return }
|
||||
guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
|
||||
|
||||
switch item {
|
||||
case .middleLoader(let upper):
|
||||
guard let stateMachine = viewModel.loadMiddleSateMachineList.value[upper] else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
stateMachine.enter(PublicTimelineViewModel.LoadMiddleState.Loading.self)
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@ extension PublicTimelineViewModel {
|
|||
func setupDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
timelinePostTableViewCellDelegate: TimelinePostTableViewCellDelegate
|
||||
timelinePostTableViewCellDelegate: TimelinePostTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||
) {
|
||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
|
@ -26,7 +27,8 @@ extension PublicTimelineViewModel {
|
|||
dependency: dependency,
|
||||
managedObjectContext: fetchedResultsController.managedObjectContext,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate
|
||||
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
|
||||
)
|
||||
items.value = []
|
||||
stateMachine.enter(PublicTimelineViewModel.State.Loading.self)
|
||||
|
@ -42,13 +44,20 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
|||
let indexes = tootIDs.value
|
||||
let toots = fetchedResultsController.fetchedObjects ?? []
|
||||
guard toots.count == indexes.count else { return }
|
||||
let items: [Item] = toots
|
||||
let indexTootTuples: [(Int, Toot)] = toots
|
||||
.compactMap { toot -> (Int, Toot)? in
|
||||
guard toot.deletedAt == nil else { return nil }
|
||||
return indexes.firstIndex(of: toot.id).map { index in (index, toot) }
|
||||
}
|
||||
.sorted { $0.0 < $1.0 }
|
||||
.map { Item.toot(objectID: $0.1.objectID) }
|
||||
var items = [Item]()
|
||||
for tuple in indexTootTuples {
|
||||
if tootIDsWhichHasGap.contains(tuple.1.id) {
|
||||
items.append(Item.middleLoader(tootID: tuple.1.id))
|
||||
}
|
||||
items.append(Item.toot(objectID: tuple.1.objectID))
|
||||
}
|
||||
|
||||
self.items.value = items
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
//
|
||||
// PublicTimelineViewModel+LoadMiddleState.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/2/4.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import GameplayKit
|
||||
import os.log
|
||||
|
||||
extension PublicTimelineViewModel {
|
||||
class LoadMiddleState: GKState {
|
||||
weak var viewModel: PublicTimelineViewModel?
|
||||
let upperTimelineTootID: String
|
||||
|
||||
init(viewModel: PublicTimelineViewModel, upperTimelineTootID: String) {
|
||||
self.viewModel = viewModel
|
||||
self.upperTimelineTootID = upperTimelineTootID
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", (#file as NSString).lastPathComponent, #line, #function, self.debugDescription, previousState.debugDescription)
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
var dict = viewModel.loadMiddleSateMachineList.value
|
||||
dict[self.upperTimelineTootID] = stateMachine
|
||||
viewModel.loadMiddleSateMachineList.value = dict // trigger value change
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PublicTimelineViewModel.LoadMiddleState {
|
||||
class Initial: PublicTimelineViewModel.LoadMiddleState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return stateClass == Loading.self
|
||||
}
|
||||
}
|
||||
|
||||
class Loading: PublicTimelineViewModel.LoadMiddleState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
// guard let viewModel = viewModel else { return false }
|
||||
return stateClass == Success.self || stateClass == Fail.self
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
let maxID = upperTimelineTootID
|
||||
viewModel.context.apiService.publicTimeline(
|
||||
domain: activeMastodonAuthenticationBox.domain,
|
||||
maxID: maxID
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log("%{public}s[%{public}ld], %{public}s: fetch tweets failed. %s", (#file as NSString).lastPathComponent, #line, #function, error.localizedDescription)
|
||||
stateMachine.enter(Fail.self)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { response in
|
||||
let toots = response.value
|
||||
let addedToots = toots.filter { !viewModel.tootIDs.value.contains($0.id) }
|
||||
|
||||
guard let gapIndex = viewModel.tootIDs.value.firstIndex(of: self.upperTimelineTootID) else { return }
|
||||
let upToots = Array(viewModel.tootIDs.value[0...(gapIndex-1)])
|
||||
let downToots = Array(viewModel.tootIDs.value[gapIndex...viewModel.tootIDs.value.count-1])
|
||||
|
||||
// construct newTootIDs
|
||||
var newTootIDs = upToots
|
||||
newTootIDs.append(contentsOf: addedToots.map { $0.id })
|
||||
newTootIDs.append(contentsOf: downToots)
|
||||
// remove old gap from viewmodel
|
||||
if let index = viewModel.tootIDsWhichHasGap.firstIndex(of: self.upperTimelineTootID) {
|
||||
viewModel.tootIDsWhichHasGap.remove(at: index)
|
||||
}
|
||||
// add new gap from viewmodel if need
|
||||
let intersection = toots.filter { upToots.contains($0.id) }
|
||||
if intersection.isEmpty {
|
||||
toots.first.flatMap { viewModel.tootIDsWhichHasGap.append($0.id) }
|
||||
}
|
||||
|
||||
viewModel.tootIDs.value = newTootIDs
|
||||
os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld tweets, %{public}%ld new tweets", (#file as NSString).lastPathComponent, #line, #function, toots.count, addedToots.count)
|
||||
if addedToots.isEmpty {
|
||||
stateMachine.enter(Fail.self)
|
||||
} else {
|
||||
stateMachine.enter(Success.self)
|
||||
}
|
||||
}
|
||||
.store(in: &viewModel.disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
class Fail: PublicTimelineViewModel.LoadMiddleState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
// guard let viewModel = viewModel else { return false }
|
||||
return stateClass == Loading.self
|
||||
}
|
||||
}
|
||||
|
||||
class Success: PublicTimelineViewModel.LoadMiddleState {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
// guard let viewModel = viewModel else { return false }
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -69,8 +69,20 @@ extension PublicTimelineViewModel.State {
|
|||
}
|
||||
} receiveValue: { response in
|
||||
viewModel.isFetchingLatestTimeline.value = false
|
||||
let tootsIDs = response.value.map { $0.id }
|
||||
viewModel.tootIDs.value = tootsIDs
|
||||
var newTootsIDs = response.value.compactMap { $0.id }
|
||||
let oldTootsIDs = viewModel.tootIDs.value
|
||||
var hasGap = true
|
||||
for tootID in oldTootsIDs {
|
||||
if !newTootsIDs.contains(tootID) {
|
||||
newTootsIDs.append(tootID)
|
||||
} else {
|
||||
hasGap = false
|
||||
}
|
||||
}
|
||||
if hasGap {
|
||||
oldTootsIDs.first.flatMap { viewModel.tootIDsWhichHasGap.append($0) }
|
||||
}
|
||||
viewModel.tootIDs.value = newTootsIDs
|
||||
stateMachine.enter(Idle.self)
|
||||
}
|
||||
.store(in: &viewModel.disposeBag)
|
||||
|
@ -149,7 +161,6 @@ extension PublicTimelineViewModel.State {
|
|||
}
|
||||
|
||||
viewModel.tootIDs.value = oldTootsIDs
|
||||
|
||||
}
|
||||
.store(in: &viewModel.disposeBag)
|
||||
}
|
||||
|
|
|
@ -20,9 +20,15 @@ class PublicTimelineViewModel: NSObject {
|
|||
// input
|
||||
let context: AppContext
|
||||
let fetchedResultsController: NSFetchedResultsController<Toot>
|
||||
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||
weak var tableView: UITableView?
|
||||
|
||||
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
// middle loader
|
||||
let loadMiddleSateMachineList = CurrentValueSubject<[String: GKStateMachine], Never>([:])
|
||||
|
||||
weak var tableView: UITableView?
|
||||
//
|
||||
var tootIDsWhichHasGap = [String]()
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<TimelineSection, Item>?
|
||||
|
||||
|
@ -104,10 +110,3 @@ class PublicTimelineViewModel: NSObject {
|
|||
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||
}
|
||||
}
|
||||
|
||||
extension PublicTimelineViewModel {
|
||||
|
||||
func loadMore() -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
||||
return context.apiService.publicTimeline(domain: "mstdn.jp")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// TimelineMiddleLoaderTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by sxiaojian on 2021/2/4.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
protocol TimelineMiddleLoaderTableViewCellDelegate: class {
|
||||
func configure(cell: TimelineMiddleLoaderTableViewCell, upperTimelineTootID: String)
|
||||
func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton)
|
||||
}
|
||||
|
||||
final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell {
|
||||
weak var delegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||
|
||||
override func _init() {
|
||||
super._init()
|
||||
|
||||
backgroundColor = Asset.Colors.tootDark.color
|
||||
|
||||
let separatorLine = UIView.separatorLine
|
||||
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(separatorLine)
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
contentView.trailingAnchor.constraint(equalTo: separatorLine.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: separatorLine.bottomAnchor),
|
||||
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: separatorLine))
|
||||
])
|
||||
|
||||
loadMoreButton.isHidden = false
|
||||
loadMoreButton.setImage(Asset.Arrows.arrowTriangle2Circlepath.image.withRenderingMode(.alwaysTemplate), for: .normal)
|
||||
loadMoreButton.setInsets(forContentPadding: .zero, imageTitlePadding: 4)
|
||||
loadMoreButton.addTarget(self, action: #selector(TimelineMiddleLoaderTableViewCell.loadMoreButtonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
}
|
||||
|
||||
extension TimelineMiddleLoaderTableViewCell {
|
||||
@objc private func loadMoreButtonDidPressed(_ sender: UIButton) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||
delegate?.timelineMiddleLoaderTableViewCell(self, loadMoreButtonDidPressed: sender)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue