Skip to content

Commit b4c2c8f

Browse files
committed
Refactor visitor stats empty view with StoreStatsDataOrRedactedView.
1 parent b60a2f9 commit b4c2c8f

File tree

6 files changed

+235
-76
lines changed

6 files changed

+235
-76
lines changed

WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/OldStoreStatsV4PeriodViewController.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,11 @@ final class OldStoreStatsV4PeriodViewController: UIViewController {
147147
roundSmallNumbers: false) ?? String()
148148
}
149149

150-
private lazy var visitorsEmptyView = StoreStatsSiteVisitEmptyView()
150+
private lazy var visitorsEmptyView: StoreStatsEmptyView = {
151+
let emptyView = StoreStatsEmptyView()
152+
emptyView.showJetpackImage = true
153+
return emptyView
154+
}()
151155
// MARK: - Initialization
152156

153157
/// Designated Initializer
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import Combine
2+
import Foundation
3+
import UIKit
4+
5+
/// Shows either a data label for store stats (e.g. order/visitor/conversion stats), or a redacted view when data are unavailable.
6+
final class StoreStatsDataOrRedactedView: UIView {
7+
/// State of store stats data UI.
8+
enum State {
9+
/// Store stats data are available, and a label is shown.
10+
case data
11+
/// Store stats data are unavailable, and a redacted view is shown.
12+
case redacted
13+
/// Store stats data are unavailable due to Jetpack-the-plugin, and a redacted view with Jetpack logo is shown.
14+
case redactedDueToJetpack
15+
}
16+
17+
@Published var state: State = .data
18+
@Published var data: String?
19+
@Published var isHighlighted: Bool = false
20+
21+
private let dataLabel = UILabel()
22+
private let redactedView = StoreStatsEmptyView()
23+
private let stackView: UIStackView
24+
25+
private var subscriptions: Set<AnyCancellable> = []
26+
27+
init() {
28+
stackView = UIStackView(arrangedSubviews: [dataLabel, redactedView])
29+
super.init(frame: .zero)
30+
31+
configureView()
32+
configureDataLabel()
33+
observeStateForUIUpdates()
34+
observeIsHighlightedForLabelTextColor()
35+
observeDataForLabelText()
36+
}
37+
38+
required convenience init?(coder: NSCoder) {
39+
self.init()
40+
}
41+
}
42+
43+
private extension StoreStatsDataOrRedactedView {
44+
func configureView() {
45+
translatesAutoresizingMaskIntoConstraints = false
46+
47+
addSubview(stackView)
48+
stackView.translatesAutoresizingMaskIntoConstraints = false
49+
pinSubviewToAllEdges(stackView)
50+
}
51+
52+
func configureDataLabel() {
53+
dataLabel.font = Constants.statsFont
54+
dataLabel.textColor = Constants.statsTextColor
55+
}
56+
}
57+
58+
private extension StoreStatsDataOrRedactedView {
59+
func observeStateForUIUpdates() {
60+
$state.sink { [weak self] state in
61+
self?.updateUI(state: state)
62+
}.store(in: &subscriptions)
63+
}
64+
65+
func updateUI(state: State) {
66+
let isDataLabelShown = state == .data
67+
dataLabel.isHidden = isDataLabelShown == false
68+
redactedView.isHidden = !dataLabel.isHidden
69+
switch state {
70+
case .redacted, .redactedDueToJetpack:
71+
redactedView.showJetpackImage = state == .redactedDueToJetpack
72+
default:
73+
break
74+
}
75+
}
76+
77+
func observeDataForLabelText() {
78+
$data.sink { [weak self] data in
79+
self?.dataLabel.text = data
80+
}.store(in: &subscriptions)
81+
}
82+
83+
func observeIsHighlightedForLabelTextColor() {
84+
$isHighlighted.sink { [weak self] isHighlighted in
85+
self?.dataLabel.textColor = isHighlighted ? Constants.statsHighlightTextColor: Constants.statsTextColor
86+
}.store(in: &subscriptions)
87+
}
88+
}
89+
90+
private extension StoreStatsDataOrRedactedView {
91+
enum Constants {
92+
static let statsTextColor: UIColor = .text
93+
static let statsHighlightTextColor: UIColor = .accent
94+
static let statsFont: UIFont = .font(forStyle: .title3, weight: .semibold)
95+
}
96+
}
97+
98+
#if canImport(SwiftUI) && DEBUG
99+
100+
import SwiftUI
101+
102+
private struct StoreStatsDataOrRedactedViewRepresentable: UIViewRepresentable {
103+
private let state: StoreStatsDataOrRedactedView.State
104+
private let isHighlighted: Bool
105+
private let data: String?
106+
107+
init(state: StoreStatsDataOrRedactedView.State, isHighlighted: Bool = false, data: String? = nil) {
108+
self.state = state
109+
self.isHighlighted = isHighlighted
110+
self.data = data
111+
}
112+
113+
func makeUIView(context: Context) -> StoreStatsDataOrRedactedView {
114+
let view = StoreStatsDataOrRedactedView()
115+
view.state = state
116+
view.isHighlighted = isHighlighted
117+
view.data = data
118+
return view
119+
}
120+
121+
func updateUIView(_ uiView: StoreStatsDataOrRedactedView, context: Context) {
122+
uiView.state = state
123+
uiView.isHighlighted = isHighlighted
124+
uiView.data = data
125+
}
126+
}
127+
128+
struct StoreStatsDataOrRedactedView_Previews: PreviewProvider {
129+
private static func makeStack() -> some View {
130+
VStack {
131+
StoreStatsDataOrRedactedViewRepresentable(state: .data, isHighlighted: false, data: "$32.5")
132+
StoreStatsDataOrRedactedViewRepresentable(state: .redacted)
133+
StoreStatsDataOrRedactedViewRepresentable(state: .redactedDueToJetpack, isHighlighted: false)
134+
}
135+
.background(Color(UIColor.systemBackground))
136+
}
137+
138+
static var previews: some View {
139+
Group {
140+
makeStack()
141+
.previewLayout(.fixed(width: 100, height: 150))
142+
.previewDisplayName("Light")
143+
144+
makeStack()
145+
.previewLayout(.fixed(width: 100, height: 150))
146+
.environment(\.colorScheme, .dark)
147+
.previewDisplayName("Dark")
148+
149+
makeStack()
150+
.previewLayout(.fixed(width: 100, height: 400))
151+
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
152+
.previewDisplayName("Large Font")
153+
}
154+
}
155+
}
156+
157+
#endif

WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/StoreStatsSiteVisitEmptyView.swift renamed to WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/StoreStatsEmptyView.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import UIKit
22

3-
final class StoreStatsSiteVisitEmptyView: UIView {
3+
final class StoreStatsEmptyView: UIView {
4+
/// Whether the Jetpack image is shown to indicate the data are unavailable due to Jetpack-the-plugin.
5+
var showJetpackImage: Bool = false {
6+
didSet {
7+
updateJetpackImageVisibility()
8+
}
9+
}
10+
11+
private lazy var jetpackImageView = UIImageView(image: .jetpackLogoImage.withRenderingMode(.alwaysTemplate))
12+
413
convenience init() {
514
self.init(frame: .zero)
615
}
@@ -22,10 +31,10 @@ final class StoreStatsSiteVisitEmptyView: UIView {
2231
emptyView.layer.cornerRadius = 2.0
2332
emptyView.translatesAutoresizingMaskIntoConstraints = false
2433

25-
let jetpackImageView = UIImageView(image: .jetpackLogoImage.withRenderingMode(.alwaysTemplate))
2634
jetpackImageView.contentMode = .scaleAspectFit
2735
jetpackImageView.tintColor = .jetpackGreen
2836
jetpackImageView.translatesAutoresizingMaskIntoConstraints = false
37+
updateJetpackImageVisibility()
2938

3039
addSubview(emptyView)
3140
addSubview(jetpackImageView)
@@ -43,3 +52,9 @@ final class StoreStatsSiteVisitEmptyView: UIView {
4352
])
4453
}
4554
}
55+
56+
private extension StoreStatsEmptyView {
57+
func updateJetpackImageVisibility() {
58+
jetpackImageView.isHidden = showJetpackImage == false
59+
}
60+
}

WooCommerce/Classes/ViewRelated/Dashboard/Stats v4/StoreStatsV4PeriodViewController.swift

Lines changed: 24 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,8 @@ final class StoreStatsV4PeriodViewController: UIViewController {
4444
// MARK: - Subviews
4545

4646
@IBOutlet private weak var containerStackView: UIStackView!
47-
@IBOutlet private weak var visitorsStackView: UIStackView!
4847
@IBOutlet private weak var visitorsTitle: UILabel!
49-
@IBOutlet private weak var visitorsData: UILabel!
48+
@IBOutlet private weak var visitorsDataOrRedactedView: StoreStatsDataOrRedactedView!
5049
@IBOutlet private weak var ordersTitle: UILabel!
5150
@IBOutlet private weak var ordersData: UILabel!
5251
@IBOutlet private weak var conversionStackView: UIStackView!
@@ -118,8 +117,6 @@ final class StoreStatsV4PeriodViewController: UIViewController {
118117
roundSmallNumbers: false) ?? String()
119118
}
120119

121-
private lazy var visitorsEmptyView = StoreStatsSiteVisitEmptyView()
122-
123120
private var cancellables: Set<AnyCancellable> = []
124121

125122
// MARK: - Initialization
@@ -192,7 +189,7 @@ private extension StoreStatsV4PeriodViewController {
192189
}.store(in: &cancellables)
193190

194191
viewModel.visitorStatsText.sink { [weak self] visitorStatsLabel in
195-
self?.visitorsData.text = visitorStatsLabel
192+
self?.visitorsDataOrRedactedView.data = visitorStatsLabel
196193
}.store(in: &cancellables)
197194

