Swift - How do I decode json from a REST API - ios

I am trying to make a GET from a REST API in swift. When I use the print statement (print(clubs)) I see the expected response in the proper format. But in the VC is gives me an empty array.
Here is the code to talk to the API
extension ClubAPI {
public enum ClubError: Error {
case unknown(message: String)
}
func getClubs(completion: #escaping ((Result<[Club], ClubError>) -> Void)) {
let baseURL = self.configuration.baseURL
let endPoint = baseURL.appendingPathComponent("/club")
print(endPoint)
API.shared.httpClient.get(endPoint) { (result) in
switch result {
case .success(let response):
let clubs = (try? JSONDecoder().decode([Club].self, from: response.data)) ?? []
print(clubs)
completion(.success(clubs))
case .failure(let error):
completion(.failure(.unknown(message: error.localizedDescription)))
}
}
}
}
and here is the code in the VC
private class ClubViewModel {
#Published private(set) var clubs = [Club]()
#Published private(set) var error: String?
func refresh() {
ClubAPI.shared.getClubs { (result) in
switch result {
case .success(let club):
print("We have \(club.count)")
self.clubs = club
print("we have \(club.count)")
case .failure(let error):
self.error = error.localizedDescription
}
}
}
}
and here is the view controller code (Before the extension)
class ClubViewController: UIViewController {
private var clubs = [Club]()
private var subscriptions = Set<AnyCancellable>()
private lazy var dataSource = makeDataSource()
enum Section {
case main
}
private var errorMessage: String? {
didSet {
}
}
private let viewModel = ClubViewModel()
#IBOutlet private weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
self.subscriptions = [
self.viewModel.$clubs.assign(to: \.clubs, on: self),
self.viewModel.$error.assign(to: \.errorMessage, on: self)
]
applySnapshot(animatingDifferences: false)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.viewModel.refresh()
}
}
extension ClubViewController {
typealias DataSource = UITableViewDiffableDataSource<Section, Club>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Club>
func applySnapshot(animatingDifferences: Bool = true) {
// Create a snapshot object.
var snapshot = Snapshot()
// Add the section
snapshot.appendSections([.main])
// Add the player array
snapshot.appendItems(clubs)
print(clubs.count)
// Tell the dataSource about the latest snapshot so it can update and animate.
dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}
func makeDataSource() -> DataSource {
let dataSource = DataSource(tableView: tableView) { (tableView, indexPath, club) -> UITableViewCell? in
let cell = tableView.dequeueReusableCell(withIdentifier: "ClubCell", for: indexPath)
let club = self.clubs[indexPath.row]
print("The name is \(club.name)")
cell.textLabel?.text = club.name
return cell
}
return dataSource
}
}

You need to apply a new snapshot to your table view once you have fetched the clubs. Your current subscriber simply assigns a value to clubs and nothing more.
You can use a sink subscriber to assign the new clubs value and then call applySnapshot. You need to ensure that this happens on the main queue, so you can use receive(on:).
self.subscriptions = [
self.viewModel.$clubs.receive(on: RunLoop.main).sink { clubs in
self.clubs = clubs
self.applySnapshot()
},
self.viewModel.$error.assign(to: \.errorMessage, on: self)
]

Related

RxSwift - Receive the same event at two ViewControllers at the same time

