Tableview not receiving signals from Driver - ios

I have the following MVVM-C + RxSwift code.
The problem is that the TableView is not receiving any signals. When I
debug the results I can see that the API call is returning what it should, the objects array is populated with objects but the tableview does not show any results. Here is the console output:
2018-11-13 16:12:08.107: searchText -> Event next(qwerty)
Search something: qwerty
2018-11-13 16:12:08.324: viewModel.data -> Event next([])
Could it be the tableview itself? Maybe wrong custom cell setup?
ViewController.swift:
tableView = UITableView(frame: self.view.frame)
tableView.delegate = nil
tableView.dataSource = nil
tableView.register(SearchResultCell.self, forCellReuseIdentifier: "SearchResultCell")
viewModel.data
.debug("viewModel.data", trimOutput: false)
.drive(tableView.rx.items(cellIdentifier: "SearchResultCell")) { row, object, cell in
cell.name.text = object.name
cell.something.text = object.something
}
.disposed(by: disposeBag)
ViewModel.swift:
let disposeBag = DisposeBag()
var searchText = BehaviorRelay(value: "something to search for")
lazy var data: Driver<[Object]> = {
return self.searchText.asObservable()
.debug("searchText", trimOutput: false)
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest(searchSomething)
.asDriver(onErrorJustReturn: [])
}()
func searchSomething(query: String) -> Observable<[Object]> {
print("Search something: \(query)")
let provider = MoyaProvider<APIService>()
var objects = [Object]()
provider.rx.request(.search(query: query)).subscribe { event in
switch event {
case let .success(response):
do {
let responseJSON: NSDictionary = try (response.mapJSON() as? NSDictionary)!
objects = self.parse(json: responseJSON["results"] as Any)
} catch(let error) {
print(error)
}
break
case let .error(error):
print(error)
break
}
}
.disposed(by: disposeBag)
let result: Observable<[Object]> = Observable.from(optional: objects)
return result
}

When using flatMap, you do not want to create nested subscriptions. You will create an Observable that returns the expected result, and flatMap will take care of subscribing to it. In the current state of things, searchSomething will always return an empty array, as Observable.from(optional: objects) will be called before the request has a chance to complete.
Since version 10.0 of Moya, provider will cancel the requests it created when deallocated. Here, it will be deallocated when execution exits searchSomething, hence the network request won't have time to finish. Moving provider's declaration to the view model's level solves this issue.
Here's searchSomething(query: String) -> Observable<[Object]> rewritten.
let provider = MoyaProvider<APIService>()
func searchSomething(query: String) -> Observable<[Object]> {
print("Search something: \(query)")
return provider.rx.request(.search(query: query)).map { (response) -> [Object] in
let responseJSON: NSDictionary = try (response.mapJSON() as? NSDictionary)!
return self.parse(json: responseJSON["results"] as Any)
}
}
Instead of doing the transformation in subscribe, it's done in map, which will be called for every next event, being passed the value associated with the event.

Related

do(onNext:) called twice when table view row is selected

