|
| 1 | +import Foundation |
| 2 | +import UIKit |
| 3 | +import Yosemite |
| 4 | + |
| 5 | + |
| 6 | +// MARK: - OrderLoaderViewController: Loads asynchronously an Order (given it's OrderID + SiteID). |
| 7 | +// On Success the OrderDetailsViewController will be rendered "in place". |
| 8 | +// |
| 9 | +class OrderLoaderViewController: UIViewController { |
| 10 | + |
| 11 | + /// UI Spinner |
| 12 | + /// |
| 13 | + private let activityIndicator = UIActivityIndicatorView(style: .gray) |
| 14 | + |
| 15 | + /// Target OrderID |
| 16 | + /// |
| 17 | + private let orderID: Int |
| 18 | + |
| 19 | + /// Target Order's SiteID |
| 20 | + /// |
| 21 | + private let siteID: Int |
| 22 | + |
| 23 | + /// UI Active State |
| 24 | + /// |
| 25 | + private var state: State = .loading { |
| 26 | + didSet { |
| 27 | + didLeave(state: oldValue) |
| 28 | + didEnter(state: state) |
| 29 | + } |
| 30 | + } |
| 31 | + |
| 32 | + |
| 33 | + // MARK: - Initializers |
| 34 | + |
| 35 | + init(orderID: Int, siteID: Int) { |
| 36 | + self.orderID = orderID |
| 37 | + self.siteID = siteID |
| 38 | + |
| 39 | + super.init(nibName: nil, bundle: nil) |
| 40 | + } |
| 41 | + |
| 42 | + required init?(coder aDecoder: NSCoder) { |
| 43 | + fatalError("Please specify the OrderID and SiteID!") |
| 44 | + } |
| 45 | + |
| 46 | + |
| 47 | + // MARK: - Overridden Methods |
| 48 | + |
| 49 | + override func viewDidLoad() { |
| 50 | + super.viewDidLoad() |
| 51 | + |
| 52 | + configureNavigationItem() |
| 53 | + configureSpinner() |
| 54 | + configureMainView() |
| 55 | + } |
| 56 | + |
| 57 | + override func viewWillAppear(_ animated: Bool) { |
| 58 | + super.viewWillAppear(animated) |
| 59 | + reloadOrder() |
| 60 | + } |
| 61 | +} |
| 62 | + |
| 63 | + |
| 64 | +// MARK: - Actions |
| 65 | +// |
| 66 | +private extension OrderLoaderViewController { |
| 67 | + |
| 68 | + /// Loads (and displays) the specified Order. |
| 69 | + /// |
| 70 | + func reloadOrder() { |
| 71 | + let action = OrderAction.retrieveOrder(siteID: siteID, orderID: orderID) { [weak self] (order, error) in |
| 72 | + guard let `self` = self else { |
| 73 | + return |
| 74 | + } |
| 75 | + |
| 76 | + guard let order = order else { |
| 77 | + DDLogError("## Error loading Order \(self.siteID).\(self.orderID): \(error.debugDescription)") |
| 78 | + self.state = .failure |
| 79 | + return |
| 80 | + } |
| 81 | + |
| 82 | + self.state = .success(order: order) |
| 83 | + } |
| 84 | + |
| 85 | + state = .loading |
| 86 | + StoresManager.shared.dispatch(action) |
| 87 | + } |
| 88 | +} |
| 89 | + |
| 90 | + |
| 91 | +// MARK: - Configuration |
| 92 | +// |
| 93 | +private extension OrderLoaderViewController { |
| 94 | + |
| 95 | + /// Setup: Navigation |
| 96 | + /// |
| 97 | + func configureNavigationItem() { |
| 98 | + title = NSLocalizedString("Loading Order", comment: "Displayed when an Order is being retrieved") |
| 99 | + navigationItem.backBarButtonItem = UIBarButtonItem(title: String(), style: .plain, target: nil, action: nil) |
| 100 | + } |
| 101 | + |
| 102 | + /// Setup: Main View |
| 103 | + /// |
| 104 | + func configureMainView() { |
| 105 | + view.backgroundColor = StyleManager.tableViewBackgroundColor |
| 106 | + view.addSubview(activityIndicator) |
| 107 | + view.pinSubviewAtCenter(activityIndicator) |
| 108 | + } |
| 109 | + |
| 110 | + /// Setup: Spinner |
| 111 | + /// |
| 112 | + func configureSpinner() { |
| 113 | + activityIndicator.hidesWhenStopped = true |
| 114 | + activityIndicator.translatesAutoresizingMaskIntoConstraints = false |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | + |
| 119 | +// MARK: - Overlays |
| 120 | +// |
| 121 | +private extension OrderLoaderViewController { |
| 122 | + |
| 123 | + /// Starts the Spinner |
| 124 | + /// |
| 125 | + func startSpinner() { |
| 126 | + activityIndicator.startAnimating() |
| 127 | + } |
| 128 | + |
| 129 | + /// Stops the Spinner |
| 130 | + /// |
| 131 | + func stopSpinner() { |
| 132 | + activityIndicator.stopAnimating() |
| 133 | + } |
| 134 | + |
| 135 | + /// Displays the Loading Overlay. |
| 136 | + /// |
| 137 | + func displayFailureOverlay() { |
| 138 | + let overlayView: OverlayMessageView = OverlayMessageView.instantiateFromNib() |
| 139 | + overlayView.messageImage = .waitingForCustomersImage |
| 140 | + overlayView.messageText = NSLocalizedString("The Order couldn't be loaded!", comment: "Fetching an Order Failed") |
| 141 | + overlayView.actionText = NSLocalizedString("Retry", comment: "Retry the last action") |
| 142 | + overlayView.onAction = { [weak self] in |
| 143 | + self?.reloadOrder() |
| 144 | + } |
| 145 | + |
| 146 | + overlayView.attach(to: view) |
| 147 | + } |
| 148 | + |
| 149 | + /// Removes all of the the OverlayMessageView instances in the view hierarchy. |
| 150 | + /// |
| 151 | + func removeAllOverlays() { |
| 152 | + for subview in view.subviews where subview is OverlayMessageView { |
| 153 | + subview.removeFromSuperview() |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + /// Presents the OrderDetailsViewController, as a childViewController, for a given Order. |
| 158 | + /// |
| 159 | + func presentOrderDetails(for order: Order) { |
| 160 | + let identifier = OrderDetailsViewController.classNameWithoutNamespaces |
| 161 | + guard let detailsViewController = UIStoryboard.orders.instantiateViewController(withIdentifier: identifier) as? OrderDetailsViewController else { |
| 162 | + fatalError() |
| 163 | + } |
| 164 | + |
| 165 | + // Setup the DetailsViewController |
| 166 | + detailsViewController.viewModel = OrderDetailsViewModel(order: order) |
| 167 | + |
| 168 | + // Attach |
| 169 | + addChild(detailsViewController) |
| 170 | + attachSubview(detailsViewController.view) |
| 171 | + detailsViewController.didMove(toParent: self) |
| 172 | + |
| 173 | + // And, of course, borrow the Child's Title |
| 174 | + title = detailsViewController.title |
| 175 | + } |
| 176 | + |
| 177 | + /// Removes all of the children UIViewControllers |
| 178 | + /// |
| 179 | + func detachChildrenViewControllers() { |
| 180 | + for child in children { |
| 181 | + child.view.removeFromSuperview() |
| 182 | + child.removeFromParent() |
| 183 | + child.didMove(toParent: nil) |
| 184 | + } |
| 185 | + } |
| 186 | +} |
| 187 | + |
| 188 | + |
| 189 | +// MARK: - UI Methods |
| 190 | +// |
| 191 | +private extension OrderLoaderViewController { |
| 192 | + |
| 193 | + /// Attaches a given Subview, and ensures it's pinned to all the edges |
| 194 | + /// |
| 195 | + func attachSubview(_ subview: UIView) { |
| 196 | + subview.translatesAutoresizingMaskIntoConstraints = false |
| 197 | + view.addSubview(subview) |
| 198 | + view.pinSubviewToAllEdges(subview) |
| 199 | + } |
| 200 | +} |
| 201 | + |
| 202 | + |
| 203 | +// MARK: - Finite State Machine Management |
| 204 | +// |
| 205 | +private extension OrderLoaderViewController { |
| 206 | + |
| 207 | + /// Runs whenever the FSM enters a State. |
| 208 | + /// |
| 209 | + func didEnter(state: State) { |
| 210 | + switch state { |
| 211 | + case .loading: |
| 212 | + startSpinner() |
| 213 | + case .success(let order): |
| 214 | + presentOrderDetails(for: order) |
| 215 | + case .failure: |
| 216 | + displayFailureOverlay() |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + /// Runs whenever the FSM leaves a State. |
| 221 | + /// |
| 222 | + func didLeave(state: State) { |
| 223 | + switch state { |
| 224 | + case .loading: |
| 225 | + stopSpinner() |
| 226 | + case .success(_): |
| 227 | + detachChildrenViewControllers() |
| 228 | + case .failure: |
| 229 | + removeAllOverlays() |
| 230 | + } |
| 231 | + } |
| 232 | +} |
| 233 | + |
| 234 | + |
| 235 | +// MARK: - OrderLoader Possible Status(es) |
| 236 | +// |
| 237 | +private enum State { |
| 238 | + case loading |
| 239 | + case success(order: Order) |
| 240 | + case failure |
| 241 | +} |
0 commit comments