Hi i'm just getting started with RxSwift and decided to make simple Currency Exchange application. My app has two view's (allCurrenciesList and userFavouritesView). Basically all logic works, but only if i run networking func every single time one of view didAppear/didLoad. My point is two fetch it only once, and received many times, when necessary. Application fetch dictionary of currencies and in ViewModel pass it to BehaviorSubject, and when view being load/appear it just subscribe it, and use it in UITableView. Thanks
class ListViewModel {
let service: CurrencyService!
var curriencies = BehaviorRelay<[Currency]>(value: [])
var currienciesObservable: Observable<[Currency]> {
return curriencies.asObservable().share()
}
let disposeBag = DisposeBag()
init(service: CurrencyService) {
self.service = service
}
func fetchAllCurrencies() {
self.service.fetchAllSymbols { result in
switch result{
case .success(let currencies):
self.dictionaryIntoArray(currencies: currencies["symbols"] as! [String : Any])
case .failure:
print("error")
}
}
}
private func dictionaryIntoArray(currencies: [String: Any]) {
var currencyArray = [Currency]()
for (symbol, name) in currencies {
currencyArray.append(Currency(symbol: symbol, fullName: name as! String))
}
let sortedArray = currencyArray.sorted { $0.fullName < $1.fullName }
self.curriencies.accept(sortedArray)
}
allCurrenciesList
override func viewDidLoad() {
super.viewDidLoad()
setupView()
configureTableViewDataSource()
tableView.delegate = self
fetchData()
}
private func fetchData() {
viewModel.fetchAllCurrencies() // this func is necceserry everysingle time
viewModel.currienciesObservable.subscribe(onNext: { curriencies in
self.applySnapshot(curriencies: curriencies)
}).disposed(by: disposeBag)
}
userFavouritesView
override func viewDidLoad() {
super.viewDidLoad()
viewModel.fetchAllCurrencies() // this func is necceserry everysingle time
viewModel.currienciesObservable.subscribe(onNext: { allCurencies in
let selectedItems = UserDefaults.standard.array(forKey: "SelectedCells") as? [Int] ?? [Int]()
var currenciesArray: [Currency] = []
selectedItems.forEach { int in
self.pickerValues.append(allCurencies[int])
currenciesArray.append(allCurencies[int])
}
self.applySnapshot(curriencies: currenciesArray)
}).disposed(by: disposeBag)
}
The key here is to not use a Subject. They aren't recommended for regular use. Just define the currienciesObservable directly.
Something like this:
class ListViewModel {
let currienciesObservable: Observable<[Currency]>
init(service: CurrencyService) {
self.currienciesObservable = service.rx_fetchAllSymbols()
.map { currencies in
currencies["symbols"]?.map { Currency(symbol: $0.key, fullName: $0.value as! String) }
.sorted(by: { $0.fullName < $1.fullName }) ?? []
}
}
}
extension CurrencyService {
func rx_fetchAllSymbols() -> Observable<[String: [String: Any]]> {
Observable.create { observer in
self.fetchAllSymbols { result in
switch result {
case let .success(currencies):
observer.onNext(currencies)
observer.onCompleted()
case let .failure(error):
observer.onError(error)
}
}
return Disposables.create()
}
}
}
With the above, every time you subscribe to the currenciesObservable the fetch will be called.
As I understand, it's because your fetchAllSymbols function was not stored in the DisposeBag.
func fetchAllCurrencies() {
self.service.fetchAllSymbols { result in
switch result{
case .success(let currencies):
self.dictionaryIntoArray(currencies: currencies["symbols"] as! [String : Any])
case .failure:
print("error")
}
}.dispose(by: disposeBag)
}

The app crashes when adding new keys in firestore

I made a test application following the example of Google with github, but with a few changes (less keys used in the firestore and less filters).
The problem is this, the app crashing when I added new keys in the firestore, but the app works with two keys previously added.
Crashes and shows error on fatalError("error"). I can not understand why with two keys the application works, but if i begin to use the third key (hall) then the app crashes.
What could be the problem?
It's my code:
class ViewControllerTwo: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet var tableView: UITableView!
private var sweets: [Sweet] = []
private var document: [DocumentSnapshot] = []
fileprivate var query: Query? {
didSet {
if let listener = listener {
listener.remove()
}
}
}
private var listener: FIRListenerRegistration?
fileprivate func observeQuery() {
guard let query = query else { return }
stopObserving()
listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
let models = snapshot.documents.map { (document) -> Sweet in
if let model = Sweet(dictionary: document.data()) {
return model
} else {
fatalError("error")
}
}
self.sweets = models
self.document = snapshot.documents
self.tableView.reloadData()
}
}
#IBAction func filterButton(_ sender: Any) {
present(filters.navigationController, animated: true, completion: nil)
}
lazy private var filters: (navigationController: UINavigationController, filtersController: FilterViewController) = {
return FilterViewController.fromStoryboard(delegate: self)
}()
fileprivate func stopObserving() {
listener?.remove()
}
fileprivate func baseQuery() -> Query {
return Firestore.firestore().collection("sweets").limit(to: 50)
}
override func viewDidLoad() {
super.viewDidLoad()
query = baseQuery()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
observeQuery()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
stopObserving()
}
deinit {
listener?.remove()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return sweets.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ViewControllerCell
let sweet = sweets[indexPath.row]
cell.studioNameLabel.text = sweet.name
cell.studioAddressLabel.text = sweet.content
cell.hallNameLabel.text = sweet.hall
return cell
}
}
extension ViewControllerTwo: FiltersViewControllerDelegate {
func query(withCategory title: String?) -> Query {
var filtered = baseQuery()
if let title = title, !title.isEmpty {
filtered = filtered.whereField("title", isEqualTo: title)
}
return filtered
}
func controller(_ controller: FilterViewController, didSelectCategory title: String?) {
let filtered = query(withCategory: title)
self.query = filtered
observeQuery()
}
}
class ViewControllerCell: UITableViewCell {
#IBOutlet weak var studioNameLabel: UILabel!
#IBOutlet weak var studioAddressLabel: UILabel!
#IBOutlet weak var hallNameLabel: UILabel!
}
And my struct:
protocol DocumentSerializable {
init?(dictionary:[String:Any])
}
struct Sweet {
var name:String
var content:String
var hall:String
var dictionary:[String:Any] {
return [
"name": name,
"content" : content,
"hall" : hall
]
}
}
extension Sweet : DocumentSerializable {
static let title = [
"one",
"two",
"three",
"four"
]
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let content = dictionary["content"] as? String,
let hall = dictionary["hall"] as? String else { return nil }
self.init(name: name, content: content, hall: hall)
}
}
My project in google drive
google drive
google service info.plist
You just need to reinstall app once you add any new key to you existing structure.
So you should decide before structure implementation that what keys you will need. Or you can reinstall app if you add new key in future.

