RxSwift Subscriber receiving multiple events - ios

Consider the below code.
On tapButton, we subscribe to an Observable isFetched and then call fetchPopularMovies().
fetchPopularMovies() in turn calls an API. When the response is received, we will send OnNext(true) event.
Problem is, I receive multiple events on 2nd button tap onwards. If I add onCompleted(), I don't even receive events on 2nd button tap onwards. My expectation is that one event will be triggered on each button tap. What am I missing here?
class ViewController: UIViewController {
let popularMoviesURL = URL(string: "https://api.themoviedb.org/3/movie/popular?api_key=API_KEY")
var isFetched = BehaviorSubject<Bool?>(value:nil)
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func tapButton(_ sender: Any) {
let observable = isFetched.asObservable()
observable.subscribe(onNext: { observer in
guard let result = observer else { return }
print(result)
print("onNext Recieved")
}, onError: { _ in
print("onError Recieved")
}).disposed(by: disposeBag)
fetchPopularMovies()
}
func fetchPopularMovies() {
let task = URLSession.shared.dataTask(with: popularMoviesURL!) {(data, response, error) in
guard let _ = data else { return }
self.isFetched.onNext(true)
//self.isFetched.onCompleted()
}
task.resume()
}
}

Reactive code is declarative. It's "setup" code. So it should be placed where the comment says "Do any additional setup after loading the view."
The simplest change you can do to fix the problem you are having is to move the subscription into the viewDidLoad method as Satish Patel referenced in his comment.
override func viewDidLoad() {
super.viewDidLoad()
isFetched.subscribe(onNext: { observer in
guard let result = observer else { return }
print(result)
print("onNext Recieved")
}, onError: { _ in
print("onError Recieved")
}).disposed(by: disposeBag)
}
#IBAction func tapButton(_ sender: Any) {
fetchPopularMovies()
}
(Note that Subjects should always be held with lets never vars.)
If you use RxCocoa, you can simplify this code even more:
class ViewController: UIViewController {
let button = UIButton()
let isFetched = BehaviorSubject<Bool?>(value:nil)
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let popularMoviesURL = URL(string: "https://api.themoviedb.org/3/movie/popular?api_key=API_KEY")!
let fetchedData = button.rx.tap
.flatMapLatest {
URLSession.shared.rx.data(request: URLRequest(url: popularMoviesURL))
.catch { error in
print("onError Recieved")
return Observable.empty()
}
}
fetchedData
.map { _ in true }
.bind(to: isFetched)
.disposed(by: disposeBag)
}
}
Now, all of your code is "setup code" so it all goes in viewDidLoad.

Related

Protocol-Delegate pattern not notifying View Controller

