Skip to content

Commit e102336

Browse files
authored
Merge pull request #18511 from wordpress-mobile/feature/stats-revamp-likes-comments-followers-i
Stats Revamp: New Likes, Comments, Followers Totals Card I
2 parents 5bccda8 + 10ac336 commit e102336

10 files changed

+292
-5
lines changed

WordPress/Classes/Stores/StatsInsightsStore.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,19 @@ extension StatsInsightsStore {
938938
return state.emailFollowers
939939
}
940940

941+
func getTotalFollowerCount() -> Int {
942+
let totalDotComFollowers = getDotComFollowers()?.dotComFollowersCount ?? 0
943+
let totalEmailFollowers = getEmailFollowers()?.emailFollowersCount ?? 0
944+
945+
var totalPublicize = 0
946+
if let publicize = getPublicize(),
947+
!publicize.publicizeServices.isEmpty {
948+
totalPublicize = publicize.publicizeServices.compactMap({$0.followers}).reduce(0, +)
949+
}
950+
951+
return totalDotComFollowers + totalEmailFollowers + totalPublicize
952+
}
953+
941954
func getPublicize() -> StatsPublicizeInsight? {
942955
return state.publicizeFollowers
943956
}

WordPress/Classes/ViewRelated/Stats/Extensions/WPStyleGuide+Stats.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ extension WPStyleGuide {
203203
static let subTitleFont = WPStyleGuide.fontForTextStyle(.footnote, fontWeight: .medium)
204204
static let summaryFont = WPStyleGuide.fontForTextStyle(.subheadline, fontWeight: .regular)
205205
static let substringHighlightFont = WPStyleGuide.fontForTextStyle(.subheadline, fontWeight: .semibold)
206+
static let insightsCountFont = UIFont.preferredFont(forTextStyle: .title1).bold()
206207

207208
static let tableBackgroundColor = UIColor.listBackground
208209
static let cellBackgroundColor = UIColor.listForeground

WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsTableViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ private extension SiteStatsInsightsTableViewController {
177177
TabbedTotalsStatsRow.self,
178178
TopTotalsInsightStatsRow.self,
179179
MostPopularTimeInsightStatsRow.self,
180+
TotalInsightStatsRow.self,
180181
TableFooterRow.self,
181182
StatsErrorRow.self,
182183
StatsGhostGrowAudienceImmutableRow.self,

WordPress/Classes/ViewRelated/Stats/Insights/SiteStatsInsightsViewModel.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,13 @@ class SiteStatsInsightsViewModel: Observable {
126126
type: .insights,
127127
status: insightsStore.followersTotalsStatus,
128128
block: {
129+
if FeatureFlag.statsNewInsights.enabled {
130+
return TotalInsightStatsRow(dataRow: createFollowerTotalInsightsRow(), statSection: .insightsFollowerTotals, siteStatsInsightsDelegate: siteStatsInsightsDelegate)
131+
} else {
129132
return TwoColumnStatsRow(dataRows: createTotalFollowersRows(),
130133
statSection: .insightsFollowerTotals,
131134
siteStatsInsightsDelegate: nil)
135+
}
132136
}, loading: {
133137
return StatsGhostTwoColumnImmutableRow()
134138
}, error: errorBlock))
@@ -413,12 +417,12 @@ private extension SiteStatsInsightsViewModel {
413417
let totalEmailFollowers = insightsStore.getEmailFollowers()?.emailFollowersCount ?? 0
414418

415419
var totalPublicize = 0
416-
if let publicize = insightsStore.getPublicize(), !publicize.publicizeServices.isEmpty {
420+
if let publicize = insightsStore.getPublicize(),
421+
!publicize.publicizeServices.isEmpty {
417422
totalPublicize = publicize.publicizeServices.compactMap({$0.followers}).reduce(0, +)
418423
}
419424

420-
let totalFollowers = totalDotComFollowers + totalEmailFollowers + totalPublicize
421-
425+
let totalFollowers = insightsStore.getTotalFollowerCount()
422426
guard totalFollowers > 0 else {
423427
return []
424428
}
@@ -438,6 +442,10 @@ private extension SiteStatsInsightsViewModel {
438442
return dataRows
439443
}
440444

445+
func createFollowerTotalInsightsRow() -> StatsTotalInsightsData {
446+
return StatsTotalInsightsData(count: insightsStore.getTotalFollowerCount().abbreviatedString())
447+
}
448+
441449
func createPublicizeRows() -> [StatsTotalRowData] {
442450
guard let services = insightsStore.getPublicize()?.publicizeServices else {
443451
return []
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import UIKit
2+
import simd
3+
4+
class SparklineView: UIView {
5+
private let lineLayer = CAShapeLayer()
6+
private let maskLayer = CAShapeLayer()
7+
private let gradientLayer = CAGradientLayer()
8+
9+
private static let defaultChartColor = UIColor.muriel(name: .blue, .shade50)
10+
var chartColor: UIColor! = SparklineView.defaultChartColor {
11+
didSet {
12+
if chartColor == nil {
13+
chartColor = SparklineView.defaultChartColor
14+
}
15+
}
16+
}
17+
18+
var data: [CGFloat] = []
19+
20+
override init(frame: CGRect) {
21+
super.init(frame: frame)
22+
let initialData = [102, 109, 526, 253, 163, 227, 101].map({ CGFloat($0) })
23+
data = interpolateData(initialData)
24+
25+
initializeChart()
26+
layoutChart()
27+
}
28+
29+
required init?(coder: NSCoder) {
30+
fatalError()
31+
}
32+
33+
override func layoutSubviews() {
34+
super.layoutSubviews()
35+
36+
layoutChart()
37+
}
38+
39+
func initializeChart() {layer.isGeometryFlipped = true
40+
41+
lineLayer.strokeColor = chartColor.cgColor
42+
lineLayer.lineWidth = Constants.lineWidth
43+
lineLayer.fillColor = UIColor.clear.cgColor
44+
45+
maskLayer.strokeColor = UIColor.clear.cgColor
46+
maskLayer.fillColor = UIColor.black.cgColor
47+
48+
gradientLayer.startPoint = Constants.gradientStart
49+
gradientLayer.endPoint = Constants.gradientEnd
50+
gradientLayer.colors = [chartColor.cgColor, UIColor(white: 1.0, alpha: 0.0).cgColor]
51+
gradientLayer.mask = maskLayer
52+
gradientLayer.opacity = Constants.gradientOpacity
53+
54+
layer.addSublayer(gradientLayer)
55+
layer.addSublayer(lineLayer)
56+
}
57+
58+
private func interpolateData(_ inputData: [CGFloat]) -> [CGFloat] {
59+
guard inputData.count > 0,
60+
let first = inputData.first else {
61+
return []
62+
}
63+
64+
var interpolatedData = [first]
65+
66+
for (this, next) in zip(inputData, inputData.dropFirst()) {
67+
let interpolationIncrement = (next - this) / Constants.interpolationCount
68+
69+
for index in stride(from: 1, through: Constants.interpolationCount, by: 1) {
70+
let normalized = simd_smoothstep(this, next, this + (index * interpolationIncrement))
71+
let actual = simd_mix(this, next, normalized)
72+
73+
interpolatedData.append(actual)
74+
}
75+
}
76+
77+
return interpolatedData
78+
}
79+
80+
private func layoutChart() {
81+
CATransaction.begin()
82+
CATransaction.setDisableActions(true)
83+
84+
lineLayer.frame = bounds
85+
maskLayer.frame = bounds
86+
gradientLayer.frame = bounds
87+
88+
// Calculate points to fit along X axis, using existing interpolated Y values
89+
let segmentWidth = bounds.width / CGFloat(data.count-1)
90+
let points = data.enumerated().map({ CGPoint(x: CGFloat($0.offset) * segmentWidth, y: $0.element) })
91+
92+
// Scale Y values to fit within our bounds
93+
let maxYValue = points.map(\.y).max() ?? 1.0
94+
let scaleFactor = bounds.height / maxYValue
95+
let scaleTransform = CGAffineTransform(scaleX: 1.0, y: scaleFactor)
96+
97+
// Scale the points slightly so that the line remains within bounds, based on the line width.
98+
let xScaleFactor = (bounds.width - Constants.lineWidth) / bounds.width
99+
let yScaleFactor = (bounds.height - Constants.lineWidth) / bounds.height
100+
101+
let halfLineWidth = Constants.lineWidth / 2.0
102+
var lineTransform = CGAffineTransform(translationX: halfLineWidth, y: halfLineWidth)
103+
lineTransform = lineTransform.scaledBy(x: xScaleFactor, y: yScaleFactor)
104+
lineTransform = lineTransform.concatenating(scaleTransform)
105+
106+
// Finally, create the paths – first the line...
107+
let lineLayerPath = CGMutablePath()
108+
lineLayerPath.addLines(between: points, transform: lineTransform)
109+
110+
lineLayer.path = lineLayerPath
111+
112+
// ... then the bottom gradient
113+
if let maskLayerPath = lineLayerPath.mutableCopy() {
114+
maskLayerPath.addLine(to: CGPoint(x: bounds.width, y: 0))
115+
maskLayerPath.addLine(to: CGPoint(x: 0, y: 0))
116+
maskLayer.path = maskLayerPath
117+
}
118+
119+
CATransaction.commit()
120+
}
121+
122+
private enum Constants {
123+
static let lineWidth: CGFloat = 2.0
124+
static let gradientOpacity: Float = 0.1
125+
126+
// This number of extra data points will be interpolated in between each pair of original data points.
127+
// The higher the number, the smoother the chart line.
128+
static let interpolationCount: Double = 20
129+
130+
static let gradientStart = CGPoint(x: 0.0, y: 0.5)
131+
static let gradientEnd = CGPoint(x: 1.0, y: 0.5)
132+
}
133+
}

WordPress/Classes/ViewRelated/Stats/Insights/StatsLatestPostSummaryInsightsCell.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ class StatsLatestPostSummaryInsightsCell: StatsBaseCell, LatestPostSummaryConfig
147147
topLabel.textColor = .text
148148
topLabel.text = title
149149

150-
countLabel.font = UIFont.preferredFont(forTextStyle: .title1).bold()
150+
countLabel.font = Style.insightsCountFont
151151
countLabel.textColor = .text
152152
countLabel.adjustsFontSizeToFitWidth = true
153153
countLabel.text = "0"

WordPress/Classes/ViewRelated/Stats/Insights/StatsMostPopularTimeInsightsCell.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ class StatsMostPopularTimeInsightsCell: StatsBaseCell {
126126

127127
let middleLabel = UILabel()
128128
middleLabel.textColor = .text
129-
middleLabel.font = .preferredFont(forTextStyle: .title1).bold()
129+
middleLabel.font = WPStyleGuide.Stats.insightsCountFont
130130
middleLabel.adjustsFontSizeToFitWidth = true
131131

132132
let bottomLabel = UILabel()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import UIKit
2+
import WordPressShared
3+
4+
5+
struct StatsTotalInsightsData {
6+
var count: String
7+
var comparison: String = ""
8+
}
9+
10+
class StatsTotalInsightsCell: StatsBaseCell {
11+
private weak var siteStatsInsightsDelegate: SiteStatsInsightsDelegate?
12+
private var lastPostInsight: StatsLastPostInsight?
13+
14+
private let outerStackView = UIStackView()
15+
private let topInnerStackView = UIStackView()
16+
private let countLabel = UILabel()
17+
private let comparisonLabel = UILabel()
18+
private let graphView = SparklineView()
19+
20+
// MARK: - Initialization
21+
22+
required override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
23+
super.init(style: style, reuseIdentifier: reuseIdentifier)
24+
25+
configureView()
26+
}
27+
28+
required init(coder: NSCoder) {
29+
fatalError()
30+
}
31+
32+
private func configureView() {
33+
configureStackViews()
34+
configureGraphView()
35+
configureLabels()
36+
configureConstraints()
37+
}
38+
39+
private func configureStackViews() {
40+
outerStackView.translatesAutoresizingMaskIntoConstraints = false
41+
outerStackView.axis = .vertical
42+
outerStackView.spacing = Metrics.outerStackViewSpacing
43+
contentView.addSubview(outerStackView)
44+
45+
topInnerStackView.axis = .horizontal
46+
topInnerStackView.spacing = Metrics.stackViewSpacing
47+
48+
topInnerStackView.addArrangedSubviews([countLabel, graphView])
49+
outerStackView.addArrangedSubviews([topInnerStackView, comparisonLabel])
50+
}
51+
52+
private func configureGraphView() {
53+
graphView.translatesAutoresizingMaskIntoConstraints = false
54+
graphView.setContentHuggingPriority(.required, for: .horizontal)
55+
graphView.setContentHuggingPriority(.required, for: .vertical)
56+
}
57+
58+
private func configureLabels() {
59+
countLabel.font = WPStyleGuide.Stats.insightsCountFont
60+
countLabel.textColor = .text
61+
countLabel.text = "0"
62+
countLabel.adjustsFontSizeToFitWidth = true
63+
countLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
64+
countLabel.setContentHuggingPriority(.required, for: .vertical)
65+
66+
comparisonLabel.font = .preferredFont(forTextStyle: .subheadline)
67+
comparisonLabel.textColor = .textSubtle
68+
comparisonLabel.text = "+87 (40%) compared to last week"
69+
}
70+
71+
private func configureConstraints() {
72+
topConstraint = outerStackView.topAnchor.constraint(equalTo: contentView.topAnchor)
73+
74+
NSLayoutConstraint.activate([
75+
topConstraint,
76+
outerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -StatsBaseCell.Metrics.padding),
77+
outerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: StatsBaseCell.Metrics.padding),
78+
outerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -StatsBaseCell.Metrics.padding),
79+
graphView.widthAnchor.constraint(equalTo: graphView.heightAnchor, multiplier: Metrics.graphViewAspectRatio),
80+
graphView.heightAnchor.constraint(equalTo: countLabel.heightAnchor)
81+
])
82+
}
83+
84+
// TODO: This will need updating to pass some graph data too.
85+
// Assuming this will be something like a small array of ints
86+
func configure(count: String, statSection: StatSection, siteStatsInsightsDelegate: SiteStatsInsightsDelegate?) {
87+
self.statSection = statSection
88+
self.siteStatsInsightsDelegate = siteStatsInsightsDelegate
89+
90+
countLabel.text = count
91+
}
92+
93+
private enum Metrics {
94+
static let outerStackViewSpacing: CGFloat = 16.0
95+
static let stackViewSpacing: CGFloat = 8.0
96+
static let graphViewAspectRatio: CGFloat = 3.27
97+
}
98+
}

WordPress/Classes/ViewRelated/Stats/SiteStatsTableViewCells.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,27 @@ struct MostPopularTimeInsightStatsRow: ImmuTableRow {
287287
}
288288
}
289289

290+
struct TotalInsightStatsRow: ImmuTableRow {
291+
292+
typealias CellType = StatsTotalInsightsCell
293+
294+
static let cell: ImmuTableCell = {
295+
return ImmuTableCell.class(CellType.self)
296+
}()
297+
298+
let dataRow: StatsTotalInsightsData
299+
let statSection: StatSection
300+
weak var siteStatsInsightsDelegate: SiteStatsInsightsDelegate?
301+
let action: ImmuTableAction? = nil
302+
303+
func configureCell(_ cell: UITableViewCell) {
304+
guard let cell = cell as? CellType else {
305+
return
306+
}
307+
308+
cell.configure(count: dataRow.count, statSection: statSection, siteStatsInsightsDelegate: siteStatsInsightsDelegate)
309+
}
310+
}
290311

291312
// MARK: - Insights Management
292313

0 commit comments

Comments
 (0)