How to treat Disposables correct in project with RxSwift? - ios

When I started to use RxSwift I used to create BaseViewController and extend it with all my controllers where I use RxSwift.
The code of BaseViewController.swift:
class BaseViewController: UIViewController {
var mSubscriptions: CompositeDisposable?
func addSubscription(subscription: Disposable){
if(mSubscriptions == nil){
mSubscriptions = CompositeDisposable()
}
if let mSub = mSubscriptions{
mSub.addDisposable(subscription)
}
}
func unsubscribeAll(){
if let mSub = mSubscriptions{
mSub.dispose()
mSubscriptions = nil
}
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
unsubscribeAll()
}
deinit{
unsubscribeAll()
}
}
And I use addSubscription(:_) method everywhere in my child controllers. For example a piece of code from:
class TasksViewController: BaseViewController{
overrided func viewWillAppear(){
//...
var subscribe = dataLoader.load(requestNetwork, dataManager: taskDataManager)
.observeOn(ConcurrentDispatchQueueScheduler(queue: queue))
.subscribe({ (event) -> Void in
//...
})
addSubscription(subscribe!)
}
}
What if I do not use BaseViewController and just create an instance of DisposeBag() in every controller and add all my subscriptions to that disposeBag? And how should I treat Disposables correct?

You could just add a let disposeBag = DisposeBag() property to your view controllers. Adding Disposables to that is all you need to do. DisposeBag is like a CompositeDisposeBag that will dispose the Disposables for you when DisposeBag is deallocated (which will happen when the UIViewController is deallocated). There's no need to manage it manually.
However, you can continue to use a subclass if you wanted:
class BaseViewController: UIViewController {
let disposeBag = DisposeBag()
}
And then to use it:
override func viewDidLoad() {
super.viewDidLoad()
Observable.just(42)
.subscribeNext { i in
print(i)
}
.addDisposableTo(disposeBag)
}
This is in fact what ViewController base class does in RxExample:
property in ViewController
Usage in a subclass
If you're actually wanting to be able to deallocate everything manually (as you were doing with unsubscribeAll), then you can just set the disposeBag to nil or a new DisposeBag so that it gets deallocated: disposeBag = DisposeBag() or disposeBag = nil.

Related

Should I use a PublishSubject if I am notifying an action is complete?

I am learning Viper w/ RxSwift.
I would like to notify my Presenter that viewDidLoad was called in my ViewController.
To do this I have the following:
class LoginPresenter {
weak var view: LoginView?
var interactor: LoginUseCase?
var router: LoginRouter?
private(set) var viewDidLoad = PublishSubject<Void>()
private lazy var disposeBag = DisposeBag()
required init(view: LoginView?, interactor: LoginUseCase?, router: LoginRouter?) {
self.view = view
self.interactor = interactor
self.router = router
viewDidLoad
.subscribe(onNext: { _ in
// do something on viewDidLoad
}).disposed(by: disposeBag)
}
}
class LoginViewController: UIViewController {
var presenter: LoginPresenter?
override func viewDidLoad() {
super.viewDidLoad()
presenter?.viewDidLoad.onNext(())
}
}
Once my view is loaded I am calling presenter?.viewDidLoad.onNext(())
I am then able to trigger any actions within my presenter, such as calling out to my router to ensure navigation is configured or my interactor.
Should I be using a PublishSubject for this? Or does RxSwift have a better suited type?
I feel like this approach means I will end up with something like
viewDidLoad
.subscribe(onNext: { _ in
self.router?.viewDidLoad.onNext(())
}).disposed(by: disposeBag)
Hmm... A Presenter's job is to gather up user actions and I'm not so sure we should consider viewDidLoad a user action. And in any case, the Wireframe (which handles routing) shouldn't need to know when viewDidLoad is called in the first place; its job is to present new screens and you can't present a screen in viewDidLoad.
That said, you can setup your connection in the ViewController's presenter didSet:
final class ViewController: UIViewController {
var presenter: Presenter? {
didSet {
guard let presenter = presenter else { viewDidLoadDisposable.dispose(); return }
viewDidLoadDisposable.disposable = rx.methodInvoked(#selector(viewDidLoad))
.map { _ in }
.bind(to: presenter.viewDidLoad)
}
}
let viewDidLoadDisposable = SerialDisposable()
deinit {
viewDidLoadDisposable.dispose()
}
}
final class Presenter {
let viewDidLoad = PublishSubject<Void>()
}
In general though, it is in the viewDidLoad where the presenter and viewController elements are normally bound together so the above code has a pretty unnatural feel.
Also, Observables, Subjects and the DisposeBag should not be vars, use lets instead. That's the "functional" part of functional reactive programming.

Delegate not executing after call swift

I have a viewController with another containerView insider set up to appear temporarily (added programmatically). The containerView is a sort of operation bar, which allows you to change values of the viewController. The protocol called from an IBAction of a button however, does not call the protocol set up inside the viewController class.
Here is the code from both classes:
class viewController: UIViewController, updateListDelegate {
let dataSource = containerView()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.delegate = self
}
func updateList(sender: containerView) {
print("is called") //is not printed
}
}
The code from the containerView:
protocol updateListDelegate {
func updateList(containerView)
}
class containerView: UIViewController {
var delegate: updateListDelegate?
#IBAction func AddSong(_ sender: UIButton) {
self.delegate?.updateList(sender: self)
}
}
If this method is only to be called from one object, then, in my opinion, I would not define a protocol. If multiple objects are to call this method, then I would define a protocol. This is typically how you would call a method backwards, using a basic delegate.
class ViewController: UIViewController {
let container = ContainerView()
override func viewDidLoad() {
super.viewDidLoad()
container.viewControllerDelegate = self
// push to this instance of container at some point
}
func doSomething() {
print("great success")
}
}
class ContainerView: UIViewController {
weak var viewControllerDelegate: ViewController?
#objc func someAction() {
if let viewControllerDelegate = viewControllerDelegate {
viewControllerDelegate.doSomething()
}
}
}
// prints "great success" when someAction() called
One of the most common mistakes people make is not keeping track of instances. For delegates to work, you must be sure you are using the specific instances that you've instantiated and assigned those delegates to.