Dynamically filter results with RxSwift and Realm

I have a very simple project, where I want to dynamically filter content in UITableView regarding pressed index in UISegmentedControl. I'm using MVVM with RxSwift, Realm and RxDataSources. So my problem, that if I want to update content in UITableView I need to create 'special' DisposeBag, only for that purposes, and on each selection in UISegmentedControl nil it and create again. Only in this case, if I'm understand right, subscription is re-newed, and UITableView displays new results from Realm.
So is there any better way to do such operation? Without subscribing every time, when I switch tab in UISegmentedControl. Here's my code:
//ViewController
class MyViewController : UIViewController {
//MARK: - Props
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var segmentedControl: UISegmentedControl!
let dataSource = RxTableViewSectionedReloadDataSource<ItemsSection>()
let disposeBag = DisposeBag()
var tableViewBag: DisposeBag!
var viewModel: MyViewModel = MyViewModel()
//MARK: - View lifecycle
override func viewDidLoad() {
super.viewDidLoad()
self.setupRxTableView()
}
//MARK: - Setup observables
fileprivate func setupRxTableView() {
dataSource.configureCell = { ds, tv, ip, item in
let cell = tv.dequeueReusableCell(withIdentifier: "ItemCell") as! ItemTableViewCell
return cell
}
bindDataSource()
segmentedControl.rx.value.asDriver()
.drive(onNext: {[weak self] index in
guard let sSelf = self else { return }
switch index {
case 1:
sSelf.bindDataSource(filter: .active)
case 2:
sSelf.bindDataSource(filter: .groups)
default:
sSelf.bindDataSource()
}
}).disposed(by: disposeBag)
}
private func bindDataSource(filter: Filter = .all) {
tableViewBag = nil
tableViewBag = DisposeBag()
viewModel.populateApplying(filter: filter)
}).bind(to: self.tableView.rx.items(dataSource: dataSource))
.disposed(by: tableViewBag)
}
}
//ViewModel
class MyViewModel {
func populateApplying(filter: Filter) -> Observable<[ItemsSection]> {
return Observable.create { [weak self] observable -> Disposable in
guard let sSelf = self else { return Disposables.create() }
let realm = try! Realm()
var items = realm.objects(Item.self).sorted(byKeyPath: "date", ascending: false)
if let predicate = filter.makePredicate() { items = items.filter(predicate) }
let section = [ItemsSection(model: "", items: Array(items))]
observable.onNext(section)
sSelf.itemsToken = items.addNotificationBlock { changes in
switch changes {
case .update(_, _, _, _):
let section = [ItemsSection(model: "", items: Array(items))]
observable.onNext(section)
default: break
}
}
return Disposables.create()
}
}
}
Don't recall if this is breaking MVVM off the top of my head, but would Variable not be what you're looking for?
Variable<[TableData]> data = new Variable<[TableData]>([])
func applyFilter(filter: Predicate){
data.value = items.filter(predicate) //Any change to to the value will cause the table to reload
}
and somewhere in the viewController
viewModel.data.rx.asDriver().drive
(tableView.rx.items(cellIdentifier: "ItemCell", cellType: ItemTableViewCell.self))
{ row, data, cell in
//initialize cells with data
}

RxSwift+Moya+Moya_ObjectMapper+MJRefresh, refresh failed?

