Hi So i am trying to wrap an async network request with Combine's Future/Promise.
my code goes something like that:
enum State {
case initial
case loading
case success(movies: [Movie])
case error(message: String)
}
protocol ViewModelProtocol {
var statePublisher: AnyPublisher<State, Never> { get }
func load(genreId: String)
}
class ViewModel: ViewModelProtocol {
var remoteDataSource = RemoteDataSource()
#Published state: State = .initial
var statePublisher: AnyPublisher<State, Never> { $state.eraseToAnyPubliher() }
public func load(genreId: String) {
self.state = .loading
self.getMovies(for: genreId)
.sink { [weak self] (moveis) in
guard let self = self else { return }
if let movies = movies {
self.state = .success(movies: movies)
} else {
self.state = .error(message: "failed to load movies")
}
}
}
func getMovies(for genreId: String) -> AnyPublisher<[Movie]?, Never> {
Future { promise in
self.remoteDataSource.getMovies(for: genreId) { (result) in
switch result {
case .success(let movies): promise(.success(movies))
case .failure: promise(.success(nil))
}
}
}.eraseToAnyPublisher()
}
}
I was trying to see if there are any memory leaks and found that there is a reference to the Future that is not being deallocated
same as here: Combine Future Publisher is not getting deallocated
You are strongly capturing self inside of your escaping Future init (just capture remoteDataSource). It doesn't seem to me like this should cause a memory leak. As the link you put in the question suggests Future does not behave like most other publishers; it does work as soon as you create it and not when you subscribe. It also memoizes and shares its results. I strongly suggest that you do no use this behind an AnyPublisher because its very non obvious to the caller that this thing is backed by a Future and that it will start work immediately. I would use URLSession.shared.dataTaskPublisher instead and then you get a regular publisher and you don't need the completion handler or the Future. Otherwise wrap the Future in a Deferred so you don't get the eager evaluation.
Related
I am new to working with dependency injection. When I run a test with a mocked object the success result case isn't getting triggered and the object isn't added to my #Published property which I am trying to test.
When I go to test my viewModel I can see the array to be tested but when getShows() gets called in the test the Result case is used.
Test File
import XCTest
#testable import PopularTVViewer
class PopularTVViewerTests: XCTestCase {
func testPopularTVModel() {
let mockedManager = MockedPopularTVManager()
mockedManager.result = .success(mockedManager.mockPopularShows)
let viewModel = PopularTVViewModel(manager: mockedManager)
#warning("getShows() success case isn't being called even through viewModel has its reference.")
viewModel.getShows()
XCTAssertNotNil(viewModel)
XCTAssertNotNil(mockedManager.result)
// Currently failing
// XCTAssertEqual(viewModel.popularTV.count, 4)
XCTAssertEqual(mockedManager.getPopularShowsCallCounter, 1)
}
}
MockedManager
class MockedPopularTVManager: PopularTVManagerProtocol {
var result: Result<[PopularTV], NetworkError>!
var getPopularShowsCallCounter = 0
func getPopularShows(completion: #escaping (Result<[PopularTV], NetworkError>) -> Void) {
completion(result)
getPopularShowsCallCounter += 1
}
let mockPopularShow = PopularTV(name: "House of the Dragon", posterPath: "/mYLOqiStMxDK3fYZFirgrMt8z5d.jpg", popularity: 4987.163, voteAverage: 7.7, voteCount: 881)
let mockPopularShows = [
PopularTV(name: "The Lord of the Rings: The Rings of Power", posterPath: "/mYLOqiStMxDK3fYZFirgrMt8z5d.jpg", popularity: 4987.163, voteAverage: 7.7, voteCount: 881),
PopularTV(name: "House of the Dragon", posterPath: "/z2yahl2uefxDCl0nogcRBstwruJ.jpg", popularity: 4979.127, voteAverage: 8.6, voteCount: 1513),
PopularTV(name: "She-Hulk: Attorney at Law", posterPath: "/hJfI6AGrmr4uSHRccfJuSsapvOb.jpg", popularity: 2823.739, voteAverage: 7.1, voteCount: 846),
PopularTV(name: "Dahmer – Monster: The Jeffrey Dahmer Story", posterPath: "/f2PVrphK0u81ES256lw3oAZuF3x.jpg", popularity: 1774.56, voteAverage: 8.3, voteCount: 402)
]
}
ViewModel
final class PopularTVViewModel: ObservableObject {
#Published var popularTV = [PopularTV]()
let manager: PopularTVManagerProtocol
let columns = ColumnLayoutHelper.threeColumnLayout
// Injecting for testing.
init(manager: PopularTVManagerProtocol = PopularTVManager()) {
self.manager = manager
}
// Grabbing next page of results
func getMoreShows() {
getShows()
}
// Initial network call.
func getShows() {
manager.getPopularShows() { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let popularTV):
for show in popularTV {
self?.popularTV.append(show)
}
case .failure(let error):
switch error {
case .invalidURL:
print("Invalid URL")
case .invalidData:
print("Invalid Data")
case .unableToComplete:
print("Unable to complete")
case .invalidResponse:
print("Invalid response")
}
}
}
}
}
}
I've done everything I can think of to make sure the object exists and the viewModel has access to it but its as if the mockedManager in my test doesn't have the success when getShows() is run.
The test is failing because you are switching threads during your test. The DispatchQueue.main.async in your PopularTVViewModel is causing it to fail.
You need to remove the use of DispatchQueue.main.async from your test. This is possible to do.
We first need to create a function that will stand in place of the call to DispatchQueue.main.async. This function will check that we are on the main thread and if we are execute the code directly without switching threads, otherwise if we are on a background thread it will dispatch on the main thread. This should mean that in your application it works exactly the same as before, and in your tests it avoids the thread hop so that they now pass.
/// You could make this a global function, an extension on DispatchQueue,
/// the choice where to put it is up to you, but it should be accessible
/// by whichever classes need to use it as chances are you may need to use
/// it in multiple places.
func performUIUpdate(using closure: #escaping () -> Void) {
if Thread.isMainThread {
closure()
} else {
DispatchQueue.main.async(execute: closure)
}
}
We can then update your PopularTVViewModel to use the new function.
final class PopularTVViewModel: ObservableObject {
#Published var popularTV = [PopularTV]()
let manager: PopularTVManagerProtocol
let columns = ColumnLayoutHelper.threeColumnLayout
// Injecting for testing.
init(manager: PopularTVManagerProtocol = PopularTVManager()) {
self.manager = manager
}
// Grabbing next page of results
func getMoreShows() {
getShows()
}
// Initial network call.
func getShows() {
manager.getPopularShows() { [weak self] result in
performUIUpdate { // Note we use the new function here instead of DispatchQueue.main.async
switch result {
case .success(let popularTV):
// you could use the following instead of your for loop.
// self?.popularTV.append(contentsOf: popularTV)
for show in popularTV {
self?.popularTV.append(show)
}
case .failure(let error):
switch error {
case .invalidURL:
print("Invalid URL")
case .invalidData:
print("Invalid Data")
case .unableToComplete:
print("Unable to complete")
case .invalidResponse:
print("Invalid response")
}
}
}
}
}
}
Your tests should now pass.
There is a great article by John Sundell that shows how to reduce flakiness in testing.
Also this book, iOS Unit Testing by Example, by Jon Reid is very good and well worth having on your bookshelf.
I am trying to create a class that executes data loading once and returns the data to all callers of the method while the data was loading to not perform the data loading for the same item (identifier) more than once. The issue I am having is that it seems to crash on the first initialization of CurrentValueSubject for an identifier. This only happens if the downloadStuff returns an Error I have no idea what's wrong. Here is a reproduction of the issue.
Class that does the synchronization:
class FetchSynchronizer<T, ItemIdentifier: Hashable> {
typealias CustomParams = (isFirstLoad: Bool, result: Result<T, Error>)
enum FetchCondition {
// executes data fetching only once
case executeFetchOnlyOnce
// re-executes fetch if request failed
case retryOnlyIfFailure
// always executes fetch even if response is cached
case noDataCache
// custom condition
case custom((CustomParams) -> Bool)
}
struct LoadingState<T> {
let result: Result<T, Error>
let isLoading: Bool
init(result: Result<T, Error>? = nil, isLoading: Bool = false) {
self.result = result ?? .failure(NoResultsError())
self.isLoading = isLoading
}
}
private var cancellables = Set<AnyCancellable>()
private var isLoading: [ItemIdentifier: CurrentValueSubject<LoadingState<T>, Never>] = [:]
func startLoading(identifier: ItemIdentifier,
fetchCondition: FetchCondition = .executeFetchOnlyOnce,
loaderMethod: #escaping () async -> Result<T, Error>) async -> Result<T, Error> {
// initialize loading tracker for identifier on first execution
var isFirstExecution = false
if isLoading[identifier] == nil {
print("----0")
isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())
isFirstExecution = true
}
guard let currentIsLoading = isLoading[identifier] else {
assertionFailure("Should never be nil because it's set above")
return .failure(NoResultsError())
}
if currentIsLoading.value.isLoading {
// loading in progress, wait for finish and call pending callbacks
return await withCheckedContinuation { continuation in
currentIsLoading.filter { !$0.isLoading }.sink { currentIsLoading in
continuation.resume(returning: currentIsLoading.result)
}.store(in: &cancellables)
}
} else {
// no fetching in progress, check if it can be executed
let shouldFetchData: Bool
switch fetchCondition {
case .executeFetchOnlyOnce:
// first execution -> fetch data
shouldFetchData = isFirstExecution
case .retryOnlyIfFailure:
// no cached data -> fetch data
switch currentIsLoading.value.result {
case .success:
shouldFetchData = false
case .failure:
shouldFetchData = true
}
case .noDataCache:
// always fetch
shouldFetchData = true
case .custom(let completion):
shouldFetchData = completion((isFirstLoad: isFirstExecution,
result: currentIsLoading.value.result))
}
if shouldFetchData {
currentIsLoading.send(LoadingState(isLoading: true))
// fetch data
return await withCheckedContinuation { continuation in
Task {
// execute loader method
let result = await loaderMethod()
let state = LoadingState(result: result,
isLoading: false)
currentIsLoading.send(state)
continuation.resume(returning: result)
}
}
} else {
// use existing data
return currentIsLoading.value.result
}
}
}
}
Example usage:
class Executer {
let fetchSynchronizer = FetchSynchronizer<Data?, String>()
func downloadStuff() async -> Result<Data?, Error> {
await fetchSynchronizer.startLoading(identifier: "1") {
return await withCheckedContinuation { continuation in
sleep(UInt32.random(in: 1...3))
print("-------request")
continuation.resume(returning: .failure(NSError() as Error))
}
}
}
init() {
start()
}
func start() {
Task {
await downloadStuff()
print("-----3")
}
DispatchQueue.global(qos: .utility).async {
Task {
await self.downloadStuff()
print("-----2")
}
}
DispatchQueue.global(qos: .background).async {
Task {
await self.downloadStuff()
print("-----1")
}
}
}
}
Start the execution:
Executer()
Crashes at
isLoading[identifier] = CurrentValueSubject<LoadingState<T>, Never>(LoadingState<T>())
Any guidance would be appreciated.
Swift Dictionary is not thread-safe.
You need to make sure it is being accessed from only one thread (i.e queue) or using locks.
EDIT - another solution suggested by #Bogdan the question writer is to make the class an actor class which the concurrency safety is taken care of by the compiler!
By dispatching to a global queue, you increase the chance that two threads will try and write into the dictionary “at the same time” which probably causes the crash
Take a look at these examples.
How to implement a Thread Safe HashTable (PhoneBook) Data Structure in Swift?
https://github.com/iThink32/Thread-Safe-Dictionary/blob/main/ThreadSafeDictionary.swift
I am trying to pass the value of gyroX to another function but it just ends up in it having a value of 0 when I use it as gyroX in that other function.
Here is the code:
var gyroX = Float()
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
gyroX = Float(myData.rotationRate.x)
}
}
With Xcode 13 Beta and Swift 5.5
This is a problem that we can now solve with Async/Await's Continuations
We would first make a function that converts the callback into an awaitable result like:
func getXRotation(from motion: CMMotionManager) async throws -> Float {
try await withCheckedThrowingContinuation { continuation in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
continuation.resume(returning: Float(myData.rotationRate.x))
} else {
throw GyroUpdateFailure()
}
}
}
}
Then we can assign the variable and use it like so:
let gyroX = try await getXRotation(from: motion)
callSomeOtherFunction(with: gyroX)
With Xcode <= 12 and Combine
In the current release of Swift and Xcode we can use the Combine framework to make callback handling a little easier for us. First we'll convert the closure from the motion manager into a "Future". Then we can use that future in a combine chain.
func getXRotation(from motion: CMMotionManager) -> Future<CMGyroData, Error> {
Future { promise in
class GyroUpdateFailure: Error {} // make error to throw
motion.startGyroUpdates(to: .main) { (data, error) in
if let myData = data {
promise(.success(myData))
} else {
promise(.failure(GyroUpdateFailure()))
}
}
}
}
// This is the other function you want to call
func someOtherFunction(_ x: Float) {}
// Then we can use it like so
_ = getXRotation(from: motion)
.eraseToAnyPublisher()
.map { Float($0.rotationRate.x) }
.map(someOtherFunction)
.sink { completion in
switch completion {
case .failure(let error):
print(error.localizedDescription)
default: break
}
} receiveValue: {
print($0)
}
There are some important parts to the combine flow. The _ = is one of them. The result of "sinking" on a publisher is a "cancellable" object. If we don't store that in a local variable the system can clean up the task before it fishes executing. So you will want to do that for sure.
I highly recommend you checkout SwiftBySundell.com to learn more about Combine or Async/Await and RayWenderlich.com for mobile development in general.
I have an object created from flatMaping an observable.
private lazy var childObj: chilView? = {
let keychainStore = Realm().getStore()
let selectedElementID = keychainStore.elementID
.asObservable()
.distinctUntilChanged {$0 == $1}
.flatMap({ (elementID) -> Observable<Element?> in
guard let elementID = elementID else {
return Observable.error(Errors.InvalidElementID)
}
return Observable.create { observer in
let elementStream: Observable<Result<Element>> = keychainStore.getObservable(id: elementID)
elementStream.subscribe(onNext: { (result) in
switch result {
case .success(let element):
observer.onNext(element)
default: break
}
})
.disposed(by: self.disposeBag)
return Disposables.create()
}
})
return self.createChildObject(with: selectedElementID)
}()
The selectedElement is of type flatMap observable. createChildObject(with:) is called even before observer.onNext(element) is executed. How do I fix this?
I am not sure I completely understand your problem from the description but here are some thoughts:
It's not a race condition. Your selectedElementID chain and the flatMap code within it will not be executed before you subscribe to the selectedElementID. So I guess you subscribe to the selectedElementID somewhere within createChildObject method and obviously get the createChildObject code executed before the selectedElementID chain.
Depending on how keychainStore rx calls are designed and how you subscribe to selectedElementID observable you might get a race condition but I am not sure that the using RxSwift is a good decision in this case. Try making your chains more atomic.
I am new to RxSwift and MVVM.
my viewModel has a method named rx_fetchItems(for:) that does the heavy lifting of fetching relevant content from backend, and returns Observable<[Item]>.
My goal is to supply an observable property of the viewModel named collectionItems, with the last emitted element returned from rx_fetchItems(for:), to supply my collectionView with data.
Daniel T has provided this solution that I could potentially use:
protocol ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]>
}
struct ViewModel {
let collectionItems: Observable<[Item]>
let error: Observable<Error>
init(controlValue: Observable<Int>, api: ServerAPI) {
let serverItems = controlValue
.map { ItemCategory(rawValue: $0) }
.filter { $0 != nil }.map { $0! } // or use a `filterNil` operator if you already have one implemented.
.flatMap { api.rx_fetchItems(for: $0)
.materialize()
}
.filter { $0.isCompleted == false }
.shareReplayLatestWhileConnected()
collectionItems = serverItems.filter { $0.element != nil }.dematerialize()
error = serverItems.filter { $0.error != nil }.map { $0.error! }
}
}
The only problem here is that my current ServerAPI aka FirebaseAPI, has no such protocol method, because it is designed with a single method that fires all requests like this:
class FirebaseAPI {
private let session: URLSession
init() {
self.session = URLSession.shared
}
/// Responsible for Making actual API requests & Handling response
/// Returns an observable object that conforms to JSONable protocol.
/// Entities that confrom to JSONable just means they can be initialized with json.
func rx_fireRequest<Entity: JSONable>(_ endpoint: FirebaseEndpoint, ofType _: Entity.Type ) -> Observable<[Entity]> {
return Observable.create { [weak self] observer in
self?.session.dataTask(with: endpoint.request, completionHandler: { (data, response, error) in
/// Parse response from request.
let parsedResponse = Parser(data: data, response: response, error: error)
.parse()
switch parsedResponse {
case .error(let error):
observer.onError(error)
return
case .success(let data):
var entities = [Entity]()
switch endpoint.method {
/// Flatten JSON strucuture to retrieve a list of entities.
/// Denoted by 'GETALL' method.
case .GETALL:
/// Key (underscored) is unique identifier for each entity, which is not needed here.
/// value is k/v pairs of entity attributes.
for (_, value) in data {
if let value = value as? [String: AnyObject], let entity = Entity(json: value) {
entities.append(entity)
}
}
// Need to force downcast for generic type inference.
observer.onNext(entities as! [Entity])
observer.onCompleted()
/// All other methods return JSON that can be used to initialize JSONable entities
default:
if let entity = Entity(json: data) {
observer.onNext([entity] as! [Entity])
observer.onCompleted()
} else {
observer.onError(NetworkError.initializationFailure)
}
}
}
}).resume()
return Disposables.create()
}
}
}
The most important thing about the rx_fireRequest method is that it takes in a FirebaseEndpoint.
/// Conforms to Endpoint protocol in extension, so one of these enum members will be the input for FirebaseAPI's `fireRequest` method.
enum FirebaseEndpoint {
case saveUser(data: [String: AnyObject])
case fetchUser(id: String)
case removeUser(id: String)
case saveItem(data: [String: AnyObject])
case fetchItem(id: String)
case fetchItems
case removeItem(id: String)
case saveMessage(data: [String: AnyObject])
case fetchMessages(chatroomId: String)
case removeMessage(id: String)
}
In order to use Daniel T's solution, Id have to convert each enum case from FirebaseEndpoint into methods inside FirebaseAPI. And within each method, call rx_fireRequest... If I'm correct.
Id be eager to make this change if it makes for a better Server API design. So the simple question is, Will this refactor improve my overall API design and how it interacts with ViewModels. And I realize this is now evolving into a code review.
ALSO... Here is implementation of that protocol method, and its helper:
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]> {
// fetched items returns all items in database as Observable<[Item]>
let fetchedItems = client.rx_fireRequest(.fetchItems, ofType: Item.self)
switch category {
case .Local:
let localItems = fetchedItems
.flatMapLatest { [weak self] (itemList) -> Observable<[Item]> in
return self!.rx_localItems(items: itemList)
}
return localItems
// TODO: Handle other cases like RecentlyAdded, Trending, etc..
}
}
// Helper method to filter items for only local items nearby user.
private func rx_localItems(items: [Item]) -> Observable<[Item]> {
return Observable.create { observable in
observable.onNext(items.filter { $0.location == "LA" })
observable.onCompleted()
return Disposables.create()
}
}
If my approach to MVVM or RxSwift or API design is wrong PLEASE do critique.
I know it is tough to start understanding RxSwift
I like to use Subjects or Variables as inputs for the ViewModel and Observables or Drivers as outputs for the ViewModel
This way you can bind the actions that happen on the ViewController to the ViewModel, handle the logic there, and update the outputs
Here is an example by refactoring your code
View Model
// Inputs
let didSelectItemCategory: PublishSubject<ItemCategory> = .init()
// Outputs
let items: Observable<[Item]>
init() {
let client = FirebaseAPI()
let fetchedItems = client.rx_fireRequest(.fetchItems, ofType: Item.self)
self.items = didSelectItemCategory
.withLatestFrom(fetchedItems, resultSelector: { itemCategory, fetchedItems in
switch itemCategory {
case .Local:
return fetchedItems.filter { $0.location == "Los Angeles" }
default: return []
}
})
}
ViewController
segmentedControl.rx.value
.map(ItemCategory.init(rawValue:))
.startWith(.Local)
.bind(to: viewModel.didSelectItemCategory)
.disposed(by: disposeBag)
viewModel.items
.subscribe(onNext: { items in
// Do something
})
.disposed(by: disposeBag)
I think the problem you are having is that you are only going half-way with the observable paradigm and that's throwing you off. Try taking it all the way and see if that helps. For example:
protocol ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]>
}
struct ViewModel {
let collectionItems: Observable<[Item]>
let error: Observable<Error>
init(controlValue: Observable<Int>, api: ServerAPI) {
let serverItems = controlValue
.map { ItemCategory(rawValue: $0) }
.filter { $0 != nil }.map { $0! } // or use a `filterNil` operator if you already have one implemented.
.flatMap { api.rx_fetchItems(for: $0)
.materialize()
}
.filter { $0.isCompleted == false }
.shareReplayLatestWhileConnected()
collectionItems = serverItems.filter { $0.element != nil }.dematerialize()
error = serverItems.filter { $0.error != nil }.map { $0.error! }
}
}
EDIT to handle problem mentioned in comment. You now need to pass in the object that has the rx_fetchItems(for:) method. You should have more than one such object: one that points to the server and one that doesn't point to any server, but instead returns canned data so you can test for any possible response, including errors. (The view model should not talk to the server directly, but should do so through an intermediary...
The secret sauce in the above is the materialize operator that wraps error events into a normal event that contains an error object. That way you stop a network error from shutting down the whole system.
In response to the changes in your question... You can simply make the FirebaseAPI conform to ServerAPI:
extension FirebaseAPI: ServerAPI {
func rx_fetchItems(for category: ItemCategory) -> Observable<[Item]> {
// fetched items returns all items in database as Observable<[Item]>
let fetchedItems = self.rx_fireRequest(.fetchItems, ofType: Item.self)
switch category {
case .Local:
let localItems = fetchedItems
.flatMapLatest { [weak self] (itemList) -> Observable<[Item]> in
return self!.rx_localItems(items: itemList)
}
return localItems
// TODO: Handle other cases like RecentlyAdded, Trending, etc..
}
}
// Helper method to filter items for only local items nearby user.
private func rx_localItems(items: [Item]) -> Observable<[Item]> {
return Observable.create { observable in
observable.onNext(items.filter { $0.location == "LA" })
observable.onCompleted()
return Disposables.create()
}
}
}
You should probably change the name of ServerAPI at this point to something like FetchItemsAPI.
You run into a tricky situation here because your observable can throw an error and once it does throw an error the observable sequence errors out and no more events can be emitted. So to handle subsequent network requests, you must reassign taking the approach you're currently taking. However, this is generally not good for driving UI elements such as a collection view because you would have to bind to the reassigned observable every time. When driving UI elements, you should lean towards types that are guaranteed to not error out (i.e. Variable and Driver). You could make your Observable<[Item]> to be let items = Variable<[Item]>([]) and then you could just set the value on that variable to be the array of items that came in from the new network request. You can safely bind this variable to your collection view using RxDataSources or something like that. Then you could make a separate variable for the error message, let's say let errorMessage = Variable<String?>(nil), for the error message that comes from the network request and then you could bind the errorMessage string to a label or something like that to display your error message.