How to avoid force type casting view model instance in child view controllers - MVVM

I am new to MVVM, am little confused how to avoid typecasting a view model instance in child view controllers. To explain it better am adding the code below.
Now I know that each ViewController is supposed to have a view model and it should be initialized in init or awakeFromNib.So I declared a protocol as
public protocol ViewModelCreatorProtocol : class {
var viewModel : BaseControllerViewModel! {get}
func createViewModel()
}
and my BaseControllerViewModel looks like
public class BaseControllerViewModel {
//some variables common to all child class
}
Now I have a BaseViewController which confirms to this protocol and all other VCs in my case will extend from BaseViewController
class BaseViewController : UIViewController, ViewModelCreatorProtocol {
var viewModel: BaseControllerViewModel!
lazy var disposeBag : DisposeBag = {
return DisposeBag()
}()
override func awakeFromNib() {
super.awakeFromNib()
self.createViewModel()
}
override func viewDidLoad() {
super.viewDidLoad()
}
func createViewModel() {
viewModel = BaseControllerViewModel()
}
func hookupViewModelObservables() {
//hookup observables
}
}
As you can see BaseViewController provides some common properties like disposeBag to all childViewControllers and also calls createViewModel in awakeFromNib and ensures that viewModel is always instantiated even before ViewController is loaded. (Lets assume for now that all my viewModels are initialized using a default init and has no param passed to them)
Issue:
Now I create a ChildViewController lets say AudioPlayerViewController
class AudioPlayerViewController: BaseViewController {
//override createViewModel of parent to pass my ViewController specific view model
override func createViewModel() {
self.viewModel = AudioPlayerViewModel()
}
}
And obviously my AudioPlayerViewModel extends from BaseControllerViewModel as
class AudioPlayerViewModel : BaseControllerViewModel {
var totalDuration : Int = 0
var timeObserver : Any? = nil
//some more variables and logics
}
Everything works fine, but now If I have to access totalDuration in my AudioPlayerViewController I have to access it as
(self.viewModel as! AudioPlayerViewModel).totalDuration
This makes my entire code base in ViewController to be filled with (self.viewModel as! AudioPlayerViewModel) which I think is running my code readability.
Is there a better way to solve this?
Did you try to use generics? So BaseViewController will have a generic type that must inherit the BaseControllerViewModel. Something like this:
public class BaseControllerViewModel {
required public init() {
// needed to do T() in view model creation.
}
}
class BaseViewController<T: BaseControllerViewModel> : UIViewController, ViewModelCreatorProtocol {
var viewModel: T! // this will change based on the type you pass in subclass
lazy var disposeBag : DisposeBag = {
return DisposeBag()
}()
override func awakeFromNib() {
super.awakeFromNib()
self.createViewModel()
}
override func viewDidLoad() {
super.viewDidLoad()
}
func createViewModel() {
viewModel = T() // instantiate the view model
}
func hookupViewModelObservables() {
//hookup observables
}
}
And for example the concrete classes will be:
class ExampleVcViewModel: BaseControllerViewModel {
var subclassVar: Int?
}
class EampleVc: BaseViewController<ExampleVcViewModel> {
override func viewDidLoad() {
super.viewDidLoad()
print(viewModel.subclassVar) // no need to cast
}
}
Remove viewModel from BaseViewController, every view controller should have it's own viewModel, there is no other way
For this situation i personally use another property that returns appropriate viewModel. So, it could be:
var audioPlayerViewModel: AudioPlayerViewModel? {
return viewModel as? AudioPlayerViewModel
}
func createViewModel() {
viewModel = AudioPlayerViewModel()
}
Or you can remove optional in property, because you know that it will be exactly AudioPlayerViewModel.
var audioPlayerViewModel: AudioPlayerViewModel {
return viewModel as! AudioPlayerViewModel
}