My Model saves data to Firestore. Once that data is saved, I'd like it to alert my ViewController so that a function can be called. However, nothing is being passed to my ViewController.
This is my Model:
protocol ProtocolModel {
func wasDataSavedSuccessfully(dataSavedSuccessfully:Bool)
}
class Model {
var delegate:ProtocolModel?
func createUserAddedRecipe(
docId:String,
completion: #escaping (Recipe?) -> Void) {
let db = Firestore.firestore()
do {
try db.collection("userFavourites").document(currentUserId).collection("userRecipes").document(docId).setData(from: recipe) { (error) in
print("Data Saved Successfully") // THIS OUTPUTS TO THE CONSOLE
// Notify delegate that data was saved to Firestore
self.delegate?.wasDataSavedSuccessfully(dataSavedSuccessfully: true)
}
}
catch {
print("Error \(error)")
}
}
}
The print("Data Saved Successfully") outputs to the console, but the delegate method right below it doesn't get called.
And this is my ViewController:
class ViewController: UIViewController {
private var model = Model()
override func viewDidLoad() {
super.viewDidLoad()
model.delegate = self
}
}
extension ViewController: ProtocolModel {
func wasDataSavedSuccessfully(dataSavedSuccessfully: Bool) {
if dataSavedSuccessfully == true {
print("Result is true.")
}
else {
print("Result is false.")
}
print("Protocol-Delegate Pattern Works")
}
}
Is there something I'm missing from this pattern? I haven't been able to notice anything different in the articles I've reviewed.
So I test your code and simulate something like that
import UIKit
protocol ProtocolModel {
func wasDataSavedSuccessfully(dataSavedSuccessfully:Bool)
}
class Model {
var delegate:ProtocolModel?
// I use this timer for simulate that firebase store data every 3 seconds for example
var timer: Timer?
func createUserAddedRecipe(
docId:String) {
timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true, block: { _ in
self.delegate?.wasDataSavedSuccessfully(dataSavedSuccessfully: true)
})
}
}
class NavigationController: UINavigationController {
var model = Model()
override func viewDidLoad() {
super.viewDidLoad()
model.delegate = self
// Call this method to register for network notification
model.createUserAddedRecipe(docId: "exampleId")
}
}
extension NavigationController: ProtocolModel {
func wasDataSavedSuccessfully(dataSavedSuccessfully: Bool) {
print(#function)
}
}
so you can see the result as image below, my delegate update controller that conform to that protocol.

Cannot transfer Data from VC1 to VC2 using protocols in Swift

well, im using 2 VCs, one with a textField where the user inputs the CityName, and another VC where it takes care of all the UI elements(like the temp, cityname, etc..), now I use also a NetWorkManager to take care of all the networking&JSON stuff.
the problem is im trying to transfer the data from the NetWorkManager to VC1 but for some reason the delegate aint working :( - basically the road should be like this : VC2 -> NetWorkManager -> VC1.
Here's my Code:
import Foundation
protocol NetworkManagerDelegate {
func didUpdateWeather(weather: WeatherModel)
}
struct NetworkManager {
let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=2da9980c9a43e21c2cdb1f28316d151d&units=metric"
var delegate: NetworkManagerDelegate?
func fetchWeather(cityName: String) {
let urlString = "\(weatherURL)&q=\(cityName)"
performRequest(urlString: urlString)
}
func performRequest(urlString: String) {
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, respone, error) in
if error != nil {
print(error!)
}
if let safeData = data {
if let weather = self.parseJSON(weatherData: safeData) {
print("Im not nil")
self.delegate?.didUpdateWeather(weather: weather)
}
}
}
task.resume()
}
}
func parseJSON(weatherData: Data) -> WeatherModel? {
let decoder = JSONDecoder()
do {
let decodedData = try decoder.decode(WeatherDataModel.self, from: weatherData)
let id = decodedData.weather[0].id
let cityName = decodedData.name
let temp = decodedData.main.temp
let weather = WeatherModel(conditionId: id, cityName: cityName ,temperatrue: temp)
print("Temp is: \(weather.temperatrueString)")
return weather
} catch {
print(error)
return nil
}
}
}
VC2:
import UIKit
import Foundation
class WeatherByCityController: UIViewController, UITextFieldDelegate {
// func didUpdateWeather(weather: WeatherModel) {
// print("Hi")
// }
//
#IBOutlet weak var cityTextField: UITextField!
#IBOutlet weak var updateWeatherBtn: UIButton!
var netWorkManager = NetworkManager()
override func viewDidLoad() {
super.viewDidLoad()
// netWorkManager.delegate = self
cityTextField.delegate = self
}
#IBAction func closeButtonTapped(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
#IBAction func updateWeatherByCityTapped(_ sender: UIButton) {
//Calling delegate to update the City:
//Dismiss the VC:
dismiss(animated: true, completion: nil)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
cityTextField.endEditing(true)
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
if let city = cityTextField.text {
netWorkManager.fetchWeather(cityName: city)
}
cityTextField.text = ""
}
}
VC1:
import UIKit
import Foundation
import CoreLocation
class WeatherScreen: UIViewController,NetworkManagerDelegate {
//Objects outlets:
#IBOutlet weak var conditionIcon: UIImageView!
#IBOutlet weak var tempLabel: UILabel!
#IBOutlet weak var cityLabel: UILabel!
//TableView Outlet:
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var segeControl: UISegmentedControl!
var models = [DailyWeatherEntry]()
var hourlyModels = [HourlyWeatherEntry]()
var netWorkManager = NetworkManager()
override func viewDidLoad() {
netWorkManager.delegate = self
tableView.register(HourlyTableViewCell.nib(), forCellReuseIdentifier: HourlyTableViewCell.identifier)
tableView.dataSource = self
}
override func viewWillAppear(_ animated: Bool) {
// Load things once the view will appear
}
#IBAction func locationBtnTapped(_ sender: UIButton) {
//Asking the user for a permission for using his location:
}
func didUpdateWeather(weather: WeatherModel) {
print("Hi")
}
}
extension WeatherScreen: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//creating the cell:
let cell = tableView.dequeueReusableCell(withIdentifier: "weatherCell", for: indexPath) as! HourlyTableViewCell
//Cell Configure:
cell.textLabel!.font = UIFont.systemFont(ofSize: 10)
return cell
}
}
Where is the code for the VC1 ? Without the code for VC 1 it is hard to give an answer. However i'll try to answer as this might be the scenario.
So basically what you are trying to do is make a network call from the VC2 and whatever the response it should be updated in VC1 which is already active somewhere else. Here you just have to set the delegate of the NetworkManger to the VC1 instance. So you have to get the instance of VC1 in VC2.
var netWorkManager = NetworkManager()
//Get this instance in your code
var vc1: UIViewController!
override func viewDidLoad() {
super.viewDidLoad()
// Here you will be setting the delegate
// to VC1 where you will be having the delegate methods
netWorkManager.delegate = vc1
cityTextField.delegate = self
}
While this method works I would not recommend using the network manager in such a way. Try to use completion handlers instead of delegates to get the data and then pass that values between the view controllers.
Edited:
Pass completion like this in the Network Manager performRequest function.
func performRequest(urlString: String, completion: #escaping (Bool, String?, Error?) -> Void) {
guard let url = URL(string: urlString) else {
completion(false, nil, NSError(domain: "URLString is not a valid URL", code: 100, userInfo: nil))
return
}
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, respone, error) in
guard let safeData = data, let weather = self.parseJSON(weatherData: safeData) else {
print("Empty data or JSON parse error")
completion(false, nil, error)
}
print("Im not nil")
completion(true, weather, nil)
}
task.resume()
}
And call the api request in the VC2.
#IBAction func updateWeatherByCityTapped(_ sender: UIButton) {
//Calling the api request. Pass your url string here
self. netWorkManager.performRequest(urlString: "") { (success, weather, error) in
guard success else {
print(error as Any)
return
}
// Here you have got the weather data.
// Don't know what is weather model. so simply passing the weather string.
self.delegate.didUpdateWeather(weather: weather)
//Dismiss the VC:
dismiss(animated: true, completion: nil)
}
}
Here adopt the NetworkManagerDelegate to VC1 and before presenting the VC2 set the delegate to VC1. Or if you are not presenting the VC2 from VC1 then use UserNotifications to send the weather data to the VC1.
This line of code var netWorkManager = NetworkManager() creates a new instance of network manager each time it is invoked.
In your case, the network manager in VC1 will not get called when the network manager you created in VC2 receives a network response. They are two separate entities.
There a few things you can think about:
If I understand your scenario correctly, your VC2 is used to get a city name from the user. Does VC2 really need to make a network call? You could restrict VC2 to only fetch the city name.
Make the networkManager a singleton. You can then call it from multiple places in your code. The networkManger can have method to 'fetch' and it can take in a completionHandler (as #Raja Vijaya kumar) had suggested.

Swift main loop for updating a view

I'm new to Swift and trying to implement a simple test Single-View app: Upon running, it should keep appending text lines in a UITextView automatically.
However, all I get is a blank screen with the following code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var m_logView: UITextView!
private let m_log = Log()
override func viewDidLoad() {
super.viewDidLoad()
addLog(msg: "Hello World!")
while true {
m_log.requestLog{ [weak self] (data: String) in
self?.consumeLog(log: data)
}
}
}
func consumeLog(log: String) {
addLog(msg: log)
}
func addLog(msg: String) {
m_logView.text += msg + "\n"
}
}
class Log {
func requestLog(consume: (_ log: String) -> Void) {
let log = "Data from wherever"
consume(log)
}
}
I suspected that I'm trying to update the UITextView at a wrong place. The above code simply blocks the viewDidLoad() from completing itself and showing the UITextView.
So I found viewDidAppear, but this time it gets stuck there and shows only "Hello World!" in the view.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var m_logView: UITextView!
private let m_log = Log()
override func viewDidLoad() {
super.viewDidLoad()
addLog(msg: "Hello World!")
}
override func viewDidAppear(_ animated: Bool) {
while true {
m_log.requestLog{ [weak self] (data: String) in
self?.consumeLog(log: data)
}
}
}
func consumeLog(log: String) {
addLog(msg: log)
}
func addLog(msg: String) {
m_logView.text += msg + "\n"
}
}
class Log {
func requestLog(consume: (_ log: String) -> Void) {
let log = "Data from wherever"
consume(log)
}
}
Most iOS MVC tutorials I found teach how to implement the user interactions, but stay away from how to update the view by programmtically changing the model from the backend.
I'm probably looking for a callback/delegate where I can refresh UITextView linked with its data model whenever the data changes, but where is it?
The while loop is completely inappropriate because it causes a deadlock.
Use a Timer which works asynchronously
Replace
while true {
m_log.requestLog{ [weak self] (data: String) in
self?.consumeLog(log: data)
}
}
with
Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { [weak self] _ in
m_log.requestLog{ data in
self?.consumeLog(log: data)
}
}
And you might update the UI explicitly on the main thread
func addLog(msg: String) {
DispatchQueue.main.async {
m_logView.text += msg + "\n"
}
}
Note:
According to the Swift naming convention name variables lowerCamelCased for example mLogView

