SoundsViewModel.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. //
  2. // SoundsViewModel.swift
  3. // Bark
  4. //
  5. // Created by huangfeng on 2020/11/17.
  6. // Copyright © 2020 Fin. All rights reserved.
  7. //
  8. import AVKit
  9. import Foundation
  10. import RxCocoa
  11. import RxDataSources
  12. import RxSwift
  13. enum SoundItem {
  14. case sound(model: SoundCellViewModel)
  15. case addSound
  16. }
  17. class SoundsViewModel: ViewModel, ViewModelType {
  18. /// 依赖
  19. struct Dependencies {
  20. /// 用于保存铃声文件
  21. let soundFileStorage: SoundFileStorageProtocol
  22. }
  23. private let dependencies: Dependencies
  24. init(dependencies: Dependencies = Dependencies(soundFileStorage: SoundFileStorage())) {
  25. self.dependencies = dependencies
  26. }
  27. struct Input {
  28. /// 铃声列表点击
  29. var soundSelected: Driver<SoundItem>
  30. /// 铃声导入
  31. var importSound: Driver<URL>
  32. /// 删除铃声
  33. var soundDeleted: Driver<SoundItem>
  34. }
  35. struct Output {
  36. /// 铃声数据源
  37. var audios: Observable<[SectionModel<String, SoundItem>]>
  38. /// 复制铃声名称
  39. var copyNameAction: Driver<String>
  40. /// 播放铃声
  41. var playAction: Driver<CFURL>
  42. /// 打开文件选择器选择铃声文件
  43. var pickerFile: Driver<Void>
  44. }
  45. /// 将铃声 URL 转换成 SoundItem
  46. func getSounds(urls: [URL]) -> [SoundItem] {
  47. let urls = urls.sorted { u1, u2 -> Bool in
  48. u1.lastPathComponent.localizedStandardCompare(u2.lastPathComponent) == ComparisonResult.orderedAscending
  49. }
  50. return urls
  51. .map { AVURLAsset(url: $0) }
  52. .map { SoundCellViewModel(model: $0) }
  53. .map { SoundItem.sound(model: $0) }
  54. }
  55. /// 返回指定文件夹,指定后缀的文件列表数组
  56. func getFilesInDirectory(directory: String, suffix: String) -> [URL] {
  57. let fileManager = FileManager.default
  58. do {
  59. let files = try fileManager.contentsOfDirectory(atPath: directory)
  60. return files.compactMap { file -> URL? in
  61. if file.hasSuffix(suffix) {
  62. return URL(fileURLWithPath: directory).appendingPathComponent(file)
  63. }
  64. return nil
  65. }
  66. } catch {
  67. return []
  68. }
  69. }
  70. func transform(input: Input) -> Output {
  71. // 保存文件
  72. input
  73. .importSound
  74. .drive { [unowned self] url in
  75. self.dependencies.soundFileStorage.saveSound(url: url)
  76. }
  77. .disposed(by: rx.disposeBag)
  78. // 删除铃声
  79. input.soundDeleted.drive(onNext: { item in
  80. guard case SoundItem.sound(let model) = item else {
  81. return
  82. }
  83. self.dependencies.soundFileStorage.deleteSound(url: model.model.url)
  84. }).disposed(by: rx.disposeBag)
  85. // 铃声列表有更新
  86. let soundsListUpdated = Observable.merge(
  87. // 刚进页面
  88. Observable.just(()),
  89. // 上传了新铃声
  90. input.importSound.map { _ in () }.asObservable(),
  91. // 删除了铃声
  92. input.soundDeleted.map { _ in () }.asObservable()
  93. ).share(replay: 1)
  94. // 所有铃声列表,包含自定义铃声和默认铃声
  95. let sounds: Observable<([SoundItem], [SoundItem])> = soundsListUpdated.map { _ in
  96. let defaultSounds = self.getSounds(
  97. urls: Bundle.main.urls(forResourcesWithExtension: "caf", subdirectory: nil) ?? []
  98. )
  99. let customSounds: [SoundItem] = {
  100. guard let soundsDirectoryUrl = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first?.appending("/Sounds") else {
  101. return [.addSound]
  102. }
  103. return self.getSounds(
  104. urls: self.getFilesInDirectory(directory: soundsDirectoryUrl, suffix: "caf")
  105. ) + [.addSound]
  106. }()
  107. return (customSounds, defaultSounds)
  108. }.share(replay: 1)
  109. // 用于 RxDataSource 的数据源
  110. let dataSource = sounds.map { sounds in
  111. return [
  112. SectionModel(model: "customSounds", items: sounds.0),
  113. SectionModel(model: "defaultSounds", items: sounds.1)
  114. ]
  115. }
  116. // 铃声列表点击复制按钮事件
  117. let copyAction = sounds.flatMapLatest { sounds in
  118. let observables = (sounds.0 + sounds.1).compactMap { item in
  119. if case SoundItem.sound(let model) = item {
  120. return model
  121. }
  122. return nil
  123. }.map { model in
  124. return model.copyNameAction.asObservable()
  125. }
  126. return Observable.merge(observables)
  127. }.asDriver(onErrorDriveWith: .empty())
  128. return Output(
  129. audios: dataSource,
  130. copyNameAction: copyAction,
  131. playAction: input.soundSelected
  132. .compactMap { item in
  133. if case SoundItem.sound(let model) = item {
  134. return model
  135. }
  136. return nil
  137. }
  138. .map { $0.model.url as CFURL },
  139. pickerFile: input.soundSelected
  140. .compactMap { item in
  141. if case SoundItem.addSound = item {
  142. return ()
  143. }
  144. return nil
  145. }
  146. )
  147. }
  148. }
  149. /// 保存铃声文件协议
  150. protocol SoundFileStorageProtocol {
  151. func saveSound(url: URL)
  152. func deleteSound(url: URL)
  153. }
  154. /// 用于将铃声文件保存在 /Library/Sounds 文件夹中
  155. class SoundFileStorage: SoundFileStorageProtocol {
  156. let fileManager: FileManager
  157. init() {
  158. fileManager = FileManager()
  159. }
  160. /// 将指定文件保存在 Library/Sound,如果存在则覆盖
  161. func saveSound(url: URL) {
  162. // 保存到Sounds文件夹
  163. let soundsDirectoryUrl = getSoundsDirectory()
  164. let soundUrl = soundsDirectoryUrl.appendingPathComponent(url.lastPathComponent)
  165. try? fileManager.copyItem(at: url, to: soundUrl)
  166. // 另外复制一份到共享目录
  167. saveSoundToGroupDirectory(url: url)
  168. }
  169. func deleteSound(url: URL) {
  170. // 删除sounds目录铃声文件
  171. try? fileManager.removeItem(at: url)
  172. // 再删除共享目录中对应的铃声文件
  173. if let groupSoundUrl = getSoundsGroupDirectory()?.appendingPathComponent(url.lastPathComponent) {
  174. try? fileManager.removeItem(at: groupSoundUrl)
  175. }
  176. }
  177. /// 获取 Library 目录下的 Sounds 文件夹
  178. /// 如果不存在就创建
  179. private func getSoundsDirectory() -> URL {
  180. let soundsDirectoryUrl = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).first!.appending("/Sounds")
  181. if !fileManager.fileExists(atPath: soundsDirectoryUrl) {
  182. try? fileManager.createDirectory(atPath: soundsDirectoryUrl, withIntermediateDirectories: true, attributes: nil)
  183. }
  184. return URL(fileURLWithPath: soundsDirectoryUrl)
  185. }
  186. /// 保存到共享文件夹,供 NotificationServiceExtension 使用(例如持续响铃需要拿到这个文件)
  187. private func saveSoundToGroupDirectory(url: URL) {
  188. guard let groupUrl = getSoundsGroupDirectory() else {
  189. return
  190. }
  191. let soundUrl = groupUrl.appendingPathComponent(url.lastPathComponent)
  192. try? fileManager.copyItem(at: url, to: soundUrl)
  193. }
  194. /// 获取共享目录下的 Sounds 文件夹
  195. /// 如果不存在就创建
  196. private func getSoundsGroupDirectory() -> URL? {
  197. if let directoryUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")?.appendingPathComponent("Sounds") {
  198. if !fileManager.fileExists(atPath: directoryUrl.path) {
  199. try? fileManager.createDirectory(atPath: directoryUrl.path, withIntermediateDirectories: true, attributes: nil)
  200. }
  201. return directoryUrl
  202. }
  203. return nil
  204. }
  205. }