- 发出异步请求
- 将主线程中的结果绑定到不同的视图
- 如有必要,在后台线程中异步更新设备上的数据库
- 如果执行这些操作时发生错误,则显示通知
- 符合SSOT原则以实现数据相关性
- 全部测试
MVVM 的体系结构方法以及RxSwift和CoreData框架大大简化了解决此问题的步骤。
下述方法使用反应式编程原理,并不专门与RxSwift和CoreData绑定。并且,如果需要,可以使用其他工具来实现。
例如,我将显示显示卖方数据的应用程序的摘要。控制器具有两个用于电话号码和地址的UILabel插座,以及一个用于呼叫该电话号码的UIButton。ContactsViewController。
让我从模型到视图来解释实现。
模型
来自DerivedSources的自动生成的文件SellerContacts + CoreDataProperties的片段,
具有以下属性:
extension SellerContacts {
@nonobjc public class func fetchRequest() -> NSFetchRequest<SellerContacts> {
return NSFetchRequest<SellerContacts>(entityName: "SellerContacts")
}
@NSManaged public var address: String?
@NSManaged public var order: Int16
@NSManaged public var phone: String?
}
仓库。
提供卖方数据的方法:
func sellerContacts() -> Observable<Event<[SellerContacts]>> {
// 1
Observable.merge([
// 2
context.rx.entities(fetchRequest: SellerContacts.fetchRequestWithSort()).materialize(),
// 3
updater.sync()
])
}
这是实施SSOT的地方。向CoreData发出请求,并根据需要更新CoreData。仅从数据库接收所有数据,并且updater.sync()只能生成带有错误的事件,而不能生成具有数据的事件。
- 使用合并运算符可以使我们实现对数据库查询的异步执行及其更新。
- 为了方便建立数据库查询,使用了RxCoreData
- 更新数据库
因为 使用接收和更新数据的异步方法,必须使用Observable <Event <... >>。这是必需的,以便订阅者在接收远程数据时收到错误时不会收到错误,而只会显示此错误并继续响应CoreData中的更改。稍后再详细介绍。
DatabaseUpdater
在示例应用程序中,从Firebase Remote Config检索远程数据。CoreData,才会更新fetchAndActivate()退出并.successFetchedFromRemote状态。
但是您可以使用任何其他更新限制,例如按时间。
Sync()方法更新数据库:
func sync<T>() -> Observable<Event<T>> {
// 1
// Check can fetch
if fetchLimiter.fetchInProcess {
return Observable.empty()
}
// 2
// Block fetch for other requests
fetchLimiter.fetchInProcess = true
// 3
// Fetch & activate remote config
return remoteConfig.rx.fetchAndActivate().flatMap { [weak self] status, error -> Observable<Event<T>> in
// 4
// Default result
var result = Observable<Event<T>>.empty()
// Update database only when config wethed from remote
switch status {
// 5
case .error:
let error = error ?? AppError.unknown
print("Remote config fetch error: \(error.localizedDescription)")
// Set error to result
result = Observable.just(Event.error(error))
// 6
case .successFetchedFromRemote:
print("Remote config fetched data from remote")
// Update database from remote config
try self?.update()
case .successUsingPreFetchedData:
print("Remote config using prefetched data")
@unknown default:
print("Remote config unknown status")
}
// 7
// Unblock fetch for other requests
self?.fetchLimiter.fetchInProcess = false
return result
}
}
- , . , sync(). fetchLimiter . , fetchInProcess .
- Event
ViewModel
在此示例中, ViewModel只是从存储库中调用SellerContacts()方法并返回结果。
func contacts() -> Observable<Event<[SellerContacts]>> {
repository.sellerContacts()
}
ViewController
在控制器中,您需要将查询结果绑定到字段。为此,在viewDidLoad()中调用bindContacts()方法:
private func bindContacts() {
// 1
viewModel?.contacts()
.subscribeOn(SerialDispatchQueueScheduler.init(qos: .userInteractive))
.observeOn(MainScheduler.instance)
// 2
.flatMapError { [weak self] in
self?.rx.showMessage($0.localizedDescription) ?? Observable.empty()
}
// 3
.compactMap { $0.first }
// 4
.subscribe(onNext: { [weak self] in
self?.phone.text = $0.phone
self?.address.text = $0.address
}).disposed(by: disposeBag)
}
- 我们在后台线程中执行一个联系人请求,结果是我们在主线程中工作
- 如果包含事件的元素到达时出现错误,则会显示错误消息并返回空序列。下面有关flatMapError和showMessage运算符的更多信息
- 使用compactMap运算符从数组获取联系人
- 将数据设置到插座
运算符.flatMapError()
要将序列的结果从Event转换为它包含的元素或显示错误,请使用运算符:
func flatMapError<T>(_ handler: ((_ error: Error) -> Observable<T>)? = nil) -> Observable<Element.Element> {
// 1
flatMap { element -> Observable<Element.Element> in
switch element.event {
// 2
case .error(let error):
return handler?(error).flatMap { _ in Observable<Element.Element>.empty() } ?? Observable.empty()
// 3
case .next(let element):
return Observable.just(element)
// 4
default:
return Observable.empty()
}
}
}
- 将序列从Event.Element转换为Element
- 如果事件包含错误,那么我们将处理程序返回转换为空序列
- 如果Event包含结果,则返回一个包含一个包含该结果的元素的序列。
- 默认情况下返回空序列
这种方法允许您处理查询执行错误,而无需将错误事件发送给订阅服务器。并且监视数据库中的更改仍处于活动状态。
运算符.showMessage()
要向用户显示消息,请使用运算符:
public func showMessage(_ text: String, withEvent: Bool = false) -> Observable<Void> {
// 1
let _alert = alert(title: nil,
message: text,
actions: [AlertAction(title: "OK", style: .default)]
// 2
).map { _ in () }
// 3
return withEvent ? _alert : _alert.flatMap { Observable.empty() }
}
- 使用RxAlert窗口将创建一条消息和一个按钮
- 结果转换为虚空
- 如果显示消息后需要事件,则返回结果。否则,我们首先将其转换为空序列,然后返回
因为 .showMessage()不仅可以用于显示错误通知,而且还可以调整序列是空还是带有事件,这很有用。
测验
上述所有内容都不难测试。让我们按演示顺序开始。
RepositoryTests DatabaseUpdaterMock
用于测试存储库。可以跟踪是否调用了sync()方法并设置其执行结果:
func testSellerContacts() throws {
// 1
// Success
// Check sequence contains only one element
XCTAssertThrowsError(try repository.sellerContacts().take(2).toBlocking(timeout: 1).toArray())
updater.isSync = false
// Check that element
var result = try repository.sellerContacts().toBlocking().first()?.element
XCTAssertTrue(updater.isSync)
XCTAssertEqual(result?.count, sellerContacts.count)
// 2
// Sync error
updater.isSync = false
updater.error = AppError.unknown
let resultArray = try repository.sellerContacts().take(2).toBlocking().toArray()
XCTAssertTrue(resultArray.contains { $0.error?.localizedDescription == AppError.unknown.localizedDescription })
XCTAssertTrue(updater.isSync)
result = resultArray.first { $0.error == nil }?.element
XCTAssertEqual(result?.count, sellerContacts.count)
}
- 我们检查序列中是否只包含一个元素,调用了sync()方法
- 我们检查序列是否包含两个元素。一个包含有错误的事件,另一个包含从数据库中查询的结果,调用sync()方法
DatabaseUpdater测试
testSync()
func testSync() throws {
let remoteConfig = RemoteConfigMock()
let fetchLimiter = FetchLimiter(serialQueue: DispatchQueue(label: "test"))
let databaseUpdater = DatabaseUpdaterImpl(remoteConfig: remoteConfig, decoder: JSONDecoderMock(), context: context, fetchLimiter: fetchLimiter)
// 1
// Not update. Fetch in process
fetchLimiter.fetchInProcess = true
XCTAssertFalse(remoteConfig.isFetchAndActivate)
XCTAssertFalse(remoteConfig.isSubscript)
expectation(forNotification: .NSManagedObjectContextDidSave, object: context)
.isInverted = true
var sync: Observable<Event<Void>> = databaseUpdater.sync()
XCTAssertNil(try sync.toBlocking().first())
XCTAssertFalse(remoteConfig.isFetchAndActivate)
XCTAssertFalse(remoteConfig.isSubscript)
XCTAssertTrue(fetchLimiter.fetchInProcess)
waitForExpectations(timeout: 1)
// 2
// Not update. successUsingPreFetchedData
fetchLimiter.fetchInProcess = false
expectation(forNotification: .NSManagedObjectContextDidSave, object: context)
.isInverted = true
sync = databaseUpdater.sync()
var result: Event<Void>?
sync.subscribe(onNext: { result = $0 }).disposed(by: disposeBag)
XCTAssertTrue(fetchLimiter.fetchInProcess)
remoteConfig.completionHandler?(RemoteConfigFetchAndActivateStatus.successUsingPreFetchedData, nil)
waitForExpectations(timeout: 1)
XCTAssertNil(result)
XCTAssertTrue(remoteConfig.isFetchAndActivate)
XCTAssertFalse(remoteConfig.isSubscript)
XCTAssertFalse(fetchLimiter.fetchInProcess)
// 3
// Not update. Error
fetchLimiter.fetchInProcess = false
remoteConfig.isFetchAndActivate = false
expectation(forNotification: .NSManagedObjectContextDidSave, object: context)
.isInverted = true
sync = databaseUpdater.sync()
sync.subscribe(onNext: { result = $0 }).disposed(by: disposeBag)
XCTAssertTrue(fetchLimiter.fetchInProcess)
remoteConfig.completionHandler?(RemoteConfigFetchAndActivateStatus.error, AppError.unknown)
waitForExpectations(timeout: 1)
XCTAssertEqual(result?.error?.localizedDescription, AppError.unknown.localizedDescription)
XCTAssertTrue(remoteConfig.isFetchAndActivate)
XCTAssertFalse(remoteConfig.isSubscript)
XCTAssertFalse(fetchLimiter.fetchInProcess)
// 4
// Update
fetchLimiter.fetchInProcess = false
remoteConfig.isFetchAndActivate = false
result = nil
expectation(forNotification: .NSManagedObjectContextDidSave, object: context)
sync = databaseUpdater.sync()
sync.subscribe(onNext: { result = $0 }).disposed(by: disposeBag)
XCTAssertTrue(fetchLimiter.fetchInProcess)
remoteConfig.completionHandler?(RemoteConfigFetchAndActivateStatus.successFetchedFromRemote, nil)
waitForExpectations(timeout: 1)
XCTAssertNil(result)
XCTAssertTrue(remoteConfig.isFetchAndActivate)
XCTAssertTrue(remoteConfig.isSubscript)
XCTAssertFalse(fetchLimiter.fetchInProcess)
}
- 如果正在进行更新,则返回一个空序列
- 如果没有收到数据,则返回一个空序列
- 事件返回错误
- 如果数据已更新,则返回空序列
ViewModelTests
ViewControllerTests
testBindContacts()
func testBindContacts() {
// 1
// Error. Show message
XCTAssertNotEqual(controller.phone.text, contacts.phone)
XCTAssertNotEqual(controller.address.text, contacts.address)
viewModel.contactsResult.accept(Event.error(AppError.unknown))
expectation(description: "wait 1 second").isInverted = true
waitForExpectations(timeout: 1)
// 2
XCTAssertNotNil(controller.presentedViewController)
let alertController = controller.presentedViewController as! UIAlertController
XCTAssertEqual(alertController.actions.count, 1)
XCTAssertEqual(alertController.actions.first?.style, .default)
XCTAssertEqual(alertController.actions.first?.title, "OK")
XCTAssertNotEqual(controller.phone.text, contacts.phone)
XCTAssertNotEqual(controller.address.text, contacts.address)
// 3
// Trigger action OK
let action = alertController.actions.first!
typealias AlertHandler = @convention(block) (UIAlertAction) -> Void
let block = action.value(forKey: "handler")
let blockPtr = UnsafeRawPointer(Unmanaged<AnyObject>.passUnretained(block as AnyObject).toOpaque())
let handler = unsafeBitCast(blockPtr, to: AlertHandler.self)
handler(action)
expectation(description: "wait 1 second").isInverted = true
waitForExpectations(timeout: 1)
// 4
XCTAssertNil(controller.presentedViewController)
XCTAssertNotEqual(controller.phone.text, contacts.phone)
XCTAssertNotEqual(controller.address.text, contacts.address)
// 5
// Empty array of contats
viewModel.contactsResult.accept(Event.next([]))
expectation(description: "wait 1 second").isInverted = true
waitForExpectations(timeout: 1)
XCTAssertNil(controller.presentedViewController)
XCTAssertNotEqual(controller.phone.text, contacts.phone)
XCTAssertNotEqual(controller.address.text, contacts.address)
// 6
// Success
viewModel.contactsResult.accept(Event.next([contacts]))
expectation(description: "wait 1 second").isInverted = true
waitForExpectations(timeout: 1)
XCTAssertNil(controller.presentedViewController)
XCTAssertEqual(controller.phone.text, contacts.phone)
XCTAssertEqual(controller.address.text, contacts.address)
}
- 显示错误讯息
- 检查controller.presentedViewController是否有错误消息
- 运行确定按钮的处理程序,并确保该消息框是隐藏的
- 对于空结果,不显示任何错误,并且不填充任何字段
- 对于成功的请求,不会显示任何错误,并且必须填写字段
操作员测试
.flatMapError().
showMessage()
使用类似的设计方法,我们遵循SSOT原理,实现了异步数据获取,更新和错误通知,而不会失去响应数据更改的能力。