Customize FUIAuthPickerViewController

How can I customize the Firebase UI Auth Picker controller with custom buttons, custom actions, background, loader etc..
I already try to subclass the FUIAuthPickerViewController but we can't access to login buttons
This is how you can create your own class of FUIAuthPickerViewController:
Create FUICustomLoginController.swift with:
import UIKit
import FirebaseUI
import FirebaseAuth
class FUICustomLoginController: ViewController {
var authUI: FUIAuth! = FUIAuth.defaultAuthUI()
var auth: Auth = Auth.auth()
private func didSignIn(auth: AuthCredential?, error: Error?, callBack: AuthResultCallback?) {
let callBack: (AuthDataResult?, Error?) -> Void = { [unowned self] result, error in
callBack?(result?.user, error)
self.authUI.delegate?.authUI?(self.authUI, didSignInWith: result, error: error)
}
if let auth = auth {
self.auth.signInAndRetrieveData(with: auth, completion: callBack)
} else if let error = error {
callBack(nil, error)
}
}
func signIn<T: FUIAuthProvider>(type: T.Type, defaultValue: String? = nil) {
try? self.authUI.signOut() // logout from google etc..
self.authUI.providers.first(where: { $0 is T })?.signIn(withDefaultValue: defaultValue, presenting: self, completion: self.didSignIn)
}
}
Subclass your controller from FUICustomLoginController:
class LoginPickerController: FUICustomLoginController {
override func viewDidLoad() {
super.viewDidLoad()
// Customize authUI if needed
//self.authUI.providers = ...
self.authUI.delegate = self
}
#IBAction func loginFacebook(_ sender: Any) {
self.signIn(type: FUIFacebookAuth.self)
}
#IBAction func loginGoogle(_ sender: Any) {
self.signIn(type: FUIGoogleAuth.self)
}
#IBAction func loginPhone(_ sender: Any) {
self.signIn(type: FUIPhoneAuth.self)
}
}
extension LoginPickerController: FUIAuthDelegate {
func authUI(_ authUI: FUIAuth, didSignInWith authDataResult: AuthDataResult?, error: Error?) {
// perform login actions
}
}
You can customize the default buttons, add images etc.. (a working hack )
class SignInViewController: FUIAuthPickerViewController {
weak var delegate: signInProtocol?
// Unhashed nonce.
fileprivate var currentNonce: String?
var backgView: UIView?
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .clear
for each in view.subviews[0].subviews[0].subviews[0].subviews {
if let button = each as? UIButton {
button.layer.cornerRadius = 20.0
button.layer.masksToBounds = true
///do any other button customization here
}
}
///add background image
let scrollView = view.subviews[0]
scrollView.backgroundColor = .clear
let contentView = scrollView.subviews[0]
contentView.backgroundColor = .clear
let background = UIImage(named: "imagename")
let backgroundImageView = UIImageView(image: background)
backgroundImageView.contentMode = .scaleToFill
view.insertSubview(backgroundImageView, at: 0)
}
}

