AuthenticationViewController.swift 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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. return authentication.credentials
  54. }
  55. public init(authentication: T) {
  56. self.authentication = authentication
  57. state = authentication.isAuthorized ? .authorized : .unauthorized
  58. super.init(style: .grouped)
  59. title = authentication.title
  60. }
  61. required public init?(coder aDecoder: NSCoder) {
  62. fatalError("init(coder:) has not been implemented")
  63. }
  64. override public func viewDidLoad() {
  65. super.viewDidLoad()
  66. tableView.register(AuthenticationTableViewCell.nib(), forCellReuseIdentifier: AuthenticationTableViewCell.className)
  67. tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className)
  68. }
  69. // MARK: - Table view data source
  70. override public func numberOfSections(in tableView: UITableView) -> Int {
  71. return Section.count
  72. }
  73. override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  74. switch Section(rawValue: section)! {
  75. case .credentials:
  76. return credentials.count
  77. case .button:
  78. return 1
  79. }
  80. }
  81. override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  82. switch Section(rawValue: indexPath.section)! {
  83. case .button:
  84. let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell
  85. cell.textLabel?.textAlignment = .center
  86. switch state {
  87. case .authorized:
  88. cell.textLabel?.text = LocalizedString("Delete", comment: "The title of the button to remove the credentials for a service")
  89. cell.tintColor = .systemRed
  90. case .empty, .unauthorized, .verifying:
  91. cell.textLabel?.text = LocalizedString("Add Account", comment: "The title of the button to add the credentials for a service")
  92. cell.tintColor = nil
  93. }
  94. if case .verifying = state {
  95. cell.isEnabled = false
  96. } else {
  97. cell.isEnabled = true
  98. }
  99. return cell
  100. case .credentials:
  101. let cell = tableView.dequeueReusableCell(withIdentifier: AuthenticationTableViewCell.className, for: indexPath) as! AuthenticationTableViewCell
  102. let credentials = self.credentials
  103. let credential = credentials[indexPath.row]
  104. cell.titleLabel.text = credential.field.title
  105. cell.textField.keyboardType = credential.field.keyboardType
  106. cell.textField.isSecureTextEntry = credential.field.isSecret
  107. cell.textField.returnKeyType = (indexPath.row < credentials.count - 1) ? .next : .done
  108. cell.textField.text = credential.value
  109. cell.textField.placeholder = credential.field.placeholder ?? LocalizedString("Required", comment: "The default placeholder string for a credential")
  110. if let options = credential.field.options {
  111. let picker = CredentialOptionPicker(options: options)
  112. picker.value = credential.value
  113. authentication.credentialValues[indexPath.row] = credential.value ?? options.first?.value
  114. cell.credentialOptionPicker = picker
  115. }
  116. cell.textField.delegate = self
  117. switch state {
  118. case .authorized, .verifying, .empty:
  119. cell.textField.isEnabled = false
  120. case .unauthorized:
  121. cell.textField.isEnabled = true
  122. }
  123. return cell
  124. }
  125. }
  126. public override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
  127. switch Section(rawValue: section)! {
  128. case .credentials:
  129. return authentication.credentialFormFieldHelperMessage
  130. case .button:
  131. return nil
  132. }
  133. }
  134. // MARK: - Table view delegate
  135. public override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
  136. switch Section(rawValue: indexPath.section)! {
  137. case .credentials:
  138. return false
  139. case .button:
  140. return true
  141. }
  142. }
  143. public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  144. switch Section(rawValue: indexPath.section)! {
  145. case .credentials:
  146. break
  147. case .button:
  148. switch state {
  149. case .authorized:
  150. tableView.endEditing(false)
  151. authentication.resetCredentials()
  152. state = .unauthorized
  153. case .unauthorized:
  154. tableView.endEditing(false)
  155. validate()
  156. case .verifying:
  157. break
  158. case .empty:
  159. break
  160. }
  161. }
  162. tableView.deselectRow(at: indexPath, animated: true)
  163. }
  164. fileprivate func validate() {
  165. state = .verifying
  166. }
  167. // MARK: - Actions
  168. // MARK: - UITextFieldDelegate
  169. public func textFieldDidEndEditing(_ textField: UITextField) {
  170. let point = tableView.convert(textField.frame.origin, from: textField.superview)
  171. guard case .unauthorized = state,
  172. let indexPath = tableView.indexPathForRow(at: point),
  173. let cell = tableView.cellForRow(at: IndexPath(row: indexPath.row, section: indexPath.section)) as? AuthenticationTableViewCell
  174. else {
  175. return
  176. }
  177. authentication.credentialValues[indexPath.row] = cell.value
  178. }
  179. public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  180. if textField.returnKeyType == .done {
  181. textField.resignFirstResponder()
  182. validate()
  183. } else {
  184. let point = tableView.convert(textField.frame.origin, from: textField.superview)
  185. if let indexPath = tableView.indexPathForRow(at: point),
  186. let cell = tableView.cellForRow(at: IndexPath(row: indexPath.row + 1, section: indexPath.section)) as? AuthenticationTableViewCell
  187. {
  188. cell.textField.becomeFirstResponder()
  189. }
  190. }
  191. return true
  192. }
  193. public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
  194. return textField.inputView == nil
  195. }
  196. }
  197. private enum Section: Int {
  198. case credentials
  199. case button
  200. static let count = 2
  201. }
  202. private enum AuthenticationState {
  203. case empty
  204. case authorized
  205. case verifying
  206. case unauthorized
  207. }