From 1a3135b9981c3e6db930ee5186399abb83c8f843 Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 19 Jun 2021 18:33:29 +0800 Subject: [PATCH] feat: [WIP] migrate to Texture (AsyncDisplayKit) for better performance --- Mastodon.xcodeproj/project.pbxproj | 78 ++++ .../xcschemes/xcschememanagement.plist | 8 +- .../xcshareddata/swiftpm/Package.resolved | 18 + .../Diffiable/DataSource/ASTableNode.swift | 81 ++++ .../TableNodeDiffableDataSource.swift | 111 ++++++ Mastodon/Diffiable/Item/Item.swift | 3 + .../Diffiable/Section/StatusSection.swift | 28 ++ .../NSDiffableDataSourceSnapshot.swift | 24 ++ ...meTimelineViewController+DebugAction.swift | 29 +- .../HomeTimelineViewController+Provider.swift | 2 +- .../HomeTimelineViewController.swift | 377 +++++++++--------- .../HomeTimelineViewModel+Diffable.swift | 92 ++--- .../HomeTimeline/HomeTimelineViewModel.swift | 15 +- .../Scene/Share/View/Node/StatusNode.swift | 109 +++++ .../View/Node/TimelineBottomLoaderNode.swift | 37 ++ .../View/Node/TimelineMiddleLoaderNode.swift | 50 +++ Mastodon/State/AppContext.swift | 4 + Mastodon/Supporting Files/AppDelegate.swift | 2 + Mastodon/Vender/ActivityIndicatorNode.swift | 71 ++++ Podfile | 3 +- Podfile.lock | 46 ++- 21 files changed, 933 insertions(+), 255 deletions(-) create mode 100644 Mastodon/Diffiable/DataSource/ASTableNode.swift create mode 100644 Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift create mode 100644 Mastodon/Extension/NSDiffableDataSourceSnapshot.swift create mode 100644 Mastodon/Scene/Share/View/Node/StatusNode.swift create mode 100644 Mastodon/Scene/Share/View/Node/TimelineBottomLoaderNode.swift create mode 100644 Mastodon/Scene/Share/View/Node/TimelineMiddleLoaderNode.swift create mode 100644 Mastodon/Vender/ActivityIndicatorNode.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 98caf2775..5201c5409 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -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 = ""; }; DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Field.swift"; sourceTree = ""; }; DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; }; + DBAC6484267D0F9E007FE9FD /* StatusNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusNode.swift; sourceTree = ""; }; + DBAC6487267D388B007FE9FD /* ASTableNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASTableNode.swift; sourceTree = ""; }; + DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSDiffableDataSourceSnapshot.swift; sourceTree = ""; }; + DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableNodeDiffableDataSource.swift; sourceTree = ""; }; + DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderNode.swift; sourceTree = ""; }; + DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderNode.swift; sourceTree = ""; }; + DBAC649A267DF8C8007FE9FD /* ActivityIndicatorNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorNode.swift; sourceTree = ""; }; DBAE3F672615DD60004B8251 /* UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProvider.swift; sourceTree = ""; }; DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewController+UserProvider.swift"; sourceTree = ""; }; DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProviderFacade.swift; sourceTree = ""; }; @@ -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 = ""; @@ -1414,6 +1433,7 @@ 2D76319D25C151F600929FB9 /* Section */, 2D7631B125C159E700929FB9 /* Item */, DBCBED2226132E1D00B49291 /* FetchedResultsController */, + DBAC6490267DC84F007FE9FD /* DataSource */, ); path = Diffiable; sourceTree = ""; @@ -1464,6 +1484,7 @@ DB87D45C2609DE6600D12C0D /* TextField */, DB1D187125EF5BBD003F1F23 /* TableView */, 2D7631A625C1533800929FB9 /* TableviewCell */, + DBAC6486267D0FAC007FE9FD /* Node */, ); path = View; sourceTree = ""; @@ -2167,6 +2188,7 @@ DBCC3B35261440BA0045B23D /* UINavigationController.swift */, DB6D1B23263684C600ACB481 /* UserDefaults.swift */, DB97131E2666078B00BD1E90 /* Date.swift */, + DBAC6489267DC355007FE9FD /* NSDiffableDataSourceSnapshot.swift */, ); path = Extension; sourceTree = ""; @@ -2328,6 +2350,25 @@ path = View; sourceTree = ""; }; + DBAC6486267D0FAC007FE9FD /* Node */ = { + isa = PBXGroup; + children = ( + DBAC6484267D0F9E007FE9FD /* StatusNode.swift */, + DBAC6496267DECCB007FE9FD /* TimelineMiddleLoaderNode.swift */, + DBAC6498267DF2C4007FE9FD /* TimelineBottomLoaderNode.swift */, + ); + path = Node; + sourceTree = ""; + }; + DBAC6490267DC84F007FE9FD /* DataSource */ = { + isa = PBXGroup; + children = ( + DBAC6487267D388B007FE9FD /* ASTableNode.swift */, + DBAC648E267DC84D007FE9FD /* TableNodeDiffableDataSource.swift */, + ); + path = DataSource; + sourceTree = ""; + }; 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" */; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 2fb2d9806..50a025853 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ AppShared.xcscheme_^#shared#^_ orderHint - 14 + 19 CoreDataStack.xcscheme_^#shared#^_ orderHint - 17 + 18 Mastodon - RTL.xcscheme_^#shared#^_ @@ -27,12 +27,12 @@ Mastodon.xcscheme_^#shared#^_ orderHint - 2 + 17 NotificationService.xcscheme_^#shared#^_ orderHint - 16 + 20 SuppressBuildableAutocreation diff --git a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1ca83d0d7..f46eff823 100644 --- a/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Mastodon.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -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", diff --git a/Mastodon/Diffiable/DataSource/ASTableNode.swift b/Mastodon/Diffiable/DataSource/ASTableNode.swift new file mode 100644 index 000000000..f2849cfe8 --- /dev/null +++ b/Mastodon/Diffiable/DataSource/ASTableNode.swift @@ -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( + using stagedChangeset: StagedChangeset, + 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) -> 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() + } + } + } +} diff --git a/Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift b/Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift new file mode 100644 index 000000000..508f07de9 --- /dev/null +++ b/Mastodon/Diffiable/DataSource/TableNodeDiffableDataSource.swift @@ -0,0 +1,111 @@ +// +// TableNodeDiffableDataSource.swift +// Mastodon +// +// Created by Cirno MainasuK on 2021-6-19. +// + +import UIKit +import AsyncDisplayKit +import DiffableDataSources + +open class TableNodeDiffableDataSource: 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() + + /// 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, 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 { + 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 + } +} diff --git a/Mastodon/Diffiable/Item/Item.swift b/Mastodon/Diffiable/Item/Item.swift index 04a1262d5..fe40cfd6c 100644 --- a/Mastodon/Diffiable/Item/Item.swift +++ b/Mastodon/Diffiable/Item/Item.swift @@ -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 { } diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index c5d0eb19b..5cc2ed198 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -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 { + 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, diff --git a/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift b/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift new file mode 100644 index 000000000..c2ff341d9 --- /dev/null +++ b/Mastodon/Extension/NSDiffableDataSourceSnapshot.swift @@ -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.. 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 diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift index d735d5843..38d843114 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+Provider.swift @@ -53,7 +53,7 @@ extension HomeTimelineViewController: StatusProvider { } var tableViewDiffableDataSource: UITableViewDiffableDataSource? { - return viewModel.diffableDataSource + return nil } func item(for cell: UITableViewCell?, indexPath: IndexPath?) -> Item? { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift index 529f2d81c..68afb41e2 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController.swift @@ -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, 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 { return viewModel.cellFrameCache } -} +//extension HomeTimelineViewController: TableViewCellHeightCacheableContainer { +// var cellFrameCache: NSCache { 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) + } +} diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 6f5e66c0e..5667af39f 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -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() -// snapshot.appendSections([.main]) -// diffableDataSource?.apply(snapshot) + + var snapshot = DiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + diffableDataSource?.apply(snapshot) } - + } // MARK: - NSFetchedResultsControllerDelegate @@ -49,21 +44,18 @@ extension HomeTimelineViewModel: NSFetchedResultsControllerDelegate { func controller(_ controller: NSFetchedResultsController, 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() + + var newSnapshot = DiffableDataSourceSnapshot() 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( navigationBar: UINavigationBar, tableView: UITableView, - oldSnapshot: NSDiffableDataSourceSnapshot, - newSnapshot: NSDiffableDataSourceSnapshot + oldSnapshot: DiffableDataSourceSnapshot, + newSnapshot: DiffableDataSourceSnapshot ) -> Difference? { guard oldSnapshot.numberOfItems != 0 else { return nil } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index fdbbfba9d..11893e33c 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -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(false) let viewDidAppear = PassthroughSubject() 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(false) let homeTimelineNeedRefresh = PassthroughSubject() // output + var diffableDataSource: TableNodeDiffableDataSource? + // 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(nil) // middle loader let loadMiddleSateMachineList = CurrentValueSubject<[NSManagedObjectID: GKStateMachine], Never>([:]) // TimelineIndex.objectID : middle loading state machine - var diffableDataSource: UITableViewDiffableDataSource? + // var diffableDataSource: UITableViewDiffableDataSource? var cellFrameCache = NSCache() @@ -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) } diff --git a/Mastodon/Scene/Share/View/Node/StatusNode.swift b/Mastodon/Scene/Share/View/Node/StatusNode.swift new file mode 100644 index 000000000..818711083 --- /dev/null +++ b/Mastodon/Scene/Share/View/Node/StatusNode.swift @@ -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() + + 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 + } + +} diff --git a/Mastodon/Scene/Share/View/Node/TimelineBottomLoaderNode.swift b/Mastodon/Scene/Share/View/Node/TimelineBottomLoaderNode.swift new file mode 100644 index 000000000..aeff71e4f --- /dev/null +++ b/Mastodon/Scene/Share/View/Node/TimelineBottomLoaderNode.swift @@ -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 + } + +} diff --git a/Mastodon/Scene/Share/View/Node/TimelineMiddleLoaderNode.swift b/Mastodon/Scene/Share/View/Node/TimelineMiddleLoaderNode.swift new file mode 100644 index 000000000..33a15dd75 --- /dev/null +++ b/Mastodon/Scene/Share/View/Node/TimelineMiddleLoaderNode.swift @@ -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 + } + +} diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index b6b0cdb55..416053b41 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -44,6 +44,10 @@ class AppContext: ObservableObject { private var documentStoreSubscription: AnyCancellable! let overrideTraitCollection = CurrentValueSubject(nil) + let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common) + .autoconnect() + .share() + .eraseToAnyPublisher() init() { let _coreDataStack = CoreDataStack() diff --git a/Mastodon/Supporting Files/AppDelegate.swift b/Mastodon/Supporting Files/AppDelegate.swift index e6ccaaac0..31a7db5ba 100644 --- a/Mastodon/Supporting Files/AppDelegate.swift +++ b/Mastodon/Supporting Files/AppDelegate.swift @@ -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 diff --git a/Mastodon/Vender/ActivityIndicatorNode.swift b/Mastodon/Vender/ActivityIndicatorNode.swift new file mode 100644 index 000000000..6d34072f3 --- /dev/null +++ b/Mastodon/Vender/ActivityIndicatorNode.swift @@ -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() + } + } + } + } +} diff --git a/Podfile b/Podfile index 796473d68..d888d37f4 100644 --- a/Podfile +++ b/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' diff --git a/Podfile.lock b/Podfile.lock index 4e7baf347..ce8b41ff8 100644 --- a/Podfile.lock +++ b/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