I'm facing a problem when selecting the table view row on RxSwift. For details, the code on the do(onNext:) function is called twice, thus lead to the navigation pushed twice too. Here is my code in the viewModel, please help me resolve it. Thanks so much.
struct Input {
let loadTrigger: Driver<String>
let searchTrigger: Driver<String>
let selectMealTrigger: Driver<IndexPath>
}
struct Output {
let mealList: Driver<[Meal]>
let selectedMeal: Driver<Meal>
}
func transform(_ input: HomeViewModel.Input) -> HomeViewModel.Output {
let popularMeals = input.loadTrigger
.flatMap { _ in
return self.useCase.getMealList()
.asDriver(onErrorJustReturn: [])
}
let mealSearchList = input.searchTrigger
.flatMap { text in
return self.useCase.getMealSearchList(mealName: text)
.asDriver(onErrorJustReturn: [])
}
let mealList = Observable.of(mealSearchList.asObservable(), popularMeals.asObservable()).merge().asDriver(onErrorJustReturn: [])
let selectedMeal = input.selectMealTrigger
.withLatestFrom(mealList) { $1[$0.row] }
.do(onNext: { meal in
self.navigator.toMealDetail(meal: meal)
})
return Output(mealList: mealList, selectedMeal: selectedMeal)
}
Edit: Here's the implemetation on the ViewController:
func bindViewModel() {
self.tableView.delegate = nil
self.tableView.dataSource = nil
let emptyTrigger = searchBar
.rx.text.orEmpty
.filter { $0.isEmpty }
.throttle(0.1, scheduler: MainScheduler.instance)
.asDriver(onErrorJustReturn: "")
let loadMealTrigger = Observable
.of(emptyTrigger.asObservable(), Observable.just(("")))
.merge()
.asDriver(onErrorJustReturn: "")
let searchTrigger = searchBar.rx.text.orEmpty.asDriver()
.distinctUntilChanged()
.filter {!$0.isEmpty }
.throttle(0.1)
let selectMealTrigger = tableView.rx.itemSelected.asDriver()
let input = HomeViewModel.Input(
loadTrigger: loadMealTrigger,
searchTrigger: searchTrigger,
selectMealTrigger: selectMealTrigger
)
let output = viewModel.transform(input)
output.mealList
.drive(tableView.rx.items(cellIdentifier: MealCell.cellIdentifier)) { index, meal, cell in
let mealCell = cell as! MealCell
mealCell.meal = meal
}
.disposed(by: bag)
output.selectedMeal
.drive()
.disposed(by: bag)
}
Firstly, is this RxSwift?
If so, the .do(onNext:) operator provides side effects when you receive a new event via a subscription; Therefore, two "reactions" will happen when a table row is tapped: 1. subscription method and 2. .do(onNext:) event. Unfortunately, I do not have any further insight into your code, so there may be other stuff creating that error aswell.
Good luck!

RxSwift: How to create cache for last network response without creating class/struct property?