swift - performSegue not working in RxSwift Observable subscribe

I have the following code:
loginViewModel.facebookLogin
.asObservable()
subscribe() { [unowned self] facebookLogin in
if let isLoggedIn = facebookLogin.element?.isLoggedIn {
if isLoggedIn {
elf.performSegue(withIdentifier: "toRestaurantSelect", sender: self)
}
}
if let didLoginFail = facebookLogin.element?.didLoginFail {
self.errorLabel.isHidden = !didLoginFail
}
}
.disposed(by: disposeBag)
The facebookLogin is a Variable and is updated when the user logs in. However, the performSegue is not called (the condition is true). Strangely enough, if I turn on Slow animations in the emulator the segue is executed. When Slow animations are turned off the segue doesn't execute (the Facebook login works). Any help is appreciated. Thanks!
i think you should use the main thread to make it work
loginViewModel.facebookLogin
.asObservable()
.subscribe() { [unowned self] facebookLogin in
if let isLoggedIn = facebookLogin.element?.isLoggedIn {
if isLoggedIn {
DispatchQueue.main.async {
self.performSegue(withIdentifier: "toRestaurantSelect", sender: self)
}
}
}
if let didLoginFail = facebookLogin.element?.didLoginFail {
self.errorLabel.isHidden = !didLoginFail
}
}
.disposed(by: disposeBag)
Do the observation with the main scheduler:
loginViewModel.facebookLogin
.asObservable()
// Switch to the main scheduler
.observeOn(MainScheduler.instance)
subscribe() { [unowned self] facebookLogin in
if let isLoggedIn = facebookLogin.element?.isLoggedIn {
if isLoggedIn {
elf.performSegue(withIdentifier: "toRestaurantSelect", sender: self)
}
}
if let didLoginFail = facebookLogin.element?.didLoginFail {
self.errorLabel.isHidden = !didLoginFail
}
}
.disposed(by: disposeBag)

Resources