From 52cb01c6c758b05d4e88025a65f369aed0dc0c84 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 29 Jul 2019 16:03:22 -0700 Subject: [PATCH] [AltStore] Revises AppViewController UI - Fades in navigation bar as user scrolls down - Displays version number, version date, and app size --- AltStore.xcodeproj/project.pbxproj | 4 + .../App Detail/AppContentViewController.swift | 18 ++ AltStore/App Detail/AppViewController.swift | 229 ++++++++++++------ AltStore/Base.lproj/Main.storyboard | 134 +++++++--- AltStore/Components/NavigationBar.swift | 12 + AltStore/Extensions/Date+RelativeDate.swift | 34 +++ AltStore/My Apps/MyAppsViewController.swift | 23 +- 7 files changed, 332 insertions(+), 122 deletions(-) create mode 100644 AltStore/Extensions/Date+RelativeDate.swift diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index a6966542..01d935d8 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ BFD52C2122A1A9EC000B7ED1 /* node_list.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1E22A1A9EC000B7ED1 /* node_list.c */; }; BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; }; BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */; }; + BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */; }; BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */; }; BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */; }; BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */; }; @@ -416,6 +417,7 @@ BFD52C1E22A1A9EC000B7ED1 /* node_list.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = node_list.c; path = Dependencies/libplist/libcnary/node_list.c; sourceTree = SOURCE_ROOT; }; BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; }; BFD6B03222DFF20800B86064 /* MyAppsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsComponents.swift; sourceTree = ""; }; + BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+RelativeDate.swift"; sourceTree = ""; }; BFDB69FC22A9A7B7007EA6D6 /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = ""; }; BFDB6A0422A9AFB2007EA6D6 /* Fetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fetchable.swift; sourceTree = ""; }; BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResignAppOperation.swift; sourceTree = ""; }; @@ -855,6 +857,7 @@ BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */, BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */, BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */, + BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */, ); path = Extensions; sourceTree = ""; @@ -1311,6 +1314,7 @@ BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */, BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */, BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */, + BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */, BF770E5622BC3C03002A40FE /* Server.swift in Sources */, BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */, ); diff --git a/AltStore/App Detail/AppContentViewController.swift b/AltStore/App Detail/AppContentViewController.swift index fee28b12..79643687 100644 --- a/AltStore/App Detail/AppContentViewController.swift +++ b/AltStore/App Detail/AppContentViewController.swift @@ -29,9 +29,24 @@ class AppContentViewController: UITableViewController private lazy var screenshotsDataSource = self.makeScreenshotsDataSource() private lazy var permissionsDataSource = self.makePermissionsDataSource() + private lazy var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + return dateFormatter + }() + + private lazy var byteCountFormatter: ByteCountFormatter = { + let formatter = ByteCountFormatter() + return formatter + }() + @IBOutlet private var subtitleLabel: UILabel! @IBOutlet private var descriptionTextView: CollapsingTextView! @IBOutlet private var versionDescriptionTextView: CollapsingTextView! + @IBOutlet private var versionLabel: UILabel! + @IBOutlet private var versionDateLabel: UILabel! + @IBOutlet private var sizeLabel: UILabel! @IBOutlet private var screenshotsCollectionView: UICollectionView! @IBOutlet private var permissionsCollectionView: UICollectionView! @@ -63,6 +78,9 @@ class AppContentViewController: UITableViewController self.subtitleLabel.text = self.app.subtitle self.descriptionTextView.text = self.app.localizedDescription self.versionDescriptionTextView.text = self.app.versionDescription + self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), self.app.version) + self.versionDateLabel.text = Date().relativeDateString(since: self.app.versionDate, dateFormatter: self.dateFormatter) + self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: Int64(self.app.size)) self.descriptionTextView.maximumNumberOfLines = 5 self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered) diff --git a/AltStore/App Detail/AppViewController.swift b/AltStore/App Detail/AppViewController.swift index eaeb11b4..2e0210b1 100644 --- a/AltStore/App Detail/AppViewController.swift +++ b/AltStore/App Detail/AppViewController.swift @@ -18,6 +18,7 @@ class AppViewController: UIViewController private var contentViewControllerShadowView: UIView! private var blurAnimator: UIViewPropertyAnimator? + private var navigationBarAnimator: UIViewPropertyAnimator? private var contentSizeObservation: NSKeyValueObservation? @@ -38,13 +39,21 @@ class AppViewController: UIViewController @IBOutlet private var backgroundAppIconImageView: UIImageView! @IBOutlet private var backgroundBlurView: UIVisualEffectView! - private var _isEnteringForeground = false + @IBOutlet private var navigationBarTitleView: UIView! + @IBOutlet private var navigationBarDownloadButton: PillButton! + @IBOutlet private var navigationBarAppIconImageView: UIImageView! + @IBOutlet private var navigationBarAppNameLabel: UILabel! + + private var _shouldResetLayout = false private var _backgroundBlurEffect: UIBlurEffect? private var _backgroundBlurTintColor: UIColor? override func viewDidLoad() { super.viewDidLoad() + + self.navigationBarTitleView.sizeToFit() + self.navigationItem.titleView = self.navigationBarTitleView self.contentViewControllerShadowView = UIView() self.contentViewControllerShadowView.backgroundColor = .white @@ -75,14 +84,21 @@ class AppViewController: UIViewController self.developerLabel.text = self.app.developerName self.developerLabel.textColor = self.app.tintColor self.appIconImageView.image = UIImage(named: self.app.iconName) + self.appIconImageView.tintColor = self.app.tintColor self.downloadButton.tintColor = self.app.tintColor self.backgroundAppIconImageView.image = UIImage(named: self.app.iconName) self.backButtonContainerView.tintColor = self.app.tintColor - self.contentSizeObservation = self.contentViewController.tableView.observe(\.contentSize) { (tableView, change) in - self.view.setNeedsLayout() - self.view.layoutIfNeeded() + self.navigationController?.navigationBar.tintColor = self.app.tintColor + self.navigationBarDownloadButton.tintColor = self.app.tintColor + self.navigationBarAppNameLabel.text = self.app.name + self.navigationBarAppIconImageView.image = UIImage(named: self.app.iconName) + self.navigationBarAppIconImageView.tintColor = self.app.tintColor + + self.contentSizeObservation = self.contentViewController.tableView.observe(\.contentSize) { [weak self] (tableView, change) in + self?.view.setNeedsLayout() + self?.view.layoutIfNeeded() } self.update() @@ -113,7 +129,9 @@ class AppViewController: UIViewController { super.viewDidAppear(animated) - self.hideNavigationBar() + self._shouldResetLayout = true + self.view.setNeedsLayout() + self.view.layoutIfNeeded() } override func viewWillDisappear(_ animated: Bool) @@ -130,17 +148,23 @@ class AppViewController: UIViewController self.transitionCoordinator?.animate(alongsideTransition: { (context) in self.showNavigationBar(for: navigationController) }, completion: { (context) in - if context.isCancelled - { - self.hideNavigationBar(for: navigationController) - } - else + if !context.isCancelled { self.showNavigationBar(for: navigationController) } }) } + override func viewDidDisappear(_ animated: Bool) + { + super.viewDidDisappear(animated) + + if self.navigationController == nil + { + self.resetNavigationBarAnimation() + } + } + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard segue.identifier == "embedAppContentViewController" else { return } @@ -153,9 +177,9 @@ class AppViewController: UIViewController { super.viewDidLayoutSubviews() - if self._isEnteringForeground + if self._shouldResetLayout { - // Returning from background messes up some of our UI, so reset affected components now. + // Various events can cause UI to mess up, so reset affected components now. if self.navigationController?.topViewController == self { @@ -164,16 +188,20 @@ class AppViewController: UIViewController self.prepareBlur() - self._isEnteringForeground = false + // Reset navigation bar animation, and create a new one later in this method if necessary. + self.resetNavigationBarAnimation() + + self._shouldResetLayout = false } let statusBarHeight = UIApplication.shared.statusBarFrame.height + let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius let inset = 12 as CGFloat let padding = 20 as CGFloat let backButtonSize = self.backButton.sizeThatFits(CGSize(width: 1000, height: 1000)) - let backButtonFrame = CGRect(x: inset, y: statusBarHeight, + var backButtonFrame = CGRect(x: inset, y: statusBarHeight, width: backButtonSize.width + 20, height: backButtonSize.height + 20) var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.headerView.bounds.height) @@ -183,58 +211,80 @@ class AppViewController: UIViewController let minimumHeaderY = backButtonFrame.maxY + 8 let minimumContentY = minimumHeaderY + headerFrame.height + padding - let maximumContentY = self.view.bounds.width * 0.75 + let maximumContentY = self.view.bounds.width * 0.667 // A full blur is too much, so we reduce the visible blur by 0.3, resulting in 70% blur. let minimumBlurFraction = 0.3 as CGFloat - let difference = (maximumContentY - minimumContentY) + contentFrame.origin.y = maximumContentY - self.scrollView.contentOffset.y + headerFrame.origin.y = contentFrame.origin.y - padding - headerFrame.height - if self.scrollView.contentOffset.y > difference + // Stretch the app icon image to fill additional vertical space if necessary. + let height = max(contentFrame.origin.y + cornerRadius * 2, backgroundIconFrame.height) + backgroundIconFrame.size.height = height + + let blurThreshold = 0 as CGFloat + if self.scrollView.contentOffset.y < blurThreshold { - // Full screen + // Determine how much to lessen blur by. - headerFrame.origin.y = minimumHeaderY - contentFrame.origin.y = minimumContentY - backgroundIconFrame.origin.y = 0 + let range = 75 as CGFloat + let difference = -self.scrollView.contentOffset.y - self.contentViewController.tableView.contentOffset.y = self.scrollView.contentOffset.y - difference + let fraction = min(difference, range) / range + + let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction + self.blurAnimator?.fractionComplete = fractionComplete } else { - // Partial screen + // Set blur to default. - contentFrame.origin.y = maximumContentY - self.scrollView.contentOffset.y - headerFrame.origin.y = contentFrame.origin.y - padding - headerFrame.height - - let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius - - // Stretch the app icon image to fill additional vertical space if necessary. - let height = max(contentFrame.origin.y + cornerRadius * 2, backgroundIconFrame.height) - backgroundIconFrame.size.height = height - - // Keep content table view's content offset at the top. - self.contentViewController.tableView.contentOffset.y = 0 - - if self.scrollView.contentOffset.y < 0 - { - // Determine how much to lessen blur by. - - let range = 75 as CGFloat - - let fraction = min(-self.scrollView.contentOffset.y, range) / range - - let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction - self.blurAnimator?.fractionComplete = fractionComplete - } - else - { - // Set blur to default. - - self.blurAnimator?.fractionComplete = minimumBlurFraction - } + self.blurAnimator?.fractionComplete = minimumBlurFraction } + // Animate navigation bar. + let showNavigationBarThreshold = (maximumContentY - minimumContentY) + backButtonFrame.origin.y + if self.scrollView.contentOffset.y > showNavigationBarThreshold + { + if self.navigationBarAnimator == nil + { + self.prepareNavigationBarAnimation() + } + + let difference = self.scrollView.contentOffset.y - showNavigationBarThreshold + let range = (headerFrame.height + padding) - (self.navigationController?.navigationBar.bounds.height ?? self.view.safeAreaInsets.top) + + let fractionComplete = min(difference, range) / range + self.navigationBarAnimator?.fractionComplete = fractionComplete + } + else + { + self.resetNavigationBarAnimation() + } + + let beginMovingBackButtonThreshold = (maximumContentY - minimumContentY) + if self.scrollView.contentOffset.y > beginMovingBackButtonThreshold + { + let difference = self.scrollView.contentOffset.y - beginMovingBackButtonThreshold + backButtonFrame.origin.y -= difference + } + + let pinContentToTopThreshold = maximumContentY + if self.scrollView.contentOffset.y > pinContentToTopThreshold + { + contentFrame.origin.y = 0 + backgroundIconFrame.origin.y = 0 + + let difference = self.scrollView.contentOffset.y - pinContentToTopThreshold + self.contentViewController.tableView.contentOffset.y = difference + } + else + { + // Keep content table view's content offset at the top. + self.contentViewController.tableView.contentOffset.y = 0 + } + // Keep background app icon centered in gap between top of content and top of screen. backgroundIconFrame.origin.y = (contentFrame.origin.y / 2) - backgroundIconFrame.height / 2 @@ -256,7 +306,7 @@ class AppViewController: UIViewController let contentOffset = self.scrollView.contentOffset var contentSize = self.contentViewController.tableView.contentSize - contentSize.height += minimumContentY + self.view.safeAreaInsets.bottom + self.contentViewController.tableView.contentInset.bottom + contentSize.height += maximumContentY self.scrollView.contentSize = contentSize self.scrollView.contentOffset = contentOffset @@ -265,6 +315,7 @@ class AppViewController: UIViewController deinit { self.blurAnimator?.stopAnimation(true) + self.navigationBarAnimator?.stopAnimation(true) } } @@ -272,21 +323,29 @@ private extension AppViewController { func update() { - self.downloadButton.isIndicatingActivity = false - - if self.app.installedApp == nil + for button in [self.downloadButton!, self.navigationBarDownloadButton!] { - self.downloadButton.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal) - self.downloadButton.isInverted = false - } - else - { - self.downloadButton.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal) - self.downloadButton.isInverted = true + button.tintColor = self.app.tintColor + button.isIndicatingActivity = false + + if self.app.installedApp == nil + { + button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal) + button.isInverted = false + } + else + { + button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal) + button.isInverted = true + } + + let progress = AppManager.shared.installationProgress(for: self.app) + button.progress = progress } - let progress = AppManager.shared.installationProgress(for: self.app) - self.downloadButton.progress = progress + let barButtonItem = self.navigationItem.rightBarButtonItem + self.navigationItem.rightBarButtonItem = nil + self.navigationItem.rightBarButtonItem = barButtonItem } func showNavigationBar(for navigationController: UINavigationController? = nil) @@ -294,7 +353,8 @@ private extension AppViewController let navigationController = navigationController ?? self.navigationController navigationController?.navigationBar.barStyle = .default navigationController?.navigationBar.alpha = 1.0 - navigationController?.navigationBar.setBackgroundImage(nil, for: .default) + navigationController?.navigationBar.barTintColor = .white + navigationController?.navigationBar.tintColor = .altGreen } func hideNavigationBar(for navigationController: UINavigationController? = nil) @@ -302,7 +362,7 @@ private extension AppViewController let navigationController = navigationController ?? self.navigationController navigationController?.navigationBar.barStyle = .black navigationController?.navigationBar.alpha = 0.0 - navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + navigationController?.navigationBar.barTintColor = .white } func prepareBlur() @@ -315,14 +375,41 @@ private extension AppViewController self.backgroundBlurView.effect = self._backgroundBlurEffect self.backgroundBlurView.contentView.backgroundColor = self._backgroundBlurTintColor - self.blurAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { - self.backgroundBlurView.effect = nil - self.backgroundBlurView.contentView.backgroundColor = .clear + self.blurAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in + self?.backgroundBlurView.effect = nil + self?.backgroundBlurView.contentView.backgroundColor = .clear } self.blurAnimator?.startAnimation() self.blurAnimator?.pauseAnimation() } + + func prepareNavigationBarAnimation() + { + self.resetNavigationBarAnimation() + + self.navigationBarAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in + self?.showNavigationBar() + self?.navigationController?.navigationBar.tintColor = self?.app.tintColor + self?.navigationController?.navigationBar.barTintColor = nil + self?.contentViewController.view.layer.cornerRadius = 0 + } + + self.navigationBarAnimator?.startAnimation() + self.navigationBarAnimator?.pauseAnimation() + + self.update() + } + + func resetNavigationBarAnimation() + { + self.navigationBarAnimator?.stopAnimation(true) + self.navigationBarAnimator = nil + + self.hideNavigationBar() + self.navigationController?.navigationBar.barTintColor = .white + self.contentViewController.view.layer.cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius + } } extension AppViewController @@ -391,7 +478,7 @@ private extension AppViewController { guard let navigationController = self.navigationController, navigationController.topViewController == self else { return } - self._isEnteringForeground = true + self._shouldResetLayout = true self.view.setNeedsLayout() } } diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard index 0fceedbf..9022cf07 100644 --- a/AltStore/Base.lproj/Main.storyboard +++ b/AltStore/Base.lproj/Main.storyboard @@ -185,6 +185,25 @@ + + + + + + + + + + + + + + @@ -217,7 +236,7 @@ - + @@ -228,16 +247,16 @@ - + + + @@ -319,6 +355,10 @@ + + + + @@ -330,7 +370,7 @@ - + @@ -346,7 +386,7 @@ @@ -400,12 +440,12 @@ - + - + @@ -413,7 +453,7 @@ - + @@ -427,23 +467,56 @@ - - + + - + + + + + + + + + + + + + + + + + + + - + - + @@ -462,7 +535,7 @@ - + @@ -950,7 +1026,7 @@ World - + diff --git a/AltStore/Components/NavigationBar.swift b/AltStore/Components/NavigationBar.swift index ed020d62..f935d808 100644 --- a/AltStore/Components/NavigationBar.swift +++ b/AltStore/Components/NavigationBar.swift @@ -31,4 +31,16 @@ class NavigationBar: UINavigationBar self.barTintColor = .white self.shadowImage = UIImage() } + + override func layoutSubviews() + { + super.layoutSubviews() + + // We can't easily shift just the back button up, so we shift the entire content view slightly. + for contentView in self.subviews + { + guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue } + contentView.center.y -= 2 + } + } } diff --git a/AltStore/Extensions/Date+RelativeDate.swift b/AltStore/Extensions/Date+RelativeDate.swift new file mode 100644 index 00000000..f5d2d77d --- /dev/null +++ b/AltStore/Extensions/Date+RelativeDate.swift @@ -0,0 +1,34 @@ +// +// Date+RelativeDate.swift +// AltStore +// +// Created by Riley Testut on 7/28/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation + +extension Date +{ + func numberOfCalendarDays(since date: Date) -> Int + { + let today = Calendar.current.startOfDay(for: self) + let previousDay = Calendar.current.startOfDay(for: date) + + let components = Calendar.current.dateComponents([.day], from: previousDay, to: today) + return components.day! + } + + func relativeDateString(since date: Date, dateFormatter: DateFormatter) -> String + { + let numberOfDays = self.numberOfCalendarDays(since: date) + + switch numberOfDays + { + case 0: return NSLocalizedString("Today", comment: "") + case 1: return NSLocalizedString("Yesterday", comment: "") + case 2...7: return String(format: NSLocalizedString("%@ days ago", comment: ""), NSNumber(value: numberOfDays)) + default: return dateFormatter.string(from: date) + } + } +} diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index f6795424..21291d2b 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -25,18 +25,6 @@ extension MyAppsViewController } } -private extension Date -{ - func numberOfCalendarDays(since date: Date) -> Int - { - let today = Calendar.current.startOfDay(for: self) - let previousDay = Calendar.current.startOfDay(for: date) - - let components = Calendar.current.dateComponents([.day], from: previousDay, to: today) - return components.day! - } -} - class MyAppsViewController: UICollectionViewController { private lazy var dataSource = self.makeDataSource() @@ -190,16 +178,7 @@ private extension MyAppsViewController let progress = AppManager.shared.installationProgress(for: app) cell.updateButton.progress = progress - cell.dateLabel.text = self.dateFormatter.string(from: app.versionDate) - - let numberOfDays = Date().numberOfCalendarDays(since: app.versionDate) - switch numberOfDays - { - case 0: cell.dateLabel.text = NSLocalizedString("Today", comment: "") - case 1: cell.dateLabel.text = NSLocalizedString("Yesterday", comment: "") - case 2...7: cell.dateLabel.text = String(format: NSLocalizedString("%@ days ago", comment: ""), NSNumber(value: numberOfDays)) - default: cell.dateLabel.text = self.dateFormatter.string(from: app.versionDate) - } + cell.dateLabel.text = Date().relativeDateString(since: app.versionDate, dateFormatter: self.dateFormatter) cell.setNeedsLayout() }