Is it allowed to make Multiple navigation in one router VIPER function? I created just one VIPER router function to multiple navigation.
My code is like this :
func navigateToView(data: [String: Any]) {
guard let view = viewController else { return }
if data["callback"] != nil && data["data"] != nil {
//navigation1(enum: data["enum"] as! Enum, from: view, data: data, callback: { param })
} else if data["callback"] != nil && data["data"] == nil {
//navigation2(enum: data["enum"] as! Enum, from: view, callback: { param })
} else if data["data"] != nil && data["callback"] == nil {
//navigation3(enum: data["enum"] as! Enum, from: view, data: data)
} else {
//navigation4(enum: data["enum"] as! Enum, from: view)
}
}
because I saw an article which written The navigation logic, which mean it can use navigation logic in router function
To answer the question, let's have a look at the parts of VIPER first.
View: Responsible from user interface. Notifies presenter about user actions, lifecycle methods and provides methods to update user interface.
Interactor: Contains business logic. Makes all third party interactions and informs its output (presenter in this case) with use cases.
Router: Responsible from navigation. Provides methods to navigate to other screens.
Presenter: Contains presentation logic such as which data to get , which view to display or which screen to navigate with any user action, lifecycle method or use case.
What navigateToView is doing here to decide which screen to navigate. This should belong to presentation logic since navigation to any screen can be done with user action or according to a field in the data but the router should not know about this.
Let's say you have another button that navigates to the screen in "navigation 1". Then, when the presenter gets the button clicked event, it tells the router where to navigate. It wouldn't tell router the action and router wouldn't decide where to go. The same applies here.
So, it wouldn't be the best practice to use this logic in the router.
Related
I have login few screens and controllers in my app. First screen is screen with button and moves user to next login view with username, password field and login button. On the controller i have function onClickButton and when i have good data i request to the server with this data.
When server give me callback i have many params about user to set in label in next view.
My structure is like this
Login View -> SecondLogin View and LoginViewController -> TabBarController -> NavigationController -> Table View with TableViewController
My code is
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "afterLoginView" {
if let secondVC = segue.destination as? TabBarViewController {
secondVC.finalName = self.username
}
}
}
When i want transfer my data directly to tableViewController i have error
Thread 1: signal SIGABRT
I do not understand what I'm doing wrong
You'll need these values in almost all view controllers. Create a singleton class to store the logged in user values like this
class UserDetails: NSObject, Codable {
static let shared = UserDetails()
private override init() {
super.init()
}
var finalName: String?
var otherDetails: String?
}
Now when you receive the response from the login api, assign the values in this singleton class.
UserDetails.shared.finalName = "something"//Name received from server callback
Now you can access these values from any view controller.
class TableViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
print(UserDetails.shared.finalName)
}
}
You have some work to do to get to the right view controller. Since your segue is only pointing at the UITabBarViewController, you should put in another guard or if/let statement to get you to the UINavigationController, and then another to finally get you to the UITableViewController, where you can actually refer to your finalName variable.
That would look something like:
if let secondVC = segue.destination as? TabBarViewController {
if let navCon = secondVC.viewController[0] as? UINavigationController {
if let tableVC = navCon.topViewController as? nameOfYourTableVC {
tableVC.finalName = self.username
}
The code is untested, just typed off the top of my head, so please proceed with due caution. Issues such as which tab is the correct NavController would also need to be addressed.
You need to use the actual name of your tableView class in that last if/let. A generic UITableViewController will not include your custom variables.
When server give me callback i have many params about user to set in label in next view.
This is a great example of why you should keep the M in MVC. When you get a response back from the server, store the returned data in your data model. (If you don't have a data model, you should make one.) When a view controller gets some data from the user, such as a user name, it should store that in the model. There's little reason to pass raw data back and forth between view controllers directly... just make sure that all your view controllers have a reference to the model, and have them get and set values there as needed.
This kind of approach will make your code a lot more flexible. It allows view controllers to worry about what they need to do their job, and it gets them out of the business of caring what other view controllers need.
My structure is like this
Login View -> SecondLogin View and LoginViewController -> TabBarController -> NavigationController -> Table View with TableViewController
It might make more sense to load the tab bar controller and then present the login view controller(s) modally. The view controllers that are managed by the tab bar controller can all be set up to refuse to do anything useful until the data they need is present in the data model, and that lets the tab bar controller be the root view controller. That will make it easy to set the model for each of it's child view controllers when the app starts up, and the app can then present the modal login view controllers, also set up with references to the model.
I'm writing an app that contains network call in every other screen. The result of calls would be the dataSource for a specific screen.
The question is, should I do network call in the parent viewController and inject the data before pushing current viewController or push currentViewController and do network call on viewDidLoad()/viewWillAppear()?
Both the methods makes sense to me.
Where you make the request to network should actually make no difference. You are requesting some data which you will have to wait for and present it. Your question is where should you wait for the data to be received.
As #Scriptable already mentioned you can do either of the two. And which to use depends on what kind of user experience you wish to have. This varies from situation to situation but in general when we create a resource we usually wait for it on current screen and when we are reading resources we wait for it on the next screen:
For instance if you are creating a new user (sign up) after you will enter a new username and password an indicator will appear and once the request is complete you will either navigate to next screen "enter your personal data" or you will receive a message like "User already exists".
When you then for instance press "My friends" you will be navigated to the list first where you will see activity indicator. Then the list appears or usually some screen like "We could not load your data, try again."
There are still other things to consider because for the 2nd situation you can add more features like data caching. A lot of messaging applications will for instance have your chats saved locally and once you press on some chat thread you will be navigated directly to seeing whatever is cached and you may see after a bit new messages are loaded and shown.
So using all of this if we get back to where you should "call" the request it seem you best do it before you show the new controller or at the same time. At the same time I mean call it the load on previous view controller but load the new view controller before you receive the new data.
How to do this best is having a data model. Consider something like this:
class UsersModel {
private(set) var users: [User]?
}
For users all we need is a list of them so all I did was wrapped an array. So in your case we should have an option to load these users:
extension UsersModel {
func fetchUsers() {
User.fetchAll { users, error in
self.users = users
self.error = error // A new property needed
}
}
}
Now a method is added that loads users and assigns them to internal property. And this is enough for what we need in the first view controller:
func goToUsers() {
let controller = UserListViewController()
let model = UserModel()
controller.model = model
model.fetchUsers()
navigationController.push(controller...
}
Now at this point all we need is to establish the communication inside the second view controller. Obviously we need to refresh on viewDidLoad or even on view will appear. But we would also want some delegate (or other type of connections) so our view controller is notified of changes made:
func viewDidLoad() {
super.viewDidLoad()
self.refreshList()
self.model.delegate = self
}
And in refresh we should now have all the data needed:
func refreshList() {
guard let model = model else {
// TODO: no model? This looks like a developer bug
return
}
if let users = model.users {
self.users = users
tableView?.reloadData()
if users.count.isEmpty {
if let error = model.error {
// TODO: show error screen
} else {
// TODO: show no data screen
}
}
} else {
// TODO: show loading indicator screen
}
}
Now all that needs to be done here is complete the model with delegate:
extension UsersModel {
func fetchUsers() {
User.fetchAll { users, error in
self.users = users
self.error = error // A new property needed
self.delegate?.usersModel(self, didUpdateUsers: self.users)
}
}
}
And the view controller simply implements:
func usersModel(_ sender: UserModel, didUpdateUsers users: [User]?) {
refreshList()
}
Now I hope you can imagine the beauty of such a system that your model could for instance first asynchronously load users from some local cache or database and call the delegate and then call the request to server and call the delegate again while your view controller would show appropriate data for any situation.
Does anybody know if there is a certain pattern for handling segues programmatically in a MVC way?
I would think the best way would be to work with an event system within a controller.
I want that all the view controllers connect to this navigationController instead of handling all the logic within the viewController logic itself. I want to out source this logic
In most of your view controllers, you will have access to a prepareForSegue function, with one parameter called sender.
If you kick off a segue programatically with performSegue(withIdentifier: "mySegueID", sender: yourVC) then this function will be called, and you'll be able to pass information from the sender to the new view controller.
In this function, to get a handle on the next VC, use segue.destinationViewController.
I don't know about a particular pattern but a simple way to programmatically handle transitions between 2 UIViewController could be to have a separated manager whose job is just to push/present/whatever new controllers over current, and to pop/dismiss/whatever current controllers to old ones.
The way I usually do this is by having a class we can name WorkflowManager, which will handle all transitions. Associated with this manager, you declare a WorkflowManagerComponent protocol and implement it :
protocol WorkflowManagerComponent {
var completionHandler: (hasCompleted:Bool,data:Any)->() {get set}
}
Make each UIViewController implement this, for example by calling completionHandler(true,someData) when the user taps a "next" button, or completionHandler(false,nil) when the user taps a "back" button.
Then in your workflow manager, you perform transitions to the next or previous UIViewController according to parameters sent in the completionHandler:
//init viewController1 ...
viewController1.completionHandler = onViewController1Completes
// ...
func onViewController1Completes(_ completed: Bool, data: Any) {
if hasCompleted {
//init viewController2 ...
viewController2.data = data
viewController2.completionHandler = onViewController2Completes
//Push the new vc
viewController1.navigationController.push(viewController2, animated: true)
} else {
//The vc1 was presented as a modal, dismiss it
viewController1.dismiss()
}
}
This way each UIViewController is separated from others, free off any transition logic.
I'm pretty new to iOS and swift development. A lot of swift + firebase tutorials out there, the firebase stuff (such as authentication, fetching and saving data) is done in the ViewController. As far as I got on learning swift, this leads directly to the problem of "Massive View Controllers". In some tutorials they use classes like "DataService.swift" and access them as a singleton:
class DataService {
static let dataService: DataService = DataService()
func createUser(FIRUser: user) {}
...
}
BUT these classes have no communication with the view controller when they're done with e.g. creating the user. Let me be more specific. I guess it should like this:
User taps login button.
Then, ViewController calls dataService.createUser(user) which handles the login stuff and saves the user data to firebase.
If it's finished it should communicate to the view controller, that it's finished.
ViewController checks the result of the createUser() and navigates the user to another view.
How can i do this? At the moment i'm using the delegation pattern. Is this a good way to handle this stuff?
You can do it this way:
// User.swift
struct User {
// Declare necessary properties.
// Such as firstName, lastName, email, etc.
}
// AuthenticationService.swift
protocol AuthenticationService {
func createUser(user: User, completion: (Error?, User?) -> Void)
}
// AuthServiceProvider.swift
class AuthServiceProvider: AuthenticationService {
func createUser(user: User, completion: (Error?, User?) -> Void) {
// Do the necessary work here.
// Convert user to an instance of 'FIRUser' if necessary.
// Use the completion block when you are done.
}
}
// RegisterViewController.swift
class RegisterViewController: UIViewController {
var service = AuthServiceProvider()
#IBAction func didTapRegisterButton(sender: UIButton) {
var user = User()
// Fill the necessary properties to be included.
// Then call the 'createUser' function.
service.createUser(user: user) { (error, user) -> Void in
// This is where you are redirected upon completion.
// Handle always the error if there is.
// If there is none, navigate to your next scene/view.
}
}
}
By the way, as much as possible, just avoid implementing a Singleton Class. This kind of implementation might give you a hard time on doing some unit tests.
Regarding with your concern on "Massive View Controller", you can consider on modularizing your project. But right now, I would suggest that it is better to experience this "MVC" problem than avoiding it.
I'm an iOS developer and I'm guilty of having Massive View Controllers in my projects so I've been searching for a better way to structure my projects and came across the MVVM (Model-View-ViewModel) architecture. I've been reading a lot of MVVM with iOS and I have a couple of questions. I'll explain my issues with an example.
I have a view controller called LoginViewController.
LoginViewController.swift
import UIKit
class LoginViewController: UIViewController {
#IBOutlet private var usernameTextField: UITextField!
#IBOutlet private var passwordTextField: UITextField!
private let loginViewModel = LoginViewModel()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func loginButtonPressed(sender: UIButton) {
loginViewModel.login()
}
}
It doesn't have a Model class. But I did create a view model called LoginViewModel to put the validation logic and network calls.
LoginViewModel.swift
import Foundation
class LoginViewModel {
var username: String?
var password: String?
init(username: String? = nil, password: String? = nil) {
self.username = username
self.password = password
}
func validate() {
if username == nil || password == nil {
// Show the user an alert with the error
}
}
func login() {
// Call the login() method in ApiHandler
let api = ApiHandler()
api.login(username!, password: password!, success: { (data) -> Void in
// Go to the next view controller
}) { (error) -> Void in
// Show the user an alert with the error
}
}
}
My first question is simply is my MVVM implementation correct? I have this doubt because for example I put the login button's tap event (loginButtonPressed) in the controller. I didn't create a separate view for the login screen because it has only a couple of textfields and a button. Is it acceptable for the controller to have event methods tied to UI elements?
My next question is also about the login button. When the user taps the button, the username and password values should gte passed into the LoginViewModel for validation and if successful, then to the API call. My question how to pass the values to the view model. Should I add two parameters to the login() method and pass them when I call it from the view controller? Or should I declare properties for them in the view model and set their values from the view controller? Which one is acceptable in MVVM?
Take the validate() method in the view model. The user should be notified if either of them are empty. That means after the checking, the result should be returned to the view controller to take necessary actions (show an alert). Same thing with the login() method. Alert the user if the request fails or go to the next view controller if it succeeds. How do I notify the controller of these events from the view model? Is it possible to use binding mechanisms like KVO in cases like this?
What are the other binding mechanisms when using MVVM for iOS? KVO is one. But I read it's not quite suitable for larger projects because it require a lot of boilerplate code (registering/unregistering observers etc). What are other options? I know ReactiveCocoa is a framework used for this but I'm looking to see if there are any other native ones.
All the materials I came across on MVVM on the Internet provided little to no information on these parts I'm looking to clarify, so I'd really appreciate your responses.
waddup dude!
1a- You're headed in the right direction. You put loginButtonPressed in the view controller and that is exactly where it should be. Event handlers for controls should always go into the view controller - so that is correct.
1b - in your view model you have comments stating, "show the user an alert with the error". You don't want to display that error from within the validate function. Instead create an enum that has an associated value (where the value is the error message you want to display to the user). Change your validate method so that it returns that enum. Then within your view controller you can evaluate that return value and from there you will display the alert dialog. Remember you only want to use UIKit related classes only within the view controller - never from the view model. View model should only contain business logic.
enum StatusCodes : Equatable
{
case PassedValidation
case FailedValidation(String)
func getFailedMessage() -> String
{
switch self
{
case StatusCodes.FailedValidation(let msg):
return msg
case StatusCodes.OperationFailed(let msg):
return msg
default:
return ""
}
}
}
func ==(lhs : StatusCodes, rhs : StatusCodes) -> Bool
{
switch (lhs, rhs)
{
case (.PassedValidation, .PassedValidation):
return true
case (.FailedValidation, .FailedValidation):
return true
default:
return false
}
}
func !=(lhs : StatusCodes, rhs : StatusCodes) -> Bool
{
return !(lhs == rhs)
}
func validate(username : String, password : String) -> StatusCodes
{
if username.isEmpty || password.isEmpty
{
return StatusCodes.FailedValidation("Username and password are required")
}
return StatusCodes.PassedValidation
}
2 - this is a matter of preference and ultimately determined by the requirements for your app. In my app I pass these values in via the login() method i.e. login(username, password).
3 - Create a protocol named LoginEventsDelegate and then have a method within it as such:
func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String)
However this method should only be used to notify the view controller of the actual results of attempting to login on the remote server. It should have nothing to do with the validation portion. Your validation routine will be handled as discussed above in #1. Have your view controller implement the LoginEventsDelegate. And create a public property on your view model i.e.
class LoginViewModel {
var delegate : LoginEventsDelegate?
}
Then in the completion block for your api call you can notify the view controller via the delegate i.e.
func login() {
// Call the login() method in ApiHandler
let api = ApiHandler()
let successBlock =
{
[weak self](data) -> Void in
if let this = self {
this.delegate?.loginViewModel_LoginCallFinished(true, "")
}
}
let errorBlock =
{
[weak self] (error) -> Void in
if let this = self {
var errMsg = (error != nil) ? error.description : ""
this.delegate?.loginViewModel_LoginCallFinished(error == nil, errMsg)
}
}
api.login(username!, password: password!, success: successBlock, error: errorBlock)
}
and your view controller would look like this:
class loginViewController : LoginEventsDelegate {
func viewDidLoad() {
viewModel.delegate = self
}
func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String) {
if successful {
//segue to another view controller here
} else {
MsgBox(errMsg)
}
}
}
Some would say you can just pass in a closure to the login method and skip the protocol altogether. There are a few reasons why I think that is a bad idea.
Passing a closure from the UI Layer (UIL) to the Business Logic Layer (BLL) would break Separation of Concerns (SOC). The Login() method resides in BLL so essentially you would be saying "hey BLL execute this UIL logic for me". That's an SOC no no!
BLL should only communicate with the UIL via delegate notifications. That way BLL is essentially saying, "Hey UIL, I'm finished executing my logic and here's some data arguments that you can use to manipulate the UI controls as you need to".
So UIL should never ask BLL to execute UI control logic for him. Should only ask BLL to notify him.
4 - I've seen ReactiveCocoa and heard good things about it but have never used it. So can't speak to it from personal experience. I would see how using simple delegate notification (as described in #3) works for you in your scenario. If it meets the need then great, if you're looking for something a bit more complex then maybe look into ReactiveCocoa.
Btw, this also is technically not an MVVM approach since binding and commands are not being used but that's just "ta-may-toe" | "ta-mah-toe" nitpicking IMHO. SOC principles are all the same regardless of which MV* approach you use.
MVVM in iOS means creating an object filled with data that your screen uses, separately from your Model classes. It usually maps all the items in your UI that consume or produce data, like labels, textboxes, datasources or dynamic images. It often does some light validation of input (empty field, is valid email or not, positive number, switch is on or not) with validators. These validators are usually separate classes not inline logic.
Your View layer knows about this VM class and observes changes in it to reflects them and also updates the VM class when the user inputs data. All properties in the VM are tied to items in the UI. So for example a user goes to a user registration screen this screen gets a VM that has none of it's properties filled except the status property that has an Incomplete status. The View knows that only a Complete form can be submitted so it sets the Submit button inactive now.
Then the user starts filling in it's details and makes a mistake in the e-mail address format. The Validator for that field in the VM now sets an error state and the View sets the error state (red border for example) and error message that's in the VM validator in the UI.
Finally, when all the required fields inside the VM get the status Complete the VM is Complete, the View observes that and now sets the Submit button to active so the user can submit it. The Submit button action is wired to the VC and the VC makes sure the VM gets linked to the right model(s) and saved. Sometimes Models are used directly as a VM, that might be useful when you have simpler CRUD-like screens.
I've worked with this pattern in WPF and it works really great. It sounds like a lot of trouble setting up all those observers in Views and putting a lot of fields in Model classes as well as ViewModel classes but a good MVVM framework will help you with that. You just need to link UI elements to VM elements of the right type, assign the right Validators and a lot of this plumbing gets done for you without the need for adding all that boilerplate code yourself.
Some advantages of this pattern:
It only exposes the data you need
Better testability
Less
boilerplate code to connect UI elements to data
Disadvantages:
Now you need to maintain both the M and the VM
You still can't completely get around using the VC iOS.
MVVM architecture in iOS can be easily implemented without using third party dependencies. For data binding, we can use a simple combination of Closure and didSet to avoid third-party dependencies.
public final class Observable<Value> {
private var closure: ((Value) -> ())?
public var value: Value {
didSet { closure?(value) }
}
public init(_ value: Value) {
self.value = value
}
public func observe(_ closure: #escaping (Value) -> Void) {
self.closure = closure
closure(value)
}
}
An example of data binding from ViewController:
final class ExampleViewController: UIViewController {
private func bind(to viewModel: ViewModel) {
viewModel.items.observe(on: self) { [weak self] items in
self?.tableViewController?.items = items
// self?.tableViewController?.items = viewModel.items.value // This would be Momory leak. You can access viewModel only with self?.viewModel
}
// Or in one line:
viewModel.items.observe(on: self) { [weak self] in self?.tableViewController?.items = $0 }
}
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
}
protocol ViewModelInput {
func viewDidLoad()
}
protocol ViewModelOutput {
var items: Observable<[ItemViewModel]> { get }
}
protocol ViewModel: ViewModelInput, ViewModelOutput {}
final class DefaultViewModel: ViewModel {
let items: Observable<[ItemViewModel]> = Observable([])
// Implmentation details...
}
Later it can be replaced with SwiftUI and Combine (when a minimum iOS version in of your app is 13)
In this article, there is a more detailed description of MVVM
https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3