View Model composition breaks mvvm structure - swift

In mvvm, the view is never able to access the model.
I define "View model composition" as a concept where a view model may have 1-to-many child view models
If a parent view model needs to mutate the model of 1 of its child view models, then if the view has access to that view model it would inherently have access to mutate the child vm's models.
What approach can I use to enforce "view never accesses model" rule?
Example code in Swift
class BigVm{
let accountVm: AccountViewModel
let anotherVm: AnotherSubviewViewModel
private func someEventHappened(){
//some logic that mutates accountVm's model based on state of anotherVm and vise versa
accountVm.mutateOrAccessModel()
}
}
class BigViewController: UIViewController{
let viewModel: BigVm
let subviewAccount: AccountView //has a viewModel of AccountViewModel
let anotherSubview: AnotherSubview //has a viewModel of AnotherSubviewViewModel
func viewDidLoad(){
super.viewDidLoad()
subviewAccount.vm = viewModel.accountVm
anotherSubview.vm = viewModel.anotherVm
//now what stops BigViewController to do the next lines
viewModel.subViewAccount.mutateOrAccessModel()
}
}

My solution to this is accessing the viewModels as protocols in the view layer. Though that adds a lot of boilerplate just to enforce mvvm.

Related

Sharing ViewModel between SwiftUI components

