Skip to content

Commit

Permalink
Stats Traffic date picker (#22400)
Browse files Browse the repository at this point in the history
  • Loading branch information
guarani authored Feb 27, 2024
2 parents 26f3d19 + 18b7d4b commit 4cb9cef
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 112 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import UIKit
import WordPressFlux
import Combine

@objc protocol SiteStatsPeriodDelegate {
@objc optional func displayWebViewWithURL(_ url: URL)
Expand All @@ -25,43 +26,24 @@ final class SiteStatsPeriodTableViewController: SiteStatsBaseTableViewController
return ContextManager.sharedInstance().mainContext
}()

var selectedDate: Date?
var selectedPeriod: StatsPeriodUnit? {
didSet {

guard let selectedPeriod else {
return
}

trackPeriodAccessEvent(selectedPeriod)

clearExpandedRows()

// If this is the first time setting the Period, need to initialize the view model.
// Otherwise, just refresh the data.
if oldValue == nil {
initViewModel()
} else {
refreshData()
}
}
}

private let store = StoreContainer.shared.statsPeriod
private var changeReceipt: Receipt?

private var viewModel: SiteStatsPeriodViewModel?
private weak var tableHeaderView: SiteStatsTableHeaderView?
private var viewModel: SiteStatsPeriodViewModel!
private let datePickerViewModel: StatsTrafficDatePickerViewModel
private let datePickerView: StatsTrafficDatePickerView
private var cancellables: Set<AnyCancellable> = []

private let analyticsTracker = BottomScrollAnalyticsTracker()

private lazy var tableHandler: ImmuTableDiffableViewHandler = {
return ImmuTableDiffableViewHandler(takeOver: self, with: analyticsTracker)
}()

init() {
init(date: Date, period: StatsPeriodUnit) {
datePickerViewModel = StatsTrafficDatePickerViewModel(period: period, date: date)
datePickerView = StatsTrafficDatePickerView(viewModel: datePickerViewModel)
super.init(nibName: nil, bundle: nil)

tableStyle = .insetGrouped
}

Expand All @@ -81,31 +63,49 @@ final class SiteStatsPeriodTableViewController: SiteStatsBaseTableViewController
tableView.estimatedRowHeight = 500
tableView.estimatedSectionHeaderHeight = SiteStatsTableHeaderView.estimatedHeight
sendScrollEventsToBanner()

viewModel = SiteStatsPeriodViewModel(store: store,
selectedDate: datePickerViewModel.date,
selectedPeriod: datePickerViewModel.period,
periodDelegate: self,
referrerDelegate: self)
addViewModelListeners()
viewModel.startFetchingOverview()

Publishers.CombineLatest(datePickerViewModel.$date, datePickerViewModel.$period)
.sink(receiveValue: { [weak self] _, _ in
DispatchQueue.main.async {
self?.refreshData()
}
})
.store(in: &cancellables)
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if !isMovingToParent {
guard let date = selectedDate, let period = selectedPeriod else {
return
}
addViewModelListeners()
viewModel?.refreshTrafficOverviewData(withDate: date, forPeriod: period)
viewModel.refreshTrafficOverviewData(withDate: datePickerViewModel.date, forPeriod: datePickerViewModel.period)
}
}

// TODO: Replace with a new Date Picker
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard section == 0 else { return nil }
override func initTableView() {
let embeddedDatePickerView = UIView.embedSwiftUIView(datePickerView)
view.addSubview(embeddedDatePickerView)
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)

guard let cell = Bundle.main.loadNibNamed("SiteStatsTableHeaderView", owner: nil, options: nil)?.first as? SiteStatsTableHeaderView else {
return nil
}
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: embeddedDatePickerView.topAnchor, constant: 0),
view.leadingAnchor.constraint(equalTo: embeddedDatePickerView.leadingAnchor, constant: 0),
view.trailingAnchor.constraint(equalTo: embeddedDatePickerView.trailingAnchor, constant: 0),
embeddedDatePickerView.bottomAnchor.constraint(equalTo: tableView.topAnchor, constant: 0),
view.leadingAnchor.constraint(equalTo: tableView.leadingAnchor, constant: 0),
view.trailingAnchor.constraint(equalTo: tableView.trailingAnchor, constant: 0),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0),
])

cell.configure(date: selectedDate, period: selectedPeriod, delegate: self)
cell.animateGhostLayers(viewModel?.isFetchingChart() == true)
tableHeaderView = cell
return cell
tableView.refreshControl = refreshControl
}

