添加 MessageListViewModel

重构 MessageListViewController
This commit is contained in:
Fin 2020-11-22 16:12:59 +08:00
parent 84a8813f5f
commit 2140923db4
6 changed files with 266 additions and 92 deletions

View File

@ -52,6 +52,7 @@
0632CE2320EC9098003FDF46 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0632CE2220EC9098003FDF46 /* NotificationViewController.swift */; };
0632CE2620EC9098003FDF46 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0632CE2420EC9098003FDF46 /* MainInterface.storyboard */; };
0632CE2A20EC9098003FDF46 /* NotificationContentExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 0632CE1E20EC9098003FDF46 /* NotificationContentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
0633E80A256A091B00ED0680 /* MJRefresh+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0633E809256A091B00ED0680 /* MJRefresh+Rx.swift */; };
0637FA7820E0926D00E80174 /* BarkTargetType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA7720E0926D00E80174 /* BarkTargetType.swift */; };
0637FA7A20E092B300E80174 /* Observable+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA7920E092B300E80174 /* Observable+Extension.swift */; };
0637FA7C20E0930E00E80174 /* BarkApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0637FA7B20E0930E00E80174 /* BarkApi.swift */; };
@ -74,6 +75,8 @@
0661A54D204FDA4100965E4E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0661A54B204FDA4100965E4E /* LaunchScreen.storyboard */; };
0667D192247D162C005DE2ED /* MessageTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0667D191247D162C005DE2ED /* MessageTableViewCell.swift */; };
0667D194247D1BA0005DE2ED /* Date+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0667D193247D1BA0005DE2ED /* Date+Extension.swift */; };
0672CB06256903F700570C9D /* MessageListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0672CB05256903F700570C9D /* MessageListViewModel.swift */; };
067B2EB525693E38008B6BE1 /* MessageTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 067B2EB425693E38008B6BE1 /* MessageTableViewCellViewModel.swift */; };
06802E5320ECC40C00767047 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0661A549204FDA4100965E4E /* Assets.xcassets */; };
06885EB6247FB9880004A303 /* MessageSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06885EB5247FB9880004A303 /* MessageSettingsViewController.swift */; };
068F66B3247BD84C00DAD25A /* MessageListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 068F66B2247BD84C00DAD25A /* MessageListViewController.swift */; };
@ -175,6 +178,7 @@
0632CE2220EC9098003FDF46 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
0632CE2520EC9098003FDF46 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
0632CE2720EC9098003FDF46 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0633E809256A091B00ED0680 /* MJRefresh+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MJRefresh+Rx.swift"; sourceTree = "<group>"; };
0637FA7720E0926D00E80174 /* BarkTargetType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkTargetType.swift; sourceTree = "<group>"; };
0637FA7920E092B300E80174 /* Observable+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Observable+Extension.swift"; sourceTree = "<group>"; };
0637FA7B20E0930E00E80174 /* BarkApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkApi.swift; sourceTree = "<group>"; };
@ -199,6 +203,8 @@
0661A54E204FDA4100965E4E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0667D191247D162C005DE2ED /* MessageTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTableViewCell.swift; sourceTree = "<group>"; };
0667D193247D1BA0005DE2ED /* Date+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Extension.swift"; sourceTree = "<group>"; };
0672CB05256903F700570C9D /* MessageListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageListViewModel.swift; sourceTree = "<group>"; };
067B2EB425693E38008B6BE1 /* MessageTableViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageTableViewCellViewModel.swift; sourceTree = "<group>"; };
0683486A2050F1310024B6DA /* Bark.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Bark.entitlements; sourceTree = "<group>"; };
0683487020510FB20024B6DA /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
0683487220510FB20024B6DA /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; };
@ -265,12 +271,13 @@
children = (
0637FA8B20E0D7A700E80174 /* BaseViewController.swift */,
0637FA8120E09C4B00E80174 /* BarkNavigationController.swift */,
0603706C20E23EC000F4CA05 /* BarkSFSafariViewController.swift */,
0661A544204FDA4100965E4E /* HomeViewController.swift */,
065BE4542565055F002A8CA4 /* HomeViewModel.swift */,
0637FA8920E0D58800E80174 /* NewServerViewController.swift */,
06BBB895256518760076F63E /* NewServerViewModel.swift */,
0603706C20E23EC000F4CA05 /* BarkSFSafariViewController.swift */,
068F66B2247BD84C00DAD25A /* MessageListViewController.swift */,
0672CB05256903F700570C9D /* MessageListViewModel.swift */,
06885EB5247FB9880004A303 /* MessageSettingsViewController.swift */,
06BBB8B62567AC140076F63E /* MessageSettingsViewModel.swift */,
060481ED250F404500BC9799 /* SoundsViewController.swift */,
@ -285,6 +292,7 @@
06BBB8C02567B3EF0076F63E /* BaseTableViewCell.swift */,
0603706820E1F89500F4CA05 /* PreviewCardCell.swift */,
0667D191247D162C005DE2ED /* MessageTableViewCell.swift */,
067B2EB425693E38008B6BE1 /* MessageTableViewCellViewModel.swift */,
06C5952C2480E3F8006B98F3 /* LabelCell.swift */,
06C5952E248107F5006B98F3 /* iCloudStatusCell.swift */,
06C5953024811392006B98F3 /* ArchiveSettingCell.swift */,
@ -370,6 +378,7 @@
0667D193247D1BA0005DE2ED /* Date+Extension.swift */,
06C5953224811505006B98F3 /* ArchiveSettingManager.swift */,
06BBB8C82567B6730076F63E /* Operators.swift */,
0633E809256A091B00ED0680 /* MJRefresh+Rx.swift */,
);
path = Common;
sourceTree = "<group>";
@ -678,12 +687,14 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Bark/Pods-Bark-resources.sh",
"${PODS_ROOT}/MJRefresh/MJRefresh/MJRefresh.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/Material/com.cosmicmind.material.icons.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/Material/com.cosmicmind.material.fonts.bundle",
"${PODS_ROOT}/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MJRefresh.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/com.cosmicmind.material.icons.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/com.cosmicmind.material.fonts.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SVProgressHUD.bundle",
@ -711,6 +722,8 @@
06BBB896256518760076F63E /* NewServerViewModel.swift in Sources */,
06BBB8C92567B6730076F63E /* Operators.swift in Sources */,
0603706920E1F89500F4CA05 /* PreviewCardCell.swift in Sources */,
0672CB06256903F700570C9D /* MessageListViewModel.swift in Sources */,
0633E80A256A091B00ED0680 /* MJRefresh+Rx.swift in Sources */,
0637FA8C20E0D7A700E80174 /* BaseViewController.swift in Sources */,
062B98C8251B27AE004562E7 /* UINavigationItem+Extension.swift in Sources */,
060481EE250F404500BC9799 /* SoundsViewController.swift in Sources */,
@ -727,6 +740,7 @@
065BE44B2563D8E1002A8CA4 /* Reusable.swift in Sources */,
0637FA8620E0AB6600E80174 /* UIColor+Extension.swift in Sources */,
0637FA8A20E0D58800E80174 /* NewServerViewController.swift in Sources */,
067B2EB525693E38008B6BE1 /* MessageTableViewCellViewModel.swift in Sources */,
0637FA8220E09C4B00E80174 /* BarkNavigationController.swift in Sources */,
0637FA7A20E092B300E80174 /* Observable+Extension.swift in Sources */,
060481F0250F51CA00BC9799 /* SoundCell.swift in Sources */,

