From 4b0b814fce2aaf1140cd8764be63ecea34857197 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Sun, 4 Feb 2024 16:37:55 +0000 Subject: [PATCH 01/13] Make CountriesMap Hashable --- .../ViewRelated/Stats/Period Stats/Countries/CountriesMap.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/Countries/CountriesMap.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/Countries/CountriesMap.swift index 5a31e6e81e1d..6f2e586384ac 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/Countries/CountriesMap.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/Countries/CountriesMap.swift @@ -1,6 +1,6 @@ import Foundation -struct CountriesMap { +struct CountriesMap: Hashable { let minViewsCount: Int let maxViewsCount: Int let data: [String: NSNumber] From 505e20a3fd01c84f0a96de6a9c94784da3a38d15 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Sun, 4 Feb 2024 16:38:08 +0000 Subject: [PATCH 02/13] Make OverviewTabData Hashable --- .../ViewRelated/Stats/Period Stats/Overview/OverviewCell.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/Overview/OverviewCell.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/Overview/OverviewCell.swift index 1515fff2c429..403a9079b358 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/Overview/OverviewCell.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/Overview/OverviewCell.swift @@ -1,6 +1,6 @@ import UIKit -struct OverviewTabData: FilterTabBarItem { +struct OverviewTabData: FilterTabBarItem, Hashable { var tabTitle: String var tabData: Int var tabDataStub: String? From bcd658c910c8c8bc50ec760c7cf032c8e65d0824 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Sun, 4 Feb 2024 16:38:37 +0000 Subject: [PATCH 03/13] Make GhostTableViews used within Stats Traffic Hashable --- .../GhostViews/StatsGhostTableViewRows.swift | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift index e02d012f1d5f..7934d6c76a09 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift @@ -40,7 +40,7 @@ struct StatsGhostTwoColumnImmutableRow: StatsRowGhostable { var statSection: StatSection? = nil } -struct StatsGhostTopImmutableRow: StatsRowGhostable { +struct StatsGhostTopImmutableRow: StatsRowGhostable, Hashable { static let cell: ImmuTableCell = { return ImmuTableCell.nib(StatsGhostTopCell.defaultNib, StatsGhostTopCell.self) }() @@ -49,6 +49,20 @@ struct StatsGhostTopImmutableRow: StatsRowGhostable { var hideBottomBorder = false var statSection: StatSection? = nil + // MARK: - Hashable + + static func == (lhs: StatsGhostTopImmutableRow, rhs: StatsGhostTopImmutableRow) -> Bool { + return lhs.hideTopBorder == rhs.hideTopBorder && + lhs.hideBottomBorder == rhs.hideBottomBorder && + lhs.statSection == rhs.statSection + } + + func hash(into hasher: inout Hasher) { + hasher.combine(hideTopBorder) + hasher.combine(hideBottomBorder) + hasher.combine(statSection) + } + func configureCell(_ cell: UITableViewCell) { DispatchQueue.main.async { cell.startGhostAnimation(style: GhostCellStyle.muriel) @@ -84,7 +98,7 @@ struct StatsGhostPostingActivitiesImmutableRow: StatsRowGhostable { var statSection: StatSection? = nil } -struct StatsGhostChartImmutableRow: StatsRowGhostable { +struct StatsGhostChartImmutableRow: StatsRowGhostable, Hashable { static let cell: ImmuTableCell = { return ImmuTableCell.nib(StatsGhostChartCell.defaultNib, StatsGhostChartCell.self) }() @@ -112,6 +126,20 @@ struct StatsGhostDetailRow: StatsRowGhostable { detailCell.enableTopPadding = enableTopPadding } } + + // MARK: - Hashable + + static func == (lhs: StatsGhostDetailRow, rhs: StatsGhostDetailRow) -> Bool { + return lhs.hideTopBorder == rhs.hideTopBorder && + lhs.isLastRow == rhs.isLastRow && + lhs.enableTopPadding == rhs.enableTopPadding + } + + func hash(into hasher: inout Hasher) { + hasher.combine(hideTopBorder) + hasher.combine(isLastRow) + hasher.combine(enableTopPadding) + } } struct StatsGhostTitleRow: StatsRowGhostable { From 70d9b256cd6c3ea7b3b93b617402b7a37e58a0c8 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Sun, 4 Feb 2024 16:38:53 +0000 Subject: [PATCH 04/13] Make StatsTableViews used within Stats Traffic Hashable --- .../Stats/SiteStatsTableViewCells.swift | 127 ++++++++++++++++-- 1 file changed, 119 insertions(+), 8 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift index 75189c90c686..c0c34dda3b32 100644 --- a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift +++ b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift @@ -1,9 +1,10 @@ import UIKit import Gridicons +import DGCharts // MARK: - Shared Rows -struct OverviewRow: ImmuTableRow { +struct OverviewRow: ImmuTableRow, Hashable { typealias CellType = OverviewCell @@ -13,12 +14,26 @@ struct OverviewRow: ImmuTableRow { let tabsData: [OverviewTabData] let action: ImmuTableAction? = nil - let chartData: [BarChartDataConvertible] + let chartData: [any BarChartDataConvertible] let chartStyling: [BarChartStyling] let period: StatsPeriodUnit? weak var statsBarChartViewDelegate: StatsBarChartViewDelegate? let chartHighlightIndex: Int? + // MARK: - Hashable + + static func == (lhs: OverviewRow, rhs: OverviewRow) -> Bool { + return lhs.tabsData == rhs.tabsData && +// lhs.chartData.barChartData.dataSets == rhs.chartData.barChartData.dataSets && + lhs.chartHighlightIndex == rhs.chartHighlightIndex + } + + func hash(into hasher: inout Hasher) { + hasher.combine(tabsData) +// hasher.combine(chartData.barChartData.dataSets) + hasher.combine(chartHighlightIndex) + } + func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? CellType else { @@ -77,9 +92,19 @@ struct CellHeaderRow: ImmuTableRow { cell.configure(statSection: statSection) } + + // MARK: - Hashable + + static func == (lhs: CellHeaderRow, rhs: CellHeaderRow) -> Bool { + return lhs.statSection == rhs.statSection + } + + func hash(into hasher: inout Hasher) { + hasher.combine(statSection) + } } -struct TableFooterRow: ImmuTableRow { +struct TableFooterRow: ImmuTableRow, Hashable { typealias CellType = StatsTableFooter @@ -93,6 +118,14 @@ struct TableFooterRow: ImmuTableRow { // No configuration needed. // This method is needed to satisfy ImmuTableRow protocol requirements. } + + // MARK: - Hashable + + static func == (lhs: TableFooterRow, rhs: TableFooterRow) -> Bool { + return true + } + + func hash(into hasher: inout Hasher) {} } // MARK: - Insights Rows @@ -372,7 +405,7 @@ struct AddInsightStatRow: ImmuTableRow { // MARK: - Period Rows -struct PeriodEmptyCellHeaderRow: ImmuTableRow { +struct PeriodEmptyCellHeaderRow: ImmuTableRow, Hashable { typealias CellType = StatsCellHeader @@ -390,6 +423,14 @@ struct PeriodEmptyCellHeaderRow: ImmuTableRow { cell.configure() } + + // MARK: - Hashable + + static func == (lhs: PeriodEmptyCellHeaderRow, rhs: PeriodEmptyCellHeaderRow) -> Bool { + return true + } + + func hash(into hasher: inout Hasher) {} } struct TopTotalsPeriodStatsRow: ImmuTableRow { @@ -411,6 +452,22 @@ struct TopTotalsPeriodStatsRow: ImmuTableRow { var topAccessoryView: UIView? = nil let action: ImmuTableAction? = nil + // MARK: - Hashable + + static func == (lhs: TopTotalsPeriodStatsRow, rhs: TopTotalsPeriodStatsRow) -> Bool { + return lhs.itemSubtitle == rhs.itemSubtitle && + lhs.dataSubtitle == rhs.dataSubtitle && + lhs.dataRows == rhs.dataRows && + lhs.statSection == rhs.statSection + } + + func hash(into hasher: inout Hasher) { + hasher.combine(itemSubtitle) + hasher.combine(dataSubtitle) + hasher.combine(dataRows) + hasher.combine(statSection) + } + func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? CellType else { @@ -429,7 +486,7 @@ struct TopTotalsPeriodStatsRow: ImmuTableRow { } } -struct TopTotalsNoSubtitlesPeriodStatsRow: ImmuTableRow { +struct TopTotalsNoSubtitlesPeriodStatsRow: ImmuTableRow, Hashable { typealias CellType = TopTotalsCell @@ -442,6 +499,18 @@ struct TopTotalsNoSubtitlesPeriodStatsRow: ImmuTableRow { weak var siteStatsPeriodDelegate: SiteStatsPeriodDelegate? let action: ImmuTableAction? = nil + // MARK: - Hashable + + static func == (lhs: TopTotalsNoSubtitlesPeriodStatsRow, rhs: TopTotalsNoSubtitlesPeriodStatsRow) -> Bool { + return lhs.dataRows == rhs.dataRows && + lhs.statSection == rhs.statSection + } + + func hash(into hasher: inout Hasher) { + hasher.combine(dataRows) + hasher.combine(statSection) + } + func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? CellType else { @@ -452,7 +521,7 @@ struct TopTotalsNoSubtitlesPeriodStatsRow: ImmuTableRow { } } -struct CountriesStatsRow: ImmuTableRow { +struct CountriesStatsRow: ImmuTableRow, Hashable { typealias CellType = CountriesCell @@ -468,6 +537,22 @@ struct CountriesStatsRow: ImmuTableRow { weak var siteStatsInsightsDetailsDelegate: SiteStatsInsightsDelegate? let action: ImmuTableAction? = nil + // MARK: - Hashable + + static func == (lhs: CountriesStatsRow, rhs: CountriesStatsRow) -> Bool { + return lhs.itemSubtitle == rhs.itemSubtitle && + lhs.dataSubtitle == rhs.dataSubtitle && + lhs.statSection == rhs.statSection && + lhs.dataRows == rhs.dataRows + } + + func hash(into hasher: inout Hasher) { + hasher.combine(dataRows) + hasher.combine(itemSubtitle) + hasher.combine(dataSubtitle) + hasher.combine(statSection) + } + func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? CellType else { @@ -483,7 +568,7 @@ struct CountriesStatsRow: ImmuTableRow { } } -struct CountriesMapRow: ImmuTableRow { +struct CountriesMapRow: ImmuTableRow, Hashable { let action: ImmuTableAction? = nil let countriesMap: CountriesMap var statSection: StatSection? @@ -494,6 +579,18 @@ struct CountriesMapRow: ImmuTableRow { return ImmuTableCell.nib(CellType.defaultNib, CellType.self) }() + // MARK: - Hashable + + static func == (lhs: CountriesMapRow, rhs: CountriesMapRow) -> Bool { + return lhs.countriesMap == rhs.countriesMap && + lhs.statSection == rhs.statSection + } + + func hash(into hasher: inout Hasher) { + hasher.combine(countriesMap) + hasher.combine(statSection) + } + func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? CellType else { return @@ -740,7 +837,7 @@ struct DetailSubtitlesTabbedHeaderRow: ImmuTableRow { } } -struct StatsErrorRow: ImmuTableRow { +struct StatsErrorRow: ImmuTableRow, Hashable { static let cell: ImmuTableCell = { return ImmuTableCell.nib(StatsStackViewCell.defaultNib, StatsStackViewCell.self) }() @@ -751,6 +848,20 @@ struct StatsErrorRow: ImmuTableRow { private let noDataRow = StatsNoDataRow.loadFromNib() + // MARK: - Hashable + + static func == (lhs: StatsErrorRow, rhs: StatsErrorRow) -> Bool { + return lhs.rowStatus == rhs.rowStatus && + lhs.statType == rhs.statType && + lhs.statSection == rhs.statSection + } + + func hash(into hasher: inout Hasher) { + hasher.combine(rowStatus) + hasher.combine(statType) + hasher.combine(statSection) + } + func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? StatsStackViewCell else { return From 9a0475c4821a827e689b28585d502c516cbc707e Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Sun, 4 Feb 2024 16:39:02 +0000 Subject: [PATCH 05/13] Make StatsTotalRow Hashable --- .../ViewRelated/Stats/Shared Views/StatsTotalRow.swift | 2 +- .../Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift index 4bcb123c8e65..7a2c869e79a5 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/StatsTotalRow.swift @@ -1,6 +1,6 @@ import UIKit -struct StatsTotalRowData { +struct StatsTotalRowData: Hashable { var name: String var data: String var mediaID: NSNumber? diff --git a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift index c0c34dda3b32..847c0abfd56b 100644 --- a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift +++ b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift @@ -73,7 +73,7 @@ struct ViewsVisitorsRow: ImmuTableRow { } } -struct CellHeaderRow: ImmuTableRow { +struct CellHeaderRow: ImmuTableRow, Hashable { typealias CellType = StatsCellHeader @@ -433,7 +433,7 @@ struct PeriodEmptyCellHeaderRow: ImmuTableRow, Hashable { func hash(into hasher: inout Hasher) {} } -struct TopTotalsPeriodStatsRow: ImmuTableRow { +struct TopTotalsPeriodStatsRow: ImmuTableRow, Hashable { typealias CellType = TopTotalsCell From c0f10582156bd0131516b67b26e32ab8665d9da7 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Sun, 4 Feb 2024 18:06:20 +0000 Subject: [PATCH 06/13] Create data structures for Stats Traffic rows and sections --- .../SiteStatsPeriodViewModel.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift index a7d120352878..f04f92319998 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift @@ -4,6 +4,32 @@ import WordPressFlux /// The view model used by Period Stats. /// +enum StatsTrafficRow: Hashable { + case emptyPeriodCellHeader(PeriodEmptyCellHeaderRow) + case cellHeader(CellHeaderRow) + case topTotals(TopTotalsPeriodStatsRow) + case topTotalsNoSubtitles(TopTotalsNoSubtitlesPeriodStatsRow) + case countries(CountriesStatsRow) + case countriesMap(CountriesMapRow) + case overview(OverviewRow) + case footer(TableFooterRow) + case error(StatsErrorRow) + case ghostChart(StatsGhostChartImmutableRow) + case ghostTop(StatsGhostTopImmutableRow) +} + +struct StatsTrafficSection: Hashable { + let headerText: String? + let rows: [StatsTrafficRow] + let footerText: String? + + init(headerText: String? = nil, rows: [StatsTrafficRow], footerText: String? = nil) { + self.headerText = headerText + self.rows = rows + self.footerText = footerText + } +} + class SiteStatsPeriodViewModel: Observable { // MARK: - Properties From e51b1b3712fc00c02e8cc2ac921ec2d124c45d2d Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Sun, 4 Feb 2024 18:07:22 +0000 Subject: [PATCH 07/13] Define DiffableDataSource structures for Stats Traffic --- .../Stats/Period Stats/SiteStatsPeriodViewModel.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift index f04f92319998..b6dfc447d577 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift @@ -4,6 +4,9 @@ import WordPressFlux /// The view model used by Period Stats. /// +typealias StatsTrafficSnapshot = NSDiffableDataSourceSnapshot +typealias StatsTrafficDataSource = UITableViewDiffableDataSource + enum StatsTrafficRow: Hashable { case emptyPeriodCellHeader(PeriodEmptyCellHeaderRow) case cellHeader(CellHeaderRow) From 7e1a3c18b03503c8d73dc802c688d8d2968c43ff Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Mon, 12 Feb 2024 15:01:09 +0200 Subject: [PATCH 08/13] Define StatsTrafficModels that support DiffableDataSource in a single place The diffable data source relies on both the identity and the equality of the items it manages. The identity is determined by the item's hash, and equality is determined by whether the item's content has changed. If the content of an item is considered to have changed (even if its hash hasn't), the diffable data source may decide to reload that item --- .../Period Stats/StatsTrafficModels.swift | 41 +++++++++++++++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 26 +++++++----- 2 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Stats/Period Stats/StatsTrafficModels.swift diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/StatsTrafficModels.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/StatsTrafficModels.swift new file mode 100644 index 000000000000..46ae82b384f6 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/StatsTrafficModels.swift @@ -0,0 +1,41 @@ +import UIKit + +protocol HashableImmuTableRow: ImmuTableRow, Hashable { + var statSection: StatSection? { get } +} + +typealias StatsTrafficSnapshot = NSDiffableDataSourceSnapshot +typealias StatsTrafficDataSource = UITableViewDiffableDataSource + +struct StatsTrafficRow: Hashable { + let immuTableRow: any HashableImmuTableRow + + static func == (lhs: StatsTrafficRow, rhs: StatsTrafficRow) -> Bool { + lhs.immuTableRow.hashValue == rhs.immuTableRow.hashValue + } + + func hash(into hasher: inout Hasher) { + hasher.combine(immuTableRow) + } +} + +struct StatsTrafficSection: Hashable { + let periodType: PeriodType + + init(periodType: PeriodType) { + self.periodType = periodType + } +} + +extension HashableImmuTableRow { + /// The diffable data source relies on both the identity and the equality of the items it manages. + /// The identity is determined by the item's hash, and equality is determined by whether the item's content has changed. + /// If the content of an item is considered to have changed (even if its hash hasn't), the diffable data source may decide to reload that item. + /// + /// Calculate hash (identity) based on StatSection type and Row type + /// If identity is equal particular cell reloads only if content changes + func hash(into hasher: inout Hasher) { + hasher.combine(statSection) + hasher.combine(String(describing: type(of: self))) + } +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 9eb9f2d02128..a26f9611b1ab 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -223,6 +223,8 @@ 01D2FF652AA77F790038E040 /* LockScreenTodayViewsVisitorsStatWidgetConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D2FF632AA77F790038E040 /* LockScreenTodayViewsVisitorsStatWidgetConfig.swift */; }; 01D2FF682AA780DC0038E040 /* LockScreenAllTimeViewsVisitorsStatWidgetConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D2FF662AA780DC0038E040 /* LockScreenAllTimeViewsVisitorsStatWidgetConfig.swift */; }; 01D2FF6B2AA782720038E040 /* LockScreenAllTimePostsBestViewsStatWidgetConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D2FF692AA782720038E040 /* LockScreenAllTimePostsBestViewsStatWidgetConfig.swift */; }; + 01D7EBA42B7A4BBD00F14992 /* StatsTrafficModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D7EBA32B7A4BBD00F14992 /* StatsTrafficModels.swift */; }; + 01D7EBA52B7A4BBD00F14992 /* StatsTrafficModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01D7EBA32B7A4BBD00F14992 /* StatsTrafficModels.swift */; }; 01DBFD8729BDCBF200F3720F /* JetpackNativeConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DBFD8629BDCBF200F3720F /* JetpackNativeConnectionService.swift */; }; 01DBFD8829BDCBF200F3720F /* JetpackNativeConnectionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DBFD8629BDCBF200F3720F /* JetpackNativeConnectionService.swift */; }; 01DC4CD82B5FC8CE000110E5 /* SiteStatsPeriodTableViewControllerDeprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DC4CD72B5FC8CE000110E5 /* SiteStatsPeriodTableViewControllerDeprecated.swift */; }; @@ -5927,6 +5929,7 @@ 01D2FF632AA77F790038E040 /* LockScreenTodayViewsVisitorsStatWidgetConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenTodayViewsVisitorsStatWidgetConfig.swift; sourceTree = ""; }; 01D2FF662AA780DC0038E040 /* LockScreenAllTimeViewsVisitorsStatWidgetConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenAllTimeViewsVisitorsStatWidgetConfig.swift; sourceTree = ""; }; 01D2FF692AA782720038E040 /* LockScreenAllTimePostsBestViewsStatWidgetConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenAllTimePostsBestViewsStatWidgetConfig.swift; sourceTree = ""; }; + 01D7EBA32B7A4BBD00F14992 /* StatsTrafficModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsTrafficModels.swift; sourceTree = ""; }; 01DBFD8629BDCBF200F3720F /* JetpackNativeConnectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JetpackNativeConnectionService.swift; sourceTree = ""; }; 01DC4CD72B5FC8CE000110E5 /* SiteStatsPeriodTableViewControllerDeprecated.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SiteStatsPeriodTableViewControllerDeprecated.swift; sourceTree = ""; }; 01DC4CDA2B5FCA7B000110E5 /* SiteStatsDashboardDeprecated.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = SiteStatsDashboardDeprecated.storyboard; sourceTree = ""; }; @@ -10833,7 +10836,7 @@ path = Classes; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA = { + 29B97314FDCFA39411CA2CEA /* CustomTemplate */ = { isa = PBXGroup; children = ( 3F20FDF3276BF21000DA3CAD /* Packages */, @@ -14434,6 +14437,7 @@ 981676D3221B7A2C00B81C3F /* Countries */, 984B138D21F65F860004B6A2 /* SiteStatsPeriodTableViewController.swift */, 984B139121F66AC50004B6A2 /* SiteStatsPeriodViewModel.swift */, + 01D7EBA32B7A4BBD00F14992 /* StatsTrafficModels.swift */, ); path = "Period Stats"; sourceTree = ""; @@ -19184,13 +19188,13 @@ bg, sk, ); - mainGroup = 29B97314FDCFA39411CA2CEA; + mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; packageReferences = ( 3FF1442E266F3C2400138163 /* XCRemoteSwiftPackageReference "ScreenObject" */, 3FC2C33B26C4CF0A00C6D98F /* XCRemoteSwiftPackageReference "XCUITestHelpers" */, 17A8858B2757B97F0071FCA3 /* XCRemoteSwiftPackageReference "AutomatticAbout-swift" */, 3F3B23C02858A1B300CACE60 /* XCRemoteSwiftPackageReference "test-collector-swift" */, - 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios.git" */, + 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios" */, 3F338B6F289BD3040014ADC5 /* XCRemoteSwiftPackageReference "Nimble" */, 0CD9FB852AFA71B9009D9C7A /* XCRemoteSwiftPackageReference "Charts" */, ); @@ -20773,11 +20777,11 @@ files = ( ); inputPaths = ( - "$SRCROOT/../Scripts/BuildPhases/CopyGutenbergJS.inputs.xcfilelist", + $SRCROOT/../Scripts/BuildPhases/CopyGutenbergJS.inputs.xcfilelist, ); name = "Copy Gutenberg JS"; outputFileListPaths = ( - "$SRCROOT/../Scripts/BuildPhases/CopyGutenbergJS.outputs.xcfilelist", + $SRCROOT/../Scripts/BuildPhases/CopyGutenbergJS.outputs.xcfilelist, ); outputPaths = ( "", @@ -20966,13 +20970,13 @@ files = ( ); inputFileListPaths = ( - "$SRCROOT/../Scripts/BuildPhases/CopyGutenbergJS.inputs.xcfilelist", + $SRCROOT/../Scripts/BuildPhases/CopyGutenbergJS.inputs.xcfilelist, ); inputPaths = ( ); name = "Copy Gutenberg JS"; outputFileListPaths = ( - "$SRCROOT/../Scripts/BuildPhases/CopyGutenbergJS.outputs.xcfilelist", + $SRCROOT/../Scripts/BuildPhases/CopyGutenbergJS.outputs.xcfilelist, ); outputPaths = ( ); @@ -21985,6 +21989,7 @@ 3FCCAA1523F4A1A3004064C0 /* UIBarButtonItem+MeBarButton.swift in Sources */, FFEECFFC2084DE2B009B8CDB /* PostSettingsViewController+FeaturedImageUpload.swift in Sources */, D8212CB120AA64E1008E8AE8 /* ReaderLikeAction.swift in Sources */, + 01D7EBA42B7A4BBD00F14992 /* StatsTrafficModels.swift in Sources */, D8212CC920AA87E5008E8AE8 /* ReaderMenuAction.swift in Sources */, 593F26611CAB00CA00F14073 /* PostSharingController.swift in Sources */, 2FA37B1A215724E900C80377 /* LongPressGestureLabel.swift in Sources */, @@ -24846,6 +24851,7 @@ FABB235D2602FC2C00C8785C /* AztecAttachmentViewController.swift in Sources */, FABB235F2602FC2C00C8785C /* LoginEpilogueBlogCell.swift in Sources */, FABB23602602FC2C00C8785C /* PostServiceRemoteFactory.swift in Sources */, + 01D7EBA52B7A4BBD00F14992 /* StatsTrafficModels.swift in Sources */, FABB23612602FC2C00C8785C /* ReaderTabItemsStore.swift in Sources */, FABB23622602FC2C00C8785C /* NoResultsViewController+Model.swift in Sources */, 3F435220289B2B2B00CE19ED /* JetpackBrandingCoordinator.swift in Sources */, @@ -30406,7 +30412,7 @@ minimumVersion = 0.3.0; }; }; - 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios.git" */ = { + 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/airbnb/lottie-ios.git"; requirement = { @@ -30495,12 +30501,12 @@ }; 3F411B6E28987E3F002513AE /* Lottie */ = { isa = XCSwiftPackageProductDependency; - package = 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios.git" */; + package = 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios" */; productName = Lottie; }; 3F44DD57289C379C006334CD /* Lottie */ = { isa = XCSwiftPackageProductDependency; - package = 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios.git" */; + package = 3F411B6D28987E3F002513AE /* XCRemoteSwiftPackageReference "lottie-ios" */; productName = Lottie; }; 3F9F23242B0AE1AC00B56061 /* JetpackStatsWidgetsCore */ = { From 30661ef2c1a56a1933f705a485db3b406271ad91 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Mon, 12 Feb 2024 15:02:36 +0200 Subject: [PATCH 09/13] Conform to HashableImmuTableRow of SiteStatsRows Use default hash(into...) implementation that calculates identity based on StatSection and RowType --- .../GhostViews/StatsGhostTableViewRows.swift | 26 +------ .../Stats/SiteStatsTableViewCells.swift | 78 +++---------------- 2 files changed, 14 insertions(+), 90 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift index 7934d6c76a09..3cd6d0629854 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift @@ -1,6 +1,6 @@ import WordPressUI -protocol StatsRowGhostable: ImmuTableRow { +protocol StatsRowGhostable: HashableImmuTableRow { var statSection: StatSection? { get } } @@ -40,7 +40,7 @@ struct StatsGhostTwoColumnImmutableRow: StatsRowGhostable { var statSection: StatSection? = nil } -struct StatsGhostTopImmutableRow: StatsRowGhostable, Hashable { +struct StatsGhostTopImmutableRow: StatsRowGhostable { static let cell: ImmuTableCell = { return ImmuTableCell.nib(StatsGhostTopCell.defaultNib, StatsGhostTopCell.self) }() @@ -57,12 +57,6 @@ struct StatsGhostTopImmutableRow: StatsRowGhostable, Hashable { lhs.statSection == rhs.statSection } - func hash(into hasher: inout Hasher) { - hasher.combine(hideTopBorder) - hasher.combine(hideBottomBorder) - hasher.combine(statSection) - } - func configureCell(_ cell: UITableViewCell) { DispatchQueue.main.async { cell.startGhostAnimation(style: GhostCellStyle.muriel) @@ -98,7 +92,7 @@ struct StatsGhostPostingActivitiesImmutableRow: StatsRowGhostable { var statSection: StatSection? = nil } -struct StatsGhostChartImmutableRow: StatsRowGhostable, Hashable { +struct StatsGhostChartImmutableRow: StatsRowGhostable { static let cell: ImmuTableCell = { return ImmuTableCell.nib(StatsGhostChartCell.defaultNib, StatsGhostChartCell.self) }() @@ -126,20 +120,6 @@ struct StatsGhostDetailRow: StatsRowGhostable { detailCell.enableTopPadding = enableTopPadding } } - - // MARK: - Hashable - - static func == (lhs: StatsGhostDetailRow, rhs: StatsGhostDetailRow) -> Bool { - return lhs.hideTopBorder == rhs.hideTopBorder && - lhs.isLastRow == rhs.isLastRow && - lhs.enableTopPadding == rhs.enableTopPadding - } - - func hash(into hasher: inout Hasher) { - hasher.combine(hideTopBorder) - hasher.combine(isLastRow) - hasher.combine(enableTopPadding) - } } struct StatsGhostTitleRow: StatsRowGhostable { diff --git a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift index 847c0abfd56b..f5bd11903f62 100644 --- a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift +++ b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift @@ -4,7 +4,7 @@ import DGCharts // MARK: - Shared Rows -struct OverviewRow: ImmuTableRow, Hashable { +struct OverviewRow: HashableImmuTableRow { typealias CellType = OverviewCell @@ -19,21 +19,15 @@ struct OverviewRow: ImmuTableRow, Hashable { let period: StatsPeriodUnit? weak var statsBarChartViewDelegate: StatsBarChartViewDelegate? let chartHighlightIndex: Int? + let statSection: StatSection? = nil // MARK: - Hashable static func == (lhs: OverviewRow, rhs: OverviewRow) -> Bool { return lhs.tabsData == rhs.tabsData && -// lhs.chartData.barChartData.dataSets == rhs.chartData.barChartData.dataSets && lhs.chartHighlightIndex == rhs.chartHighlightIndex } - func hash(into hasher: inout Hasher) { - hasher.combine(tabsData) -// hasher.combine(chartData.barChartData.dataSets) - hasher.combine(chartHighlightIndex) - } - func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? CellType else { @@ -73,7 +67,7 @@ struct ViewsVisitorsRow: ImmuTableRow { } } -struct CellHeaderRow: ImmuTableRow, Hashable { +struct CellHeaderRow: HashableImmuTableRow { typealias CellType = StatsCellHeader @@ -81,7 +75,7 @@ struct CellHeaderRow: ImmuTableRow, Hashable { return ImmuTableCell.nib(CellType.defaultNib, CellType.self) }() - let statSection: StatSection + let statSection: StatSection? let action: ImmuTableAction? = nil func configureCell(_ cell: UITableViewCell) { @@ -98,13 +92,9 @@ struct CellHeaderRow: ImmuTableRow, Hashable { static func == (lhs: CellHeaderRow, rhs: CellHeaderRow) -> Bool { return lhs.statSection == rhs.statSection } - - func hash(into hasher: inout Hasher) { - hasher.combine(statSection) - } } -struct TableFooterRow: ImmuTableRow, Hashable { +struct TableFooterRow: ImmuTableRow { typealias CellType = StatsTableFooter @@ -118,14 +108,6 @@ struct TableFooterRow: ImmuTableRow, Hashable { // No configuration needed. // This method is needed to satisfy ImmuTableRow protocol requirements. } - - // MARK: - Hashable - - static func == (lhs: TableFooterRow, rhs: TableFooterRow) -> Bool { - return true - } - - func hash(into hasher: inout Hasher) {} } // MARK: - Insights Rows @@ -405,7 +387,7 @@ struct AddInsightStatRow: ImmuTableRow { // MARK: - Period Rows -struct PeriodEmptyCellHeaderRow: ImmuTableRow, Hashable { +struct PeriodEmptyCellHeaderRow: ImmuTableRow { typealias CellType = StatsCellHeader @@ -423,17 +405,9 @@ struct PeriodEmptyCellHeaderRow: ImmuTableRow, Hashable { cell.configure() } - - // MARK: - Hashable - - static func == (lhs: PeriodEmptyCellHeaderRow, rhs: PeriodEmptyCellHeaderRow) -> Bool { - return true - } - - func hash(into hasher: inout Hasher) {} } -struct TopTotalsPeriodStatsRow: ImmuTableRow, Hashable { +struct TopTotalsPeriodStatsRow: HashableImmuTableRow { typealias CellType = TopTotalsCell @@ -461,13 +435,6 @@ struct TopTotalsPeriodStatsRow: ImmuTableRow, Hashable { lhs.statSection == rhs.statSection } - func hash(into hasher: inout Hasher) { - hasher.combine(itemSubtitle) - hasher.combine(dataSubtitle) - hasher.combine(dataRows) - hasher.combine(statSection) - } - func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? CellType else { @@ -486,7 +453,7 @@ struct TopTotalsPeriodStatsRow: ImmuTableRow, Hashable { } } -struct TopTotalsNoSubtitlesPeriodStatsRow: ImmuTableRow, Hashable { +struct TopTotalsNoSubtitlesPeriodStatsRow: HashableImmuTableRow { typealias CellType = TopTotalsCell @@ -506,11 +473,6 @@ struct TopTotalsNoSubtitlesPeriodStatsRow: ImmuTableRow, Hashable { lhs.statSection == rhs.statSection } - func hash(into hasher: inout Hasher) { - hasher.combine(dataRows) - hasher.combine(statSection) - } - func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? CellType else { @@ -521,7 +483,7 @@ struct TopTotalsNoSubtitlesPeriodStatsRow: ImmuTableRow, Hashable { } } -struct CountriesStatsRow: ImmuTableRow, Hashable { +struct CountriesStatsRow: HashableImmuTableRow { typealias CellType = CountriesCell @@ -546,13 +508,6 @@ struct CountriesStatsRow: ImmuTableRow, Hashable { lhs.dataRows == rhs.dataRows } - func hash(into hasher: inout Hasher) { - hasher.combine(dataRows) - hasher.combine(itemSubtitle) - hasher.combine(dataSubtitle) - hasher.combine(statSection) - } - func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? CellType else { @@ -568,7 +523,7 @@ struct CountriesStatsRow: ImmuTableRow, Hashable { } } -struct CountriesMapRow: ImmuTableRow, Hashable { +struct CountriesMapRow: HashableImmuTableRow { let action: ImmuTableAction? = nil let countriesMap: CountriesMap var statSection: StatSection? @@ -586,11 +541,6 @@ struct CountriesMapRow: ImmuTableRow, Hashable { lhs.statSection == rhs.statSection } - func hash(into hasher: inout Hasher) { - hasher.combine(countriesMap) - hasher.combine(statSection) - } - func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? CellType else { return @@ -837,7 +787,7 @@ struct DetailSubtitlesTabbedHeaderRow: ImmuTableRow { } } -struct StatsErrorRow: ImmuTableRow, Hashable { +struct StatsErrorRow: HashableImmuTableRow { static let cell: ImmuTableCell = { return ImmuTableCell.nib(StatsStackViewCell.defaultNib, StatsStackViewCell.self) }() @@ -856,12 +806,6 @@ struct StatsErrorRow: ImmuTableRow, Hashable { lhs.statSection == rhs.statSection } - func hash(into hasher: inout Hasher) { - hasher.combine(rowStatus) - hasher.combine(statType) - hasher.combine(statSection) - } - func configureCell(_ cell: UITableViewCell) { guard let cell = cell as? StatsStackViewCell else { return From 2e4a109cca13f36241a07d682f65722c1cfb5d84 Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Mon, 12 Feb 2024 15:06:16 +0200 Subject: [PATCH 10/13] Build StatsTrafficSnapshot in SiteStatsPeriodViewModel Build StatsTrafficSnapshot instead of ImmuTable to be used in VC without changing any logic --- .../SiteStatsPeriodViewModel.swift | 345 +++++++++--------- 1 file changed, 180 insertions(+), 165 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift index b6dfc447d577..8cd401a260dd 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift @@ -1,39 +1,7 @@ import Foundation import WordPressFlux -/// The view model used by Period Stats. -/// - -typealias StatsTrafficSnapshot = NSDiffableDataSourceSnapshot -typealias StatsTrafficDataSource = UITableViewDiffableDataSource - -enum StatsTrafficRow: Hashable { - case emptyPeriodCellHeader(PeriodEmptyCellHeaderRow) - case cellHeader(CellHeaderRow) - case topTotals(TopTotalsPeriodStatsRow) - case topTotalsNoSubtitles(TopTotalsNoSubtitlesPeriodStatsRow) - case countries(CountriesStatsRow) - case countriesMap(CountriesMapRow) - case overview(OverviewRow) - case footer(TableFooterRow) - case error(StatsErrorRow) - case ghostChart(StatsGhostChartImmutableRow) - case ghostTop(StatsGhostTopImmutableRow) -} - -struct StatsTrafficSection: Hashable { - let headerText: String? - let rows: [StatsTrafficRow] - let footerText: String? - - init(headerText: String? = nil, rows: [StatsTrafficRow], footerText: String? = nil) { - self.headerText = headerText - self.rows = rows - self.footerText = footerText - } -} - -class SiteStatsPeriodViewModel: Observable { +final class SiteStatsPeriodViewModel: Observable { // MARK: - Properties @@ -111,130 +79,177 @@ class SiteStatsPeriodViewModel: Observable { // MARK: - Table Model - func tableViewModel() -> ImmuTable { + func tableViewSnapshot() -> StatsTrafficSnapshot { + var snapshot = StatsTrafficSnapshot() if !store.containsCachedData && store.fetchingOverviewHasFailed { - return ImmuTable.Empty + return snapshot } - let errorBlock: (StatSection) -> [ImmuTableRow] = { section in + let errorBlock: (StatSection) -> [any HashableImmuTableRow] = { section in return [StatsErrorRow(rowStatus: .error, statType: .period, statSection: section)] } - let summaryErrorBlock: AsyncBlock<[ImmuTableRow]> = { - return [StatsErrorRow(rowStatus: .error, statType: .period, statSection: nil)] + let summaryErrorBlock: AsyncBlock<[any HashableImmuTableRow]> = { + return [StatsErrorRow(rowStatus: .error, statType: .period, statSection: .periodOverviewViews)] } - let loadingBlock: (StatSection) -> [ImmuTableRow] = { section in + let loadingBlock: (StatSection) -> [any HashableImmuTableRow] = { section in return [StatsGhostTopImmutableRow(statSection: section)] } - var sections: [ImmuTableSection] = [] - - // TODO: Replace with a new Bar Chart - sections.append(.init(rows: blocks(for: .summary, - type: .period, - status: store.summaryStatus, - checkingCache: { [weak self] in - return self?.mostRecentChartData != nil - }, - block: { [weak self] in - return self?.overviewTableRows() ?? summaryErrorBlock() - }, loading: { - return [StatsGhostChartImmutableRow()] - }, error: summaryErrorBlock))) - - sections.append(.init(rows: blocks(for: .topPostsAndPages, - type: .period, - status: store.topPostsAndPagesStatus, - block: { [weak self] in - return self?.postsAndPagesTableRows() ?? errorBlock(.periodPostsAndPages) - }, loading: { - return loadingBlock(.periodPostsAndPages) - }, error: { - return errorBlock(.periodPostsAndPages) - }))) - sections.append(.init(rows: blocks(for: .topReferrers, - type: .period, - status: store.topReferrersStatus, - block: { [weak self] in - return self?.referrersTableRows() ?? errorBlock(.periodReferrers) - }, loading: { - return loadingBlock(.periodReferrers) - }, error: { - return errorBlock(.periodReferrers) - }))) - sections.append(.init(rows: blocks(for: .topClicks, - type: .period, - status: store.topClicksStatus, - block: { [weak self] in - return self?.clicksTableRows() ?? errorBlock(.periodClicks) - }, loading: { - return loadingBlock(.periodClicks) - }, error: { - return errorBlock(.periodClicks) - }))) - sections.append(.init(rows: blocks(for: .topAuthors, - type: .period, - status: store.topAuthorsStatus, - block: { [weak self] in - return self?.authorsTableRows() ?? errorBlock(.periodAuthors) - }, loading: { - return loadingBlock(.periodAuthors) - }, error: { - return errorBlock(.periodAuthors) - }))) - sections.append(.init(rows: blocks(for: .topCountries, - type: .period, - status: store.topCountriesStatus, - block: { [weak self] in - return self?.countriesTableRows() ?? errorBlock(.periodCountries) - }, loading: { - return loadingBlock(.periodCountries) - }, error: { - return errorBlock(.periodCountries) - }))) - sections.append(.init(rows: blocks(for: .topSearchTerms, - type: .period, - status: store.topSearchTermsStatus, - block: { [weak self] in - return self?.searchTermsTableRows() ?? errorBlock(.periodSearchTerms) - }, loading: { - return loadingBlock(.periodSearchTerms) - }, error: { - return errorBlock(.periodSearchTerms) - }))) - sections.append(.init(rows: blocks(for: .topPublished, - type: .period, - status: store.topPublishedStatus, - block: { [weak self] in - return self?.publishedTableRows() ?? errorBlock(.periodPublished) - }, loading: { - return loadingBlock(.periodPublished) - }, error: { - return errorBlock(.periodPublished) - }))) - sections.append(.init(rows: blocks(for: .topVideos, - type: .period, - status: store.topVideosStatus, - block: { [weak self] in - return self?.videosTableRows() ?? errorBlock(.periodVideos) - }, loading: { - return loadingBlock(.periodVideos) - }, error: { - return errorBlock(.periodVideos) - }))) + let summarySection = StatsTrafficSection(periodType: .summary) + let summaryRows = blocks(for: .summary, + type: .period, + status: store.summaryStatus, + checkingCache: { [weak self] in + return self?.mostRecentChartData != nil + }, + block: { [weak self] in + return self?.overviewTableRows() ?? summaryErrorBlock() + }, loading: { + return [StatsGhostChartImmutableRow(statSection: .periodOverviewViews)] + }, error: summaryErrorBlock) + .map { StatsTrafficRow(immuTableRow: $0) } + snapshot.appendSections([summarySection]) + snapshot.appendItems(summaryRows, toSection: summarySection) + + let topPostsAndPagesSection = StatsTrafficSection(periodType: .topPostsAndPages) + let topPostsAndPagesRows = blocks(for: .topPostsAndPages, + type: .period, + status: store.topPostsAndPagesStatus, + block: { [weak self] in + return self?.postsAndPagesTableRows() ?? errorBlock(.periodPostsAndPages) + }, loading: { + return loadingBlock(.periodPostsAndPages) + }, error: { + return errorBlock(.periodPostsAndPages) + }) + .map { StatsTrafficRow(immuTableRow: $0) } + snapshot.appendSections([topPostsAndPagesSection]) + snapshot.appendItems(topPostsAndPagesRows, toSection: topPostsAndPagesSection) + + let topReferrersSection = StatsTrafficSection(periodType: .topReferrers) + let topReferrersRows = blocks(for: .topReferrers, + type: .period, + status: store.topReferrersStatus, + block: { [weak self] in + return self?.referrersTableRows() ?? errorBlock(.periodReferrers) + }, loading: { + return loadingBlock(.periodReferrers) + }, error: { + return errorBlock(.periodReferrers) + }) + .map { StatsTrafficRow(immuTableRow: $0) } + snapshot.appendSections([topReferrersSection]) + snapshot.appendItems(topReferrersRows, toSection: topReferrersSection) + + let topClicksSection = StatsTrafficSection(periodType: .topClicks) + let topClicksRows = blocks(for: .topClicks, + type: .period, + status: store.topClicksStatus, + block: { [weak self] in + return self?.clicksTableRows() ?? errorBlock(.periodClicks) + }, loading: { + return loadingBlock(.periodClicks) + }, error: { + return errorBlock(.periodClicks) + }) + .map { StatsTrafficRow(immuTableRow: $0) } + snapshot.appendSections([topClicksSection]) + snapshot.appendItems(topClicksRows, toSection: topClicksSection) + + let topAuthorsSection = StatsTrafficSection(periodType: .topAuthors) + let topAuthorsRows = blocks(for: .topAuthors, + type: .period, + status: store.topAuthorsStatus, + block: { [weak self] in + return self?.authorsTableRows() ?? errorBlock(.periodAuthors) + }, loading: { + return loadingBlock(.periodAuthors) + }, error: { + return errorBlock(.periodAuthors) + }) + .map { StatsTrafficRow(immuTableRow: $0) } + snapshot.appendSections([topAuthorsSection]) + snapshot.appendItems(topAuthorsRows, toSection: topAuthorsSection) + + let topCountriesSection = StatsTrafficSection(periodType: .topCountries) + let topCountriesRows = blocks(for: .topCountries, + type: .period, + status: store.topCountriesStatus, + block: { [weak self] in + return self?.countriesTableRows() ?? errorBlock(.periodCountries) + }, loading: { + return loadingBlock(.periodCountries) + }, error: { + return errorBlock(.periodCountries) + }) + .map { StatsTrafficRow(immuTableRow: $0) } + snapshot.appendSections([topCountriesSection]) + snapshot.appendItems(topCountriesRows, toSection: topCountriesSection) + + let topSearchTermsSection = StatsTrafficSection(periodType: .topSearchTerms) + let topSearchTermsRows = blocks(for: .topSearchTerms, + type: .period, + status: store.topSearchTermsStatus, + block: { [weak self] in + return self?.searchTermsTableRows() ?? errorBlock(.periodSearchTerms) + }, loading: { + return loadingBlock(.periodSearchTerms) + }, error: { + return errorBlock(.periodSearchTerms) + }) + .map { StatsTrafficRow(immuTableRow: $0) } + snapshot.appendSections([topSearchTermsSection]) + snapshot.appendItems(topSearchTermsRows, toSection: topSearchTermsSection) + + let topPublishedSection = StatsTrafficSection(periodType: .topPublished) + let topPublishedRows = blocks(for: .topPublished, + type: .period, + status: store.topPublishedStatus, + block: { [weak self] in + return self?.publishedTableRows() ?? errorBlock(.periodPublished) + }, loading: { + return loadingBlock(.periodPublished) + }, error: { + return errorBlock(.periodPublished) + }) + .map { StatsTrafficRow(immuTableRow: $0) } + snapshot.appendSections([topPublishedSection]) + snapshot.appendItems(topPublishedRows, toSection: topPublishedSection) + + let topVideosSection = StatsTrafficSection(periodType: .topVideos) + let topVideosRows = blocks(for: .topVideos, + type: .period, + status: store.topVideosStatus, + block: { [weak self] in + return self?.videosTableRows() ?? errorBlock(.periodVideos) + }, loading: { + return loadingBlock(.periodVideos) + }, error: { + return errorBlock(.periodVideos) + }) + .map { StatsTrafficRow(immuTableRow: $0) } + snapshot.appendSections([topVideosSection]) + snapshot.appendItems(topVideosRows, toSection: topVideosSection) + + // Check for supportsFileDownloads and append if necessary if SiteStatsInformation.sharedInstance.supportsFileDownloads { - sections.append(.init(rows: blocks(for: .topFileDownloads, - type: .period, - status: store.topFileDownloadsStatus, - block: { [weak self] in - return self?.fileDownloadsTableRows() ?? errorBlock(.periodFileDownloads) - }, loading: { - return loadingBlock(.periodFileDownloads) - }, error: { - return errorBlock(.periodFileDownloads) - }))) + let topFileDownloadsSection = StatsTrafficSection(periodType: .topFileDownloads) + let topFileDownloadsRows = blocks(for: .topFileDownloads, + type: .period, + status: store.topFileDownloadsStatus, + block: { [weak self] in + return self?.fileDownloadsTableRows() ?? errorBlock(.periodFileDownloads) + }, loading: { + return loadingBlock(.periodFileDownloads) + }, error: { + return errorBlock(.periodFileDownloads) + }) + .map { StatsTrafficRow(immuTableRow: $0) } + snapshot.appendSections([topFileDownloadsSection]) + snapshot.appendItems(topFileDownloadsRows, toSection: topFileDownloadsSection) } - return ImmuTable(sections: sections) + return snapshot } // MARK: - Refresh Data @@ -282,8 +297,8 @@ private extension SiteStatsPeriodViewModel { // MARK: - Create Table Rows - func overviewTableRows() -> [ImmuTableRow] { - var tableRows = [ImmuTableRow]() + func overviewTableRows() -> [any HashableImmuTableRow] { + var tableRows = [any HashableImmuTableRow]() let periodSummary = store.getSummary() let summaryData = periodSummary?.summaryData ?? [] @@ -419,8 +434,8 @@ private extension SiteStatsPeriodViewModel { return (currentCount, difference, roundedPercentage) } - func postsAndPagesTableRows() -> [ImmuTableRow] { - var tableRows = [ImmuTableRow]() + func postsAndPagesTableRows() -> [any HashableImmuTableRow] { + var tableRows = [any HashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodPostsAndPages.itemSubtitle, dataSubtitle: StatSection.periodPostsAndPages.dataSubtitle, dataRows: postsAndPagesDataRows(), @@ -458,8 +473,8 @@ private extension SiteStatsPeriodViewModel { } } - func referrersTableRows() -> [ImmuTableRow] { - var tableRows = [ImmuTableRow]() + func referrersTableRows() -> [any HashableImmuTableRow] { + var tableRows = [any HashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodReferrers.itemSubtitle, dataSubtitle: StatSection.periodReferrers.dataSubtitle, dataRows: referrersDataRows(), @@ -500,8 +515,8 @@ private extension SiteStatsPeriodViewModel { return referrers.map { rowDataFromReferrer(referrer: $0) } } - func clicksTableRows() -> [ImmuTableRow] { - var tableRows = [ImmuTableRow]() + func clicksTableRows() -> [any HashableImmuTableRow] { + var tableRows = [any HashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodClicks.itemSubtitle, dataSubtitle: StatSection.periodClicks.dataSubtitle, dataRows: clicksDataRows(), @@ -525,8 +540,8 @@ private extension SiteStatsPeriodViewModel { ?? [] } - func authorsTableRows() -> [ImmuTableRow] { - var tableRows = [ImmuTableRow]() + func authorsTableRows() -> [any HashableImmuTableRow] { + var tableRows = [any HashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodAuthors.itemSubtitle, dataSubtitle: StatSection.periodAuthors.dataSubtitle, dataRows: authorsDataRows(), @@ -549,8 +564,8 @@ private extension SiteStatsPeriodViewModel { } } - func countriesTableRows() -> [ImmuTableRow] { - var tableRows = [ImmuTableRow]() + func countriesTableRows() -> [any HashableImmuTableRow] { + var tableRows = [any HashableImmuTableRow]() let map = countriesMap() let isMapShown = !map.data.isEmpty if isMapShown { @@ -583,8 +598,8 @@ private extension SiteStatsPeriodViewModel { }) } - func searchTermsTableRows() -> [ImmuTableRow] { - var tableRows = [ImmuTableRow]() + func searchTermsTableRows() -> [any HashableImmuTableRow] { + var tableRows = [any HashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodSearchTerms.itemSubtitle, dataSubtitle: StatSection.periodSearchTerms.dataSubtitle, dataRows: searchTermsDataRows(), @@ -618,8 +633,8 @@ private extension SiteStatsPeriodViewModel { return mappedSearchTerms } - func publishedTableRows() -> [ImmuTableRow] { - var tableRows = [ImmuTableRow]() + func publishedTableRows() -> [any HashableImmuTableRow] { + var tableRows = [any HashableImmuTableRow]() tableRows.append(TopTotalsNoSubtitlesPeriodStatsRow(dataRows: publishedDataRows(), statSection: StatSection.periodPublished, siteStatsPeriodDelegate: periodDelegate)) @@ -636,8 +651,8 @@ private extension SiteStatsPeriodViewModel { ?? [] } - func videosTableRows() -> [ImmuTableRow] { - var tableRows = [ImmuTableRow]() + func videosTableRows() -> [any HashableImmuTableRow] { + var tableRows = [any HashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodVideos.itemSubtitle, dataSubtitle: StatSection.periodVideos.dataSubtitle, dataRows: videosDataRows(), @@ -657,8 +672,8 @@ private extension SiteStatsPeriodViewModel { ?? [] } - func fileDownloadsTableRows() -> [ImmuTableRow] { - var tableRows = [ImmuTableRow]() + func fileDownloadsTableRows() -> [any HashableImmuTableRow] { + var tableRows = [any HashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodFileDownloads.itemSubtitle, dataSubtitle: StatSection.periodFileDownloads.dataSubtitle, dataRows: fileDownloadsDataRows(), From e63ddecff5a50c1ceacf070f3116397cfe339c6a Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Mon, 12 Feb 2024 15:12:58 +0200 Subject: [PATCH 11/13] Update SiteStatsPeriodTableViewController to support DiffableDataSource - Create StatsTrafficDataSource - Apply snapshot instead of reloading table view --- .../SiteStatsPeriodTableViewController.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift index d299f13a450b..bda08af39c44 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift @@ -53,7 +53,19 @@ final class SiteStatsPeriodTableViewController: SiteStatsBaseTableViewController private let analyticsTracker = BottomScrollAnalyticsTracker() private lazy var tableHandler: ImmuTableViewHandler = { - return ImmuTableViewHandler(takeOver: self, with: analyticsTracker) + let handler = ImmuTableViewHandler(takeOver: self, with: analyticsTracker) + tableView.dataSource = dataSource + handler.automaticallyReloadTableView = false + return handler + }() + + private lazy var dataSource: StatsTrafficDataSource = { + return StatsTrafficDataSource(tableView: tableView) { tableView, indexPath, item in + let row = item.immuTableRow + let cell = tableView.dequeueReusableCell(withIdentifier: row.reusableIdentifier, for: indexPath) + row.configureCell(cell) + return cell + } }() init() { @@ -181,7 +193,7 @@ private extension SiteStatsPeriodTableViewController { return } - tableHandler.viewModel = viewModel.tableViewModel() + dataSource.apply(viewModel.tableViewSnapshot(), animatingDifferences: false) refreshControl.endRefreshing() From 0b8d77acf84c38b2d666bfcf7506ad26b6dfdb7b Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Tue, 13 Feb 2024 10:48:55 +0200 Subject: [PATCH 12/13] Create ImmuTableDiffableViewHandler Make DiffableDataSource easier to integrate for existing ImmuTableVieHandler users Define common Snapshot and DataSource. Override delegate methods that rely on fetching row items. --- WordPress/Classes/Utility/ImmuTable.swift | 61 ++++++++++++++++ .../SiteStatsPeriodTableViewController.swift | 23 ++---- .../SiteStatsPeriodViewModel.swift | 70 +++++++++---------- .../Period Stats/StatsTrafficModels.swift | 27 ++----- .../GhostViews/StatsGhostTableViewRows.swift | 2 +- .../Stats/SiteStatsTableViewCells.swift | 14 ++-- 6 files changed, 116 insertions(+), 81 deletions(-) diff --git a/WordPress/Classes/Utility/ImmuTable.swift b/WordPress/Classes/Utility/ImmuTable.swift index 9bea7a3c22a7..408388269319 100644 --- a/WordPress/Classes/Utility/ImmuTable.swift +++ b/WordPress/Classes/Utility/ImmuTable.swift @@ -483,3 +483,64 @@ extension UITableView: CellRegistrar { } extension UITableViewController: TableViewContainer {} + +// MARK: - Diffable + +typealias ImmuTableDiffableDataSourceSnapshot = NSDiffableDataSourceSnapshot +typealias ImmuTableDiffableDataSource = UITableViewDiffableDataSource + +struct AnyHashableImmuTableRow: Hashable { + let immuTableRow: any (ImmuTableRow & Hashable) + + static func == (lhs: AnyHashableImmuTableRow, rhs: AnyHashableImmuTableRow) -> Bool { + lhs.immuTableRow.hashValue == rhs.immuTableRow.hashValue + } + + func hash(into hasher: inout Hasher) { + hasher.combine(immuTableRow) + } +} + +class ImmuTableDiffableViewHandler: ImmuTableViewHandler { + lazy var diffableDataSource: ImmuTableDiffableDataSource = { + return ImmuTableDiffableDataSource(tableView: target.tableView) { tableView, indexPath, item in + let row = item.immuTableRow + let cell = tableView.dequeueReusableCell(withIdentifier: row.reusableIdentifier, for: indexPath) + row.configureCell(cell) + return cell + } + }() + + override init(takeOver target: ImmuTableViewHandler.UIViewControllerWithTableView, with passthroughScrollViewDelegate: UIScrollViewDelegate? = nil) { + super.init(takeOver: target, with: passthroughScrollViewDelegate) + + self.target.tableView.dataSource = diffableDataSource + self.automaticallyReloadTableView = false + } + + func item(for indexPath: IndexPath) -> ImmuTableRow? { + guard let diffableDataSource = target.tableView.dataSource as? UITableViewDiffableDataSource else { + return nil + } + + return diffableDataSource.itemIdentifier(for: indexPath)?.immuTableRow + } + + open override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if target.responds(to: #selector(UITableViewDelegate.tableView(_:didSelectRowAt:))) { + target.tableView?(tableView, didSelectRowAt: indexPath) + } else if let item = item(for: indexPath) { + item.action?(item) + } + if automaticallyDeselectCells { + tableView.deselectRow(at: indexPath, animated: true) + } + } + + open override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if let item = item(for: indexPath), let customHeight = type(of: item).customHeight { + return CGFloat(customHeight) + } + return tableView.rowHeight + } +} diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift index bda08af39c44..d95f31329aaa 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodTableViewController.swift @@ -48,24 +48,12 @@ final class SiteStatsPeriodTableViewController: SiteStatsBaseTableViewController private var changeReceipt: Receipt? private var viewModel: SiteStatsPeriodViewModel? - private var tableHeaderView: SiteStatsTableHeaderView? + private weak var tableHeaderView: SiteStatsTableHeaderView? private let analyticsTracker = BottomScrollAnalyticsTracker() - private lazy var tableHandler: ImmuTableViewHandler = { - let handler = ImmuTableViewHandler(takeOver: self, with: analyticsTracker) - tableView.dataSource = dataSource - handler.automaticallyReloadTableView = false - return handler - }() - - private lazy var dataSource: StatsTrafficDataSource = { - return StatsTrafficDataSource(tableView: tableView) { tableView, indexPath, item in - let row = item.immuTableRow - let cell = tableView.dequeueReusableCell(withIdentifier: row.reusableIdentifier, for: indexPath) - row.configureCell(cell) - return cell - } + private lazy var tableHandler: ImmuTableDiffableViewHandler = { + return ImmuTableDiffableViewHandler(takeOver: self, with: analyticsTracker) }() init() { @@ -193,9 +181,10 @@ private extension SiteStatsPeriodTableViewController { return } - dataSource.apply(viewModel.tableViewSnapshot(), animatingDifferences: false) + tableHandler.diffableDataSource.apply(viewModel.tableViewSnapshot(), animatingDifferences: false) refreshControl.endRefreshing() + tableHeaderView?.animateGhostLayers(viewModel.isFetchingChart() == true) if viewModel.fetchingFailed() { displayFailureViewIfNecessary() @@ -237,7 +226,7 @@ private extension SiteStatsPeriodTableViewController { extension SiteStatsPeriodTableViewController: NoResultsViewHost { private func displayFailureViewIfNecessary() { - guard tableHandler.viewModel.sections.isEmpty else { + guard tableHandler.diffableDataSource.snapshot().numberOfSections == 0 else { return } diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift index 8cd401a260dd..78ee554229b3 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/SiteStatsPeriodViewModel.swift @@ -79,19 +79,19 @@ final class SiteStatsPeriodViewModel: Observable { // MARK: - Table Model - func tableViewSnapshot() -> StatsTrafficSnapshot { - var snapshot = StatsTrafficSnapshot() + func tableViewSnapshot() -> ImmuTableDiffableDataSourceSnapshot { + var snapshot = ImmuTableDiffableDataSourceSnapshot() if !store.containsCachedData && store.fetchingOverviewHasFailed { return snapshot } - let errorBlock: (StatSection) -> [any HashableImmuTableRow] = { section in + let errorBlock: (StatSection) -> [any StatsHashableImmuTableRow] = { section in return [StatsErrorRow(rowStatus: .error, statType: .period, statSection: section)] } - let summaryErrorBlock: AsyncBlock<[any HashableImmuTableRow]> = { + let summaryErrorBlock: AsyncBlock<[any StatsHashableImmuTableRow]> = { return [StatsErrorRow(rowStatus: .error, statType: .period, statSection: .periodOverviewViews)] } - let loadingBlock: (StatSection) -> [any HashableImmuTableRow] = { section in + let loadingBlock: (StatSection) -> [any StatsHashableImmuTableRow] = { section in return [StatsGhostTopImmutableRow(statSection: section)] } @@ -107,7 +107,7 @@ final class SiteStatsPeriodViewModel: Observable { }, loading: { return [StatsGhostChartImmutableRow(statSection: .periodOverviewViews)] }, error: summaryErrorBlock) - .map { StatsTrafficRow(immuTableRow: $0) } + .map { AnyHashableImmuTableRow(immuTableRow: $0) } snapshot.appendSections([summarySection]) snapshot.appendItems(summaryRows, toSection: summarySection) @@ -122,7 +122,7 @@ final class SiteStatsPeriodViewModel: Observable { }, error: { return errorBlock(.periodPostsAndPages) }) - .map { StatsTrafficRow(immuTableRow: $0) } + .map { AnyHashableImmuTableRow(immuTableRow: $0) } snapshot.appendSections([topPostsAndPagesSection]) snapshot.appendItems(topPostsAndPagesRows, toSection: topPostsAndPagesSection) @@ -137,7 +137,7 @@ final class SiteStatsPeriodViewModel: Observable { }, error: { return errorBlock(.periodReferrers) }) - .map { StatsTrafficRow(immuTableRow: $0) } + .map { AnyHashableImmuTableRow(immuTableRow: $0) } snapshot.appendSections([topReferrersSection]) snapshot.appendItems(topReferrersRows, toSection: topReferrersSection) @@ -152,7 +152,7 @@ final class SiteStatsPeriodViewModel: Observable { }, error: { return errorBlock(.periodClicks) }) - .map { StatsTrafficRow(immuTableRow: $0) } + .map { AnyHashableImmuTableRow(immuTableRow: $0) } snapshot.appendSections([topClicksSection]) snapshot.appendItems(topClicksRows, toSection: topClicksSection) @@ -167,7 +167,7 @@ final class SiteStatsPeriodViewModel: Observable { }, error: { return errorBlock(.periodAuthors) }) - .map { StatsTrafficRow(immuTableRow: $0) } + .map { AnyHashableImmuTableRow(immuTableRow: $0) } snapshot.appendSections([topAuthorsSection]) snapshot.appendItems(topAuthorsRows, toSection: topAuthorsSection) @@ -182,7 +182,7 @@ final class SiteStatsPeriodViewModel: Observable { }, error: { return errorBlock(.periodCountries) }) - .map { StatsTrafficRow(immuTableRow: $0) } + .map { AnyHashableImmuTableRow(immuTableRow: $0) } snapshot.appendSections([topCountriesSection]) snapshot.appendItems(topCountriesRows, toSection: topCountriesSection) @@ -197,7 +197,7 @@ final class SiteStatsPeriodViewModel: Observable { }, error: { return errorBlock(.periodSearchTerms) }) - .map { StatsTrafficRow(immuTableRow: $0) } + .map { AnyHashableImmuTableRow(immuTableRow: $0) } snapshot.appendSections([topSearchTermsSection]) snapshot.appendItems(topSearchTermsRows, toSection: topSearchTermsSection) @@ -212,7 +212,7 @@ final class SiteStatsPeriodViewModel: Observable { }, error: { return errorBlock(.periodPublished) }) - .map { StatsTrafficRow(immuTableRow: $0) } + .map { AnyHashableImmuTableRow(immuTableRow: $0) } snapshot.appendSections([topPublishedSection]) snapshot.appendItems(topPublishedRows, toSection: topPublishedSection) @@ -227,7 +227,7 @@ final class SiteStatsPeriodViewModel: Observable { }, error: { return errorBlock(.periodVideos) }) - .map { StatsTrafficRow(immuTableRow: $0) } + .map { AnyHashableImmuTableRow(immuTableRow: $0) } snapshot.appendSections([topVideosSection]) snapshot.appendItems(topVideosRows, toSection: topVideosSection) @@ -244,7 +244,7 @@ final class SiteStatsPeriodViewModel: Observable { }, error: { return errorBlock(.periodFileDownloads) }) - .map { StatsTrafficRow(immuTableRow: $0) } + .map { AnyHashableImmuTableRow(immuTableRow: $0) } snapshot.appendSections([topFileDownloadsSection]) snapshot.appendItems(topFileDownloadsRows, toSection: topFileDownloadsSection) } @@ -297,8 +297,8 @@ private extension SiteStatsPeriodViewModel { // MARK: - Create Table Rows - func overviewTableRows() -> [any HashableImmuTableRow] { - var tableRows = [any HashableImmuTableRow]() + func overviewTableRows() -> [any StatsHashableImmuTableRow] { + var tableRows = [any StatsHashableImmuTableRow]() let periodSummary = store.getSummary() let summaryData = periodSummary?.summaryData ?? [] @@ -434,8 +434,8 @@ private extension SiteStatsPeriodViewModel { return (currentCount, difference, roundedPercentage) } - func postsAndPagesTableRows() -> [any HashableImmuTableRow] { - var tableRows = [any HashableImmuTableRow]() + func postsAndPagesTableRows() -> [any StatsHashableImmuTableRow] { + var tableRows = [any StatsHashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodPostsAndPages.itemSubtitle, dataSubtitle: StatSection.periodPostsAndPages.dataSubtitle, dataRows: postsAndPagesDataRows(), @@ -473,8 +473,8 @@ private extension SiteStatsPeriodViewModel { } } - func referrersTableRows() -> [any HashableImmuTableRow] { - var tableRows = [any HashableImmuTableRow]() + func referrersTableRows() -> [any StatsHashableImmuTableRow] { + var tableRows = [any StatsHashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodReferrers.itemSubtitle, dataSubtitle: StatSection.periodReferrers.dataSubtitle, dataRows: referrersDataRows(), @@ -515,8 +515,8 @@ private extension SiteStatsPeriodViewModel { return referrers.map { rowDataFromReferrer(referrer: $0) } } - func clicksTableRows() -> [any HashableImmuTableRow] { - var tableRows = [any HashableImmuTableRow]() + func clicksTableRows() -> [any StatsHashableImmuTableRow] { + var tableRows = [any StatsHashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodClicks.itemSubtitle, dataSubtitle: StatSection.periodClicks.dataSubtitle, dataRows: clicksDataRows(), @@ -540,8 +540,8 @@ private extension SiteStatsPeriodViewModel { ?? [] } - func authorsTableRows() -> [any HashableImmuTableRow] { - var tableRows = [any HashableImmuTableRow]() + func authorsTableRows() -> [any StatsHashableImmuTableRow] { + var tableRows = [any StatsHashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodAuthors.itemSubtitle, dataSubtitle: StatSection.periodAuthors.dataSubtitle, dataRows: authorsDataRows(), @@ -564,8 +564,8 @@ private extension SiteStatsPeriodViewModel { } } - func countriesTableRows() -> [any HashableImmuTableRow] { - var tableRows = [any HashableImmuTableRow]() + func countriesTableRows() -> [any StatsHashableImmuTableRow] { + var tableRows = [any StatsHashableImmuTableRow]() let map = countriesMap() let isMapShown = !map.data.isEmpty if isMapShown { @@ -598,8 +598,8 @@ private extension SiteStatsPeriodViewModel { }) } - func searchTermsTableRows() -> [any HashableImmuTableRow] { - var tableRows = [any HashableImmuTableRow]() + func searchTermsTableRows() -> [any StatsHashableImmuTableRow] { + var tableRows = [any StatsHashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodSearchTerms.itemSubtitle, dataSubtitle: StatSection.periodSearchTerms.dataSubtitle, dataRows: searchTermsDataRows(), @@ -633,8 +633,8 @@ private extension SiteStatsPeriodViewModel { return mappedSearchTerms } - func publishedTableRows() -> [any HashableImmuTableRow] { - var tableRows = [any HashableImmuTableRow]() + func publishedTableRows() -> [any StatsHashableImmuTableRow] { + var tableRows = [any StatsHashableImmuTableRow]() tableRows.append(TopTotalsNoSubtitlesPeriodStatsRow(dataRows: publishedDataRows(), statSection: StatSection.periodPublished, siteStatsPeriodDelegate: periodDelegate)) @@ -651,8 +651,8 @@ private extension SiteStatsPeriodViewModel { ?? [] } - func videosTableRows() -> [any HashableImmuTableRow] { - var tableRows = [any HashableImmuTableRow]() + func videosTableRows() -> [any StatsHashableImmuTableRow] { + var tableRows = [any StatsHashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodVideos.itemSubtitle, dataSubtitle: StatSection.periodVideos.dataSubtitle, dataRows: videosDataRows(), @@ -672,8 +672,8 @@ private extension SiteStatsPeriodViewModel { ?? [] } - func fileDownloadsTableRows() -> [any HashableImmuTableRow] { - var tableRows = [any HashableImmuTableRow]() + func fileDownloadsTableRows() -> [any StatsHashableImmuTableRow] { + var tableRows = [any StatsHashableImmuTableRow]() tableRows.append(TopTotalsPeriodStatsRow(itemSubtitle: StatSection.periodFileDownloads.itemSubtitle, dataSubtitle: StatSection.periodFileDownloads.dataSubtitle, dataRows: fileDownloadsDataRows(), diff --git a/WordPress/Classes/ViewRelated/Stats/Period Stats/StatsTrafficModels.swift b/WordPress/Classes/ViewRelated/Stats/Period Stats/StatsTrafficModels.swift index 46ae82b384f6..19a81d18cf4d 100644 --- a/WordPress/Classes/ViewRelated/Stats/Period Stats/StatsTrafficModels.swift +++ b/WordPress/Classes/ViewRelated/Stats/Period Stats/StatsTrafficModels.swift @@ -1,23 +1,4 @@ -import UIKit - -protocol HashableImmuTableRow: ImmuTableRow, Hashable { - var statSection: StatSection? { get } -} - -typealias StatsTrafficSnapshot = NSDiffableDataSourceSnapshot -typealias StatsTrafficDataSource = UITableViewDiffableDataSource - -struct StatsTrafficRow: Hashable { - let immuTableRow: any HashableImmuTableRow - - static func == (lhs: StatsTrafficRow, rhs: StatsTrafficRow) -> Bool { - lhs.immuTableRow.hashValue == rhs.immuTableRow.hashValue - } - - func hash(into hasher: inout Hasher) { - hasher.combine(immuTableRow) - } -} +import Foundation struct StatsTrafficSection: Hashable { let periodType: PeriodType @@ -27,7 +8,11 @@ struct StatsTrafficSection: Hashable { } } -extension HashableImmuTableRow { +protocol StatsHashableImmuTableRow: ImmuTableRow, Hashable { + var statSection: StatSection? { get } +} + +extension StatsHashableImmuTableRow { /// The diffable data source relies on both the identity and the equality of the items it manages. /// The identity is determined by the item's hash, and equality is determined by whether the item's content has changed. /// If the content of an item is considered to have changed (even if its hash hasn't), the diffable data source may decide to reload that item. diff --git a/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift b/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift index 3cd6d0629854..c9535484b284 100644 --- a/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift +++ b/WordPress/Classes/ViewRelated/Stats/Shared Views/GhostViews/StatsGhostTableViewRows.swift @@ -1,6 +1,6 @@ import WordPressUI -protocol StatsRowGhostable: HashableImmuTableRow { +protocol StatsRowGhostable: StatsHashableImmuTableRow { var statSection: StatSection? { get } } diff --git a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift index f5bd11903f62..42c1b5c4864f 100644 --- a/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift +++ b/WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift @@ -4,7 +4,7 @@ import DGCharts // MARK: - Shared Rows -struct OverviewRow: HashableImmuTableRow { +struct OverviewRow: StatsHashableImmuTableRow { typealias CellType = OverviewCell @@ -67,7 +67,7 @@ struct ViewsVisitorsRow: ImmuTableRow { } } -struct CellHeaderRow: HashableImmuTableRow { +struct CellHeaderRow: StatsHashableImmuTableRow { typealias CellType = StatsCellHeader @@ -407,7 +407,7 @@ struct PeriodEmptyCellHeaderRow: ImmuTableRow { } } -struct TopTotalsPeriodStatsRow: HashableImmuTableRow { +struct TopTotalsPeriodStatsRow: StatsHashableImmuTableRow { typealias CellType = TopTotalsCell @@ -453,7 +453,7 @@ struct TopTotalsPeriodStatsRow: HashableImmuTableRow { } } -struct TopTotalsNoSubtitlesPeriodStatsRow: HashableImmuTableRow { +struct TopTotalsNoSubtitlesPeriodStatsRow: StatsHashableImmuTableRow { typealias CellType = TopTotalsCell @@ -483,7 +483,7 @@ struct TopTotalsNoSubtitlesPeriodStatsRow: HashableImmuTableRow { } } -struct CountriesStatsRow: HashableImmuTableRow { +struct CountriesStatsRow: StatsHashableImmuTableRow { typealias CellType = CountriesCell @@ -523,7 +523,7 @@ struct CountriesStatsRow: HashableImmuTableRow { } } -struct CountriesMapRow: HashableImmuTableRow { +struct CountriesMapRow: StatsHashableImmuTableRow { let action: ImmuTableAction? = nil let countriesMap: CountriesMap var statSection: StatSection? @@ -787,7 +787,7 @@ struct DetailSubtitlesTabbedHeaderRow: ImmuTableRow { } } -struct StatsErrorRow: HashableImmuTableRow { +struct StatsErrorRow: StatsHashableImmuTableRow { static let cell: ImmuTableCell = { return ImmuTableCell.nib(StatsStackViewCell.defaultNib, StatsStackViewCell.self) }() From d130c3af5b635ad32e129225dc0c8fb62e393a2b Mon Sep 17 00:00:00 2001 From: Povilas Staskus Date: Thu, 22 Feb 2024 11:09:10 +0200 Subject: [PATCH 13/13] Update SiteStatsPeriodViewModelTests.swift --- .../Stats/Traffic/SiteStatsPeriodViewModelTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WordPress/WordPressTest/ViewRelated/Stats/Traffic/SiteStatsPeriodViewModelTests.swift b/WordPress/WordPressTest/ViewRelated/Stats/Traffic/SiteStatsPeriodViewModelTests.swift index f2d61d0a6fee..83dd0b6fe603 100644 --- a/WordPress/WordPressTest/ViewRelated/Stats/Traffic/SiteStatsPeriodViewModelTests.swift +++ b/WordPress/WordPressTest/ViewRelated/Stats/Traffic/SiteStatsPeriodViewModelTests.swift @@ -8,9 +8,9 @@ final class SiteStatsPeriodViewModelTests: XCTestCase { private var store: StatsPeriodStoreMock! private var firstRow: ImmuTableRow { - let tableViewModel = sut.tableViewModel() - let section = tableViewModel.sections[0] - return section.rows[0] + let section = sut.tableViewSnapshot().sectionIdentifiers[0] + let rows = sut.tableViewSnapshot().itemIdentifiers(inSection: section) + return rows[0].immuTableRow } override func setUpWithError() throws {