AuthenticationViewController.swift 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. //
  2. // AuthenticationViewController.swift
  3. // Loop
  4. //
  5. // Created by Nate Racklyeft on 7/2/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import UIKit
  9. import LoopKit
  10. public final class AuthenticationViewController<T: ServiceAuthenticationUI>: UITableViewController, UITextFieldDelegate {
  11. public typealias AuthenticationObserver = (_ authentication: T) -> Void
  12. public var authenticationObserver: AuthenticationObserver?
  13. public let authentication: T
  14. private var state: AuthenticationState = .empty {
  15. didSet {
  16. switch (oldValue, state) {
  17. case let (x, y) where x == y:
  18. break
  19. case (_, .verifying):
  20. let titleView = ValidatingIndicatorView(frame: CGRect.zero)
  21. UIView.animate(withDuration: 0.25, animations: {
  22. self.navigationItem.hidesBackButton = true
  23. self.navigationItem.titleView = titleView
  24. })
  25. tableView.reloadSections(IndexSet(integersIn: 0...1), with: .automatic)
  26. authentication.verify { (success, error) in
  27. DispatchQueue.main.async {
  28. UIView.animate(withDuration: 0.25, animations: {
  29. self.navigationItem.titleView = nil
  30. self.navigationItem.hidesBackButton = false
  31. })
  32. if let error = error {
  33. let alert = UIAlertController(with: error)
  34. self.present(alert, animated: true)
  35. }
  36. if success {
  37. self.state = .authorized
  38. } else {
  39. self.state = .unauthorized
  40. }
  41. }
  42. }
  43. case (_, .authorized), (_, .unauthorized):
  44. authentication.isAuthorized = (state == .authorized)
  45. authenticationObserver?(authentication)
  46. tableView.reloadSections(IndexSet(integersIn: 0...1), with: .automatic)
  47. default:
  48. break
  49. }
  50. }
  51. }
  52. var credentials: [(field: ServiceCredential, value: String?)] {
  53. switch state {
  54. case .authorized:
  55. return authentication.credentials.filter({ !$0.field.isSecret })
  56. default:
  57. return authentication.credentials
  58. }
  59. }
  60. public init(authentication: T) {
  61. self.authentication = authentication
  62. state = authentication.isAuthorized ? .authorized : .unauthorized
  63. super.init(style: .grouped)
  64. title = authentication.title
  65. }
  66. required public init?(coder aDecoder: NSCoder) {
  67. fatalError("init(coder:) has not been implemented")
  68. }
  69. override public func viewDidLoad() {
  70. super.viewDidLoad()
  71. tableView.register(AuthenticationTableViewCell.nib(), forCellReuseIdentifier: AuthenticationTableViewCell.className)
  72. tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
  73. }
  74. // MARK: - Table view data source
  75. override public func numberOfSections(in tableView: UITableView) -> Int {
  76. return Section.count
  77. }
  78. override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  79. switch Section(rawValue: section)! {
  80. case .credentials:
  81. return credentials.count
  82. case .button:
  83. return 1
  84. }
  85. }
  86. override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  87. switch Section(rawValue: indexPath.section)! {
  88. case .button:
  89. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  90. cell.textLabel?.textAlignment = .center
  91. switch state {
  92. case .authorized:
  93. cell.textLabel?.text = LocalizedString("Delete Account", comment: "The title of the button to remove the credentials for a service")
  94. cell.tintColor = .delete
  95. case .empty, .unauthorized, .verifying:
  96. cell.textLabel?.text = LocalizedString("Add Account", comment: "The title of the button to add the credentials for a service")
  97. cell.tintColor = nil
  98. }
  99. if case .verifying = state {
  100. cell.isEnabled = false
  101. } else {
  102. cell.isEnabled = true
  103. }
  104. return cell
  105. case .credentials:
  106. let cell = tableView.dequeueReusableCell(withIdentifier: AuthenticationTableViewCell.className, for: indexPath) as! AuthenticationTableViewCell
  107. let credentials = self.credentials
  108. let credential = credentials[indexPath.row]
  109. cell.titleLabel.text = credential.field.title
  110. cell.textField.keyboardType = credential.field.keyboardType
  111. cell.textField.isSecureTextEntry = credential.field.isSecret
  112. cell.textField.returnKeyType = (indexPath.row < credentials.count - 1) ? .next : .done
  113. cell.textField.text = credential.value
  114. cell.textField.placeholder = credential.field.placeholder ?? LocalizedString("Required", comment: "The default placeholder string for a credential")
  115. if let options = credential.field.options {
  116. let picker = CredentialOptionPicker(options: options)
  117. picker.value = credential.value
  118. authentication.credentialValues[indexPath.row] = credential.value ?? options.first?.value
  119. cell.credentialOptionPicker = picker
  120. }
  121. cell.textField.delegate = self
  122. switch state {
  123. case .authorized, .verifying, .empty:
  124. cell.textField.isEnabled = false
  125. case .unauthorized:
  126. cell.textField.isEnabled = true
  127. }
  128. return cell
  129. }
  130. }
  131. public override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
  132. switch Section(rawValue: indexPath.section)! {
  133. case .credentials:
  134. return false
  135. case .button:
  136. return true
  137. }
  138. }
  139. public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  140. switch Section(rawValue: indexPath.section)! {
  141. case .credentials:
  142. break
  143. case .button:
  144. switch state {
  145. case .authorized:
  146. tableView.endEditing(false)
  147. authentication.resetCredentials()
  148. state = .unauthorized
  149. case .unauthorized:
  150. tableView.endEditing(false)
  151. validate()
  152. case .verifying:
  153. break
  154. case .empty:
  155. break
  156. }
  157. }
  158. tableView.deselectRow(at: indexPath, animated: true)
  159. }
  160. fileprivate func validate() {
  161. state = .verifying
  162. }
  163. // MARK: - Actions
  164. // MARK: - UITextFieldDelegate
  165. public func textFieldDidEndEditing(_ textField: UITextField) {
  166. let point = tableView.convert(textField.frame.origin, from: textField.superview)
  167. guard case .unauthorized = state,
  168. let indexPath = tableView.indexPathForRow(at: point),
  169. let cell = tableView.cellForRow(at: IndexPath(row: indexPath.row, section: indexPath.section)) as? AuthenticationTableViewCell
  170. else {
  171. return
  172. }
  173. authentication.credentialValues[indexPath.row] = cell.value
  174. }
  175. public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  176. if textField.returnKeyType == .done {
  177. textField.resignFirstResponder()
  178. validate()
  179. } else {
  180. let point = tableView.convert(textField.frame.origin, from: textField.superview)
  181. if let indexPath = tableView.indexPathForRow(at: point),
  182. let cell = tableView.cellForRow(at: IndexPath(row: indexPath.row + 1, section: indexPath.section)) as? AuthenticationTableViewCell
  183. {
  184. cell.textField.becomeFirstResponder()
  185. }
  186. }
  187. return true
  188. }
  189. public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
  190. return textField.inputView == nil
  191. }
  192. }
  193. private enum Section: Int {
  194. case credentials
  195. case button
  196. static let count = 2
  197. }
  198. private enum AuthenticationState {
  199. case empty
  200. case authorized
  201. case verifying
  202. case unauthorized
  203. }