Passing data between controllers using combine not working - swift

I am starting on combine with swift, but having some hard time (had experience working in swiftui before).
So the question is how to perform certain operation:
lets say i have vc1. and I go to vc2 from there
2.then i start asynchronos network closure and come back to vc1 (by popping out vc2).
Now say i want to ge just a string from vc2's asycnrhoss clousre to vc1 when i am back to vc1.
how can i achieve this?
I want to use publisher of lets say <String, Never>
how can I subscribe in my vc1 and publish or send it from vc2 ?
I am using this approach but its not working, it never comes to code under sink.....
public class Parent {
public static let shared = Parent()
public var publisher = PassthroughSibject<String,Never>()
}
class vc1: ViewController {
func viewdidLoad() {
let subscription = Parent.shared.oublisehr.sink { (result) in
print(result)
}
}
func navigatetoVC1() {
///// some code to navigate to vc1
}
func button() {
self.navigatetoVC1
}
}
class vc2: ViewController {
func viewDidload() {
///
}
func performsomeOperation() {
someasyncoperation(completion: { result in
switch result {
case .success:
//send some data to vc1
Parent.shared.publisher.send("testdata")
case .failure:
//send some data to vc1
})
self.dismisVC2() //some method to pop out vc2
}
}

Your code is almost right, except you are using your Anycancellable inside viewdidload, so its scope is getting exhausted. So use it outside in the view controller as an optional AnyCancellable type.
Below code should work.
class vc1: ViewController {
var subscription = AnyCancellable?
func viewdidLoad() {
self.subscription = Parent.shared.oublisehr.sink { (result) in
print(result)
}
}
func navigatetoVC1() {
///// some code to navigate to vc1
}
func button() {
self.navigatetoVC1
}
}

Related

How to pass a (changing) variable between two view controllers?

I have two view controllers. VC1 & VC2. VC1 is passing a variable which keeps changing & updating every second on VC1, in my case, it is the nearest beacon which is handled by a method in VC1.
VC1 code:
var id: Int = 0 // initializing
// send data to VC2
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let vc2 = segue.destination as? navigationScreenVC else { return }
vc2.id2 = id
}
VC2 code:
var id2: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
print(VC2)
}
It is working where it sends the first value it encounters, but not when the value keeps changing, so I want that to be sent as soon as it triggers a change.
I tried to do didSet{} but it doesn't work that way.
Use a delegate pattern.
In VC2:
protocol VC2Delegate: AnyObject {
var id2: Int { get }
}
class VC2 {
weak var delegate: VC2Delegate?
override func viewDidLoad() {
super.viewDidLoad()
print(delegate?.id2)
}
}
In VC1:
class VC1: UIViewController, VC2Delegate {
...
var id2: Int = 0
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let vc2 = segue.destination as? navigationScreenVC else { return }
vc2.delegate = self
}
...
}
A ViewController should not be managing stuff after it stopped being visible. You should managing this in a separate class and waiting for updates from a delegate, lets say:
protocol BeaconUpdateListener : AnyObject {
func currentBeaconIdWasUpdated(to newValue: Int)
}
class BeaconManager {
struct DelegateWrapper {
weak var delegate : BeaconUpdateListener?
}
static let delegates = [DelegateWrapper]()
static var currentId : Int = -1 {
didSet {
delegates.forEach { (delegate) in
delegate.delegate?.currentBeaconIdWasUpdated(to: currentId)
}
}
}
}
Sample code, missing details. You could make your own or update this one. Now, having that data outside your UI code makes it easier to use from anywhere else, and update that code in the future. This way, you "subscribe" to id updates like this:
BeaconManager.delegates.append(OBJECT_THAT_NEEDS_TO_BE_NOTIFIED)
... update your id like this:
BeaconManager.currentId = 65421687543152
... and wait for updates like this:
class VC2 : ViewController, BeaconUpdateListener {
func currentBeaconIdWasUpdated(to newValue: Int) {
// Do stuff once i receive the update
}
// ...
}

Why is my data not passing between View Controllers using closure?