View File

@ -183,7 +183,7 @@ extension HomeViewController {
})
}
@objc func history(){
self.navigationController?.pushViewController(MessageListViewController(), animated: true)
self.navigationController?.pushViewController(MessageListViewController(viewModel: MessageListViewModel()), animated: true)
}
@objc func refreshState() {
switch Client.shared.state {

View File

@ -9,120 +9,131 @@
import UIKit
import Material
import RealmSwift
import RxCocoa
import RxDataSources
import MJRefresh
class MessageListViewController: UIViewController {
class MessageListViewController: BaseViewController {
let tableView: UITableView = {
let tableView = UITableView()
tableView.separatorStyle = .none
tableView.backgroundColor = Color.grey.lighten5
tableView.register(MessageTableViewCell.self, forCellReuseIdentifier: "cell")
tableView.register(MessageTableViewCell.self, forCellReuseIdentifier: "\(MessageTableViewCell.self)")
return tableView
}()
var results:Results<Message>?
deinit {
print("message list deinit")
}
override func viewDidLoad() {
super.viewDidLoad()
self.title = NSLocalizedString("historyMessage")
let settingButton: BKButton = {
let settingButton = BKButton()
settingButton.setImage(Icon.settings, for: .normal)
settingButton.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
settingButton.addTarget(self, action: #selector(settingClick), for: .touchUpInside)
return settingButton
}()
deinit {
print("message list deinit")
}
override func makeUI() {
self.title = NSLocalizedString("historyMessage")
navigationItem.setRightBarButtonItem(item: UIBarButtonItem(customView: settingButton))
self.view.addSubview(tableView)
tableView.dataSource = self
tableView.delegate = self
tableView.snp.makeConstraints { (make) in
make.edges.equalToSuperview()
}
self.refresh()
tableView.rx.setDelegate(self).disposed(by: rx.disposeBag)
tableView.mj_footer = MJRefreshAutoFooter()
}
@objc func settingClick (){
self.navigationController?.pushViewController(MessageSettingsViewController(viewModel: MessageSettingsViewModel()), animated: true)
}
func refresh() {
if let realm = try? Realm() {
results = realm.objects(Message.self).filter("isDeleted != true").sorted(byKeyPath: "createDate", ascending: false)
self.tableView.reloadData()
}
else {
override func bindViewModel() {
guard let viewModel = self.viewModel as? MessageListViewModel else {
return
}
}
}
extension MessageListViewController: UITableViewDataSource,UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let results = results{
return results.count
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MessageTableViewCell
cell.message = results![indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let action = UIContextualAction(style: .destructive, title: "删除") {[weak self] (action, sourceView, actionPerformed) in
if let realm = try? Realm() {
try? realm.write {
let message = self?.results?[indexPath.row]
message?.isDeleted = true
let output = viewModel.transform(input: MessageListViewModel.Input(
settingClick: self.settingButton.rx.tap.asDriver(),
loadMore: tableView.mj_footer!.rx.refresh.asDriver(),
itemDelete: tableView.rx.itemDeleted.asDriver(),
itemSelected: tableView.rx.modelSelected(MessageTableViewCellViewModel.self).asDriver()
))
//
output.settingClick
.drive(onNext: {[weak self] viewModel in
self?.navigationController?
.pushViewController(MessageSettingsViewController(viewModel: viewModel),
animated: true)
})
.disposed(by: rx.disposeBag)
//tableView
output.refreshAction
.drive(tableView.rx.refreshAction)
.disposed(by: rx.disposeBag)
//tableView
let dataSource = RxTableViewSectionedAnimatedDataSource<MessageSection>(
animationConfiguration: AnimationConfiguration(
insertAnimation: .none,
reloadAnimation: .none,
deleteAnimation: .left),
configureCell:{ (source, tableView, indexPath, item) -> UITableViewCell in
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(MessageTableViewCell.self)") as? MessageTableViewCell else {
return UITableViewCell ()
}
}
_ = self?.results?.dropFirst(indexPath.row)
self?.tableView.performBatchUpdates({
self?.tableView.deleteRows(at: [indexPath], with: .none)
}, completion: nil)
actionPerformed(true)
}
cell.bindViewModel(model: item)
return cell
}, canEditRowAtIndexPath: { _, _ in
return true
})
output.messages
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: rx.disposeBag)
//messagealert
output.alertMessage.drive(onNext: {[weak self] message in
self?.alertMessage(message: message)
}).disposed(by: rx.disposeBag)
//messageURL
output.urlTap.drive(onNext: { url in
if ["http","https"].contains(url.scheme?.lowercased() ?? ""){
Client.shared.currentNavigationController?.present(BarkSFSafariViewController(url: url), animated: true, completion: nil)
}
else{
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}).disposed(by: rx.disposeBag)
let configuration = UISwipeActionsConfiguration(actions: [action])
return configuration
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
func alertMessage(message:String) {
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
let copyAction = UIAlertAction(title: NSLocalizedString("Copy2"), style: .default, handler: {[weak self]
(alert: UIAlertAction) -> Void in
if let message = self?.results?[indexPath.row] {
var str:String = ""
if let title = message.title {
str += "\(title)\n"
}
if let body = message.body {
str += "\(body)\n"
}
if let url = message.url {
str += "\(url)"
}
str = String(str.prefix(str.count - 1))
UIPasteboard.general.string = str
self?.showSnackbar(text: NSLocalizedString("Copy"))
}
UIPasteboard.general.string = message
self?.showSnackbar(text: NSLocalizedString("Copy"))
})
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel"), style: .cancel, handler: {
(alert: UIAlertAction) -> Void in
})
let cancelAction = UIAlertAction(title: NSLocalizedString("Cancel"), style: .cancel, handler: { _ in })
alertController.addAction(copyAction)
alertController.addAction(cancelAction)
Client.shared.currentNavigationController?.present(alertController, animated: true, completion: nil)
}
}
extension MessageListViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let action = UIContextualAction(style: .destructive, title: "删除") {[weak self] (action, sourceView, actionPerformed) in
self?.tableView.dataSource?.tableView?(self!.tableView, commit: .delete, forRowAt: indexPath)
actionPerformed(true)
}
let configuration = UISwipeActionsConfiguration(actions: [action])
return configuration
}
}

View File

@ -0,0 +1,145 @@
//
// MessageListViewModel.swift
// Bark
//
// Created by huangfeng on 2020/11/21.
// Copyright © 2020 Fin. All rights reserved.
//
import Foundation
import RxSwift
import RxDataSources
import RxCocoa
import RealmSwift
class MessageListViewModel: ViewModel,ViewModelType {
struct Input {
var settingClick: Driver<Void>
var loadMore: Driver<Void>
var itemDelete: Driver<IndexPath>
var itemSelected: Driver<MessageTableViewCellViewModel>
}
struct Output {
var messages:Driver<[MessageSection]>
var settingClick: Driver<MessageSettingsViewModel>
var refreshAction:Driver<MJRefreshAction>
var alertMessage:Driver<String>
var urlTap:Driver<URL>
}
let results:Results<Message>? = {
if let realm = try? Realm() {
return realm.objects(Message.self)
.filter("isDeleted != true")
.sorted(byKeyPath: "createDate", ascending: false)
}
return nil
}()
var page = 0
let pageCount = 20
func getNextPage() -> [Message] {
if let result = results {
let startIndex = page * pageCount
let endIndex = min(startIndex + pageCount, result.count)
guard endIndex > startIndex else {
return []
}
var messages:[Message] = []
for i in startIndex ..< endIndex {
messages.append(result[i])
}
page += 1
return messages
}
return []
}
func transform(input: Input) -> Output {
let settingClick = input.settingClick
.map{
MessageSettingsViewModel()
}
.asDriver()
let alertMessage = input.itemSelected.map { (model) -> String in
let message = model.message
var copyContent:String = ""
if let title = message.title {
copyContent += "\(title)\n"
}
if let body = message.body {
copyContent += "\(body)\n"
}
if let url = message.url {
copyContent += "\(url)\n"
}
copyContent = String(copyContent.prefix(copyContent.count - 1))
return copyContent
}
//
let messagesRelay = BehaviorRelay<[MessageSection]>(value: [])
let refreshAction = BehaviorRelay<MJRefreshAction>(value: .none)
Observable<Void>.just(())
.concat(input.loadMore)
.subscribe(onNext: {[weak self] in
guard let strongSelf = self else { return }
let messages = strongSelf.getNextPage()
let cellViewModels = messages.map({ (message) -> MessageTableViewCellViewModel in
return MessageTableViewCellViewModel(message: message)
})
refreshAction.accept(.endLoadmore)
if var section = messagesRelay.value.first {
section.messages.append(contentsOf: cellViewModels)
messagesRelay.accept([section])
}
else{
messagesRelay.accept([MessageSection(header: "model", messages: cellViewModels)])
}
}).disposed(by: rx.disposeBag)
//message
input.itemDelete.drive(onNext: {[weak self] indexPath in
if var section = messagesRelay.value.first {
if let realm = try? Realm() {
try? realm.write {
let message = self?.results?[indexPath.row]
message?.isDeleted = true
}
}
section.messages.remove(at: indexPath.row)
messagesRelay.accept([section])
}
}).disposed(by: rx.disposeBag)
// cell url
let urlTap = messagesRelay.flatMapLatest { (section) -> Observable<String> in
if let section = section.first {
let taps = section.messages.compactMap { (model) -> Observable<String> in
return model.urlTap.asObservable()
}
return Observable.merge(taps)
}
return .empty()
}
.compactMap { URL(string: $0) } //url
return Output(
messages: messagesRelay.asDriver(onErrorJustReturn: []),
settingClick: settingClick,
refreshAction: refreshAction.asDriver(),
alertMessage: alertMessage,
urlTap: urlTap.asDriver(onErrorDriveWith: .empty())
)
}
}

View File

@ -22,6 +22,7 @@ def pods
pod 'RxDataSources'
pod 'NSObject+Rx'
pod 'MJRefresh'
end
target 'Bark' do

View File

@ -11,6 +11,7 @@ PODS:
- Material/Core (= 3.1.8)
- Material/Core (3.1.8):
- Motion (~> 3.1.1)
- MJRefresh (3.5.0)
- Motion (3.1.3):
- Motion/Core (= 3.1.3)
- Motion/Core (3.1.3)
@ -51,6 +52,7 @@ DEPENDENCIES:
- IceCream
- KVOController
- Material
- MJRefresh
- Moya/RxSwift
- "NSObject+Rx"
- ObjectMapper
@ -64,24 +66,24 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/CocoaPods/Specs.git:
- Differentiator
- "NSObject+Rx"
- RxCocoa
- RxDataSources
- RxGesture
- RxRelay
trunk:
- Alamofire
- DeviceKit
- Differentiator
- FDFullscreenPopGesture
- IceCream
- KVOController
- Material
- MJRefresh
- Motion
- Moya
- "NSObject+Rx"
- ObjectMapper
- Realm
- RealmSwift
- RxCocoa
- RxDataSources
- RxGesture
- RxRelay
- RxSwift
- SnapKit
- SVProgressHUD
@ -105,6 +107,7 @@ SPEC CHECKSUMS:
IceCream: 0447d87b55df85651dd60b15712cef64dbe1cb54
KVOController: d72ace34afea42468329623b3379ab3cd1d286b6
Material: a2a3f400a3b549d53ef89e56c58c4535b29db387
MJRefresh: 6afc955813966afb08305477dd7a0d9ad5e79a16
Motion: cf1e060e489f6661126374d5c60dbd2ed991605c
Moya: 5b45dacb75adb009f97fde91c204c1e565d31916
"NSObject+Rx": fa6bbcc1ab1faa06b01466bc09b1e0692bbc5946
@ -120,6 +123,6 @@ SPEC CHECKSUMS:
SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6
SwiftyJSON: 36413e04c44ee145039d332b4f4e2d3e8d6c4db7
PODFILE CHECKSUM: f0191b272c872b4d43da19a114ad4be556fb7eba
PODFILE CHECKSUM: d8fe9b628db96415f1c950df034b6353a4921da5
COCOAPODS: 1.10.0