198195
viewModel.conversionStatsText.sink { [weak self] conversionStatsLabel in
@@ -205,7 +202,7 @@ private extension StoreStatsV4PeriodViewController {
205202
guard let self = self else { return }
206203
let textColor = selectedIndex == nil ? Constants.statsTextColor: Constants.statsHighlightTextColor
207204
self.ordersData.textColor = textColor
208-
self.visitorsData.textColor = textColor
205+
self.visitorsDataOrRedactedView.isHighlighted = selectedIndex != nil
209206
self.revenueData.textColor = textColor
210207
self.conversionData.textColor = textColor
211208

@@ -290,13 +287,6 @@ private extension StoreStatsV4PeriodViewController {
290287
view.backgroundColor = Constants.containerBackgroundColor
291288
containerStackView.backgroundColor = Constants.containerBackgroundColor
292289
timeRangeBarView.backgroundColor = Constants.headerComponentBackgroundColor
293-
visitorsStackView.backgroundColor = Constants.headerComponentBackgroundColor
294-
295-
// Visitor empty view - insert it at the second-to-last index,
296-
// since we need the footer view (with height = 20) as the last item in the stack view.
297-
let emptyViewIndex = max(0, visitorsStackView.arrangedSubviews.count - 2)
298-
visitorsStackView.insertArrangedSubview(visitorsEmptyView, at: emptyViewIndex)
299-
visitorsEmptyView.isHidden = true
300290

301291
// Titles
302292
visitorsTitle.text = NSLocalizedString("Visitors", comment: "Visitors stat label on dashboard - should be plural.")
@@ -416,7 +406,6 @@ private extension StoreStatsV4PeriodViewController {
416406
//
417407
private extension StoreStatsV4PeriodViewController {
418408
func updateSiteVisitStats(mode: SiteVisitStatsMode) {
419-
visitorsStackView.isHidden = mode == .hidden
420409
reloadSiteVisitUI()
421410
updateConversionStatsVisibility(visitStatsMode: mode)
422411
}
@@ -472,11 +461,10 @@ private extension StoreStatsV4PeriodViewController {
472461
reloadSiteVisitUI()
473462
return
474463
}
475-
guard visitorsData != nil else {
464+
guard visitorsDataOrRedactedView != nil else {
476465
return
477466
}
478-
visitorsData.isHidden = false
479-
visitorsEmptyView.isHidden = true
467+
visitorsDataOrRedactedView.state = .data
480468
}
481469
}
482470

@@ -603,43 +591,32 @@ private extension StoreStatsV4PeriodViewController {
603591
reloadSiteVisitUI()
604592
updateConversionStatsVisibility(visitStatsMode: siteVisitStatsMode)
605593
reloadChart(animateChart: animateChart)
606-
let visitStatsElements: [Any] = {
607-
switch siteVisitStatsMode {
608-
case .default:
609-
return [visitorsTitle as Any,
610-
visitorsData as Any]
611-
case .redactedDueToJetpack:
612-
return [visitorsTitle as Any,
613-
visitorsEmptyView as Any]
614-
case .hidden:
615-
return []
616-
}
617-
}()
618594

619-
view.accessibilityElements = visitStatsElements + [ordersTitle as Any,
620-
ordersData as Any,
621-
revenueTitle as Any,
622-
revenueData as Any,
623-
conversionTitle as Any,
624-
conversionData as Any,
625-
yAxisAccessibilityView as Any,
626-
xAxisAccessibilityView as Any,
627-
chartAccessibilityView as Any]
595+
view.accessibilityElements = [ordersTitle as Any,
596+
ordersData as Any,
597+
visitorsTitle as Any,
598+
visitorsDataOrRedactedView as Any,
599+
revenueTitle as Any,
600+
revenueData as Any,
601+
conversionTitle as Any,
602+
conversionData as Any,
603+
yAxisAccessibilityView as Any,
604+
xAxisAccessibilityView as Any,
605+
chartAccessibilityView as Any]
628606
}
629607

630608
func reloadSiteVisitUI() {
609+
guard visitorsDataOrRedactedView != nil else {
610+
return
611+
}
612+
631613
switch siteVisitStatsMode {
632614
case .hidden:
633-
break
615+
visitorsDataOrRedactedView.state = .redacted
634616
case .redactedDueToJetpack:
635-
visitorsData.isHidden = true
636-
visitorsEmptyView.isHidden = false
617+
visitorsDataOrRedactedView.state = .redactedDueToJetpack
637618
case .default:
638-
guard visitorsData != nil else {
639-
return
640-
}
641-
visitorsData.isHidden = false
642-
visitorsEmptyView.isHidden = true
619+
visitorsDataOrRedactedView.state = .data
643620
}
644621
}
645622

@@ -720,7 +697,7 @@ private extension StoreStatsV4PeriodViewController {
720697
}
721698

722699
func updateStatsDataToDefaultStyles() {
723-
[visitorsData, ordersData, conversionData].forEach { label in
700+
[ordersData, conversionData].forEach { label in
724701
label?.font = Constants.statsFont
725702
label?.textColor = Constants.statsTextColor
726703
}

0 commit comments

Comments
 (0)