override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
Expand All @@ -124,28 +124,12 @@ private extension SiteStatsPeriodTableViewController {

// MARK: - View Model

func initViewModel() {

guard let selectedDate = selectedDate,
let selectedPeriod = selectedPeriod else {
return
}

viewModel = SiteStatsPeriodViewModel(store: store,
selectedDate: selectedDate,
selectedPeriod: selectedPeriod,
periodDelegate: self,
referrerDelegate: self)
addViewModelListeners()
viewModel?.startFetchingOverview()
}

func addViewModelListeners() {
if changeReceipt != nil {
return
}

changeReceipt = viewModel?.onChange { [weak self] in
changeReceipt = viewModel.onChange { [weak self] in
self?.refreshTableView()
}
}
Expand All @@ -172,14 +156,9 @@ private extension SiteStatsPeriodTableViewController {
// MARK: - Table Refreshing

func refreshTableView() {
guard let viewModel = viewModel else {
return
}

tableHandler.diffableDataSource.apply(viewModel.tableViewSnapshot(), animatingDifferences: false)

refreshControl.endRefreshing()
tableHeaderView?.animateGhostLayers(viewModel.isFetchingChart() == true)

if viewModel.fetchingFailed() {
displayFailureViewIfNecessary()
Expand All @@ -193,14 +172,12 @@ private extension SiteStatsPeriodTableViewController {
}

func refreshData() {
guard let selectedDate = selectedDate,
let selectedPeriod = selectedPeriod,
viewIsVisible() else {
guard viewIsVisible() else {
refreshControl.endRefreshing()
return
return
}
addViewModelListeners()
viewModel?.refreshTrafficOverviewData(withDate: selectedDate, forPeriod: selectedPeriod)
viewModel.refreshTrafficOverviewData(withDate: datePickerViewModel.date, forPeriod: datePickerViewModel.period)
}

func applyTableUpdates() {
Expand Down Expand Up @@ -303,8 +280,8 @@ extension SiteStatsPeriodTableViewController: SiteStatsPeriodDelegate {

let detailTableViewController = SiteStatsDetailTableViewController.loadFromStoryboard()
detailTableViewController.configure(statSection: statSection,
selectedDate: selectedDate,
selectedPeriod: selectedPeriod)
selectedDate: datePickerViewModel.date,
selectedPeriod: datePickerViewModel.period)
navigationController?.pushViewController(detailTableViewController, animated: true)
}

Expand All @@ -318,8 +295,8 @@ extension SiteStatsPeriodTableViewController: SiteStatsPeriodDelegate {
}

func barChartTabSelected(_ tabIndex: StatsTrafficBarChartTabIndex) {
if let tab = StatsTrafficBarChartTabs(rawValue: tabIndex), let period = selectedPeriod {
trackBarChartTabSelectionEvent(tab: tab, period: period)
if let tab = StatsTrafficBarChartTabs(rawValue: tabIndex) {
trackBarChartTabSelectionEvent(tab: tab, period: datePickerViewModel.period)
}
}
}
Expand All @@ -332,21 +309,6 @@ extension SiteStatsPeriodTableViewController: SiteStatsReferrerDelegate {
}
}

// MARK: - SiteStatsTableHeaderDelegate Methods

extension SiteStatsPeriodTableViewController: SiteStatsTableHeaderDateButtonDelegate {
func dateChangedTo(_ newDate: Date?) {
selectedDate = newDate
refreshData()
}

func didTouchHeaderButton(forward: Bool) {
if let intervalDate = viewModel?.updateDate(forward: forward) {
tableHeaderView?.updateDate(with: intervalDate)
}
}
}

// MARK: Jetpack powered banner

private extension SiteStatsPeriodTableViewController {
Expand All @@ -361,23 +323,6 @@ private extension SiteStatsPeriodTableViewController {
// MARK: - Tracking

private extension SiteStatsPeriodTableViewController {
func trackPeriodAccessEvent(_ period: StatsPeriodUnit) {
let event: WPAnalyticsStat = {
switch period {
case .day:
return .statsPeriodDaysAccessed
case .week:
return .statsPeriodWeeksAccessed
case .month:
return .statsPeriodMonthsAccessed
case .year:
return .statsPeriodYearsAccessed
}
}()

WPAppAnalytics.track(event)
}

func trackBarChartTabSelectionEvent(tab: StatsTrafficBarChartTabs, period: StatsPeriodUnit) {
let properties: [AnyHashable: Any] = [StatsPeriodUnit.analyticsPeriodKey: period.description as Any]
WPAppAnalytics.track(tab.analyticsEvent, withProperties: properties)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,18 @@ class SiteStatsDashboardViewController: UIViewController {

private var insightsTableViewController = SiteStatsInsightsTableViewController.loadFromStoryboard()
private lazy var periodTableViewControllerDeprecated = SiteStatsPeriodTableViewControllerDeprecated.loadFromStoryboard()
private lazy var trafficTableViewController = SiteStatsPeriodTableViewController()
private lazy var trafficTableViewController = {
let date: Date
if let selectedDate = getLastSelectedDateFromUserDefaults() {
date = selectedDate
} else {
date = StatsDataHelper.currentDateForSite()
}

let currentPeriod = StatsPeriodUnit(rawValue: currentSelectedPeriod.rawValue - 1) ?? .day

return SiteStatsPeriodTableViewController(date: date, period: currentPeriod)
}()
private var pageViewController: UIPageViewController?
private lazy var displayedPeriods: [StatsPeriodType] = StatsPeriodType.displayedPeriods

Expand Down Expand Up @@ -250,7 +261,6 @@ private extension SiteStatsDashboardViewController {

func restoreSelectedDateFromUserDefaults() {
periodTableViewControllerDeprecated.selectedDate = getLastSelectedDateFromUserDefaults()
trafficTableViewController.selectedDate = getLastSelectedDateFromUserDefaults()
removeLastSelectedDateFromUserDefaults()
}

Expand Down Expand Up @@ -278,13 +288,6 @@ private extension SiteStatsDashboardViewController {
direction: .forward,
animated: false)
}

if trafficTableViewController.selectedDate == nil {
trafficTableViewController.selectedDate = StatsDataHelper.currentDateForSite()
}

let selectedPeriod = StatsPeriodUnit(rawValue: currentSelectedPeriod.rawValue - 1) ?? .day
trafficTableViewController.selectedPeriod = selectedPeriod
case .days, .weeks, .months, .years:
if previousSelectedPeriodWasInsights || pageViewControllerIsEmpty {
pageViewController?.setViewControllers([periodTableViewControllerDeprecated],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import SwiftUI
import DesignSystem

struct StatsTrafficDatePickerView: View {
@ObservedObject var viewModel: StatsTrafficDatePickerViewModel

var body: some View {
HStack {
Menu {
ForEach([StatsPeriodUnit.day, .week, .month, .year], id: \.self) { period in
Button(period.label, action: {
viewModel.period = period
})
}
} label: {
Text(viewModel.period.label)
.style(TextStyle.bodySmall(.emphasized))
.foregroundColor(Color.DS.Foreground.primary)
Image(systemName: "chevron.down")
.font(.system(size: 8))
.foregroundColor(Color.DS.Foreground.secondary)

}
.menuStyle(.borderlessButton)
.padding(.vertical, Length.Padding.single)
.padding(.horizontal, Length.Padding.double)
.background(Color.DS.Background.secondary)
.clipShape(RoundedRectangle(cornerRadius: Length.Radius.max))
.overlay(
RoundedRectangle(cornerRadius: Length.Radius.max)
.strokeBorder(.clear, lineWidth: 0)
)

Spacer()

Text(viewModel.formattedCurrentPeriod())
.style(TextStyle.bodySmall(.emphasized))
.foregroundColor(Color.DS.Foreground.primary)
.lineLimit(1)

Spacer().frame(width: Length.Padding.split)

Button(action: {
viewModel.goToPreviousPeriod()
}) {
Image(systemName: "chevron.left")
.imageScale(.small)
.foregroundColor(Color.DS.Foreground.secondary)
.flipsForRightToLeftLayoutDirection(true)
}
.padding(.trailing, Length.Padding.single)

let isNextDisabled = !viewModel.isNextPeriodAvailable
let enabledColor = Color.DS.Foreground.secondary
let disabledColor = enabledColor.opacity(0.5)

Button(action: {
viewModel.goToNextPeriod()
}) {
Image(systemName: "chevron.right")
.imageScale(.small)
.foregroundColor(isNextDisabled ? disabledColor : enabledColor)
.flipsForRightToLeftLayoutDirection(true)
}
.disabled(isNextDisabled)
}.padding(.vertical, Length.Padding.single)
.padding(.horizontal, Length.Padding.double)
.background(Color.DS.Background.primary)
.overlay(
Rectangle()
.frame(height: Length.Border.thin)
.foregroundColor(Color.DS.Foreground.tertiary),
alignment: .bottom
)
.background(Color.DS.Background.secondary)
}
}

private extension StatsPeriodUnit {
var label: String {
switch self {
case .day:
return NSLocalizedString("stats.traffic.day", value: "By day", comment: "The label for the option to show Stats Traffic by day.")
case .week:
return NSLocalizedString("stats.traffic.week", value: "By week", comment: "The label for the option to show Stats Traffic by week.")
case .month:
return NSLocalizedString("stats.traffic.month", value: "By month", comment: "The label for the option to show Stats Traffic by month.")
case .year:
return NSLocalizedString("stats.traffic.year", value: "By year", comment: "The label for the option to show Stats Traffic by year.")
}
}
}
Loading

0 comments on commit 4cb9cef

Please sign in to comment.