Merge pull request #8 from tootsuite/feature/MiddleLoader
feat: add MiddleLoader
This commit is contained in:
commit
a180f62daf
|
@ -11,6 +11,10 @@
|
||||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */; };
|
||||||
2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* TimelinePostView.swift */; };
|
2D152A8C25C295CC009AA50C /* TimelinePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A8B25C295CC009AA50C /* TimelinePostView.swift */; };
|
||||||
2D152A9225C2980C009AA50C /* UIFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D152A9125C2980C009AA50C /* UIFont.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 */; };
|
||||||
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; };
|
||||||
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; };
|
2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; };
|
||||||
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; };
|
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonContent.swift */; };
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; };
|
||||||
|
@ -155,6 +159,10 @@
|
||||||
2D04F42425C255B9003F936F /* APIService+PublicTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+PublicTimeline.swift"; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
||||||
|
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = "<group>"; };
|
||||||
2D42FF6A25C817D2004A627A /* MastodonContent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonContent.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>"; };
|
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>"; };
|
2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HitTestExpandedButton.swift; sourceTree = "<group>"; };
|
||||||
|
@ -365,6 +373,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */,
|
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */,
|
||||||
|
2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */,
|
||||||
);
|
);
|
||||||
path = Protocol;
|
path = Protocol;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -375,6 +384,7 @@
|
||||||
2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */,
|
2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */,
|
||||||
2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */,
|
2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */,
|
||||||
2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */,
|
2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */,
|
||||||
|
2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */,
|
||||||
2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */,
|
2D76318225C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift */,
|
||||||
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */,
|
2D45E5BE25C9549700A6D639 /* PublicTimelineViewModel+State.swift */,
|
||||||
);
|
);
|
||||||
|
@ -423,6 +433,7 @@
|
||||||
2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */,
|
2D7631A725C1535600929FB9 /* TimelinePostTableViewCell.swift */,
|
||||||
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */,
|
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */,
|
||||||
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
|
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */,
|
||||||
|
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */,
|
||||||
);
|
);
|
||||||
path = TableviewCell;
|
path = TableviewCell;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -698,6 +709,7 @@
|
||||||
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
|
DB45FAB525CA5485005A8AC7 /* UIAlertController.swift */,
|
||||||
2D42FF8E25C8228A004A627A /* UIButton.swift */,
|
2D42FF8E25C8228A004A627A /* UIButton.swift */,
|
||||||
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */,
|
DB45FAD625CA6C76005A8AC7 /* UIBarButtonItem.swift */,
|
||||||
|
2D32EAB925CB9B0500C9ED86 /* UIView.swift */,
|
||||||
);
|
);
|
||||||
path = Extension;
|
path = Extension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1090,8 +1102,10 @@
|
||||||
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */,
|
||||||
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */,
|
||||||
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
DB8AF55025C13703002E6C99 /* MainTabBarController.swift in Sources */,
|
||||||
|
2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */,
|
||||||
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
DB8AF54525C13647002E6C99 /* NeedsDependency.swift in Sources */,
|
||||||
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
DB45FADD25CA6F6B005A8AC7 /* APIService+CoreData+MastodonUser.swift in Sources */,
|
||||||
|
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||||
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */,
|
DB98334725C8056600AD9700 /* AuthenticationViewModel.swift in Sources */,
|
||||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||||
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */,
|
2D76319F25C1521200929FB9 /* TimelineSection.swift in Sources */,
|
||||||
|
@ -1101,6 +1115,7 @@
|
||||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||||
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */,
|
||||||
|
2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||||
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
DB8AF54425C13647002E6C99 /* SceneCoordinator.swift in Sources */,
|
||||||
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */,
|
||||||
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */,
|
||||||
|
@ -1117,6 +1132,7 @@
|
||||||
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */,
|
DB01409625C40B6700F9F3CF /* AuthenticationViewController.swift in Sources */,
|
||||||
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */,
|
||||||
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */,
|
||||||
|
2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */,
|
||||||
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -15,6 +15,8 @@ enum Item {
|
||||||
// normal list
|
// normal list
|
||||||
case toot(objectID: NSManagedObjectID)
|
case toot(objectID: NSManagedObjectID)
|
||||||
|
|
||||||
|
// loader
|
||||||
|
case middleLoader(tootID: String)
|
||||||
case bottomLoader
|
case bottomLoader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +27,8 @@ extension Item: Equatable {
|
||||||
return objectIDLeft == objectIDRight
|
return objectIDLeft == objectIDRight
|
||||||
case (.bottomLoader, .bottomLoader):
|
case (.bottomLoader, .bottomLoader):
|
||||||
return true
|
return true
|
||||||
|
case (.middleLoader(let upperLeft), .middleLoader(let upperRight)):
|
||||||
|
return upperLeft == upperRight
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -36,6 +40,9 @@ extension Item: Hashable {
|
||||||
switch self {
|
switch self {
|
||||||
case .toot(let objectID):
|
case .toot(let objectID):
|
||||||
hasher.combine(objectID)
|
hasher.combine(objectID)
|
||||||
|
case .middleLoader(let upper):
|
||||||
|
hasher.combine(String(describing: Item.middleLoader.self))
|
||||||
|
hasher.combine(upper)
|
||||||
case .bottomLoader:
|
case .bottomLoader:
|
||||||
hasher.combine(String(describing: Item.bottomLoader.self))
|
hasher.combine(String(describing: Item.bottomLoader.self))
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,10 @@ extension TimelineSection {
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
managedObjectContext: NSManagedObjectContext,
|
managedObjectContext: NSManagedObjectContext,
|
||||||
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||||
timelinePostTableViewCellDelegate: TimelinePostTableViewCellDelegate
|
timelinePostTableViewCellDelegate: TimelinePostTableViewCellDelegate,
|
||||||
|
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||||
) -> UITableViewDiffableDataSource<TimelineSection, Item> {
|
) -> 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() }
|
guard let timelinePostTableViewCellDelegate = timelinePostTableViewCellDelegate else { return UITableViewCell() }
|
||||||
|
|
||||||
switch item {
|
switch item {
|
||||||
|
@ -37,6 +38,11 @@ extension TimelineSection {
|
||||||
}
|
}
|
||||||
cell.delegate = timelinePostTableViewCellDelegate
|
cell.delegate = timelinePostTableViewCellDelegate
|
||||||
return cell
|
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:
|
case .bottomLoader:
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||||
cell.activityIndicatorView.startAnimating()
|
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
|
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
|
||||||
internal enum Asset {
|
internal enum Asset {
|
||||||
internal static let accentColor = ColorAsset(name: "AccentColor")
|
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 enum Colors {
|
||||||
internal static let tootDark = ColorAsset(name: "Colors/Toot.Dark")
|
internal static let tootDark = ColorAsset(name: "Colors/Toot.Dark")
|
||||||
internal static let tootGray = ColorAsset(name: "Colors/Toot.Gray")
|
internal static let tootGray = ColorAsset(name: "Colors/Toot.Gray")
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
//
|
||||||
|
// ContentOffsetAdjustableTimelineViewControllerDelegate.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by sxiaojian on 2021/2/5.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol ContentOffsetAdjustableTimelineViewControllerDelegate: class {
|
||||||
|
func navigationBar() -> UINavigationBar?
|
||||||
|
}
|
||||||
|
|
|
@ -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 AVKit
|
||||||
import Combine
|
import Combine
|
||||||
|
import CoreData
|
||||||
import CoreDataStack
|
import CoreDataStack
|
||||||
import GameplayKit
|
import GameplayKit
|
||||||
import os.log
|
import os.log
|
||||||
|
@ -24,6 +25,7 @@ final class PublicTimelineViewController: UIViewController, NeedsDependency, Tim
|
||||||
lazy var tableView: UITableView = {
|
lazy var tableView: UITableView = {
|
||||||
let tableView = UITableView()
|
let tableView = UITableView()
|
||||||
tableView.register(TimelinePostTableViewCell.self, forCellReuseIdentifier: String(describing: TimelinePostTableViewCell.self))
|
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.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||||
tableView.rowHeight = UITableView.automaticDimension
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
tableView.separatorStyle = .none
|
tableView.separatorStyle = .none
|
||||||
|
@ -68,11 +70,13 @@ extension PublicTimelineViewController {
|
||||||
])
|
])
|
||||||
|
|
||||||
viewModel.tableView = tableView
|
viewModel.tableView = tableView
|
||||||
|
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
|
||||||
tableView.delegate = self
|
tableView.delegate = self
|
||||||
viewModel.setupDiffableDataSource(
|
viewModel.setupDiffableDataSource(
|
||||||
for: tableView,
|
for: tableView,
|
||||||
dependency: self,
|
dependency: self,
|
||||||
timelinePostTableViewCellDelegate: self
|
timelinePostTableViewCellDelegate: self,
|
||||||
|
timelineMiddleLoaderTableViewCellDelegate: self
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +123,14 @@ extension PublicTimelineViewController: UITableViewDelegate {
|
||||||
viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key))
|
viewModel.cellFrameCache.setObject(NSValue(cgRect: frame), forKey: NSNumber(value: key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||||
|
extension PublicTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate {
|
||||||
|
func navigationBar() -> UINavigationBar? {
|
||||||
|
return navigationController?.navigationBar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - LoadMoreConfigurableTableViewContainer
|
// MARK: - LoadMoreConfigurableTableViewContainer
|
||||||
extension PublicTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
extension PublicTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||||
|
@ -127,3 +139,66 @@ extension PublicTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
||||||
var loadMoreConfigurableTableView: UITableView { return tableView }
|
var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||||
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.stateMachine }
|
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(
|
func setupDiffableDataSource(
|
||||||
for tableView: UITableView,
|
for tableView: UITableView,
|
||||||
dependency: NeedsDependency,
|
dependency: NeedsDependency,
|
||||||
timelinePostTableViewCellDelegate: TimelinePostTableViewCellDelegate
|
timelinePostTableViewCellDelegate: TimelinePostTableViewCellDelegate,
|
||||||
|
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate
|
||||||
) {
|
) {
|
||||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||||
.autoconnect()
|
.autoconnect()
|
||||||
|
@ -26,7 +27,8 @@ extension PublicTimelineViewModel {
|
||||||
dependency: dependency,
|
dependency: dependency,
|
||||||
managedObjectContext: fetchedResultsController.managedObjectContext,
|
managedObjectContext: fetchedResultsController.managedObjectContext,
|
||||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate
|
timelinePostTableViewCellDelegate: timelinePostTableViewCellDelegate,
|
||||||
|
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate
|
||||||
)
|
)
|
||||||
items.value = []
|
items.value = []
|
||||||
stateMachine.enter(PublicTimelineViewModel.State.Loading.self)
|
stateMachine.enter(PublicTimelineViewModel.State.Loading.self)
|
||||||
|
@ -42,13 +44,20 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||||
let indexes = tootIDs.value
|
let indexes = tootIDs.value
|
||||||
let toots = fetchedResultsController.fetchedObjects ?? []
|
let toots = fetchedResultsController.fetchedObjects ?? []
|
||||||
guard toots.count == indexes.count else { return }
|
guard toots.count == indexes.count else { return }
|
||||||
let items: [Item] = toots
|
let indexTootTuples: [(Int, Toot)] = toots
|
||||||
.compactMap { toot -> (Int, Toot)? in
|
.compactMap { toot -> (Int, Toot)? in
|
||||||
guard toot.deletedAt == nil else { return nil }
|
guard toot.deletedAt == nil else { return nil }
|
||||||
return indexes.firstIndex(of: toot.id).map { index in (index, toot) }
|
return indexes.firstIndex(of: toot.id).map { index in (index, toot) }
|
||||||
}
|
}
|
||||||
.sorted { $0.0 < $1.0 }
|
.sorted { $0.0 < $1.0 }
|
||||||
.map { Item.toot(objectID: $0.1.objectID) }
|
var items = [Item]()
|
||||||
|
for tuple in indexTootTuples {
|
||||||
|
items.append(Item.toot(objectID: tuple.1.objectID))
|
||||||
|
if tootIDsWhichHasGap.contains(tuple.1.id) {
|
||||||
|
items.append(Item.middleLoader(tootID: tuple.1.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.items.value = items
|
self.items.value = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
viewModel.context.apiService.publicTimeline(
|
||||||
|
domain: activeMastodonAuthenticationBox.domain,
|
||||||
|
maxID: upperTimelineTootID
|
||||||
|
)
|
||||||
|
.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[...gapIndex])
|
||||||
|
let downToots = Array(viewModel.tootIDs.value[(gapIndex + 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 { downToots.contains($0.id) }
|
||||||
|
if intersection.isEmpty {
|
||||||
|
addedToots.last.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,9 +68,21 @@ extension PublicTimelineViewModel.State {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} receiveValue: { response in
|
} receiveValue: { response in
|
||||||
viewModel.isFetchingLatestTimeline.value = false
|
let resposeTootIDs = response.value.compactMap { $0.id }
|
||||||
let tootsIDs = response.value.map { $0.id }
|
var newTootsIDs = resposeTootIDs
|
||||||
viewModel.tootIDs.value = tootsIDs
|
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.count > 0 {
|
||||||
|
resposeTootIDs.last.flatMap { viewModel.tootIDsWhichHasGap.append($0) }
|
||||||
|
}
|
||||||
|
viewModel.tootIDs.value = newTootsIDs
|
||||||
stateMachine.enter(Idle.self)
|
stateMachine.enter(Idle.self)
|
||||||
}
|
}
|
||||||
.store(in: &viewModel.disposeBag)
|
.store(in: &viewModel.disposeBag)
|
||||||
|
@ -149,7 +161,6 @@ extension PublicTimelineViewModel.State {
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.tootIDs.value = oldTootsIDs
|
viewModel.tootIDs.value = oldTootsIDs
|
||||||
|
|
||||||
}
|
}
|
||||||
.store(in: &viewModel.disposeBag)
|
.store(in: &viewModel.disposeBag)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,18 @@ class PublicTimelineViewModel: NSObject {
|
||||||
// input
|
// input
|
||||||
let context: AppContext
|
let context: AppContext
|
||||||
let fetchedResultsController: NSFetchedResultsController<Toot>
|
let fetchedResultsController: NSFetchedResultsController<Toot>
|
||||||
|
|
||||||
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
|
// middle loader
|
||||||
|
let loadMiddleSateMachineList = CurrentValueSubject<[String: GKStateMachine], Never>([:])
|
||||||
|
|
||||||
weak var tableView: UITableView?
|
weak var tableView: UITableView?
|
||||||
|
|
||||||
|
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||||
|
|
||||||
|
//
|
||||||
|
var tootIDsWhichHasGap = [String]()
|
||||||
// output
|
// output
|
||||||
var diffableDataSource: UITableViewDiffableDataSource<TimelineSection, Item>?
|
var diffableDataSource: UITableViewDiffableDataSource<TimelineSection, Item>?
|
||||||
|
|
||||||
|
@ -68,6 +77,9 @@ class PublicTimelineViewModel: NSObject {
|
||||||
.sink { [weak self] items in
|
.sink { [weak self] items in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
guard let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return }
|
||||||
|
guard let tableView = self.tableView else { return }
|
||||||
|
let oldSnapshot = diffableDataSource.snapshot()
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: items did change", (#file as NSString).lastPathComponent, #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s: items did change", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
|
|
||||||
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, Item>()
|
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, Item>()
|
||||||
|
@ -81,7 +93,21 @@ class PublicTimelineViewModel: NSObject {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty)
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
|
guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: snapshot) else {
|
||||||
|
diffableDataSource.apply(snapshot)
|
||||||
|
self.isFetchingLatestTimeline.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
diffableDataSource.apply(snapshot, animatingDifferences: false) {
|
||||||
|
tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false)
|
||||||
|
tableView.contentOffset.y = tableView.contentOffset.y - difference.offset
|
||||||
|
self.isFetchingLatestTimeline.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
@ -103,11 +129,37 @@ class PublicTimelineViewModel: NSObject {
|
||||||
deinit {
|
deinit {
|
||||||
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
os_log("%{public}s[%{public}ld], %{public}s", (#file as NSString).lastPathComponent, #line, #function)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct Difference<T> {
|
||||||
|
let item: T
|
||||||
|
let sourceIndexPath: IndexPath
|
||||||
|
let targetIndexPath: IndexPath
|
||||||
|
let offset: CGFloat
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PublicTimelineViewModel {
|
private func calculateReloadSnapshotDifference<T: Hashable>(
|
||||||
|
navigationBar: UINavigationBar,
|
||||||
|
tableView: UITableView,
|
||||||
|
oldSnapshot: NSDiffableDataSourceSnapshot<TimelineSection, T>,
|
||||||
|
newSnapshot: NSDiffableDataSourceSnapshot<TimelineSection, T>
|
||||||
|
) -> Difference<T>? {
|
||||||
|
guard oldSnapshot.numberOfItems != 0 else { return nil }
|
||||||
|
|
||||||
func loadMore() -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Toot]>, Error> {
|
// old snapshot not empty. set source index path to first item if not match
|
||||||
return context.apiService.publicTimeline(domain: "mstdn.jp")
|
let sourceIndexPath = UIViewController.topVisibleTableViewCellIndexPath(in: tableView, navigationBar: navigationBar) ?? IndexPath(row: 0, section: 0)
|
||||||
|
|
||||||
|
guard sourceIndexPath.row < oldSnapshot.itemIdentifiers(inSection: .main).count else { return nil }
|
||||||
|
|
||||||
|
let timelineItem = oldSnapshot.itemIdentifiers(inSection: .main)[sourceIndexPath.row]
|
||||||
|
guard let itemIndex = newSnapshot.itemIdentifiers(inSection: .main).firstIndex(of: timelineItem) else { return nil }
|
||||||
|
let targetIndexPath = IndexPath(row: itemIndex, section: 0)
|
||||||
|
|
||||||
|
let offset = UIViewController.tableViewCellOriginOffsetToWindowTop(in: tableView, at: sourceIndexPath, navigationBar: navigationBar)
|
||||||
|
return Difference(
|
||||||
|
item: timelineItem,
|
||||||
|
sourceIndexPath: sourceIndexPath,
|
||||||
|
targetIndexPath: targetIndexPath,
|
||||||
|
offset: offset
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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