Which is the best way to init presenter in MVP-pattern (swift)

Usually, I write this function to init my presenter from viewController. But I want to use init() for making this. What can I do?
ViewController :
class ViewController: UIViewController {
private let viewPresenter = viewPresenter()
override func viewDidLoad() {
self.viewPresenter.attachView(controller: self)
}
#IBAction func faceBookButtonTapped(sender: UIButton) {
self.viewPresenter.showLoginWindow()
}
}
ViewPresenter :
class ViewPresenter {
var controller: ViewController?
func attachView(controller: ViewController) {
self.controller = controller
}
}
So, If I make this Init in my presenter :
let controller: ViewController?
init (controller:ViewController) {
self.controller = controller
}
And try to init like this in viewController:
private let viewPresenter = ViewPresenter(controller: self)
Xcode give me this error:
Cannot convert value of type '(NSObject) -> () -> ViewController' to expected argument type 'ViewController'
the problem is that there is no "self" at that point in time where you want to initialize your presenter because your view controller is initialized later. You could work around this problem by declaring your presenter as lazy like this
fileprivate lazy var viewPresenter: ViewPresenter = {
return ViewPresenter(controller: self)
}()
or you could just initialize your presenter later in viewDidLoad
private var viewPresenter: ViewPresenter!
override func viewDidLoad() {
super.viewDidLoad()
self.viewPresenter = ViewPresenter(controller: self)
}
If you ask me the best approach is (as you already did) to attach the view in viewDidLoad to the presenter.
Hope this helps.

Swift can't call protocol method via delegate

I have two classes. One class is named ViewController and the other class is named TabView.
My goal is to call a function changeTab() which is inside the TabView class from the ViewController.
Somehow I am having trouble with it because everytime my delegate is nil.
Here is my code for ViewController:
protocol TabViewProtocol: class {
func changeTab()
}
class ViewController: NSViewController {
// delegate
weak var delegateCustom : TabViewProtocol?
override func viewDidLoad() {
print(delegateCustom) // outputs "nil"
}
buttonClickFunction() {
print(delegateCustom) // outputs "nil"
delegateCustom?.changeTab() // doesn't work
}
}
Here is my code for TabView:
class TabView: NSTabViewController, TabViewProtocol {
let myVC = ViewController()
override func viewDidLoad() {
super.viewDidLoad()
myVC.delegateCustom = self
}
func changeTab() {
print("test succeed")
}
}
Can someone explain me what I am doing wrong? - I am new to delegates and protocols...
You are using the delegate pattern wrongly. It is hard to tell which controller you want to define the protocol for and which one you want to adopt it - but here is one possible way.
// 1. Define your protocol in the same class file as delegate property.
protocol TabViewProtocol: class {
func changeTab()
}
// 2. Define your delegate property
class ViewController: NSViewController {
// delegate
weak var delegateCustom : TabViewProtocol?
override func viewDidLoad() {
// It should be nil as you have not set the delegate yet.
print(delegateCustom) // outputs "nil"
}
func buttonClickFunction() {
print(delegateCustom) // outputs "nil"
delegateCustom?.changeTab() // doesn't work
}
}
// 3. In the class that will use the protocol add it to the class definition statement
class TabView: NSTabViewController, TabViewProtocol {
let myVC = ViewController()
override func viewDidLoad() {
super.viewDidLoad()
myVC.delegateCustom = self
// Should output a value now
print(myVC.delegateCustom) // outputs "self"
}
func changeTab() {
print("test succeed")
}
}
you are creating a new instance in this line:
let myVC = ViewController()
you should get existing instance of your ViewController.then set
myVC.delegateCustom = self

Resources