SettingsViewModel.swift 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. //
  2. // SettingsViewModel.swift
  3. // Mastodon
  4. //
  5. // Created by ihugo on 2021/4/7.
  6. //
  7. import Combine
  8. import CoreData
  9. import CoreDataStack
  10. import Foundation
  11. import MastodonSDK
  12. import UIKit
  13. import os.log
  14. import AuthenticationServices
  15. import MastodonCore
  16. class SettingsViewModel {
  17. var disposeBag = Set<AnyCancellable>()
  18. // input
  19. let context: AppContext
  20. let authContext: AuthContext
  21. var mastodonAuthenticationController: MastodonAuthenticationController?
  22. let setting: CurrentValueSubject<Setting, Never>
  23. var updateDisposeBag = Set<AnyCancellable>()
  24. var createDisposeBag = Set<AnyCancellable>()
  25. let viewDidLoad = PassthroughSubject<Void, Never>()
  26. // output
  27. var dataSource: UITableViewDiffableDataSource<SettingsSection, SettingsItem>!
  28. /// create a subscription when:
  29. /// - does not has one
  30. /// - does not find subscription for selected trigger when change trigger
  31. let createSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
  32. let currentInstance = CurrentValueSubject<Mastodon.Entity.Instance?, Never>(nil)
  33. /// update a subscription when:
  34. /// - change switch for specified alerts
  35. let updateSubscriptionSubject = PassthroughSubject<(triggerBy: String, values: [Bool?]), Never>()
  36. lazy var privacyURL: URL? = {
  37. let domain = authContext.mastodonAuthenticationBox.domain
  38. return Mastodon.API.privacyURL(domain: domain)
  39. }()
  40. init(context: AppContext, authContext: AuthContext, setting: Setting) {
  41. self.context = context
  42. self.authContext = authContext
  43. self.setting = CurrentValueSubject(setting)
  44. self.setting
  45. .sink(receiveValue: { [weak self] setting in
  46. guard let self = self else { return }
  47. self.processDataSource(setting)
  48. })
  49. .store(in: &disposeBag)
  50. context.apiService.instance(domain: authContext.mastodonAuthenticationBox.domain)
  51. .sink { [weak self] completion in
  52. guard let self = self else { return }
  53. switch completion {
  54. case .failure(let error):
  55. os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch instance fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
  56. self.currentInstance.value = nil
  57. case .finished:
  58. os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch instance success", ((#file as NSString).lastPathComponent), #line, #function)
  59. }
  60. } receiveValue: { [weak self] response in
  61. guard let self = self else { return }
  62. self.currentInstance.value = response.value
  63. }
  64. .store(in: &disposeBag)
  65. }
  66. deinit {
  67. os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
  68. }
  69. }
  70. extension SettingsViewModel {
  71. func openAuthenticationPage(
  72. authenticateURL: URL,
  73. presentationContextProvider: ASWebAuthenticationPresentationContextProviding
  74. ) {
  75. let authenticationController = MastodonAuthenticationController(
  76. context: self.context,
  77. authenticateURL: authenticateURL
  78. )
  79. self.mastodonAuthenticationController = authenticationController
  80. authenticationController.authenticationSession?.presentationContextProvider = presentationContextProvider
  81. authenticationController.authenticationSession?.start()
  82. }
  83. // MARK: - Private methods
  84. private func processDataSource(_ setting: Setting) {
  85. guard let dataSource = self.dataSource else { return }
  86. var snapshot = NSDiffableDataSourceSnapshot<SettingsSection, SettingsItem>()
  87. // appearance
  88. let appearanceItems = [
  89. SettingsItem.appearance(record: .init(objectID: setting.objectID))
  90. ]
  91. snapshot.appendSections([.appearance])
  92. snapshot.appendItems(appearanceItems, toSection: .appearance)
  93. // appearancePreference
  94. snapshot.appendSections([.appearancePreference])
  95. snapshot.appendItems([SettingsItem.appearancePreference(record: .init(objectID: setting.objectID), appearanceType: .preferredTrueDarkMode)], toSection: .appearancePreference)
  96. // preference
  97. snapshot.appendSections([.preference])
  98. let preferenceItems: [SettingsItem] = SettingsItem.PreferenceType.allCases.map { preferenceType in
  99. SettingsItem.preference(settingRecord: .init(objectID: setting.objectID), preferenceType: preferenceType)
  100. }
  101. snapshot.appendItems(preferenceItems,toSection: .preference)
  102. // notification
  103. let notificationItems = SettingsItem.NotificationSwitchMode.allCases.map { mode in
  104. SettingsItem.notification(settingRecord: .init(objectID: setting.objectID), switchMode: mode)
  105. }
  106. snapshot.appendSections([.notifications])
  107. snapshot.appendItems(notificationItems, toSection: .notifications)
  108. // boring zone
  109. let boringZoneSettingsItems: [SettingsItem] = {
  110. let links: [SettingsItem.Link] = [
  111. .accountSettings,
  112. .github,
  113. .termsOfService,
  114. .privacyPolicy
  115. ]
  116. let items = links.map { SettingsItem.boringZone(item: $0) }
  117. return items
  118. }()
  119. snapshot.appendSections([.boringZone])
  120. snapshot.appendItems(boringZoneSettingsItems, toSection: .boringZone)
  121. let spicyZoneSettingsItems: [SettingsItem] = {
  122. let links: [SettingsItem.Link] = [
  123. .clearMediaCache,
  124. .signOut
  125. ]
  126. let items = links.map { SettingsItem.spicyZone(item: $0) }
  127. return items
  128. }()
  129. snapshot.appendSections([.spicyZone])
  130. snapshot.appendItems(spicyZoneSettingsItems, toSection: .spicyZone)
  131. dataSource.apply(snapshot, animatingDifferences: false)
  132. }
  133. }
  134. extension SettingsViewModel {
  135. func setupDiffableDataSource(
  136. for tableView: UITableView,
  137. settingsAppearanceTableViewCellDelegate: SettingsAppearanceTableViewCellDelegate,
  138. settingsToggleCellDelegate: SettingsToggleCellDelegate
  139. ) {
  140. dataSource = SettingsSection.tableViewDiffableDataSource(
  141. for: tableView,
  142. managedObjectContext: context.managedObjectContext,
  143. settingsAppearanceTableViewCellDelegate: settingsAppearanceTableViewCellDelegate,
  144. settingsToggleCellDelegate: settingsToggleCellDelegate
  145. )
  146. processDataSource(self.setting.value)
  147. }
  148. }