I'm newer to RxSwift. I want to refresh the tableview to show new data.The first request that I can get the data. but when I pull down the tableview, the request didn't finished. I have no ideas about this? My code is belowing:
1: My viewController's code:
class RecommendViewController: UIViewController {
lazy var tableView = DefaultManager.createTableView(HomeImageCell.self,
HomeImageCell.idenfitier)
let disposeBag = DisposeBag()
lazy var viewModel = HomeViewModel()
lazy var dataSource: [HomeListDetailModel] = []
override func viewDidLoad() {
super.viewDidLoad()
viewModel.fetchRecommendList("answer_feed",0)
setupTableView()
configureRefresh()
bindDataToTableView()
}
func setupTableView() {
view.addSubview(tableView)
tableView.snp.makeConstraints { (make) in
make.edges.equalTo(0)
}
tableView.estimatedHeight(200)
}
func bindDataToTableView() {
viewModel.recommend
.observeOn(MainScheduler.instance)
.do(onNext: { [unowned self] model in
print("endAllRefresh")
self.endAllRefresh()
}, onError: { (error) in
self.endAllRefresh()
print("error = \(error)")
})
.map { [unowned self] model in
return self.handleData(model)
}.bind(to: tableView.rx.items(cellIdentifier: HomeImageCell.idenfitier , cellType: HomeImageCell.self )) { index, model, cell in
cell.updateCell(data: model)
}.disposed(by: disposeBag)
}
func configureRefresh() {
tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: { [unowned self] in
let model = self.dataSource[0]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
tableView.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: { [unowned self] in
let model = self.dataSource[self.dataSource.count - 1]
self.viewModel.fetchRecommendList("answer_feed",model.behot_time)
})
}
func endAllRefresh() {
self.tableView.mj_header.endRefreshing()
self.tableView.mj_footer.endRefreshing()
}
func handleData(_ model: HomeListModel) -> [HomeListDetailModel] {
guard let data = model.detailData else {
return dataSource
}
self.dataSource = data
return data
}
}
2: My ViewModel
protocol HomeProtocol {
func fetchRecommendList(_ category: String, _ behot_time: Int)
}
class HomeViewModel: HomeProtocol {
lazy var provider = HTTPServiceProvider.shared
var recommend: Observable<HomeListModel>!
init() {}
init(_ provider: RxMoyaProvider<MultiTarget>) {
self.provider = provider
}
func fetchRecommendList(_ category: String, _ behot_time: Int) {
recommend = provider.request(MultiTarget(HomeAPI.homeList(category: category,behot_time: behot_time)))
.debug()
.mapObject(HomeListModel.self)
}
}
When I made a breakpoint at request method, it didn't do a request? Does anyone know it ? Thanks first
SomeOne told me the reason,So I write it here. In my ViewModel recommend should be backed by PublishSubject or BehaviourSubject or ReplaySubject and then I should share this for View as Observable. In fetchRecommentList method I should bind request to created Subject.
Now I have created observable, but request will run after subsribe or bind

Firebase not updating values

I'm using a FirebaseManager singleton class to fetch data from Firebase. I'm calling getAllCharts in my ViewController, which needs this data. I receive the data initially, but when I update my data in firebase it won't update the values in the app (getAllCharts() is never called).
FirebaseManager:
final class FirebaseManager {
static let sharedInstance = FirebaseManager()
private init() {}
private let reference = FIRDatabase.database().reference()
private var charts = [ChartRepresentable]()
func getAllCharts(completionHandler: ((_ charts: [ChartRepresentable]) -> ())? = nil) {
guard let path = FirebasePath.allCharts.path else {
return
}
guard charts.isEmpty else {
completionHandler?(charts)
return
}
reference.child(path).observe(.value, with: { (snapshot) in
if snapshot.hasChildren() {
var availableCharts = [ChartRepresentable]()
for child in snapshot.children {
guard let chartSnapshot = child as? FIRDataSnapshot else {
continue
}
switch chartSnapshot.key {
case "bar":
availableGraphs.append(BarChart(snapshot: chartSnapshot))
case "line":
availableGraphs.append(LineChart(snapshot: chartSnapshot))
default: break
}
}
self.charts = availableCharts
completionHandler?(self.charts)
}
})
}
}
ViewController:
class ViewController: UIViewController {
var setupCards: [SetupCard]?
override func viewDidLoad() {
super.viewDidLoad()
FirebaseManager.sharedInstance.getAllCharts { [weak self] (charts) in
self?.setupCards = [SetupCard(charts: charts)]
self?.setupView.collectionView.reloadData()
self?.setupView.pageControl.numberOfPages = self?.setupCards?.count ?? 0
}
}
How do I get the updates?

Resources