What the advantages & disadvantages to each of these approaches pertaining to creating file that takes care of the view configuration to reduce a controllers file size.
Main Focuses Are:
Memory
Performance
Testing
Usability
This is the simplest, capable of working, example to demonstrate the question, but when many views are present using many methods do any of the above concerns alter when comparing the Extension & ViewModel Class?
Reminder: The ViewModel Class or Extension would be placed in a separate file.
ViewModel Approach
class VC: UIViewController {
lazy var viewModel: ViewModel {
return (main: self)
}()
ovverride viewDidLoad() {
super.viewDidLoad()
initializeUI()
}
func initializeUI() {
viewModel.configureView()
}
}
class ViewModel {
private let main: UIViewController
init(main: UIViewController) {
self.main = main
}
func configureView() {
main.view.backgroundColor = UIColor.blue
}
}
Extension Approach
class VC: UIViewController {
lazy var viewModel: ViewModel(main: self)
ovverride viewDidLoad() {
super.viewDidLoad()
initializeUI()
}
func initializeUI() {
configureView()
}
}
extension VC {
func configureView() {
main.view.backgroundColor = UIColor.blue
}
}
Related
I would like to know which component of the MVP architecture should know and set the title property of an object type of UINavigationItem class? Should it be the model, the presenter or the view (UIViewController) ?
It’s Presenter’s responsibility. View should be as stupid(plain) as possible. I think it should look something like this:
protocol ViewInput {
func setup(title: String)
}
protocol ViewOutput: AnyObject {
func viewDidLoad()
}
class Presenter: ViewOutput {
unowned var view: ViewOutput
init(view: ViewOutput) {
self.view = view
}
func viewDidLoad() {
view.setup(title: "Your title")
}
}
class ViewController: UIViewController, ViewInput {
var presenter: ViewOutput?
override func viewDidLoad() {
super.viewDidLoad()
presenter.viewDidLoad()
}
func setup(title: String) {
self.title = title
}
}
If you write like this you can easily write unit tests for the view’s title. In a test you just put fake view in a presenter which implements ViewInput and can compare expected title and title that presenter setup.
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.
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
}
I want to remove repetitive code so I would like to create a simple MVP base view controller that will tie together a model, view and presenter types and automatically connect them e.g.:
class BaseMvpViewController<M: MvpModel, V: MvpView, P: MvpPresenter>: UIViewController {
Where my model and view are empty protocols:
protocol MvpModel {}
protocol MvpView: class {} // class is needed for weak property
and presenter looks like this:
protocol MvpPresenter {
associatedtype View: MvpView
weak var view: View? { get set }
func onAttach(view: View)
func onDetach(view: View)
}
This is my whole BaseMvpViewController:
class BaseMvpViewController<M: MvpModel, V, P: MvpPresenter>: UIViewController, MvpView {
typealias View = V
var model: M? = nil
var presenter: P!
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
deinit {
presenter.onDetach(view: self as! View)
}
override func viewDidLoad() {
createPresenter()
super.viewDidLoad()
presenter.onAttach(view: self as! View)
}
func createPresenter() {
guard presenter != nil else {
preconditionFailure("Presenter was not created or it was not assigned into the `presenter` property!")
}
}
}
The problem is that the V must be without the protocol i.e. cannot be V: MvpView. Otherwise specific implementation of a VC must have a class/struct and not just a protocol for the MvpView. All my views are just protocols and my VCs will implement them e.g.
class MyViewController: BaseMvpViewController<MyModel, MyView, MyPresenter>, MyView
Now the compiler complains in the onAttach() and onDetach() methods that "argument type 'V' does not conform to expected type 'MvpView'"
So I tried an extension:
extension BaseMvpViewController where V: MvpView {
override func viewDidLoad() {
presenter.onAttach(view: self as! View)
}
}
yet another compiler error: "cannot invoke 'onAttach' with an argument list of type '(view: V)'". There is another small compilation error "Members of constrained extensions cannot be declared #objc" where I override func viewDidLoad() in the extension. This can be fixed by my own method and calling that one from viewDidLoad in the custom class. Any idea how to achieve what I want?
This is a similar/same issue like Using some protocol as a concrete type conforming to another protocol is not supported but maybe something has been improved in the Swift world since then. Or did I really hit a hard limit in the current Swift's capabilities?
In have finally found a solution, the problem was in casting self as! View, it must be self as! P.View. And there cannot be a base protocol for view because protocols do not conform to themselves in Swift. Here is my complete code:
protocol MvpPresenter {
associatedtype View
var view: View? { get set }
var isAttached: Bool { get }
func onAttach(view: View)
func onDetach(view: View)
}
/// Default implementation for the `isAttached()` method just checks if the `view` is non nil.
extension MvpPresenter {
var isAttached: Bool { return view != nil }
}
class BaseMvpViewController<M, V, P: MvpPresenter>: UIViewController {
typealias View = V
var viewModel: M? = nil
private(set) var presenter: P!
//MARK: - Initializers
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override public init(nibName: String?, bundle: Bundle?) {
super.init(nibName: nibName, bundle: bundle)
}
deinit {
presenter.onDetach(view: self as! P.View)
}
//MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
presenter = createPresenter()
}
override func viewWillAppear(_ animated: Bool) {
guard let view = self as? P.View else {
preconditionFailure("MVP ViewController must implement the view protocol `\(View.self)`!")
}
super.viewWillAppear(animated)
if (!presenter.isAttached) {
presenter.onAttach(view: view)
}
}
//MARK: - MVP
/// Override and return a presenter in a subclass.
func createPresenter() -> P {
preconditionFailure("MVP method `createPresenter()` must be override in a subclass and do not call `super.createPresenter()`!")
}
}
And a sample VC:
class MyGenericViewController: BaseMvpViewController<MyModel, MyView, MyPresenter>, MyView {
...
override func createPresenter() -> MainPresenter {
return MyPresenter()
}
...
}
This VC will automatically have a viewModel property of type MyModel (could be anything e.g. struct, class, enum, etc), property presenter of type MyPresenter and this presenter will be automatically attached between viewDidLoad and viewWillAppear. One method must be overridden, the createPresenter() where you must create and return a presenter. This is called before the custom VC's viewDidLoad method. Presenter is detached in the deinit.
The last problem is that generic view controllers cannot be used in interface builder (IB), because IB talks to code via Objective-C runtime and that does not know true generics, thus does not see our generic VC. The app crashes when instantiating a generic VC from a storyboard/xib. There is a trick though that fixes this. Just load the generic VC manually into the Objective-C runtime before any instantiation from storyboard/xib. Good is in AppDelegate's init method:
init() {
...
MyGenericViewController.load()
...
}
EDIT 1:
I have found the loading of generic VC into Objective-C runtime in this SO answer https://stackoverflow.com/a/43896830/671580
EDIT 2:
Sample presenter class. The mandatory things is the typealias, the weak var view: View? and the onAttach & onDetach methods. Minimum implementation of the attach/detach methods is also provided.
class SamplePresenter: MvpPresenter {
// These two are needed!
typealias View = SampleView
weak var view: View?
private let object: SomeObject
private let dao: SomeDao
//MARK: - Initializers
/// Sample init method which accepts some parameters.
init(someObject id: String, someDao dao: SomeDao) {
guard let object = dao.getObject(id: id) else {
preconditionFailure("Object does not exist!")
}
self.object = object
self.dao = dao
}
//MARK: - MVP. Both the onAttach and onDetach must assign the self.view property!
func onAttach(view: View) {
self.view = view
}
func onDetach(view: View) {
self.view = nil
}
//MARK: - Public interface
/// Sample public method that can be called from the view (e.g. a ViewController)
/// that will load some data and tell the view to display them.
func loadData() {
guard let view = view else {
return
}
let items = dao.getItem(forObject: object)
view.showItems(items)
}
//MARK: - Private
}
I would like to have a BaseViewController that subclasses UIViewController, overrides one of it's methods, but also require it's subclasses to implement new ones.
My case is a bit more complex, but this simple case represents my problem:
My BaseViewController would override viewWillAppear to set it's title, but the title string would come from it's subclasses. I thought about some options, not sure if/which one of them is best.
1 - Class with error throwing methods (my current solution):
class BaseViewController: UIViewController {
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
title = getTitle()
}
func getTitle() -> String {
fatalError("Unimplemented method")
}
}
2 - Receive the title in constructor:
class BaseViewController: UIViewController {
var myTitle: String!
convenience init(title titleSent: String) {
self.init(nibName: nil, bundle: nil)
myTitle = sentTitle
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
title = myTitle
}
}
Note that this options gets really bad if there's more parameters to send
I thought that using protocol would be perfect, until I find out that (of course) protocols can't subclass a class.
Didn't anybody do anything like this before? I don't think so, please share your thoughts.
Update
I tried another way, but got stuck in a compiler error, would this ever work?
procotol BaseViewController {
var myTitle: String { get }
}
extension BaseViewController where Self: UIViewController {
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
title = myTitle
}
}
The compiler says Method does not override any method from its superclass.
I usually create protocol in which I declare what would be nice to have in the controllers. Then I check in the base controller if it's actually implemented and if so, just use the values, like this:
protocol ControllerType {
var navigationTitle: String { get }
}
extension ControllerType {
var navigationTitle: String {
return "Default title"
}
}
class BaseViewController: UIViewController {
override func viewDidLoad() {
if let controller = self as? ControllerType {
self.title = controller.navigationTitle
}
}
}
class ViewController: BaseViewController, ControllerType {
var navigationTitle: String {
return "ViewController"
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
Downfall is you have to implement the ControllerType protocol and there's no way to enforce it.
Something similar would work.
class BaseViewController: UIViewController {
override func viewDidLoad() { }
func setTitle(_ title: String) {
self.title = title
}
}
class ViewController: BaseViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.setTitle("ViewController")
}
}