I'm working on iOS App that uses the IP Stack API for geolocation. I'd like to optimise the IP Stack Api usage by asking for external (public) IP address first and then re-use lat response for that IP if it hasn't changed.
So what I'm after is that I ask every time the https://www.ipify.org about external IP, then ask https://ipstack.com with given IP address. If I ask the second time but IP doesn't changed then re-use last response (or actually cached dictionary with IP's as keys and responses as values).
I have a solution but I'm not happy with this cache property in my code. It is some state and some other part of code can mutate this. I was thinking about using some scan() operator in RxSwfit but I just can't figure out any new ideas.
class ViewController: UIViewController {
#IBOutlet var geoButton: UIButton!
let disposeBag = DisposeBag()
let API_KEY = "my_private_API_KEY"
let provider = PublicIPProvider()
var cachedResponse: [String: Any] = [:] // <-- THIS
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func geoButtonTapped(_ sender: UIButton) {
// my IP provider for ipify.org
// .flatMap to ignore all nil values,
// $0 - my structure to contains IP address as string
let fetchedIP = provider.currentPublicIP()
.timeout(3.0, scheduler: MainScheduler.instance)
.flatMapLatest { Observable.from(optional: $0.ip) }
.distinctUntilChanged()
// excuse me my barbaric URL creation, it's just for demonstration
let geoLocalization = fetchedIP
.flatMapLatest { ip -> Observable<Any> in
// check if cache contains response for given IP address
guard let lastResponse = self.cachedResponse[ip] else {
return URLSession.shared.rx.json(request: URLRequest(url: URL(string: "http://api.ipstack.com/\(ip)?access_key=\(API_KEY)")! ))
.do(onNext: { result in
// store cache as a "side effect"
print("My result 1: \(result)")
self.cachedResponse[ip] = result
})
}
return Observable.just(lastResponse)
}
geoLocalization
.subscribe(onNext: { result in
print("My result 2: \(result)")
})
.disposed(by: disposeBag)
}
}
Is it possible to achieve the same functionality but without var cachedResponse: [String: Any] = [:] property in my class?
OMG! I spent a bunch of time with the answer for this question (see below) and then realized that there is a much simpler solution. Just pass the correct caching parameter in your URLRequest and you can do away with the internal cache completely! I left the original answer because I also do a general review of your code.
class ViewController: UIViewController {
let disposeBag = DisposeBag()
let API_KEY = "my_private_API_KEY"
let provider = PublicIPProvider()
#IBAction func geoButtonTapped(_ sender: UIButton) {
// my IP provider for ipify.org
let fetchedIP: Maybe<String> = provider.currentPublicIP() // `currentPublicIP()` returns a Single
.timeout(3.0, scheduler: MainScheduler.instance)
.map { $0.ip ?? "" }
.filter { !$0.isEmpty }
// excuse me my barbaric URL creation, it's just for demonstration
let geoLocalization = fetchedIP
.flatMap { (ip) -> Maybe<Any> in
let url = URL(string: "http://api.ipstack.com/\(ip)?access_key=cce3a2a23ce22922afc229b154d08393")!
return URLSession.shared.rx.json(request: URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad))
.asMaybe()
}
geoLocalization
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { result in
print("My result 2: \(result)")
})
.disposed(by: disposeBag)
}
}
Original Answer
The short answer here is no. The best you can do is wrap the state in a class to limit its access. Something like this generic approach:
final class Cache<Key: Hashable, State> {
init(qos: DispatchQoS, source: #escaping (Key) -> Single<State>) {
scheduler = SerialDispatchQueueScheduler(qos: qos)
getState = source
}
func data(for key: Key) -> Single<State> {
lock.lock(); defer { lock.unlock() }
guard let state = cache[key] else {
let state = ReplaySubject<State>.create(bufferSize: 1)
getState(key)
.observeOn(scheduler)
.subscribe(onSuccess: { state.onNext($0) })
.disposed(by: bag)
cache[key] = state
return state.asSingle()
}
return state.asSingle()
}
private var cache: [Key: ReplaySubject<State>] = [:]
private let scheduler: SerialDispatchQueueScheduler
private let lock = NSRecursiveLock()
private let getState: (Key) -> Single<State>
private let bag = DisposeBag()
}
Using the above isolates your state and creates a nice reusable component for other situations where a cache is necessary.
I know it looks more complex than your current code, but gracefully handles the situation where there are multiple requests for the same key before any response is returned. It does this by pushing the same response object to all observers. (The scheduler and lock exist to protect data(for:) which could be called on any thread.)
I have some other suggested improvements for your code as well.
Instead of using flatMapLatest to unwrap an optional, just filter optionals out. But in this case, what's the difference between an empty String and a nil String? Better would be to use the nil coalescing operator and filter out empties.
Since you have the code in an IBAction, I assume that currentPublicIP() only emits one value and completes or errors. Make that clear by having it return a Single. If it does emit multiple values, then you are creating a new chain with every function call and all of them will be emitting values. It's unlikely that this is what you want.
URLSession's json(request:) function emits on a background thread. If you are going to be doing anything with UIKit, you will need to observe on the main thread.
Here is the resulting code with the adjustments mentioned above:
class ViewController: UIViewController {
private let disposeBag = DisposeBag()
private let provider = PublicIPProvider()
private let responses: Cache<String, Any> = Cache(qos: .userInitiated) { ip in
return URLSession.shared.rx.json(request: URLRequest(url: URL(string: "http://api.ipstack.com/\(ip)?access_key=cce3a2a23ce22922afc229b154d08393")!))
.asSingle()
}
#IBAction func geoButtonTapped(_ sender: UIButton) {
// my IP provider for ipify.org
let fetchedIP: Maybe<String> = provider.currentPublicIP() // `currentPublicIP()` returns a Single
.timeout(3.0, scheduler: MainScheduler.instance)
.map { $0.ip ?? "" }
.filter { !$0.isEmpty }
let geoLocalization: Maybe<Any> = fetchedIP
.flatMap { [weak responses] ip in
return responses?.data(for: ip).asMaybe() ?? Maybe.empty()
}
geoLocalization
.observeOn(MainScheduler.instance) // this is necessary if your subscribe messes with UIKit
.subscribe(onSuccess: { result in
print("My result 2: \(result)")
}, onError: { error in
// don't forget to handle errors.
})
.disposed(by: disposeBag)
}
}
I'm afraid that unless you have some way to cache your network responses (ideally with URLRequest's native caching mechanism), there will always be side-effects.
Here's a suggestion to try to keep them contained though:
Use Rx for your button tap as well, and get rid of the #IBAction. It's not great to have all that code in an #IBAction anyway (unless you did that for demonstration purposes).
That way, you can use a local-scope variable inside the setup function, which will only be captured by your flatMapLatest closure. It makes for some nice, clean code and helps you make sure that your cachedResponse dictionary is not tampered by other functions in your class.
class ViewController: UIViewController {
#IBOutlet var geoButton: UIButton!
let disposeBag = DisposeBag()
let API_KEY = "my_private_API_KEY"
let provider = PublicIPProvider()
override func viewDidLoad() {
super.viewDidLoad()
prepareGeoButton()
}
func prepareGeoButton() {
// ----> Use RxCocoa UIButton.rx.tap instead of #IBAction
let fetchedIP = geoButton.rx.tap
.flatMap { _ in self.provider.currentPublicIP() }
.timeout(3.0, scheduler: MainScheduler.instance)
.flatMapLatest { Observable.from(optional: $0.ip) }
.distinctUntilChanged()
// ----> Use local variable.
// Still has side-effects, but is much cleaner and safer.
var cachedResponse: [String: Any] = [:]
let geoLocalization = fetchedIP
.flatMapLatest { ip -> Observable<Any> in
// check if cache contains response for given IP address
guard let lastResponse = cachedResponse[ip] else {
return URLSession.shared.rx.json(request: URLRequest(url: URL(string: "http://api.ipstack.com/\(ip)?access_key=cce3a2a23ce22922afc229b154d08393")! ))
.do(onNext: { result in
print("My result 1: \(result)")
cachedResponse[ip] = result
})
}
return Observable.just(lastResponse)
}
geoLocalization
.subscribe(onNext: { result in
print("My result 2: \(result)")
})
.disposed(by: disposeBag)
}
}
I did not want to change the code too much from what you had and add more implementation details into the mix, but if you choose to go this way, please:
a) Use a Driver instead of an observable for your button tap. More on Drivers here.
b) Use [weak self] inside your closures. Don't retain self as this might lead to your ViewController being retained in memory multiple times when you move away from the current screen in the middle of a network request, or some other long-running action.