I'd like to implement a navigator/router for an architecture implemented with SwiftUI and Combine. In a few words the View will share viewModel with Router. When the View triggers a change on the viewModel the Router should navigate to a new sheet.
This is a version of my code where I'm directly passing the viewModel from View to Router. Is there anything wrong? My biggest doubt is that since I'm using #ObservedObject on both the Router and the View, two different instances of the viewModel are created.
VIEW MODEL
class BootViewModel:ObservableObject{
#Published var presentSignIn = false
}
VIEW
struct BootView: View {
#ObservedObject var viewModel:BootViewModel
var navigator:BootNavigator<BootView>? = nil
init(viewModel:BootViewModel) {
self.viewModel = viewModel
self.navigator = BootNavigator(view: self, viewModel: viewModel)
self.navigator.setSubscriptions()
}
var body: some View {
VStack{
Text("Hello")
Button("Button"){
self.viewModel.presentSignIn.toggle()
}
}
}
}
NAVIGATOR
class BootNavigator<T:View>{
var view:T? = nil
#ObservedObject var viewModel:BootViewModel
init(view:T, viewModel:BootViewModel) {
self.view = view
self.viewModel = viewModel
}
func setSubscriptions(){
subscribe(onSigninPressed: $viewModel.presentSignIn)
}
func subscribe(onSigninPressed : Binding<Bool>){
_ = view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
}
}
}
Why the SignInView is never presented?
Without taking into account the fact that using a router with swiftUI is not needed in general(I'm mostly doing an exercise)... is there anything wrong with this implementation?
This
view.sheet(isPresented: $viewModel.presentSignIn){
SignInView()
MUST be somewhere in body (directly or via computed property or func) but inside body's ViewBuilder
Some notes I have to point out here:
ValueType
There is a difference between an UIView and a SwiftUI View. All SwiftUI Views are value type! So they get copied when you pass them around. Be aware of that.
Single instance
If you want a single instance like a regular navigator for your entire app, you can use singleton pattern. But there is a better approach in SwiftUI universe called #Environment objects. You can take advantage of that.
Trigger a view refresh
To refresh the view (including presenting something), you must code inside the var body. But it can be directly written on indirectly through a function or etc.

Passing data back to controllers when struct model modified

I have a series of View Controllers which pass a struct model object down the chain.
If a user modifies the value of a property on the model, I update the view controller's model instance, and now I need to inform the parent view controllers that this object's value has changed.
Previously I would have used classes over structs for my model object and so I wouldn't have this issue as the object would have been directly written to.
But since structs are pass by value, I have to update the state on other view controllers. I have been using a singleton Manager object to handle state changes through a call to updateModel(). Is there a better way?
I have used something similar to this; keep a reference to the neighbouring view controller (with care to avoid a reference cycle) and a property observer on the struct property to update it when it changes.
This could also be updated prior to presenting a new view controller or before a segue, depending on your needs.
class myViewController: UIViewController {
// Your struct
var model: MyStruct? {
didSet {
if let pvc = previousVC {
pvc.model = model
}
}
}
// Keep a reference to the previous view controller on your stack
var previousVC: UIViewController?
override viewDidLoad() {
super.viewDidLoad()
self.model = MyStruct()
}
}

Making ViewController slimmer by moving TableView away

I am quite new into programming and facing some issues while trying to slim down my ViewController by moving creation of the tableView and associated views to the separate class and moving delegates and datasource from VC to separate one.
My constellation of current files and work is as follows:
After network connection, when data are received, I am calling inside a closure class to set view which will have embedded UITableView.
Fetched data are being saved into the CoreData stack
I instantiate view from another class
var detailView: DetailView! { return self.view as? DetailView }
Once I will download first part of the UI (separate call which works fine)
I am moving onto the part which is messy and surpass my abilities
I call a function createReposCard()
dispatchGroup.notify(queue: dispatchQueue) {
DispatchQueue.main.async {
// slide animation
UIView.animate(withDuration: 0.75, delay: 0.5, usingSpringWithDamping: 0.9, initialSpringVelocity: 0.0, options: [], animations: {
self.detailView.reposCard.center = CGPoint(x: self.detailView.reposCard.center.x, y: self.detailView.reposCard.center.y-UIScreen.main.bounds.height)
}, completion: nil)
self.detailView.createReposCard(for: self.repos)
self.detailView.detailsTableView.reloadData()
self.activityIndicator.stopAnimating()
self.activityIndicator.removeFromSuperview()
}
}
Code is incomplete to depict the problem only, but what it does one by one.
I am passing info about selected user (irrelevant info)
then I am making a reference to my DetailsViewController which still e.g. holds CoreData stack
In initializer I am instantiating detailsViewController and passing it onto the class which holds delegates (I am passing it to have there reference to the CoreData)
class DetailView: UIView {
var selectedUser: User?
var detailsViewController: DetailsViewController!
let detailsTableView: UITableView = {
let tableView = UITableView()
tableView.frame = CGRect.zero
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reposCell")
return tableView
}()
init(selectedUser:User, frame: CGRect) {
super.init(frame: frame)
self.selectedUser = selectedUser
detailsViewController = DetailsViewController()
let tableViewDelegates = TableViewDelegates(detailsViewController: detailsViewController)
detailsTableView.delegate = tableViewDelegates
detailsTableView.dataSource = tableViewDelegates
}
And finally code jumps into the depicted class where I am knocked down by "Unexpectedly found nil while implicitly unwrapping..."
public class TableViewDelegates: NSObject, UITableViewDataSource,UITableViewDelegate {
private let detailsViewController: DetailsViewController
init(detailsViewController: DetailsViewController){
self.detailsViewController = detailsViewController
super.init()
}
public func numberOfSections(in tableView: UITableView) -> Int {
return detailsViewController.fetchedResultsController.sections?.count ?? 1
}
...
Actually I don't know if my concept is good as I feel quite lost, but my intentions were as follows:
Move View creation to other class
Move TableView delegates to other class
Finally - move networking and CoreData to other class.
But as I see, simple data passing is overcoming my abilities.
I think this question can be divided into two parts:
1) Why is my variable nil when I unwrap it?
I don't think we have enough information to answer this accurately, but my overall approach would be like this:
Check what nil variable is being unwrapped;
Make sure this variable is being properly initialized;
Make sure that the object is not being incorrectly deinitialized;
If you're using Storyboard, use the inspectors to check if everything is set correctly.
There's a particular observation about step 2: you should check the order of execution of your methods to make sure that the variable is properly initialized. Why am I emphasizing this? Because there's a chance that some view (e.g., detailView) is initialized like an ordinary UIView, and then you try to access an element that is not part of a UIView object (e.g., a table view). In other words, check if you're setting the custom views before you try to access them.
2) How to structure the project in a more organized way?
This is a more interesting question, and I think that choosing a better approach will help you to avoid issues like what you're experiencing. I will divide this into some topics. Everything here is my personal opinion and doesn't necessarily reflect the best approach, especially because "best" is subjective here.
PersistenceManager class
First, passing a reference of a view controller to another class just to access CoreData doesn't seem like a good option. A better approach would be to have a PersistenceManager class, for example. You could use an object of this class to fetch and save data. You could pass this object instead of the view controller.
In some architectures (e.g., VIPER), it wouldn't be correct for the view controller to access the persistence directly, so it would be more appropriate to pass an array of already fetched objects. For example:
class TableViewController {
private let tableView: UITableView!
private var currentlyDisplayedUsers: [Users]?
func displayUsers(_ users: [Users]) {
self.currentlyDisplayedUsers = users
self.tableView.reloadData()
}
}
In the example above, the tableView would display currentlyDisplayedUsers, which would be updated by the method displayUsers, which would be called by someone else, like a Presenter.
Network class
Second, I think you should have a network class to download data from the internet. You would use instances of the Network class in the application logic. In other words, you would have something like
// This is in the application logic
// In MVC, the logic is in the Controller, while in VIPER the logic is in the Interactor
class ApplicationLogic {
let networkAPI: Network?
...
func fetchUserData() {
networkAPI?.fetchUsers() { data, error in
// Update the UI based on the response
// Using the previous example, you could call displayUsers here
}
}
}
TableView, TableViewDelegate, and TableViewDataSource
Finally, how to organize these guys. UITableViewDelegate is responsible for telling us about events in the table, while UITableViewDataSource is responsible for filling the table with data, which means that both are strongly related to the table view itself. That said, imho, both should be implemented in different swift files, but as extensions of the view controller that has a reference to the table view. Something like
// TableViewController+UITableViewDelegate.swift
extension TableViewController: UITableViewDelegate {
...
}
// TableViewController+UITableViewDataSource.swift
extension TableViewController: UITableViewDataSource {
...
}
Using this approach, the delegate and the data source would have access to the users array, mentioned earlier. Here is an example of how to implement a similar approach using VIPER.
So, I hope I could provide a basic idea on these topics. If you want to understand more about how to structure your code, I suggest researching iOS design patterns and architectural patterns. Some architectural design patterns that are famous in iOS development are MVC, MVP, MVVM, and VIPER.

What is the best way to reload a child tableview contained within a parent VC that is being fed fresh data?

I have been playing with the concept of the parent/child view delegation for a few days now, and currently understand how to feed data from parent to child. However, now, I want a button in the parent (main VC) to reload the data presented in the child VC.
I'm trying to delegate a method that is activated in the child VC's class but is activated in the parent's navigation controller. So that when I press the button, the delegated method in the child VC is performed; in my case, that method would be reload table. Why am I getting so many errors when trying to set up this simple delegation relationship?
My parent/container View is currently delegating a method to the child, so I have it set up from child -> parent. But I want to set it up from parent -> child. Pretty much I have:
struct Constants {
static let embedSegue = "containerToCollectionView"
}
class ContainerViewController: UIViewController, CollectionViewControllerDelegate {
func giveMeData(collectionViewController: CollectionViewController) {
println("This data will be passed")
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == Constants.embedSegue {
let childViewController = segue.destinationViewController as! CollectionViewController
childViewController.delegate = self
}
}
FROM CHILD:
protocol CollectionViewControllerDelegate {
func giveMeData(collectionViewController: CollectionViewController)
}
class CollectionViewController: UIViewController {
var delegate:CollectionViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
self.delegate?.giveMeData(self)
// Do any additional setup after loading the view.
}
I think my trouble is the fact that I'm declaring the child delegate in a prepareforsegue, so that was straight forward, but now I want the reverse delegation. How do I set that up so that I can use a child-method from the parent VC?
The child view controller has no business supplying other controllers with data. It should actually not even have any data fetching logic that is so generic it is also used by other controllers. You should refactor the data methods out into a new class.
This pattern is called Model-View-Controller, or MVC, and is a very basic concept that you should understand and follow. Apple explains it pretty well.
In general, to send data to from a controller to a detail controller, use prepareForSegue to set properties, etc. To communicate back to the parent controller, you use delegate protocols, but usually these are called when the detail controller is finished with its work and just reports the result up to the parent.
If you want to update the detail VC with new data (without dismissing it and with the parent not visible) you should not put the logic to update it into the parent. Instead, use the structure suggested above.

How do I handle callbacks when using selectors?

I'm placing my logic code in a viewModel. The view calls one method in the viewController. That method then calls the rest of the methods in the viewModel by using #selectors. This works fine up until the tableView needs to be reloaded with tableView.reloadData(). That part obviously needs to be in the view.
Normally, this would be accomplished by using multiple closures. But as #selectors can't have parameters I can't have a completion() callback in the last method that is called. So, my question is, how do I get around this problem? Is there any good alternatives to using #selectors? Should I have an observer in the view subscribing to the last method of the viewModel? Is RxSwift an alternative? Or is there a workaround using #selectors?
RxSwift is a good alternative, but in case you need something not as heavy, the delegate pattern is what you need:
protocol ViewDelegate {
// Other functions you might need
func reloadTableView()
}
Then in your viewController, you implement these:
class ViewController: ViewDelegate {
func reloadTableView() {
tableView.reloadData()
}
}
And somewhere, in your view model you need to define the delegate:
weak var viewDelegate: ViewDelegate
As well as assign it when creating the classes:
let model = ViewModel()
let view = ViewController()
model.viewDelegate = view
Swift official documentation has a lot more on protocols: https://docs.swift.org/swift-book/LanguageGuide/Protocols.html