I am trying to pass data receive from a network call to another view controller when user has clicked on a button. When making printing on the FirstVC, data is in, but when printing the result in the SecondVC, there is no more value. I don' t want to use delegate but closure instead.
Also, when trying to retain the memory cycle, an error appear...
class APIsRuler {
static var oneRecipeFound: ((OneRecipeSearch) -> ())?
}
class FirstVC: UIViewController {
func cellIsClicked(index: Int) {
APIsRuler.shared.getRecipe(from: recipeID) { (success, oneRecipe) in
if success, let oneRecipe = oneRecipe {
APIsRuler.oneRecipeFound?(oneRecipe)
self.performSegue(withIdentifier: "goToSecondVC", sender: self)
}
}
}
}
Class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
APIsRuler.oneRecipeFound = { result in
print(result)
}
}
}
Doing this in SecondVC
APIsRuler.oneRecipeFound = { result in
print(result)
}
and this in first
APIsRuler.oneRecipeFound?(oneRecipe)
have no inner communications , you need to read your data directly from the shared class in the secondVc after the segue or send it in
self.performSegue(withIdentifier: "goToSecondVC", sender: <#Herererere#>)
and implement prepareForSegue
Let’s think about the order in which things are happening:
class APIsRuler {
static var oneRecipeFound: ((OneRecipeSearch) -> ())? // 1
}
class FirstVC: UIViewController {
func cellIsClicked(index: Int) {
APIsRuler.shared.getRecipe(from: recipeID) { (success, oneRecipe) in
if success, let oneRecipe = oneRecipe {
APIsRuler.oneRecipeFound?(oneRecipe) // 2
self.performSegue(withIdentifier: "goToSecondVC", sender: self)
}
}
}
}
Class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
APIsRuler.oneRecipeFound = { result in // 3
print(result)
}
}
}
oneRecipeFound starts out life empty: it is nil.
In FirstVC, the cell is clicked. We call oneRecipeFound. It is still nil, so nothing happens.
In SecondVC, we set the value of oneRecipeFound. Now it has a value, but the call has already happened.
So unless you have a time machine in your pocket, so that you can reverse that order of events somehow, the strategy you’ve outlined is doomed to failure. Of course, if you call oneRecipeFound after setting it, it will work. For example:
Class SecondVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
APIsRuler.oneRecipeFound = { result in
print(result)
}
APIsRuler.oneRecipeFound?(oneRecipe) // prints
}
}

RxSwift Observe changes on model and Make request

I'm trying to learn RxSwift concept and got stuck somewhere unfortunately. There is two different screen connected to my TabBarController. On my SettingsViewController, I'm getting two string values and creating a model, On TransactionListViewController, I need to observe changes on and make a new request to fill list.
On parent tab bar controller, I have a Variable and when didLoadCall I'm subscribing this model with wallet.asObservable().subscribe
On SettingViewController when user presses the login button I'm trying to change UserModel with this code:
if let tabBar = parent?.parent as? TransactionTabBarController{
Observable.just(wallet).bind(to: tabBar.wallet)
}
I realized that onNext function for wallet.asObservable().subscribe is calling.
There is also another wallet model on my TransactionListViewController,
on viewDidLoad function I'm running this code:
wallet.asObservable().subscribe(onNext: { (wallet) in
APIClient.getTransaction(address: wallet.walletAddress)
.observeOn(MainScheduler.instance)
.subscribe(onNext: { (model) in
self.changeModels(items: model.result)
.bind(to: self.transactionTableView.rx.items(dataSource: self.dataSource))
.disposed(by: self.disposeBag)
})
.disposed(by: self.disposeBag)}, onError: nil, onCompleted: nil, onDisposed: nil)
.disposed(by: disposeBag)
I tried to set wallet on TabBar's onNext function and I got crush couple of times on TransactionListViewController.
Can anyone help me with that?
Sadly, your code sample is inscrutable. However, it seems as though you are asking how to transmit data between two view controllers that are connected through a tab bar view controller. Below is one way you could go about doing it...
In order to use this code, you only need to assign a function to TabBarController.logic which takes a TabBarController.Inputs as an input parameter and returns a TabBarController.Outputs. You could make this assignment in the AppDelegate.
The key thing to note in this code is that every ViewController subclass has a struct Inputs, a struct Outputs and a var logic in it.
The Inputs has all the UI elements that a user can input to (e.g., Buttons and TextFields,) and the Outputs has all the UI elements that the user can see (e.g., Label text, isHidden flags.)
The logic var is a closure that contains all the logic for that view controller. Note that it can be assigned to. That means that you can develop and test the logic independently of the view controller and you can provide a view controller with a different logic object if necessary depending on context.
For somewhat more complex example code that uses a Coordinator instead of embedding code in the container view controller, see this repo: https://github.com/danielt1263/RxEarthquake
class TabBarController: UITabBarController {
struct Inputs {
let login: Observable<Void>
}
struct Outputs {
let transactions: Observable<[Transaction]>
}
var logic: (Inputs) -> Outputs = { _ in fatalError("Forgot to set logic.") }
private let bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
let settings = children[0] as! SettingsViewController
let transactionList = children[1] as! TransactionListViewController
let login = PublishSubject<Void>()
let outputs = logic(Inputs(login: login.asObservable()))
let bag = self.bag
settings.logic = { inputs in
inputs.login
.bind(to: login)
.disposed(by: bag)
return SettingsViewController.Outputs()
}
transactionList.logic = { inputs in
return TransactionListViewController.Outputs(transactions: outputs.transactions)
}
}
}
class SettingsViewController: UIViewController {
struct Inputs {
let login: Observable<Void>
}
struct Outputs {
}
var logic: (Inputs) -> Outputs = { _ in fatalError("Forgot to set logic.") }
private let bag = DisposeBag()
#IBOutlet weak var login: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
_ = logic(Inputs(login: login.rx.tap.asObservable()))
}
}
class TransactionListViewController: UIViewController {
struct Inputs {
}
struct Outputs {
let transactions: Observable<[Transaction]>
}
var logic: (Inputs) -> Outputs = { _ in fatalError("Forgot to set logic.") }
private let bag = DisposeBag()
#IBOutlet weak var transactionTableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
let output = logic(Inputs())
let dataSource = MyDataSource()
output.transactions
.bind(to: transactionTableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
}
}

