I have MainViewController that is my UIPageViewController. Ther are 2 views FirstDataViewController and SecondDataViewController that i want to show.
I want to execute my request that updates items in MainViewControllre and then pass it to my 2 child views.
getData is my function that contains data from Request and then updates MainViewController's items
extension MainViewController {
func getData() {
getBasicData() { [weak self] (basicModel) in
guard let strongSelf = self else { return }
strongSelf.getExperienceData() { [weak self] (experienceModel) in
guard let strongSelf = self else { return }
let items = RequestItems(basicData: basicModel,
experienceData: experienceModel)
strongSelf.updateItems(items: items)
}
}
}
}
MainViewController:
class MainViewController: UIPageViewController {
var items: RequestItems
let firstDataViewController: FirstDataViewController
let secondDataViewController: SecondDataViewController
let basicDataManager: APIManagerProtocol
let experienceDataManager: APIManagerProtocol
private(set) lazy var orderedViewControllers: [UIViewController] = {
return [self.firstDataViewController, self.secondDataViewController]
}()
convenience init() {
self.init(with: RequestItems())
}
init(with items: RequestItems) {
self.items = items
let apiManager = APIManager()
basicDataManager = apiManager.createBasicDataManager()
experienceDataManager = apiManager.createExperienceDataManager()
self.firstDataViewController = FirstDataViewController(with: items)
self.secondDataViewController = SecondDataViewController(with: items)
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
self.edgesForExtendedLayout = []
}
func updateItems(items: RequestItems) {
self.items = items
}
}
How to use getData() function to update MainViewController items first and then pass data to child views? Or maybe there is just better option to do this?
It's not a good solution. "GetData" method needs some time to execute completely so you should:
Execute "getData" before load MainViewController, then pass "request items" to it and update yours views.
But I assume that your MainViewController is first controller in your application so you should:
Add completion block to your "getData" method:
func getData(_ completion: [RequestItems] -> ()) {
getBasicData() { [weak self] (basicModel) in
guard let strongSelf = self else { return }
strongSelf.getExperienceData() { [weak self] (experienceModel) in
guard let strongSelf = self else { return }
let items = RequestItems(basicData: basicModel,
experienceData: experienceModel)
completion(items)
}
}
}
Add "updateItems" method to yours view controllers (instead of passing it in init method)
Call your "getData" method with completion handler but outside of MainViewController init
init(with items: RequestItems) {
...
self.items = items
self.firstDataViewController = FirstDataViewController()
self.secondDataViewController = SecondDataViewController()
...
}
func viewDidLoad() {
super.viewDidLoad()
getBasicData { [weak self] (items) in
guard let strongSelf = self else { return }
strongSelf.updateItems(items)
strongSelf.firstDataViewController.updateItems(with: items)
strongSelf.secondDataViewController.updateItems(with: items)
}
}
func updateItems(items: RequestItems) {
self.items = items
}
Related
I have ChatsocketIO class which handles all the SocketIO functions related to the chat functionality of the app. The skeleton of that class looks like below.
My Question is when the Socket receives a message it fires the "socket!.on("msg")", However i am confused how to pass this back to the view controller class which i am calling this API class. I have added the View Controller class as well below..
class ChatServiceAPI {
// MARK: - Properties
var manager: SocketManager? = nil
var socket: SocketIOClient? = nil
//Chat Variables
var items:[Message] = []
// MARK: - Life Cycle
init() {
setupSocket()
setupSocketEvents()
socket?.connect()
}
static let shared = ChatServiceAPI();
func stop() {
socket?.removeAllHandlers()
}
// MARK: - Socket Setup
func setupSocket() {
socket = manager!.defaultSocket;
}
func setupSocketEvents() {
if socket!.status != .connected{
socket!.connect()
}
socket!.on(clientEvent: .connect) { (data, emitter) in
print("==connecting==");
}
socket!.on("msg") { (data, emitter) in
let mesage = Message()
let jsonObject = JSON(data[0])
let messageString: String? = jsonObject["msg"].string
let userIDFrom: Int? = jsonObject["profile"]["id"].int
if(userIDFrom != Int(self.userIDTo)) {
return
}
if( userIDFrom == nil)
{
return
}
mesage.data = JSON(["from_user": self.userIDTo, "to_user": self.userID, "msg": self.convertStringToHtmlCode(msg: messageString ?? ""), "created": Date().description(with: .current)])
self.items.insert(mesage, at: 0)
//self.viewMessagesList.reload(messages: self.items, conversation: self.conversation)
}
socket!.on("disconnect") { (data, emitter) in
print("===disconnect==");
self.isConnected = false
}
socket!.connect();
}
// MARK: - Socket Emits
func register(user: String) {
socket?.emit("add user", user)
}
func send(message: String, toUser: String) {
let data = NSMutableDictionary()
data.setValue(message, forKey: "msg")
data.setValue(toUser.lowercased(), forKey: "to")
socket!.emit("send", data);
return;
}
}
In My View Controller, I have something like below, I want to pass ChatSocketAPI's "self.items" to the below-calling controller, when a msg comes, I am confused about how to do this?
import UIKit
import SocketIO
import SwiftyJSON
import EZAlertController
class MessagingViewController: UIViewController {
let viewMessagesList = MessagesListViewController()
let bottomMenuChatView = BottomMenuChatView(frame: CGRect.zero)
var isAnimation = false
let emojiView = EmojiView(frame: CGRect.zero)
func setupSocketIO() {
ChatServiceAPI.init();
self.socket = ChatServiceAPI.shared.socket;
}
override func viewDidLoad() {
super.viewDidLoad()
self.setupSocketIO()
ChatServiceAPI.shared.page = 0;
ChatServiceAPI.shared.items = []
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.setupNavigation()
if Reachability.isConnectedToNetwork() == false {
EZAlertController.alert("", message: "Please check your internet connection")
return
}
// bottom menu setup
bottomMenuChatView.btnSendMessage.addTarget(self, action: #selector(tappedBtnSendMessage(btn:)), for: .touchUpInside)
bottomMenuChatView.btnEmoji.addTarget(self, action: #selector(tappedBtnEmoji(btn:)), for: .touchUpInside)
bottomMenuChatView.textFieldMessage.delegate = self
}
Make your MessagingViewController a listener for the ChatServiceAPI. You can implement that like this:
Create a protocol like the following:
protocol MessageListener: AnyObject {
func messageReceived(text: String)
}
and make you controller conform to it:
extension MessagingViewController: MessageListener {
func messageReceived(text: String) {
// do whatever you need with the message
}
}
Then, in the ChatServiceAPI you can create a var named listeners:
private var listeners: [MessageListener] = []
and methods for adding and removing listeners:
func add(listener: MessageListener) {
self.listeners.append(listener)
}
func remove(listener: MessageListener) {
self.listeners.remove(listener)
}
For the last part, in your ChatServiceAPI, when the "msg" event is received, you need to send the message to all of your registered listeners. So, something like this:
socket!.on("msg") { (data, emitter) in
...
for listener in self.listeners {
listener.messageReceived(text: ...)
}
}
Now you also need to register your viewController as a listener. So you would call ChatServiceAPI.shared.add(listener: self) in your viewDidLoad.
Don't forget to also call ChatServiceAPI.shared.remove(listener: self) to prevent memory leaks.
You can back data to your view controller by writing socket.ON inside a function that have escaping block.
Here is my sample code that I use
func getResponse(completion: #escaping(_ resMessage: String) -> Void) {
guard let socket = manager?.defaultSocket else {
return
}
socket.on(myEventName) { (dataArray, socketAck) -> Void in
guard let data = UIApplication.jsonData(from: dataArray[0]) else {
return
}
do {
let responseMsg = try JSONDecoder().decode(String.self, from: data)
completion(responseMsg)
} catch let error {
print("Something happen wrong here...\(error)")
completion("")
}
}
}
Then you can call this function getResponse in viewDidLoad inside your viewController. Like this -
self.socketInstance.getResponse { socketResponse in
// socketResponse is the response
// this will execute when you get new response from socket.ON
}
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)
]
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
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?
I have a TableView of notification. I want to refresh by pull to refresh using UIRefreshControl. How to do that with rx-swift? This is my code. Why tableView not refreshed after I set value to variable data
var refreshControl = UIRefreshControl()
var disposeBag = DisposeBag()
let loadingData = ActivityIndicator()
var data: Observable<[Notification]>!
override func viewDidLoad() {
super.viewDidLoad()
self.view = v
v.tableView.registerClass(NotificationsViewCell.self, forCellReuseIdentifier: "Cell")
v.tableView.addSubview(refreshControl)
data = getNotifications()
configureTableDataSource()
configureActivityIndicatorsShow()
refreshControl.rx_controlEvent(.ValueChanged)
.flatMapLatest{ [unowned self] _ in
return self.getNotifications()
.trackActivity(self.loadingData)
}.subscribe(
onNext: {notification in
print("success")
self.data = Observable.just(notification) // NOT REFRESH TABLEVIEW
},
onError: { error in
print("Error \(error)")
},
onCompleted: {() in
print("complete")
},
onDisposed: {() in
print("disposed")
})
.addDisposableTo(disposeBag)
}
func configureTableDataSource(){
data
.retry(3)
.doOnError{ [weak self] error in
self?.v.emptyLabel.hidden = false
self?.v.retryButton.hidden = false
}
.doOnNext{ [weak self] result in
if result.count == 0 {
self?.v.emptyLabel.hidden = false
self?.v.emptyLabel.text = "Tidak ada bisnis favorit"
} else {
self?.v.emptyLabel.hidden = true
self?.v.retryButton.hidden = true
}
}
.trackActivity(loadingData)
.retryWhen{ _ in
self.v.retryButton.rx_tap
}
.asDriver(onErrorJustReturn: [])
.map{ results in
results.map(NotificationsViewModel.init)
}
.drive(v.tableView.rx_itemsWithCellIdentifier("Cell", cellType: NotificationsViewCell.self)) { (index, viewModel, cell) in
cell.viewModel = viewModel
let tap = UITapGestureRecognizer(target: self, action: #selector(self.goToProfile(_:)))
tap.numberOfTapsRequired = 1
cell.photo.tag = index
cell.photo.addGestureRecognizer(tap)
}
.addDisposableTo(disposeBag)
}
func configureActivityIndicatorsShow(){
loadingData
.driveNext{ isLoading in
if !isLoading {
self.v.indicatorView.stopAnimating()
} else {
self.v.indicatorView.startAnimating()
self.v.retryButton.hidden = true
self.v.emptyLabel.hidden = true
}
}
.addDisposableTo(disposeBag)
loadingData.asObservable()
.bindTo(refreshControl.rx_refreshing)
.addDisposableTo(disposeBag)
}
func getNotifications() -> Observable<[Notification]> {
let parameters = [
"token": NSUserDefaults.standardUserDefaults().objectForKey("token")! as! String
]
return string(.POST, NOTIFICATION_LIST, parameters: parameters)
.map { json in
return Notification.parseJSON(JSON.parse(json)["notifications"])
}
.observeOn(MainScheduler.instance)
}
EDIT::
var data = Variable<[Notification]>([])
override func viewDidLoad() {
getNotifications()
.retry(3)
.doOnError{ [weak self] error in
self?.v.emptyLabel.hidden = false
self?.v.retryButton.hidden = false
}
.doOnNext{ [weak self] result in
if result.count == 0 {
self?.v.emptyLabel.hidden = false
self?.v.emptyLabel.text = "Tidak ada notifikasi"
} else {
self?.v.emptyLabel.hidden = true
self?.v.retryButton.hidden = true
}
}
.trackActivity(loadingData)
.retryWhen{ _ in
self.v.retryButton.rx_tap
}
.bindTo(data)
.addDisposableTo(disposeBag)
refreshControl.rx_controlEvent(.ValueChanged)
.flatMapLatest{ [unowned self] _ in
return self.getNotifications()
.doOnError{ [weak self] error in
// This not call after the second pull to refresh if No network connection, so refresh control still appear
self?.refreshControl.endRefreshing()
}
.doOnCompleted{ [weak self] result in
self?.refreshControl.endRefreshing()
}
}.bindTo(data)
.addDisposableTo(disposeBag)
}
func configureTableDataSource(){
datas.asObservable()
.asDriver(onErrorJustReturn: [])
.map{ results in
results.map(NotificationsViewModel.init)
}
.drive(v.tableView.rx_itemsWithCellIdentifier("Cell", cellType: NotificationsViewCell.self)) { (index, viewModel, cell) in
cell.viewModel = viewModel
}
.addDisposableTo(disposeBag)
}
func configureActivityIndicatorsShow(){
loadingData
.driveNext{ isLoading in
if !isLoading {
self.v.indicatorView.stopAnimating()
} else {
self.v.indicatorView.startAnimating()
self.v.retryButton.hidden = true
self.v.emptyLabel.hidden = true
}
}
.addDisposableTo(disposeBag)
}
self.data = Observable.just(notification) is creating a new Observable and sending the new [Notification] element on that Observable, which no one is subscribed to.
You should be using a Subject such as Variable.
// instead of `var data: Observable<[Notification]>!`
let data = Variable<[Notification]>([])
// and then later, when you want to send out a new element:
self.data.value = notification
EDIT: To show you how to use this in conjunction with what you already have.
// this will update `data` upon `refreshControl` value change
refreshControl.rx_controlEvent(.ValueChanged)
.flatMapLatest{ [unowned self] _ in
return self.getNotifications()
}
.bindTo(data)
.addDisposableTo(disposeBag)
// this will update `loadingData` when `data` gets a new element
data.asDriver().trackActivity(self.loadingData)
// bind to your table view
data.asDriver().drive(//.....
Also, consider moving the retry and retryWhen to happen sooner, instead of happening downstream where you currently have it (in the table view binding). Instead, I think it should belong in getNotifications.