Skip to content

Commit 1010e4a

Browse files
authored
Merge pull request #5932 from woocommerce/feat/5745-stats-empty-state-refactor
Refactor store stats data / empty state UI and always show the stats views
2 parents 2365bb9 + 95bc32a commit 1010e4a

File tree

7 files changed

+315
-171
lines changed

7 files changed

+315
-171
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 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
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import UIKit
2+
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+
13+
convenience init() {
14+
self.init(frame: .zero)
15+
}
16+
17+
override init(frame: CGRect) {
18+
super.init(frame: frame)
19+
setupSubviews()
20+
}
21+
22+
required init?(coder: NSCoder) {
23+
fatalError("init(coder:) has not been implemented")
24+
}
25+
26+
private func setupSubviews() {
27+
translatesAutoresizingMaskIntoConstraints = false
28+
29+
let emptyView = UIView(frame: .zero)
30+
emptyView.backgroundColor = .systemColor(.secondarySystemBackground)
31+
emptyView.layer.cornerRadius = 2.0
32+
emptyView.translatesAutoresizingMaskIntoConstraints = false
33+
34+
jetpackImageView.contentMode = .scaleAspectFit
35+
jetpackImageView.tintColor = .jetpackGreen
36+
jetpackImageView.translatesAutoresizingMaskIntoConstraints = false
37+
updateJetpackImageVisibility()
38+
39+
addSubview(emptyView)
40+
addSubview(jetpackImageView)
41+
42+
if ServiceLocator.featureFlagService.isFeatureFlagEnabled(.myStoreTabUpdates) {
43+
NSLayoutConstraint.activate([
44+
emptyView.widthAnchor.constraint(equalToConstant: 32),
45+
emptyView.heightAnchor.constraint(equalToConstant: 10),
46+
emptyView.centerXAnchor.constraint(equalTo: centerXAnchor),
47+
emptyView.centerYAnchor.constraint(equalTo: centerYAnchor),
48+
jetpackImageView.widthAnchor.constraint(equalToConstant: 14),
49+
jetpackImageView.heightAnchor.constraint(equalToConstant: 14),
50+
jetpackImageView.leadingAnchor.constraint(equalTo: emptyView.trailingAnchor, constant: 2),
51+
jetpackImageView.bottomAnchor.constraint(equalTo: emptyView.topAnchor),
52+
jetpackImageView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: 0)
53+
])
54+
} else {
55+
NSLayoutConstraint.activate([
56+
widthAnchor.constraint(equalToConstant: 48),
57+
emptyView.widthAnchor.constraint(equalToConstant: 32),
58+
emptyView.heightAnchor.constraint(equalToConstant: 10),
59+
emptyView.centerXAnchor.constraint(equalTo: centerXAnchor),
60+
emptyView.topAnchor.constraint(equalTo: jetpackImageView.bottomAnchor),
61+
jetpackImageView.widthAnchor.constraint(equalToConstant: 14),
62+
jetpackImageView.heightAnchor.constraint(equalToConstant: 14),
63+
jetpackImageView.leadingAnchor.constraint(equalTo: emptyView.trailingAnchor),
64+
jetpackImageView.topAnchor.constraint(equalTo: topAnchor, constant: 4)
65+
])
66+
}
67+
}
68+
}
69+
70+
private extension StoreStatsEmptyView {
71+
func updateJetpackImageVisibility() {
72+
jetpackImageView.isHidden = showJetpackImage == false
73+
}
74+
}

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

Lines changed: 0 additions & 45 deletions
This file was deleted.

0 commit comments

Comments
 (0)