Rx Observable that gets value from other Observable

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.

Executing a ViewModel function before setting variables in ViewController

I have an issue with code order in a ItemsViewController.swift
When I run my code it starts the for items loop before my api returns the values for the items. This is done in the line: self.viewModel/getItemsTwo... Therefore it thinks that items is nil by the time the loop starts, so it errors with:
fatal error: unexpectedly found nil while unwrapping an Optional value
How can I start the loop only after items has been filled by the api call/function call?
class ItemsViewController: UIViewController {
private let viewModel : ItemsViewModel = ItemsViewModel()
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.getItemsTwo(self.viewModel.getCurrentUser())
var items = self.viewModel.items
for item in items! {
print(item)
}
}
...
The getItemsTwo function in the viewModel sets the viewModel.items variable when it is called
EDIT 1
ItemsViewModel.swift
...
var items : JSON?
...
func getItemsTwo(user: MYUser) {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = newdata
}
}
...
EDIT 2
I am trying to do this:
just change it in the ViewController to:
var items = self.viewModel.getItemsTwo(self.viewModel.getCurrentUser())
and the ViewModel to:
func getItemsTwo(user: MYUser) -> JSON {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = newdata
}
return self.items
}
But the return statement still errors as if self.items in nil.
Maybe you could expand your getItemsTwo method to take a callback closure, something like:
func getItemsTwo(user: MYUser, callback: (items: [JSON])-> Void)
Meaning that you have a parameter called callback which is a closure function that returns Void and takes an array of JSON items as an input parameter.
Once you have added newdata to self.items you could call your callback closure like so:
func getItemsTwo(user: MYUser, callback: (items: [JSON])-> Void) {
let user_id = user.getUserId()
let url = String(format:"users/%#/items", user_id)
self.get(url).responseJSON { (response) -> Void in
let dataExample = response.data
var newdata = JSON(data: dataExample!)
self.items = new data
//Items are now populated, call callback
callback(items: self.items)
}
}
And then, in your ItemsViewController you could say:
override func viewDidLoad() {
super.viewDidLoad()
self.viewModel.getItemsTwo(self.viewModel.getCurrentUser()) { items in
for item in items {
print(item)
}
}
}
Notice that if you add a closure as the last parameter you can use a so called "Trailing Closure" and place it "outside" or "after" your function as described in this chapter of "The Swift Programming Language".
Hope that helps you (I haven't checked in a compiler so you might get some errors, but then well look at them OK :)).

RxSwift: code working only first time

I'm new in RxSwift. Some strange thing happens in my code.
I have a collection view and
Driver["String"]
Data for binding.
var items = fetchImages("flower")
items.asObservable().bindTo(self.collView.rx_itemsWithCellIdentifier("cell", cellType: ImageViewCell.self)) { (row, element, cell) in
cell.imageView.setURL(NSURL(string: element), placeholderImage: UIImage(named: ""))
}.addDisposableTo(self.disposeBag)
fetchImages
Function returns data
private func fetchImages(string:String) -> Driver<[String]> {
let searchData = Observable.just(string)
return searchData.observeOn(ConcurrentDispatchQueueScheduler(globalConcurrentQueueQOS: .Background))
.flatMap
{ text in // .Background thread, network request
return RxAlamofire
.requestJSON(.GET, "https://pixabay.com/api/?key=2557096-723b632d4f027a1a50018f846&q=\(text)&image_type=photo")
.debug()
.catchError { error in
print("aaaa")
return Observable.never()
}
}
.map { (response, json) -> [String] in // again back to .Background, map objects
var arr = [String]()
for i in 0 ..< json["hits"]!!.count {
arr.append(json["hits"]!![i]["previewURL"]!! as! String)
}
return arr
}
.observeOn(MainScheduler.instance) // switch to MainScheduler, UI updates
.doOnError({ (type) in
print(type)
})
.asDriver(onErrorJustReturn: []) // This also makes sure that we are on MainScheduler
}
Strange thing is this. First time when I fetch with "flower" it works and return data, but when I add this code
self.searchBar.rx_text.subscribeNext { text in
items = self.fetchImages(text)
}.addDisposableTo(self.disposeBag)
It doesn't work. It doesn't steps in flatmap callback, and because of this, doesn't return anything.
It works in your first use case, because you're actually using the returned Driver<[String]> via a bindTo():
var items = fetchImages("flower")
items.asObservable().bindTo(...
However, in your second use case, you aren't doing anything with the returned Driver<[String]> other than saving it to a variable, which you do nothing with.
items = self.fetchImages(text)
A Driver does nothing until you subscribe to it (or in your case bindTo).
EDIT: To make this clearer, here's how you could get your second use case to work (I've avoided cleaning up the implementation to keep it simple):
self.searchBar.rx_text
.flatMap { searchText in
return self.fetchImages(searchText)
}
.bindTo(self.collView.rx_itemsWithCellIdentifier("cell", cellType: ImageViewCell.self)) { (row, element, cell) in
cell.imageView.setURL(NSURL(string: element), placeholderImage: UIImage(named: ""))
}.addDisposableTo(self.disposeBag)

Resources