mobile-ios/CovidSafe/CovidStatisticsViewController.swift

408 lines
15 KiB
Swift
Raw Normal View History

2020-09-14 01:23:11 +00:00
//
// CovidStatisticsViewController.swift
// CovidSafe
//
// Copyright © 2020 Australian Government. All rights reserved.
//
import UIKit
import Lottie
class CovidStatisticsViewController: UITableViewController {
2020-10-16 04:45:07 +00:00
private let showHideStatisticsKey = "showHideStatisticsKey"
2020-09-14 01:23:11 +00:00
private let heartImage = UIImage(named: "heart")
private let virusMoleculeImage = UIImage(named: "virus-molecule")
private let trendUpImage = UIImage(named: "trending-up")
private var statisticsUpdatedDate: Date?
private var showError: Bool = false
private var showInternetError: Bool = false
private var showRefresh: Bool = false
2020-10-16 04:45:07 +00:00
lazy var showStatistics: Bool = {
guard let value = UserDefaults.standard.value(forKey: showHideStatisticsKey) as? Bool else {
return true
}
return value
}(){
didSet {
UserDefaults.standard.set(showStatistics, forKey: showHideStatisticsKey)
}
}
2020-09-14 01:23:11 +00:00
private var statisticSections: [[StatisticRowModel]] = []
var statisticsDelegate: StatisticsDelegate?
var isLoading = false {
didSet {
reloadTable()
}
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.isScrollEnabled = false
tableView.register(UINib(nibName: "StatDetailedCell", bundle: nil), forCellReuseIdentifier: "StatDetailedCell")
tableView.register(UINib(nibName: "MainStatisticsHeader", bundle: nil), forCellReuseIdentifier: "MainStatisticsHeader")
tableView.register(UINib(nibName: "StatisticsTableHeader", bundle: nil), forCellReuseIdentifier: "StatisticsHeader")
tableView.register(UINib(nibName: "LoadingViewCell", bundle: nil), forCellReuseIdentifier: "LoadingViewCell")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
}
override func viewWillDisappear(_ animated: Bool) {
tableView.removeObserver(self, forKeyPath: "contentSize")
super.viewWillDisappear(true)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?){
if(keyPath == "contentSize"){
if let newvalue = change?[.newKey]{
let newsize = newvalue as! CGSize
statisticsDelegate?.setStatisticsContainerHeight(height: newsize.height)
}
}
}
// MARK: Process data for the table
2020-11-10 00:51:00 +00:00
func setupData(statistics: StatisticsResponse?, errorType: CovidSafeAPIError?, hasInternet: Bool) {
2020-09-14 01:23:11 +00:00
showInternetError = false
showError = false
showRefresh = false
if errorType != nil {
showError = true
showRefresh = true
}
if !hasInternet {
showInternetError = true
}
processData(statisticsData: statistics)
reloadTable()
}
fileprivate func processData(statisticsData: StatisticsResponse?) {
statisticSections = []
guard let statisticsData = statisticsData else {
// this is the edge case of no data available.
// need an empty section to render the main header
statisticSections.append([])
return
}
let mainData = statisticsData.national
// Set updated date
if let updatedDate = statisticsData.updatedDate {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // set locale to reliable US_POSIX
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
statisticsUpdatedDate = dateFormatter.date(from:updatedDate)
}
var mainSectionData: [StatisticRowModel] = []
if let cases = mainData?.newCases {
mainSectionData.append(StatisticRowModel(number: cases, description: "new_cases".localizedString(), image: trendUpImage))
}
if let cases = mainData?.totalCases {
mainSectionData.append(StatisticRowModel(number: cases, description: "total_confirmed_cases".localizedString(), image: virusMoleculeImage))
}
if let cases = mainData?.recoveredCases {
mainSectionData.append(StatisticRowModel(number: cases, description: "recovered".localizedString(), image: heartImage))
}
if let cases = mainData?.deaths {
mainSectionData.append(StatisticRowModel(number: cases, description: "deaths".localizedString(), image: virusMoleculeImage, imageBackgroundColor: UIColor.covidSafeLightGreyColor))
}
statisticSections.append(mainSectionData)
var statesSectionData: [StatisticRowModel] = []
if let cases = statisticsData.act?.totalCases {
statesSectionData.append(StatisticRowModel(number: cases, description: "australian_capital_territory".localizedString()))
}
if let cases = statisticsData.nsw?.totalCases {
statesSectionData.append(StatisticRowModel(number: cases, description: "new_south_wales".localizedString()))
}
if let cases = statisticsData.nt?.totalCases {
statesSectionData.append(StatisticRowModel(number: cases, description: "northern_territory".localizedString()))
}
if let cases = statisticsData.qld?.totalCases {
statesSectionData.append(StatisticRowModel(number: cases, description: "queensland".localizedString()))
}
if let cases = statisticsData.sa?.totalCases {
statesSectionData.append(StatisticRowModel(number: cases, description: "south_australia".localizedString()))
}
if let cases = statisticsData.tas?.totalCases {
statesSectionData.append(StatisticRowModel(number: cases, description: "tasmania".localizedString()))
}
if let cases = statisticsData.vic?.totalCases {
statesSectionData.append(StatisticRowModel(number: cases, description: "victoria".localizedString()))
}
if let cases = statisticsData.wa?.totalCases {
statesSectionData.append(StatisticRowModel(number: cases, description: "western_australia".localizedString()))
}
if statesSectionData.count > 0 {
statisticSections.append(statesSectionData)
}
}
// MARK: Table view delegate
fileprivate func reloadTable() {
tableView.reloadData()
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if isLoading {
return nil
}
if section == 0 {
let headerView = tableView.dequeueReusableCell(withIdentifier: "MainStatisticsHeader") as! MainStatisticsHeaderViewCell
2020-10-16 04:45:07 +00:00
headerView.titleLabel.font = UIFont.preferredFont(for: .title3, weight: .semibold)
2020-09-14 01:23:11 +00:00
headerView.statisticsDelegate = statisticsDelegate
2020-10-16 04:45:07 +00:00
headerView.statisticsTableDelegate = self
let hideShowLabel = showStatistics ? "hide".localizedString() : "show".localizedString()
headerView.hideShowButton.setTitle(hideShowLabel, for: .normal)
2020-09-14 01:23:11 +00:00
2020-10-16 04:45:07 +00:00
if let updateDate = statisticsUpdatedDate, showStatistics {
2020-09-14 01:23:11 +00:00
let dateFormatter = DateFormatter()
2020-10-16 04:45:07 +00:00
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .short
headerView.dateLabelContainer.isHidden = false
headerView.dateLabelDivider.isHidden = false
2020-11-10 00:51:00 +00:00
headerView.dateLabel.text = "\(dateFormatter.string(from: updateDate))"
2020-09-14 01:23:11 +00:00
} else {
2020-10-16 04:45:07 +00:00
headerView.dateLabelContainer.isHidden = true
headerView.dateLabelDivider.isHidden = true
2020-09-14 01:23:11 +00:00
}
if showInternetError {
headerView.errorLabel.text = "numbers_no_internet".localizedString()
} else {
headerView.errorLabel.text = "numbers_error".localizedString()
}
2020-10-16 04:45:07 +00:00
headerView.errorLabel.isHidden = !showError || !showStatistics
headerView.refreshViewContainer.isHidden = !showRefresh || !showStatistics
2020-09-14 01:23:11 +00:00
return headerView
}
if section == 1 {
let headerView = tableView.dequeueReusableCell(withIdentifier: "StatisticsHeader")
return headerView
}
return nil
}
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
if isLoading {
return 0 // no header for section
}
return -1 // automatic
}
2020-10-16 04:45:07 +00:00
override func tableView(_ tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat {
2020-09-14 01:23:11 +00:00
if isLoading {
2020-10-16 04:45:07 +00:00
return 0 // no header for section
}
return 53
}
override func numberOfSections(in tableView: UITableView) -> Int {
if isLoading || !showStatistics {
2020-09-14 01:23:11 +00:00
return 1
}
return statisticSections.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if isLoading {
return 1
}
2020-10-16 04:45:07 +00:00
if !showStatistics {
return 0
}
2020-09-14 01:23:11 +00:00
return statisticSections[section].count
}
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if let loadingCell = cell as? LoadingViewCell {
loadingCell.stopAnimation()
}
}
2020-10-16 04:45:07 +00:00
override func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
UIView.performWithoutAnimation {
view.layoutIfNeeded()
}
}
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
UIView.performWithoutAnimation {
cell.layoutIfNeeded()
}
}
2020-09-14 01:23:11 +00:00
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if isLoading {
let cellView = tableView.dequeueReusableCell(withIdentifier: "LoadingViewCell", for: indexPath) as! LoadingViewCell
cellView.startAnimation()
return cellView
}
// detailed stat row
if indexPath.section == 0 {
let cellView = tableView.dequeueReusableCell(withIdentifier: "StatDetailedCell", for: indexPath) as! StatDetailedViewCell
let rowData = statisticSections[indexPath.section][indexPath.row]
cellView.statImage?.image = rowData.image
cellView.statDescription.text = rowData.description
cellView.statNumberLabel.text = NumberFormatter.localizedString(from: NSNumber(value: rowData.number), number: .decimal)
if #available(iOS 11.0, *) {
cellView.statNumberLabel.font = UIFont.preferredFont(for: .largeTitle, weight: .semibold)
} else {
// Fallback on earlier versions
cellView.statNumberLabel.font = UIFont.preferredFont(for: .title1, weight: .semibold)
}
if let bkgColor = rowData.imageBackgroundColor {
cellView.imageBackgroundColor = bkgColor
}
return cellView
}
// Simple stat row
var cellView = tableView.dequeueReusableCell(withIdentifier: "StatSummaryCell")
if cellView == nil {
cellView = UITableViewCell(style: .value1, reuseIdentifier: "StatSummaryCell")
cellView?.textLabel?.font = UIFont.preferredFont(forTextStyle: .callout)
cellView?.textLabel?.textColor = UIColor.covidSafeDarkFontColor
cellView?.detailTextLabel?.font = UIFont.preferredFont(forTextStyle: .callout)
cellView?.detailTextLabel?.textColor = UIColor.covidSafeDarkFontColor
}
let rowData = statisticSections[indexPath.section][indexPath.row]
cellView!.textLabel?.text = rowData.description
cellView!.detailTextLabel?.text = NumberFormatter.localizedString(from: NSNumber(value: rowData.number), number: .decimal)
return cellView!
}
}
2020-10-16 04:45:07 +00:00
// MARK: Statistics Table Delegate Implementation
extension CovidStatisticsViewController: StatisticsTableDelegate {
func toggleDisplayStatistics() {
showStatistics = !showStatistics
if showStatistics && statisticSections.count == 0 {
statisticsDelegate?.refreshStatistics()
} else {
UIView.transition(with: tableView,
duration: 0.3,
options: .transitionCrossDissolve,
animations: { self.reloadTable() })
}
}
}
2020-09-14 01:23:11 +00:00
// MARK: Table view cells
class StatDetailedViewCell: UITableViewCell {
@IBOutlet weak var statImage: UIImageView!
@IBOutlet weak var statNumberLabel: UILabel!
@IBOutlet weak var statDescription: UILabel!
@IBOutlet weak var imageContainer: UIView!
var imageBackgroundColor: UIColor? {
didSet {
imageContainer.backgroundColor = imageBackgroundColor
}
}
}
class MainStatisticsHeaderViewCell: UITableViewCell {
@IBOutlet weak var dateLabel: UILabel!
2020-10-16 04:45:07 +00:00
@IBOutlet weak var dateLabelContainer: UIView!
@IBOutlet weak var dateLabelDivider: UIView!
2020-09-14 01:23:11 +00:00
@IBOutlet weak var errorLabel: UILabel!
@IBOutlet weak var refreshViewContainer: UIView!
@IBOutlet weak var titleLabel: UILabel!
2020-10-16 04:45:07 +00:00
@IBOutlet weak var hideShowButton: UIButton!
2020-09-14 01:23:11 +00:00
var statisticsDelegate: StatisticsDelegate?
2020-10-16 04:45:07 +00:00
var statisticsTableDelegate: StatisticsTableDelegate?
2020-09-14 01:23:11 +00:00
@IBAction func refreshButtonTapped(_ sender: Any) {
statisticsDelegate?.refreshStatistics()
}
2020-10-16 04:45:07 +00:00
@IBAction func showHideButtonTapped(_ sender: Any) {
statisticsTableDelegate?.toggleDisplayStatistics()
}
2020-09-14 01:23:11 +00:00
}
class LoadingViewCell: UITableViewCell {
@IBOutlet weak var loadingAnimationView: UIView!
var lottieLoadingView: AnimationView?
func startAnimation() {
if lottieLoadingView == nil {
let loadingAnimation = AnimationView(name: "Spinner_upload")
loadingAnimation.loopMode = .loop
loadingAnimation.frame = CGRect(origin: CGPoint(x: 0, y: 0), size: loadingAnimationView.frame.size)
loadingAnimationView.addSubview(loadingAnimation)
lottieLoadingView = loadingAnimation
}
lottieLoadingView?.play()
}
func stopAnimation() {
lottieLoadingView?.stop()
}
}
// MARK: Statistics delegate
2020-10-16 04:45:07 +00:00
protocol StatisticsTableDelegate {
func toggleDisplayStatistics()
}
2020-09-14 01:23:11 +00:00
protocol StatisticsDelegate {
func refreshStatistics()
func setStatisticsContainerHeight(height: CGFloat)
}
// MARK: Statistics row model
struct StatisticRowModel {
var number: Int
var description: String
var image: UIImage?
var imageBackgroundColor: UIColor?
}