Binding does not trigger event in ViewController using MVVM in Swift 4

I am trying to learn binding and understand the MVVM approach in Swift.
I was expecting the below example to work, essentially someEventHappened is called, this invokes the onEvent closure and my message is logged to the screen.
This does not happen however, nothing is printed and I am a little unsure as to why?
class ViewModal {
public var onEvent: (() -> Void)?
func someEventHappened() -> Void {
onEvent?()
}
}
class ViewController: UIViewController {
lazy var viewModel: ViewModal = {
let viewModal = ViewModal()
return viewModal
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
view.backgroundColor = .purple
viewModel.someEventHappened()
viewModel.onEvent = {
print("something happened")
}
}
}
Just swap assigning onEvent and calling someEventHappened
viewModel.onEvent = {
print("something happened")
}
viewModel.someEventHappened()
this is because you're calling onEvent handler inside someEventHappened and in viewDidLoad you first had called someEventHappened and then assigned onEvent

RxSwift: Observe viewDidLoad from view model without Subjects

I have a dependency problem with my UIViewController and my view model.
Basically I want to listen the viewDidLoad event inside my view model.
At the moment I have a Class A which instantiates view model and UIViewController with parameter the viewModel, so:
let viewModel = ViewModel()
let viewController = UIViewController(viewModel)
and I've created a RxCocoa extension for the viewDidLoad:
var viewDidLoad: Observable<Void> {
return self.sentMessage(#selector(Base.viewDidLoad)).map { _ in Void() }
}
now I'm stuck to bind this rx.viewDidLoad to an observable inside my view model. I am able to do it with Subjects but I want a reactive approach using just Observable.
I know that I could inject rx.viewDidLoad as constructor parameter of the view model but in this way I'd break my architecture and I don't want to allow the UIViewController to instantiate the view model internally but I want to keep it as a injected dependency.
Any suggestions?
Thanks
Solution
Thank to #tomahh I've used this solution:
My view controller:
override func configure(viewModel: ViewModel) {
viewModel.bindViewDidLoad(rx.viewDidLoad)
}
My view model:
func bindViewDidLoad(_ viewControllerDidLoad: Observable<Void>) {
//Create observers which depend on viewControllerDidLoad
}
Because ViewController already knows about view model, it could set a property on ViewModel at initialisation time
class ViewController: UIViewController {
init(_ viewModel: ViewModel) {
viewModel.viewDidLoad = self.rx.viewDidLoad
}
}
And then, observables in ViewModel could be defined as computed property deriving viewDidLoad
struct ViewModel {
var viewDidLoad: Observable<Void> = .never()
var something: Observable<String> {
return viewDidLoad.map { "Huhu, something is guuut" }
}
}
If anybody needs that rx properties here is a ready to use solution, inspired by the code of #marco-santarossa
extension Reactive where Base: UIView {
var willMoveToWindow: Observable<Bool> {
return self.sentMessage(#selector(Base.willMove(toWindow:)))
.map({ $0.filter({ !($0 is NSNull) }) })
.map({ $0.isEmpty == false })
}
var viewWillAppear: Observable<Void> {
return self.willMoveToWindow
.filter({ $0 })
.map({ _ in Void() })
}
var viewWillDisappear: Observable<Void> {
return self.willMoveToWindow
.filter({ !$0 })
.map({ _ in Void() })
}
}
let viewDidAppear = rx.sentMessage(#selector(UIViewController.viewDidAppear(_:)))
.mapToVoid()
.asDriver(onErrorJustReturn: ())