mirror of
https://github.com/mastodon/mastodon-ios.git
synced 2025-02-01 18:07:22 +01:00
feat: [WIP] migrate to Texture (AsyncDisplayKit) for better performance
This commit is contained in:
parent
e23b6cb641
commit
1a3135b998
@ -407,6 +407,15 @@
|
||||
DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */; };
|
||||
DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */; };
|
||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
|
||||
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC6482267D0B21007FE9FD /* DifferenceKit */; };
|
||||
DBAC6485267D0F9E007FE9FD /* StatusNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6484267D0F9E007FE9FD /* StatusNode.swift */; };
|
||||
DBAC6488267D388B007FE9FD /* ASTableNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6487267D388B007FE9FD /* ASTableNode.swift */; };
|
||||
DBAC648A267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */; };
|
||||
DBAC648F267DC84D007FE9FD /* TableNodeDiffableDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */; };
|
||||
DBAC6497267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */; };
|
||||
DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */; };
|
||||
DBAC649B267DF8C8007FE9FD /* ActivityIndicatorNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */; };
|
||||
DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = DBAC649D267DFE43007FE9FD /* DiffableDataSources */; };
|
||||
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F672615DD60004B8251 /* UserProvider.swift */; };
|
||||
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */; };
|
||||
DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */; };
|
||||
@ -981,6 +990,13 @@
|
||||
DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Field.swift"; sourceTree = "<group>"; };
|
||||
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
|
||||
DBAC6484267D0F9E007FE9FD /* StatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusNode.swift; sourceTree = "<group>"; };
|
||||
DBAC6487267D388B007FE9FD /* ASTableNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTableNode.swift; sourceTree = "<group>"; };
|
||||
DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSDiffableDataSourceSnapshot.swift; sourceTree = "<group>"; };
|
||||
DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableNodeDiffableDataSource.swift; sourceTree = "<group>"; };
|
||||
DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderNode.swift; sourceTree = "<group>"; };
|
||||
DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderNode.swift; sourceTree = "<group>"; };
|
||||
DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorNode.swift; sourceTree = "<group>"; };
|
||||
DBAE3F672615DD60004B8251 /* UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProvider.swift; sourceTree = "<group>"; };
|
||||
DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewController+UserProvider.swift"; sourceTree = "<group>"; };
|
||||
DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProviderFacade.swift; sourceTree = "<group>"; };
|
||||
@ -1065,8 +1081,10 @@
|
||||
DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */,
|
||||
DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */,
|
||||
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */,
|
||||
DBAC6483267D0B21007FE9FD /* DifferenceKit in Frameworks */,
|
||||
2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */,
|
||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */,
|
||||
DBAC649E267DFE43007FE9FD /* DiffableDataSources in Frameworks */,
|
||||
2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */,
|
||||
87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */,
|
||||
DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */,
|
||||
@ -1337,6 +1355,7 @@
|
||||
DB51D171262832380062B7A1 /* BlurHashEncode.swift */,
|
||||
DB6180EC26391C6C0018D199 /* TransitioningMath.swift */,
|
||||
DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */,
|
||||
DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */,
|
||||
);
|
||||
path = Vender;
|
||||
sourceTree = "<group>";
|
||||
@ -1414,6 +1433,7 @@
|
||||
2D76319D25C151F600929FB9 /* Section */,
|
||||
2D7631B125C159E700929FB9 /* Item */,
|
||||
DBCBED2226132E1D00B49291 /* FetchedResultsController */,
|
||||
DBAC6490267DC84F007FE9FD /* DataSource */,
|
||||
);
|
||||
path = Diffiable;
|
||||
sourceTree = "<group>";
|
||||
@ -1464,6 +1484,7 @@
|
||||
DB87D45C2609DE6600D12C0D /* TextField */,
|
||||
DB1D187125EF5BBD003F1F23 /* TableView */,
|
||||
2D7631A625C1533800929FB9 /* TableviewCell */,
|
||||
DBAC6486267D0FAC007FE9FD /* Node */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
@ -2167,6 +2188,7 @@
|
||||
DBCC3B35261440BA0045B23D /* UINavigationController.swift */,
|
||||
DB6D1B23263684C600ACB481 /* UserDefaults.swift */,
|
||||
DB97131E2666078B00BD1E90 /* Date.swift */,
|
||||
DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */,
|
||||
);
|
||||
path = Extension;
|
||||
sourceTree = "<group>";
|
||||
@ -2328,6 +2350,25 @@
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBAC6486267D0FAC007FE9FD /* Node */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBAC6484267D0F9E007FE9FD /* StatusNode.swift */,
|
||||
DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */,
|
||||
DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */,
|
||||
);
|
||||
path = Node;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBAC6490267DC84F007FE9FD /* DataSource */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBAC6487267D388B007FE9FD /* ASTableNode.swift */,
|
||||
DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */,
|
||||
);
|
||||
path = DataSource;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBAE3F742615DD63004B8251 /* UserProvider */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -2510,6 +2551,8 @@
|
||||
DBB525072611EAC0002F1F29 /* Tabman */,
|
||||
DB6F5E31264E7410009108F4 /* TwitterTextEditor */,
|
||||
DBAEDE5E267A0B1500D25FF5 /* Nuke */,
|
||||
DBAC6482267D0B21007FE9FD /* DifferenceKit */,
|
||||
DBAC649D267DFE43007FE9FD /* DiffableDataSources */,
|
||||
);
|
||||
productName = Mastodon;
|
||||
productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */;
|
||||
@ -2698,6 +2741,8 @@
|
||||
DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */,
|
||||
DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */,
|
||||
DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||
DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */,
|
||||
DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */,
|
||||
);
|
||||
productRefGroup = DB427DD325BAA00100D1B89D /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -2992,9 +3037,11 @@
|
||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||
DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */,
|
||||
DBB5255E2611F07A002F1F29 /* ProfileViewModel.swift in Sources */,
|
||||
DBAC648F267DC84D007FE9FD /* TableNodeDiffableDataSource.swift in Sources */,
|
||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */,
|
||||
0F1E2D0B2615C39400C38565 /* DoubleTitleLabelNavigationBarTitleView.swift in Sources */,
|
||||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
||||
DBAC649B267DF8C8007FE9FD /* ActivityIndicatorNode.swift in Sources */,
|
||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
||||
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */,
|
||||
@ -3036,6 +3083,7 @@
|
||||
DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */,
|
||||
2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */,
|
||||
DB4481C625EE2ADA00BEFB67 /* PollSection.swift in Sources */,
|
||||
DBAC648A267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift in Sources */,
|
||||
DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */,
|
||||
DB71FD4C25F8C80E00512AE1 /* StatusPrefetchingService.swift in Sources */,
|
||||
2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */,
|
||||
@ -3056,7 +3104,9 @@
|
||||
DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */,
|
||||
0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */,
|
||||
2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */,
|
||||
DBAC6497267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift in Sources */,
|
||||
DB97131F2666078B00BD1E90 /* Date.swift in Sources */,
|
||||
DBAC6485267D0F9E007FE9FD /* StatusNode.swift in Sources */,
|
||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||
DB6180E926391BDF0018D199 /* MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift in Sources */,
|
||||
DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */,
|
||||
@ -3129,6 +3179,7 @@
|
||||
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */,
|
||||
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
|
||||
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */,
|
||||
DBAC6488267D388B007FE9FD /* ASTableNode.swift in Sources */,
|
||||
DB6D9F76263587C7008423CD /* SettingFetchedResultController.swift in Sources */,
|
||||
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
|
||||
5D0393902612D259007FE196 /* WebViewController.swift in Sources */,
|
||||
@ -3262,6 +3313,7 @@
|
||||
5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */,
|
||||
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
||||
DBAC6499267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift in Sources */,
|
||||
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
||||
DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */,
|
||||
DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */,
|
||||
@ -4118,6 +4170,22 @@
|
||||
minimumVersion = 1.4.1;
|
||||
};
|
||||
};
|
||||
DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/ra1028/DifferenceKit.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 1.2.0;
|
||||
};
|
||||
};
|
||||
DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/MainasuK/DiffableDataSources.git";
|
||||
requirement = {
|
||||
branch = "feature/async-display-table";
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/kean/Nuke.git";
|
||||
@ -4201,6 +4269,16 @@
|
||||
package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */;
|
||||
productName = "UITextView+Placeholder";
|
||||
};
|
||||
DBAC6482267D0B21007FE9FD /* DifferenceKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DBAC6481267D0B21007FE9FD /* XCRemoteSwiftPackageReference "DifferenceKit" */;
|
||||
productName = DifferenceKit;
|
||||
};
|
||||
DBAC649D267DFE43007FE9FD /* DiffableDataSources */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DBAC649C267DFE43007FE9FD /* XCRemoteSwiftPackageReference "DiffableDataSources" */;
|
||||
productName = DiffableDataSources;
|
||||
};
|
||||
DBAEDE5E267A0B1500D25FF5 /* Nuke */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = DBAEDE5D267A0B1500D25FF5 /* XCRemoteSwiftPackageReference "Nuke" */;
|
||||
|
@ -7,12 +7,12 @@
|
||||
<key>AppShared.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>14</integer>
|
||||
<integer>19</integer>
|
||||
</dict>
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>17</integer>
|
||||
<integer>18</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
@ -27,12 +27,12 @@
|
||||
<key>Mastodon.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<integer>17</integer>
|
||||
</dict>
|
||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>16</integer>
|
||||
<integer>20</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
@ -46,6 +46,24 @@
|
||||
"version": "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "DiffableDataSources",
|
||||
"repositoryURL": "https://github.com/MainasuK/DiffableDataSources.git",
|
||||
"state": {
|
||||
"branch": "feature/async-display-table",
|
||||
"revision": "73393a97690959d24387c95594c045c62d9c47cf",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "DifferenceKit",
|
||||
"repositoryURL": "https://github.com/ra1028/DifferenceKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "62745d7780deef4a023a792a1f8f763ec7bf9705",
|
||||
"version": "1.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "KeychainAccess",
|
||||
"repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git",
|
||||
|
81
Mastodon/Diffiable/DataSource/ASTableNode.swift
Normal file
81
Mastodon/Diffiable/DataSource/ASTableNode.swift
Normal file
@ -0,0 +1,81 @@
|
||||
//
|
||||
// ASTableNode.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import DifferenceKit
|
||||
import DiffableDataSources
|
||||
|
||||
extension ASTableNode: ReloadableTableView {
|
||||
public func reload<C>(
|
||||
using stagedChangeset: StagedChangeset<C>,
|
||||
deleteSectionsAnimation: @autoclosure () -> UITableView.RowAnimation,
|
||||
insertSectionsAnimation: @autoclosure () -> UITableView.RowAnimation,
|
||||
reloadSectionsAnimation: @autoclosure () -> UITableView.RowAnimation,
|
||||
deleteRowsAnimation: @autoclosure () -> UITableView.RowAnimation,
|
||||
insertRowsAnimation: @autoclosure () -> UITableView.RowAnimation,
|
||||
reloadRowsAnimation: @autoclosure () -> UITableView.RowAnimation,
|
||||
interrupt: ((Changeset<C>) -> Bool)? = nil,
|
||||
setData: (C) -> Void
|
||||
) {
|
||||
if case .none = view.window, let data = stagedChangeset.last?.data {
|
||||
setData(data)
|
||||
return reloadData()
|
||||
}
|
||||
|
||||
for changeset in stagedChangeset {
|
||||
if let interrupt = interrupt, interrupt(changeset), let data = stagedChangeset.last?.data {
|
||||
setData(data)
|
||||
return reloadData()
|
||||
}
|
||||
|
||||
func updates() {
|
||||
setData(changeset.data)
|
||||
|
||||
if !changeset.sectionDeleted.isEmpty {
|
||||
deleteSections(IndexSet(changeset.sectionDeleted), with: deleteSectionsAnimation())
|
||||
}
|
||||
|
||||
if !changeset.sectionInserted.isEmpty {
|
||||
insertSections(IndexSet(changeset.sectionInserted), with: insertSectionsAnimation())
|
||||
}
|
||||
|
||||
if !changeset.sectionUpdated.isEmpty {
|
||||
reloadSections(IndexSet(changeset.sectionUpdated), with: reloadSectionsAnimation())
|
||||
}
|
||||
|
||||
for (source, target) in changeset.sectionMoved {
|
||||
moveSection(source, toSection: target)
|
||||
}
|
||||
|
||||
if !changeset.elementDeleted.isEmpty {
|
||||
deleteRows(at: changeset.elementDeleted.map { IndexPath(row: $0.element, section: $0.section) }, with: deleteRowsAnimation())
|
||||
}
|
||||
|
||||
if !changeset.elementInserted.isEmpty {
|
||||
insertRows(at: changeset.elementInserted.map { IndexPath(row: $0.element, section: $0.section) }, with: insertRowsAnimation())
|
||||
}
|
||||
|
||||
if !changeset.elementUpdated.isEmpty {
|
||||
reloadRows(at: changeset.elementUpdated.map { IndexPath(row: $0.element, section: $0.section) }, with: reloadRowsAnimation())
|
||||
}
|
||||
|
||||
for (source, target) in changeset.elementMoved {
|
||||
moveRow(at: IndexPath(row: source.element, section: source.section), to: IndexPath(row: target.element, section: target.section))
|
||||
}
|
||||
}
|
||||
|
||||
if isNodeLoaded {
|
||||
view.beginUpdates()
|
||||
updates()
|
||||
view.endUpdates(animated: false, completion: nil)
|
||||
} else {
|
||||
updates()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift
Normal file
111
Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift
Normal file
@ -0,0 +1,111 @@
|
||||
//
|
||||
// TableNodeDiffableDataSource.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import DiffableDataSources
|
||||
|
||||
open class TableNodeDiffableDataSource<SectionIdentifierType: Hashable, ItemIdentifierType: Hashable>: NSObject, ASTableDataSource {
|
||||
/// The type of closure providing the cell.
|
||||
public typealias CellProvider = (ASTableNode, IndexPath, ItemIdentifierType) -> ASCellNodeBlock?
|
||||
|
||||
/// The default animation to updating the views.
|
||||
public var defaultRowAnimation: UITableView.RowAnimation = .automatic
|
||||
|
||||
private weak var tableNode: ASTableNode?
|
||||
private let cellProvider: CellProvider
|
||||
private let core = DiffableDataSourceCore<SectionIdentifierType, ItemIdentifierType>()
|
||||
|
||||
/// Creates a new data source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - tableView: A table view instance to be managed.
|
||||
/// - cellProvider: A closure to dequeue the cell for rows.
|
||||
public init(tableNode: ASTableNode, cellProvider: @escaping CellProvider) {
|
||||
self.tableNode = tableNode
|
||||
self.cellProvider = cellProvider
|
||||
super.init()
|
||||
|
||||
tableNode.dataSource = self
|
||||
}
|
||||
|
||||
/// Applies given snapshot to perform automatic diffing update.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - snapshot: A snapshot object to be applied to data model.
|
||||
/// - animatingDifferences: A Boolean value indicating whether to update with
|
||||
/// diffing animation.
|
||||
/// - completion: An optional completion block which is called when the complete
|
||||
/// performing updates.
|
||||
public func apply(_ snapshot: DiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) {
|
||||
core.apply(snapshot, view: tableNode, animatingDifferences: animatingDifferences, completion: completion)
|
||||
}
|
||||
|
||||
/// Returns a new snapshot object of current state.
|
||||
///
|
||||
/// - Returns: A new snapshot object of current state.
|
||||
public func snapshot() -> DiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType> {
|
||||
return core.snapshot()
|
||||
}
|
||||
|
||||
/// Returns an item identifier for given index path.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - indexPath: An index path for the item identifier.
|
||||
///
|
||||
/// - Returns: An item identifier for given index path.
|
||||
public func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? {
|
||||
return core.itemIdentifier(for: indexPath)
|
||||
}
|
||||
|
||||
/// Returns an index path for given item identifier.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - itemIdentifier: An identifier of item.
|
||||
///
|
||||
/// - Returns: An index path for given item identifier.
|
||||
public func indexPath(for itemIdentifier: ItemIdentifierType) -> IndexPath? {
|
||||
return core.indexPath(for: itemIdentifier)
|
||||
}
|
||||
|
||||
/// Returns the number of sections in the data source.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - tableNode: A table node instance managed by `self`.
|
||||
///
|
||||
/// - Returns: The number of sections in the data source.
|
||||
public func numberOfSections(in tableNode: ASTableNode) -> Int {
|
||||
return core.numberOfSections()
|
||||
}
|
||||
|
||||
/// Returns the number of items in the specified section.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - tableNode: A table node instance managed by `self`.
|
||||
/// - section: An index of section.
|
||||
///
|
||||
/// - Returns: The number of items in the specified section.
|
||||
public func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
|
||||
return core.numberOfItems(inSection: section)
|
||||
}
|
||||
|
||||
/// Returns a cell for row at specified index path.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - tableView: A table view instance managed by `self`.
|
||||
/// - indexPath: An index path for cell.
|
||||
///
|
||||
/// - Returns: A cell for row at specified index path.
|
||||
open func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
|
||||
let itemIdentifier = core.unsafeItemIdentifier(for: indexPath)
|
||||
guard let block = cellProvider(tableNode, indexPath, itemIdentifier) else {
|
||||
fatalError("UITableView dataSource returned a nil cell for row at index path: \(indexPath), tableNode: \(tableNode), itemIdentifier: \(itemIdentifier)")
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import DifferenceKit
|
||||
|
||||
/// Note: update Equatable when change case
|
||||
enum Item {
|
||||
@ -158,3 +159,5 @@ extension Item: Hashable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Item: Differentiable { }
|
||||
|
@ -11,6 +11,7 @@ import CoreDataStack
|
||||
import os.log
|
||||
import UIKit
|
||||
import AVKit
|
||||
import AsyncDisplayKit
|
||||
import Nuke
|
||||
|
||||
protocol StatusCell: DisposeBagCollectable {
|
||||
@ -23,6 +24,33 @@ enum StatusSection: Equatable, Hashable {
|
||||
}
|
||||
|
||||
extension StatusSection {
|
||||
static func tableNodeDiffableDataSource(
|
||||
tableNode: ASTableNode,
|
||||
managedObjectContext: NSManagedObjectContext
|
||||
) -> TableNodeDiffableDataSource<StatusSection, Item> {
|
||||
TableNodeDiffableDataSource(tableNode: tableNode) { tableNode, indexPath, item in
|
||||
switch item {
|
||||
case .homeTimelineIndex(let objectID, let attribute):
|
||||
guard let homeTimelineIndex = try? managedObjectContext.existingObject(with: objectID) as? HomeTimelineIndex else {
|
||||
return { ASCellNode() }
|
||||
}
|
||||
let status = homeTimelineIndex.status
|
||||
|
||||
return { () -> ASCellNode in
|
||||
let cellNode = StatusNode(status: status)
|
||||
return cellNode
|
||||
}
|
||||
case .homeMiddleLoader:
|
||||
return { TimelineMiddleLoaderNode() }
|
||||
case .bottomLoader:
|
||||
return { TimelineBottomLoaderNode() }
|
||||
default:
|
||||
return { ASCellNode() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: NeedsDependency,
|
||||
|
24
Mastodon/Extension/NSDiffableDataSourceSnapshot.swift
Normal file
24
Mastodon/Extension/NSDiffableDataSourceSnapshot.swift
Normal file
@ -0,0 +1,24 @@
|
||||
//
|
||||
// NSDiffableDataSourceSnapshot.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
//extension NSDiffableDataSourceSnapshot {
|
||||
// func itemIdentifier(for indexPath: IndexPath) -> ItemIdentifierType? {
|
||||
// guard 0..<numberOfSections ~= indexPath.section else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// let items = itemIdentifiers(inSection: sectionIdentifiers[indexPath.section])
|
||||
//
|
||||
// guard 0..<items.endIndex ~= indexPath.item else {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// return items[indexPath.item]
|
||||
// }
|
||||
//}
|
@ -109,7 +109,7 @@ extension HomeTimelineViewController {
|
||||
image: UIImage(systemName: "minus.circle"),
|
||||
identifier: nil,
|
||||
options: [],
|
||||
children: [50, 100, 150, 200, 250, 300].map { count in
|
||||
children: [10, 50, 100, 150, 200, 250, 300].map { count in
|
||||
UIAction(title: "Drop Recent \(count) Statuses", image: nil, attributes: [], handler: { [weak self] action in
|
||||
guard let self = self else { return }
|
||||
self.dropRecentStatusAction(action, count: count)
|
||||
@ -269,7 +269,7 @@ extension HomeTimelineViewController {
|
||||
@objc private func dropRecentStatusAction(_ sender: UIAction, count: Int) {
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
let snapshotTransitioning = diffableDataSource.snapshot()
|
||||
|
||||
|
||||
let droppingObjectIDs = snapshotTransitioning.itemIdentifiers.prefix(count).compactMap { item -> NSManagedObjectID? in
|
||||
switch item {
|
||||
case .homeTimelineIndex(let objectID, _): return objectID
|
||||
@ -354,5 +354,30 @@ extension HomeTimelineViewController {
|
||||
transition: .modal(animated: true, completion: nil)
|
||||
)
|
||||
}
|
||||
|
||||
@objc func signOutAction(_ sender: UIAction) {
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
|
||||
context.authenticationService.signOutMastodonUser(
|
||||
domain: activeMastodonAuthenticationBox.domain,
|
||||
userID: activeMastodonAuthenticationBox.userID
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
case .success(let isSignOut):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign out %s", ((#file as NSString).lastPathComponent), #line, #function, isSignOut ? "success" : "fail")
|
||||
guard isSignOut else { return }
|
||||
self.coordinator.setup()
|
||||
self.coordinator.setupOnboardingIfNeeds(animated: true)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
@ -53,7 +53,7 @@ extension HomeTimelineViewController: StatusProvider {
|
||||
}
|
||||
|
||||
var tableViewDiffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>? {
|
||||
return viewModel.diffableDataSource
|
||||
return nil
|
||||
}
|
||||
|
||||
func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? {
|
||||
|
@ -14,12 +14,13 @@ import CoreDataStack
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import AlamofireImage
|
||||
import AsyncDisplayKit
|
||||
|
||||
#if DEBUG
|
||||
import GDPerformanceView_Swift
|
||||
#endif
|
||||
|
||||
final class HomeTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {
|
||||
final class HomeTimelineViewController: ASDKViewController<ASTableNode>, NeedsDependency, MediaPreviewableViewController {
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
@ -53,17 +54,18 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media
|
||||
barButtonItem.image = UIImage(systemName: "square.and.pencil")?.withRenderingMode(.alwaysTemplate)
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView = ControlContainableTableView()
|
||||
tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.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
|
||||
tableView.backgroundColor = .clear
|
||||
return tableView
|
||||
}()
|
||||
|
||||
var tableView: UITableView { node.view }
|
||||
//let tableView: UITableView = {
|
||||
// let tableView = ControlContainableTableView()
|
||||
// tableView.register(StatusTableViewCell.self, forCellReuseIdentifier: String(describing: StatusTableViewCell.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
|
||||
// tableView.backgroundColor = .clear
|
||||
// return tableView
|
||||
//}()
|
||||
|
||||
let publishProgressView: UIProgressView = {
|
||||
let progressView = UIProgressView(progressViewStyle: .bar)
|
||||
@ -72,7 +74,16 @@ final class HomeTimelineViewController: UIViewController, NeedsDependency, Media
|
||||
}()
|
||||
|
||||
let refreshControl = UIRefreshControl()
|
||||
|
||||
|
||||
|
||||
override init() {
|
||||
super.init(node: ASTableNode())
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
@ -83,13 +94,15 @@ extension HomeTimelineViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
node.allowsSelection = true
|
||||
|
||||
title = L10n.Scene.HomeTimeline.title
|
||||
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||
navigationItem.leftBarButtonItem = settingBarButtonItem
|
||||
navigationItem.titleView = titleView
|
||||
titleView.delegate = self
|
||||
|
||||
|
||||
viewModel.homeTimelineNavigationBarTitleViewModel.state
|
||||
.removeDuplicates()
|
||||
.receive(on: DispatchQueue.main)
|
||||
@ -98,52 +111,56 @@ extension HomeTimelineViewController {
|
||||
self.titleView.configure(state: state)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
|
||||
#if DEBUG
|
||||
// long press to trigger debug menu
|
||||
settingBarButtonItem.menu = debugMenu
|
||||
PerformanceMonitor.shared().delegate = self
|
||||
|
||||
|
||||
#else
|
||||
settingBarButtonItem.target = self
|
||||
settingBarButtonItem.action = #selector(HomeTimelineViewController.settingBarButtonItemPressed(_:))
|
||||
#endif
|
||||
|
||||
|
||||
navigationItem.rightBarButtonItem = composeBarButtonItem
|
||||
composeBarButtonItem.target = self
|
||||
composeBarButtonItem.action = #selector(HomeTimelineViewController.composeBarButtonItemPressed(_:))
|
||||
|
||||
tableView.refreshControl = refreshControl
|
||||
|
||||
node.view.refreshControl = refreshControl
|
||||
refreshControl.addTarget(self, action: #selector(HomeTimelineViewController.refreshControlValueChanged(_:)), for: .valueChanged)
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
publishProgressView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(publishProgressView)
|
||||
NSLayoutConstraint.activate([
|
||||
publishProgressView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||
publishProgressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
publishProgressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
])
|
||||
|
||||
viewModel.tableView = tableView
|
||||
//
|
||||
// tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// view.addSubview(tableView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
// tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
// tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
// tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
// ])
|
||||
//
|
||||
// publishProgressView.translatesAutoresizingMaskIntoConstraints = false
|
||||
// view.addSubview(publishProgressView)
|
||||
// NSLayoutConstraint.activate([
|
||||
// publishProgressView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||
// publishProgressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
// publishProgressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
// ])
|
||||
//
|
||||
// viewModel.tableView = tableView
|
||||
viewModel.tableNode = node
|
||||
viewModel.contentOffsetAdjustableTimelineViewControllerDelegate = self
|
||||
tableView.delegate = self
|
||||
tableView.prefetchDataSource = self
|
||||
node.delegate = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
for: tableView,
|
||||
tableNode: node,
|
||||
dependency: self,
|
||||
statusTableViewCellDelegate: self,
|
||||
timelineMiddleLoaderTableViewCellDelegate: self
|
||||
)
|
||||
|
||||
|
||||
// tableView.delegate = self
|
||||
// tableView.prefetchDataSource = self
|
||||
|
||||
// bind refresh control
|
||||
viewModel.isFetchingLatestTimeline
|
||||
.receive(on: DispatchQueue.main)
|
||||
@ -157,88 +174,88 @@ extension HomeTimelineViewController {
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.homeTimelineNavigationBarTitleViewModel.publishingProgress
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] progress in
|
||||
guard let self = self else { return }
|
||||
guard progress > 0 else {
|
||||
let dismissAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut)
|
||||
dismissAnimator.addAnimations {
|
||||
self.publishProgressView.alpha = 0
|
||||
}
|
||||
dismissAnimator.addCompletion { _ in
|
||||
self.publishProgressView.setProgress(0, animated: false)
|
||||
}
|
||||
dismissAnimator.startAnimation()
|
||||
return
|
||||
}
|
||||
if self.publishProgressView.alpha == 0 {
|
||||
let progressAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeOut)
|
||||
progressAnimator.addAnimations {
|
||||
self.publishProgressView.alpha = 1
|
||||
}
|
||||
progressAnimator.startAnimation()
|
||||
}
|
||||
|
||||
self.publishProgressView.setProgress(progress, animated: true)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.timelineIsEmpty
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isEmpty in
|
||||
if isEmpty {
|
||||
self?.showEmptyView()
|
||||
} else {
|
||||
self?.emptyView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// viewModel.homeTimelineNavigationBarTitleViewModel.publishingProgress
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] progress in
|
||||
// guard let self = self else { return }
|
||||
// guard progress > 0 else {
|
||||
// let dismissAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeInOut)
|
||||
// dismissAnimator.addAnimations {
|
||||
// self.publishProgressView.alpha = 0
|
||||
// }
|
||||
// dismissAnimator.addCompletion { _ in
|
||||
// self.publishProgressView.setProgress(0, animated: false)
|
||||
// }
|
||||
// dismissAnimator.startAnimation()
|
||||
// return
|
||||
// }
|
||||
// if self.publishProgressView.alpha == 0 {
|
||||
// let progressAnimator = UIViewPropertyAnimator(duration: 0.1, curve: .easeOut)
|
||||
// progressAnimator.addAnimations {
|
||||
// self.publishProgressView.alpha = 1
|
||||
// }
|
||||
// progressAnimator.startAnimation()
|
||||
// }
|
||||
//
|
||||
// self.publishProgressView.setProgress(progress, animated: true)
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
// viewModel.timelineIsEmpty
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] isEmpty in
|
||||
// if isEmpty {
|
||||
// self?.showEmptyView()
|
||||
// } else {
|
||||
// self?.emptyView.removeFromSuperview()
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
aspectViewWillAppear(animated)
|
||||
|
||||
// needs trigger manually after onboarding dismiss
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
|
||||
if (viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty {
|
||||
viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self)
|
||||
}
|
||||
// aspectViewWillAppear(animated)
|
||||
//
|
||||
// // needs trigger manually after onboarding dismiss
|
||||
// setNeedsStatusBarAppearanceUpdate()
|
||||
//
|
||||
// if (viewModel.fetchedResultsController.fetchedObjects ?? []).isEmpty {
|
||||
// viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self)
|
||||
// }
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
viewModel.viewDidAppear.send()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if (self.viewModel.fetchedResultsController.fetchedObjects ?? []).count == 0 {
|
||||
self.viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self)
|
||||
}
|
||||
}
|
||||
// viewModel.viewDidAppear.send()
|
||||
//
|
||||
// DispatchQueue.main.async { [weak self] in
|
||||
// guard let self = self else { return }
|
||||
// if (self.viewModel.fetchedResultsController.fetchedObjects ?? []).count == 0 {
|
||||
// self.viewModel.loadLatestStateMachine.enter(HomeTimelineViewModel.LoadLatestState.Loading.self)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
aspectViewDidDisappear(animated)
|
||||
// aspectViewDidDisappear(animated)
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
coordinator.animate { _ in
|
||||
// do nothing
|
||||
} completion: { _ in
|
||||
// fix AutoLayout cell height not update after rotate issue
|
||||
self.viewModel.cellFrameCache.removeAllObjects()
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
// coordinator.animate { _ in
|
||||
// // do nothing
|
||||
// } completion: { _ in
|
||||
// // fix AutoLayout cell height not update after rotate issue
|
||||
// self.viewModel.cellFrameCache.removeAllObjects()
|
||||
// self.tableView.reloadData()
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@ -315,100 +332,75 @@ extension HomeTimelineViewController {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@objc func signOutAction(_ sender: UIAction) {
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
|
||||
context.authenticationService.signOutMastodonUser(
|
||||
domain: activeMastodonAuthenticationBox.domain,
|
||||
userID: activeMastodonAuthenticationBox.userID
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
switch result {
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
case .success(let isSignOut):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign out %s", ((#file as NSString).lastPathComponent), #line, #function, isSignOut ? "success" : "fail")
|
||||
guard isSignOut else { return }
|
||||
self.coordinator.setup()
|
||||
self.coordinator.setupOnboardingIfNeeds(animated: true)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - StatusTableViewControllerAspect
|
||||
extension HomeTimelineViewController: StatusTableViewControllerAspect { }
|
||||
//extension HomeTimelineViewController: StatusTableViewControllerAspect { }
|
||||
|
||||
extension HomeTimelineViewController: TableViewCellHeightCacheableContainer {
|
||||
var cellFrameCache: NSCache<NSNumber, NSValue> { return viewModel.cellFrameCache }
|
||||
}
|
||||
//extension HomeTimelineViewController: TableViewCellHeightCacheableContainer {
|
||||
// var cellFrameCache: NSCache<NSNumber, NSValue> { return viewModel.cellFrameCache }
|
||||
//}
|
||||
|
||||
// MARK: - UIScrollViewDelegate
|
||||
extension HomeTimelineViewController {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
|
||||
aspectScrollViewDidScroll(scrollView)
|
||||
//aspectScrollViewDidScroll(scrollView)
|
||||
viewModel.homeTimelineNavigationBarTitleViewModel.handleScrollViewDidScroll(scrollView)
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
||||
typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||
typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading
|
||||
var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||
var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine }
|
||||
}
|
||||
//extension HomeTimelineViewController: LoadMoreConfigurableTableViewContainer {
|
||||
// typealias BottomLoaderTableViewCell = TimelineBottomLoaderTableViewCell
|
||||
// typealias LoadingState = HomeTimelineViewModel.LoadOldestState.Loading
|
||||
// var loadMoreConfigurableTableView: UITableView { return tableView }
|
||||
// var loadMoreConfigurableStateMachine: GKStateMachine { return viewModel.loadoldestStateMachine }
|
||||
//}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension HomeTimelineViewController: UITableViewDelegate {
|
||||
|
||||
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
aspectTableView(tableView, estimatedHeightForRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator)
|
||||
}
|
||||
|
||||
}
|
||||
//extension HomeTimelineViewController: UITableViewDelegate {
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
// aspectTableView(tableView, estimatedHeightForRowAt: indexPath)
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
// aspectTableView(tableView, willDisplay: cell, forRowAt: indexPath)
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||
// aspectTableView(tableView, didEndDisplaying: cell, forRowAt: indexPath)
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
// aspectTableView(tableView, didSelectRowAt: indexPath)
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
// return aspectTableView(tableView, contextMenuConfigurationForRowAt: indexPath, point: point)
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
// return aspectTableView(tableView, previewForHighlightingContextMenuWithConfiguration: configuration)
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
|
||||
// return aspectTableView(tableView, previewForDismissingContextMenuWithConfiguration: configuration)
|
||||
// }
|
||||
//
|
||||
// func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
// aspectTableView(tableView, willPerformPreviewActionForMenuWith: configuration, animator: animator)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
// MARK: - UITableViewDataSourcePrefetching
|
||||
extension HomeTimelineViewController: UITableViewDataSourcePrefetching {
|
||||
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
aspectTableView(tableView, prefetchRowsAt: indexPaths)
|
||||
}
|
||||
}
|
||||
//extension HomeTimelineViewController: UITableViewDataSourcePrefetching {
|
||||
// func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||
// aspectTableView(tableView, prefetchRowsAt: indexPaths)
|
||||
// }
|
||||
//}
|
||||
|
||||
// MARK: - ContentOffsetAdjustableTimelineViewControllerDelegate
|
||||
extension HomeTimelineViewController: ContentOffsetAdjustableTimelineViewControllerDelegate {
|
||||
@ -482,9 +474,9 @@ extension HomeTimelineViewController: TimelineMiddleLoaderTableViewCellDelegate
|
||||
|
||||
// MARK: - ScrollViewContainer
|
||||
extension HomeTimelineViewController: ScrollViewContainer {
|
||||
|
||||
|
||||
var scrollView: UIScrollView { return tableView }
|
||||
|
||||
|
||||
func scrollToTop(animated: Bool) {
|
||||
if scrollView.contentOffset.y < scrollView.frame.height,
|
||||
viewModel.loadLatestStateMachine.canEnterState(HomeTimelineViewModel.LoadLatestState.Loading.self),
|
||||
@ -499,10 +491,10 @@ extension HomeTimelineViewController: ScrollViewContainer {
|
||||
} else {
|
||||
let indexPath = IndexPath(row: 0, section: 0)
|
||||
guard viewModel.diffableDataSource?.itemIdentifier(for: indexPath) != nil else { return }
|
||||
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
|
||||
node.scrollToRow(at: indexPath, at: .top, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerViewControllerDelegate
|
||||
@ -532,7 +524,7 @@ extension HomeTimelineViewController: HomeTimelineNavigationBarTitleViewDelegate
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else { return }
|
||||
let indexPath = IndexPath(row: 0, section: 0)
|
||||
guard diffableDataSource.itemIdentifier(for: indexPath) != nil else { return }
|
||||
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
|
||||
node.scrollToRow(at: indexPath, at: .top, animated: true)
|
||||
case .offlineButton:
|
||||
// TODO: retry
|
||||
break
|
||||
@ -568,3 +560,20 @@ extension HomeTimelineViewController: PerformanceMonitorDelegate {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: - ASTableDelegate
|
||||
extension HomeTimelineViewController: ASTableDelegate {
|
||||
func shouldBatchFetch(for tableNode: ASTableNode) -> Bool {
|
||||
switch viewModel.loadLatestStateMachine.currentState {
|
||||
case is HomeTimelineViewModel.LoadOldestState.NoMore:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func tableNode(_ tableNode: ASTableNode, willBeginBatchFetchWith context: ASBatchContext) {
|
||||
viewModel.loadoldestStateMachine.enter(HomeTimelineViewModel.LoadOldestState.Loading.self)
|
||||
context.completeBatchFetching(true)
|
||||
}
|
||||
}
|
||||
|
@ -9,35 +9,30 @@ import os.log
|
||||
import UIKit
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import AsyncDisplayKit
|
||||
import DifferenceKit
|
||||
import DiffableDataSources
|
||||
|
||||
extension HomeTimelineViewModel {
|
||||
|
||||
|
||||
func setupDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
tableNode: ASTableNode,
|
||||
dependency: NeedsDependency,
|
||||
statusTableViewCellDelegate: StatusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate
|
||||
) {
|
||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.share()
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
diffableDataSource = StatusSection.tableViewDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: dependency,
|
||||
managedObjectContext: fetchedResultsController.managedObjectContext,
|
||||
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||
statusTableViewCellDelegate: statusTableViewCellDelegate,
|
||||
timelineMiddleLoaderTableViewCellDelegate: timelineMiddleLoaderTableViewCellDelegate,
|
||||
threadReplyLoaderTableViewCellDelegate: nil
|
||||
tableNode.automaticallyAdjustsContentOffset = true
|
||||
|
||||
diffableDataSource = StatusSection.tableNodeDiffableDataSource(
|
||||
tableNode: tableNode,
|
||||
managedObjectContext: fetchedResultsController.managedObjectContext
|
||||
)
|
||||
|
||||
// var snapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
|
||||
// snapshot.appendSections([.main])
|
||||
// diffableDataSource?.apply(snapshot)
|
||||
|
||||
var snapshot = DiffableDataSourceSnapshot<StatusSection, Item>()
|
||||
snapshot.appendSections([.main])
|
||||
diffableDataSource?.apply(snapshot)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - NSFetchedResultsControllerDelegate
|
||||
@ -49,21 +44,18 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||
|
||||
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChangeContentWith snapshot: NSDiffableDataSourceSnapshotReference) {
|
||||
os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
guard let tableView = self.tableView else { return }
|
||||
guard let navigationBar = self.contentOffsetAdjustableTimelineViewControllerDelegate?.navigationBar() else { return }
|
||||
|
||||
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
let oldSnapshot = diffableDataSource.snapshot()
|
||||
|
||||
|
||||
let predicate = fetchedResultsController.fetchRequest.predicate
|
||||
let parentManagedObjectContext = fetchedResultsController.managedObjectContext
|
||||
let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
|
||||
managedObjectContext.parent = parentManagedObjectContext
|
||||
|
||||
|
||||
managedObjectContext.perform {
|
||||
var shouldAddBottomLoader = false
|
||||
|
||||
|
||||
let timelineIndexes: [HomeTimelineIndex] = {
|
||||
let request = HomeTimelineIndex.sortedFetchRequest
|
||||
request.returnsObjectsAsFaults = false
|
||||
@ -75,25 +67,25 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||
return []
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
// that's will be the most fastest fetch because of upstream just update and no modify needs consider
|
||||
|
||||
|
||||
var oldSnapshotAttributeDict: [NSManagedObjectID : Item.StatusAttribute] = [:]
|
||||
|
||||
|
||||
for item in oldSnapshot.itemIdentifiers {
|
||||
guard case let .homeTimelineIndex(objectID, attribute) = item else { continue }
|
||||
oldSnapshotAttributeDict[objectID] = attribute
|
||||
}
|
||||
|
||||
|
||||
var newTimelineItems: [Item] = []
|
||||
|
||||
for (i, timelineIndex) in timelineIndexes.enumerated() {
|
||||
let attribute = oldSnapshotAttributeDict[timelineIndex.objectID] ?? Item.StatusAttribute()
|
||||
attribute.isSeparatorLineHidden = false
|
||||
|
||||
|
||||
// append new item into snapshot
|
||||
newTimelineItems.append(.homeTimelineIndex(objectID: timelineIndex.objectID, attribute: attribute))
|
||||
|
||||
|
||||
let isLast = i == timelineIndexes.count - 1
|
||||
switch (isLast, timelineIndex.hasMore) {
|
||||
case (false, true):
|
||||
@ -105,30 +97,22 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||
break
|
||||
}
|
||||
} // end for
|
||||
|
||||
var newSnapshot = NSDiffableDataSourceSnapshot<StatusSection, Item>()
|
||||
|
||||
var newSnapshot = DiffableDataSourceSnapshot<StatusSection, Item>()
|
||||
newSnapshot.appendSections([.main])
|
||||
newSnapshot.appendItems(newTimelineItems, toSection: .main)
|
||||
|
||||
|
||||
let endSnapshot = CACurrentMediaTime()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if shouldAddBottomLoader, !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) {
|
||||
newSnapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
}
|
||||
|
||||
guard let difference = self.calculateReloadSnapshotDifference(navigationBar: navigationBar, tableView: tableView, oldSnapshot: oldSnapshot, newSnapshot: newSnapshot) else {
|
||||
diffableDataSource.apply(newSnapshot)
|
||||
self.isFetchingLatestTimeline.value = false
|
||||
return
|
||||
}
|
||||
|
||||
diffableDataSource.apply(newSnapshot, animatingDifferences: false) {
|
||||
tableView.scrollToRow(at: difference.targetIndexPath, at: .top, animated: false)
|
||||
tableView.contentOffset.y = tableView.contentOffset.y - difference.offset
|
||||
|
||||
if shouldAddBottomLoader, !(self.loadoldestStateMachine.currentState is LoadOldestState.NoMore) {
|
||||
newSnapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
}
|
||||
|
||||
diffableDataSource.apply(newSnapshot, animatingDifferences: false) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.isFetchingLatestTimeline.value = false
|
||||
}
|
||||
|
||||
|
||||
let end = CACurrentMediaTime()
|
||||
os_log("%{public}s[%{public}ld], %{public}s: calculate home timeline layout cost %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - endSnapshot)
|
||||
}
|
||||
@ -145,8 +129,8 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate {
|
||||
private func calculateReloadSnapshotDifference<T: Hashable>(
|
||||
navigationBar: UINavigationBar,
|
||||
tableView: UITableView,
|
||||
oldSnapshot: NSDiffableDataSourceSnapshot<StatusSection, T>,
|
||||
newSnapshot: NSDiffableDataSourceSnapshot<StatusSection, T>
|
||||
oldSnapshot: DiffableDataSourceSnapshot<StatusSection, T>,
|
||||
newSnapshot: DiffableDataSourceSnapshot<StatusSection, T>
|
||||
) -> Difference<T>? {
|
||||
guard oldSnapshot.numberOfItems != 0 else { return nil }
|
||||
|
||||
|
@ -16,6 +16,7 @@ import GameplayKit
|
||||
import AlamofireImage
|
||||
import DateToolsSwift
|
||||
import ActiveLabel
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class HomeTimelineViewModel: NSObject {
|
||||
|
||||
@ -29,15 +30,18 @@ final class HomeTimelineViewModel: NSObject {
|
||||
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
|
||||
|
||||
|
||||
weak var tableNode: ASTableNode?
|
||||
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||
weak var tableView: UITableView?
|
||||
//weak var tableView: UITableView?
|
||||
weak var timelineMiddleLoaderTableViewCellDelegate: TimelineMiddleLoaderTableViewCellDelegate?
|
||||
|
||||
let timelineIsEmpty = CurrentValueSubject<Bool, Never>(false)
|
||||
let homeTimelineNeedRefresh = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
var diffableDataSource: TableNodeDiffableDataSource<StatusSection, Item>?
|
||||
|
||||
// top loader
|
||||
private(set) lazy var loadLatestStateMachine: GKStateMachine = {
|
||||
// exclude timeline middle fetcher state
|
||||
@ -67,7 +71,7 @@ final class HomeTimelineViewModel: NSObject {
|
||||
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<LoadOldestState?, Never>(nil)
|
||||
// middle loader
|
||||
let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine
|
||||
var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
|
||||
// var diffableDataSource: UITableViewDiffableDataSource<StatusSection, Item>?
|
||||
var cellFrameCache = NSCache<NSNumber, NSValue>()
|
||||
|
||||
|
||||
@ -100,12 +104,7 @@ final class HomeTimelineViewModel: NSObject {
|
||||
guard let self = self else { return }
|
||||
self.fetchedResultsController.fetchRequest.predicate = predicate
|
||||
do {
|
||||
self.diffableDataSource?.defaultRowAnimation = .fade
|
||||
try self.fetchedResultsController.performFetch()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.diffableDataSource?.defaultRowAnimation = .automatic
|
||||
}
|
||||
} catch {
|
||||
assertionFailure(error.localizedDescription)
|
||||
}
|
||||
|
109
Mastodon/Scene/Share/View/Node/StatusNode.swift
Normal file
109
Mastodon/Scene/Share/View/Node/StatusNode.swift
Normal file
@ -0,0 +1,109 @@
|
||||
//
|
||||
// StatusNNode.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import AsyncDisplayKit
|
||||
import CoreDataStack
|
||||
|
||||
final class StatusNode: ASCellNode {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
static let avatarImageSize = CGSize(width: 42, height: 42)
|
||||
static let avatarImageCornerRadius: CGFloat = 4
|
||||
|
||||
let avatarImageNode: ASNetworkImageNode = {
|
||||
let node = ASNetworkImageNode()
|
||||
node.contentMode = .scaleAspectFill
|
||||
node.defaultImage = UIImage.placeholder(color: .systemFill)
|
||||
node.cornerRadius = StatusNode.avatarImageCornerRadius
|
||||
// node.cornerRoundingType = .precomposited
|
||||
return node
|
||||
}()
|
||||
|
||||
let nameTextNode = ASTextNode()
|
||||
let nameDotTextNode = ASTextNode()
|
||||
let dateTextNode = ASTextNode()
|
||||
let usernameTextNode = ASTextNode()
|
||||
|
||||
init(status: Status) {
|
||||
super.init()
|
||||
|
||||
automaticallyManagesSubnodes = true
|
||||
|
||||
if let url = (status.reblog ?? status).author.avatarImageURL() {
|
||||
avatarImageNode.url = url
|
||||
}
|
||||
nameTextNode.attributedText = NSAttributedString(string: status.author.displayNameWithFallback, attributes: [
|
||||
.foregroundColor: Asset.Colors.Label.primary.color,
|
||||
.font: UIFont.systemFont(ofSize: 17, weight: .semibold)
|
||||
])
|
||||
nameDotTextNode.attributedText = NSAttributedString(string: "·", attributes: [
|
||||
.foregroundColor: Asset.Colors.Label.secondary.color,
|
||||
.font: UIFont.systemFont(ofSize: 13, weight: .regular)
|
||||
])
|
||||
// set date
|
||||
let createdAt = (status.reblog ?? status).createdAt
|
||||
dateTextNode.attributedText = NSAttributedString(string: createdAt.slowedTimeAgoSinceNow, attributes: [
|
||||
.foregroundColor: Asset.Colors.Label.secondary.color,
|
||||
.font: UIFont.systemFont(ofSize: 13, weight: .regular)
|
||||
])
|
||||
// RunLoop.main.perform { [weak self] in
|
||||
// guard let self = self else { return }
|
||||
// AppContext.shared.timestampUpdatePublisher
|
||||
// .sink { [weak self] _ in
|
||||
// guard let self = self else { return }
|
||||
// self.dateTextNode.attributedText = NSAttributedString(string: createdAt.slowedTimeAgoSinceNow, attributes: [
|
||||
// .foregroundColor: Asset.Colors.Label.secondary.color,
|
||||
// .font: UIFont.systemFont(ofSize: 13, weight: .regular)
|
||||
// ])
|
||||
// }
|
||||
// .store(in: &self.disposeBag)
|
||||
// }
|
||||
usernameTextNode.attributedText = NSAttributedString(string: "@" + status.author.acct, attributes: [
|
||||
.foregroundColor: Asset.Colors.Label.secondary.color,
|
||||
.font: UIFont.systemFont(ofSize: 15, weight: .regular)
|
||||
])
|
||||
}
|
||||
|
||||
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
|
||||
let headerStack = ASStackLayoutSpec.horizontal()
|
||||
headerStack.alignItems = .center
|
||||
headerStack.spacing = 5
|
||||
var headerStackChildren: [ASLayoutElement] = []
|
||||
|
||||
avatarImageNode.style.preferredSize = StatusNode.avatarImageSize
|
||||
headerStackChildren.append(avatarImageNode)
|
||||
|
||||
let authorMetaHeaderStack = ASStackLayoutSpec.horizontal()
|
||||
authorMetaHeaderStack.alignItems = .center
|
||||
authorMetaHeaderStack.spacing = 4
|
||||
authorMetaHeaderStack.children = [
|
||||
nameTextNode,
|
||||
nameDotTextNode,
|
||||
dateTextNode,
|
||||
]
|
||||
let authorMetaStack = ASStackLayoutSpec.vertical()
|
||||
authorMetaStack.children = [
|
||||
authorMetaHeaderStack,
|
||||
usernameTextNode,
|
||||
]
|
||||
|
||||
headerStackChildren.append(authorMetaStack)
|
||||
|
||||
headerStack.children = headerStackChildren
|
||||
|
||||
let verticalStack = ASStackLayoutSpec.vertical()
|
||||
verticalStack.children = [
|
||||
headerStack
|
||||
]
|
||||
|
||||
return verticalStack
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
//
|
||||
// TimelineBottomLoaderNode.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class TimelineBottomLoaderNode: ASCellNode {
|
||||
|
||||
let activityIndicatorNode = ActivityIndicatorNode()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
automaticallyManagesSubnodes = true
|
||||
activityIndicatorNode.bounds = CGRect(x: 0, y: 0, width: 40, height: 40)
|
||||
}
|
||||
|
||||
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
|
||||
let contentStack = ASStackLayoutSpec.horizontal()
|
||||
contentStack.alignItems = .center
|
||||
contentStack.spacing = 7
|
||||
|
||||
contentStack.children = [activityIndicatorNode]
|
||||
|
||||
return contentStack
|
||||
}
|
||||
|
||||
override func didEnterDisplayState() {
|
||||
super.didEnterDisplayState()
|
||||
activityIndicatorNode.animating = true
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
//
|
||||
// TimelineMiddleLoaderNode.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by Cirno MainasuK on 2021-6-19.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
|
||||
final class TimelineMiddleLoaderNode: ASCellNode {
|
||||
|
||||
static let loadButtonFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium))
|
||||
|
||||
let activityIndicatorNode = ASDisplayNode(viewBlock: {
|
||||
let view = UIActivityIndicatorView(style: .medium)
|
||||
view.hidesWhenStopped = true
|
||||
return view
|
||||
})
|
||||
|
||||
let loadButtonNode = ASButtonNode()
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
automaticallyManagesSubnodes = true
|
||||
|
||||
loadButtonNode.setAttributedTitle(
|
||||
NSAttributedString(
|
||||
string: L10n.Common.Controls.Timeline.Loader.loadMissingPosts,
|
||||
attributes: [
|
||||
.foregroundColor: Asset.Colors.brandBlue.color,
|
||||
.font: TimelineMiddleLoaderNode.loadButtonFont
|
||||
]),
|
||||
for: .normal
|
||||
)
|
||||
}
|
||||
|
||||
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
|
||||
let contentStack = ASStackLayoutSpec.horizontal()
|
||||
contentStack.alignItems = .center
|
||||
contentStack.spacing = 7
|
||||
|
||||
contentStack.children = [loadButtonNode]
|
||||
|
||||
|
||||
return contentStack
|
||||
}
|
||||
|
||||
}
|
@ -44,6 +44,10 @@ class AppContext: ObservableObject {
|
||||
private var documentStoreSubscription: AnyCancellable!
|
||||
|
||||
let overrideTraitCollection = CurrentValueSubject<UITraitCollection?, Never>(nil)
|
||||
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||
.autoconnect()
|
||||
.share()
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
init() {
|
||||
let _coreDataStack = CoreDataStack()
|
||||
|
@ -9,6 +9,7 @@ import os.log
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import AppShared
|
||||
import AsyncDisplayKit
|
||||
|
||||
#if DEBUG
|
||||
import GDPerformanceView_Swift
|
||||
@ -33,6 +34,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
#if DEBUG
|
||||
PerformanceMonitor.shared().start()
|
||||
// ASDisplayNode.shouldShowRangeDebugOverlay = true
|
||||
#endif
|
||||
|
||||
return true
|
||||
|
71
Mastodon/Vender/ActivityIndicatorNode.swift
Normal file
71
Mastodon/Vender/ActivityIndicatorNode.swift
Normal file
@ -0,0 +1,71 @@
|
||||
// ref: https://github.com/Adlai-Holler/ASDKPlaceholderTest/blob/eea9fa7cff2d16a57efb47d208422ea9b49a630a/ASDKPlaceholderTest/ASDisplayNodeSubclasses.swift
|
||||
|
||||
import Foundation
|
||||
import AsyncDisplayKit
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
A node that shows a `UIActivityIndicatorView`. Does not support layer backing.
|
||||
Note: You must not change the style to or from `.WhiteLarge` after init, or the node's size will not update.
|
||||
*/
|
||||
class ActivityIndicatorNode: ASDisplayNode {
|
||||
|
||||
private static let defaultSize = CGSize(width: 20, height: 20)
|
||||
private static let largeSize = CGSize(width: 37, height: 37)
|
||||
|
||||
init(style: UIActivityIndicatorView.Style = .medium) {
|
||||
super.init()
|
||||
setViewBlock {
|
||||
UIActivityIndicatorView(style: style)
|
||||
}
|
||||
|
||||
self.style.preferredSize = style == .large ? ActivityIndicatorNode.defaultSize : ActivityIndicatorNode.largeSize
|
||||
}
|
||||
|
||||
var activityIndicatorView: UIActivityIndicatorView {
|
||||
return view as! UIActivityIndicatorView
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
if animating {
|
||||
activityIndicatorView.startAnimating()
|
||||
}
|
||||
activityIndicatorView.color = color
|
||||
activityIndicatorView.hidesWhenStopped = hidesWhenStopped
|
||||
}
|
||||
|
||||
/// Wrapper for `UIActivityIndicatorView.hidesWhenStopped`. NOTE: You must respect thread affinity.
|
||||
var hidesWhenStopped = true {
|
||||
didSet {
|
||||
if isNodeLoaded {
|
||||
assert(Thread.isMainThread)
|
||||
activityIndicatorView.hidesWhenStopped = hidesWhenStopped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for `UIActivityIndicatorView.color`. NOTE: You must respect thread affinity.
|
||||
var color: UIColor? {
|
||||
didSet {
|
||||
if isNodeLoaded {
|
||||
assert(Thread.isMainThread)
|
||||
activityIndicatorView.color = color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for `UIActivityIndicatorView.animating`. NOTE: You must respect thread affinity.
|
||||
var animating = false {
|
||||
didSet {
|
||||
if isNodeLoaded {
|
||||
assert(Thread.isMainThread)
|
||||
if animating {
|
||||
activityIndicatorView.startAnimating()
|
||||
} else {
|
||||
activityIndicatorView.stopAnimating()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
Podfile
3
Podfile
@ -8,7 +8,8 @@ target 'Mastodon' do
|
||||
|
||||
# UI
|
||||
pod 'UITextField+Shake', '~> 1.2'
|
||||
|
||||
pod 'Texture', '~> 3.0.0'
|
||||
|
||||
# misc
|
||||
pod 'SwiftGen', '~> 6.4.0'
|
||||
pod 'DateToolsSwift', '~> 5.0.0'
|
||||
|
46
Podfile.lock
46
Podfile.lock
@ -4,7 +4,42 @@ PODS:
|
||||
- GDPerformanceView-Swift (2.1.1)
|
||||
- Kanna (5.2.4)
|
||||
- Keys (1.0.1)
|
||||
- PINCache (3.0.3):
|
||||
- PINCache/Arc-exception-safe (= 3.0.3)
|
||||
- PINCache/Core (= 3.0.3)
|
||||
- PINCache/Arc-exception-safe (3.0.3):
|
||||
- PINCache/Core
|
||||
- PINCache/Core (3.0.3):
|
||||
- PINOperation (~> 1.2.1)
|
||||
- PINOperation (1.2.1)
|
||||
- PINRemoteImage/Core (3.0.3):
|
||||
- PINOperation
|
||||
- PINRemoteImage/iOS (3.0.3):
|
||||
- PINRemoteImage/Core
|
||||
- PINRemoteImage/PINCache (3.0.3):
|
||||
- PINCache (~> 3.0.3)
|
||||
- PINRemoteImage/Core
|
||||
- SwiftGen (6.4.0)
|
||||
- Texture (3.0.0):
|
||||
- Texture/AssetsLibrary (= 3.0.0)
|
||||
- Texture/Core (= 3.0.0)
|
||||
- Texture/MapKit (= 3.0.0)
|
||||
- Texture/Photos (= 3.0.0)
|
||||
- Texture/PINRemoteImage (= 3.0.0)
|
||||
- Texture/Video (= 3.0.0)
|
||||
- Texture/AssetsLibrary (3.0.0):
|
||||
- Texture/Core
|
||||
- Texture/Core (3.0.0)
|
||||
- Texture/MapKit (3.0.0):
|
||||
- Texture/Core
|
||||
- Texture/Photos (3.0.0):
|
||||
- Texture/Core
|
||||
- Texture/PINRemoteImage (3.0.0):
|
||||
- PINRemoteImage/iOS (~> 3.0.0)
|
||||
- PINRemoteImage/PINCache
|
||||
- Texture/Core
|
||||
- Texture/Video (3.0.0):
|
||||
- Texture/Core
|
||||
- "UITextField+Shake (1.2.1)"
|
||||
|
||||
DEPENDENCIES:
|
||||
@ -14,6 +49,7 @@ DEPENDENCIES:
|
||||
- Kanna (~> 5.2.2)
|
||||
- Keys (from `Pods/CocoaPodsKeys`)
|
||||
- SwiftGen (~> 6.4.0)
|
||||
- Texture
|
||||
- "UITextField+Shake (~> 1.2)"
|
||||
|
||||
SPEC REPOS:
|
||||
@ -22,7 +58,11 @@ SPEC REPOS:
|
||||
- FLEX
|
||||
- GDPerformanceView-Swift
|
||||
- Kanna
|
||||
- PINCache
|
||||
- PINOperation
|
||||
- PINRemoteImage
|
||||
- SwiftGen
|
||||
- Texture
|
||||
- "UITextField+Shake"
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@ -35,9 +75,13 @@ SPEC CHECKSUMS:
|
||||
GDPerformanceView-Swift: 22d964fe40b19e3d914dba2586237d064de8fd77
|
||||
Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f
|
||||
Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9
|
||||
PINCache: 7a8fc1a691173d21dbddbf86cd515de6efa55086
|
||||
PINOperation: 00c935935f1e8cf0d1e2d6b542e75b88fc3e5e20
|
||||
PINRemoteImage: f1295b29f8c5e640e25335a1b2bd9d805171bd01
|
||||
SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108
|
||||
Texture: 2f109e937850d94d1d07232041c9c7313ccddb81
|
||||
"UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3
|
||||
|
||||
PODFILE CHECKSUM: 257c550231fcd1336a29f7835aa331171bb66ebd
|
||||
PODFILE CHECKSUM: 464046172607e3a92ad500f8050ee34566a47c73
|
||||
|
||||
COCOAPODS: 1.10.1
|
||||
|
Loading…
x
Reference in New Issue
Block a user