I have simple ListView that needs to display the records from the network layer. (first screen of the application)
I need to get some opinion as to which will be correct flow so that I can make the unit test cases easily. (No VIPER architecture)
NetworkMgr makes the network calls and create Model objects.
These Model objects needs to be populated in ListTableView.
I have a completion handler method to call the network request which give the model objects.
func getData() {
dataMgr.requestData(url: "String") { (EmployeesArray, error) in
// print(error)
}
}
Now the Question is - For unit testing when I am calling the ListDataTest since the ListVC is in storyboard when it loads the View the viewdidLoad method calls the which will initiate the network logic.
SO I am not able to test only the UI related stuffs.
I tried to create some extension in ListDataTest class but no success is achieved.
Below is the flow of the Controllers : -
===
class ListVC: UIViewController {
var dataProvider: ListData
override func viewDidLoad() {
super.viewDidLoad()
dataProvider.viewLoaded()
}
}
=======
In ListData class
protocol DatProviderLoad {
func viewLoaded()
}
class ListData: NSObject {
}
extension ListData : DatProviderLoad {
func viewLoaded() {
print("loaded")
//the network calls goes here
}
}
/—
The test class
class ListDataProviderTest: XCTestCase {
var sut: ListData!
var controller: ListVC!
override func setUp() {
super.setUp()
sut = ListData()
let storyBoard = UIStoryboard(name:"Main", bundle: nil)
controller = storyBoard.instantiateViewController(withIdentifier: "ListVC") as! ListVC
controller.dataProvider = sut //before this called the storyboard already called the viewdidload once
_ = controller.view
}
}
Your feedback will be very helpful.
Any hint or tutorial in right direction will be highly appreciable.
Lets try to do this in MVVM way.
Think ViewController as part of View layer.
To call the network layer and to convert models into view models introduce a ViewManager.
The Viewcontroller will ask ViewManager to provide the data(ViewModel) and passes all actions(like button press) to ViewManager to handle the business logic.
This way it will be easy to write test cases for ViewManager layer(which is supposed to have all the business logic) and your View is not coupled with either the Network layer or data models.
Related
I am using VIPER architecture in iOS swift. I have 2 viewcontrollers, let's say A and B. At first I'm going from A to B, performing some tasks and coming back from B to A. With MVC or MVVM, we can create protocols and transfer data from B to A. But for VIPER, i am confused. Here's my VIPER code B, while Back button is tapped:
View:
#IBAction func backButtonTapped(_ sender: UIButton) {
presenter?.goBack()
}
Presenter:
func goBack() {
router.back()
}
Router:
func back() {
viewController?.navigationController?.popViewController(animated: true)
//here I want to send data back to previous viewcontroller
}
I have tried creating one method in the Router of previous controller and sending the data through that method but it is not working as router doesn't have any instance of presenter or anything else, except view.
Note:- In Viper Router is Unidirectional.
So this might help you.
Implement ProtocolDelegate in Current Module's VC
Create a delegate variable in the Next Module's Router
Then Simply Send Delegate Dependancy to Next Module's Router
And Call your delegate method from Next Module's Router.
Module A
final class VCA: SomeProtocolDelegate {
fund someMethod() {
//Your task Action From Module B
}
}
final class ModuleARouter: WireFrameProtocol {
fund gotoModuleB(withView vc: VCA) {
ModuleBRouter.load(onView: vc)
}
}
Module B
final class ModuleBRouter: WireframeProtocol {
internal weak var delegate: SomeProtocolDelegate?
// Here you can add more argument on load method for delegate
// since in this example i'm send data back ViewController So I didn't create
class func load(onView VC: UIViewController) {
//setup your VIPER protocol and class
if let vCA = VC as? VCA {
router.delegate = vCA
}
}
func backToPreviousModule() {
self.delegate?. someMethod()
}
}
In my app, the main ViewController is getting data from a sensor 60 times a second. In order to display the data, I have two ContainerViews, one that displays the raw data and another that displays it in a graph.
How do I either constantly send data from my mainVC to the ContainerView or let the ContiainerViews access variables in my mainVC?
There are lots of ways to slice this.
I would advise against collecting that data from your sensors in a view controller. That's not really a view controller's job. It gets worse when there are multiple objects who need that sensor data.
Probably the cleanest design would be to create a separate object (I'll call it a SensorManager) that collects your sensor data and passes it to anybody who cares.
You could have the SensorManager use the NotificationCenter to broadcast notifications, and then have all interested objects add observers for the notifications that they care about. That gives you very loose coupling between the SensorManager and the objects that get notified about sensor data. The downside is that the code is harder to debug.
Alternately, could set up your SensorManager to have an array of objects that it notifies. I would define a protocol that has one or more methods that get called with sensor data, and have the SensorManager maintain an array of client objects that conform to that protocol. When the SensorManager has new sensor data, it would loop through the array of client objects and call the appropriate method on each to tell each one about the new data. This second option is sort of like the delegate design pattern, but is a one-to-many, where the delegate pattern is a one-to-one passing of info.
If you are wedded to the idea of collecting the sensor data in your main view controller and you create your child view controllers using embed segues then you could write a prepareForSegue() method in your main view controller that looks for destination view controllers that conform to a protocol. Let's call it SensorDataListener. The main view controller could save those objects in an array and notify the objects about new sensor data using the methods in the protocol. (This last approach is similar to the approach of creating a SensorManager object, but instead it would be the main view controller serving that role.)
//At the top of your class:
protocol SensorDataListener {
func newSensorData(_ SensorData)
}
var sensorClients = [SensorDataListener]()
//...
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let dest = segue.desination as SensorDataListener {
sensorClients.append(dest)
}
}
And then when you receive new data
sensorClients.forEach { sensorClient in
sensorClient.newSensorData(sensorData)
}
Have both of your child view controllers as variables in main view controller and hook them up from self.childViewControllers of main view controller in viewDidLoad like so.
class ViewController: UIViewController {
var firstViewController: FirstViewController!
var secondViewController: SecondViewController!
override func viewDidLoad() {
super.viewDidLoad()
for vc in self.childViewControllers {
if let firstViewController = vc as? FirstViewController {
self.firstViewController = firstViewController
}
if let secondViewController = vc as? SecondViewController {
self.secondViewController = secondViewController
}
}
}
func sensorDataUpdated(data: Any) {
self.firstViewController.data = data
self.secondViewController.data = data
}
}
And here's an example of how one of your inner view controllers would work, the logic is the same for both of them:
class FirstViewController: UIViewController {
var data: Any? {
didSet {
self.updateUI();
}
}
func updateUI() {
guard let data = self.data else { return }
// Perform UI updates
}
}
I am currently developing an app for iOS written in Swift and built an interface with a UIPageViewController and two child view controllers so far.
Everything works fine, but at some points I trigger the PageViewController to set a different view controller. When this happens I want to pass data from one child to the other.
Now I know that one of the most asked iOS Dev questions is how to pass data between two UIViewControllers but my case is very specific and I could not find a similar question. I would appreciate links if I am wrong. Also I am not just looking for a solution because I found one myself. I am looking for the best one I know this is hard to judge, but at least I am looking for a better one than mine.
So I figured a way out, but I think it is not very elegant. So I am exchanging data through delegates. Basically I direct it from Child View Controller A to the PageViewController to the Child View Controller B via Delegation.
All that just works fine but I don’t know if this is the best way to go for or if there are other much better ways.
Thanks a lot for all your help.
What you did is probably the best way of doing it from all perspective but amount of code that is being used. Since your parent (page view controller) is responsible for all the work it is best to also channel all data it needs. Currently that is between 2 view controllers and later it might be between 3. You might also simply change these view controllers but preserve the protocols you use to retrieve the data.
But there is a big catch here. If a number of view controllers will grow then you might find yourself in an issue where previous view controllers are being deallocated (if this is not already going on) so at the end there is no way for view controller D to access view controller A simply because A no longer exists.
What the solution to these things is really depends but from your question I can assume you are passing some data from one view controller to another like onboarding where you are collecting user data through multiple screens. In such case it is best to have a class with all the data needed like:
class MyData {
var dataA: DataA?
var dataB: DataB?
var dataC: DataC?
}
Now the page controller is responsible to create such data and pass them to each of these view controllers that will use/modify the data. So in page view controller:
var myData: MyData = MyData()
func prepareViewControllerA() {
let controller: ViewControllerA...
controller.myData = myData
...
}
Now each of the view controllers will have its own property to access the same data object and modify it. You could also add a delegate to your class so page controller may listen to its events:
protocol MyDataDelegate: class {
func myData(_ sender: MyData, updatedA: DataA?)
func myData(_ sender: MyData, updatedB: DataB?)
func myData(_ sender: MyData, updatedC: DataC?)
func myDataAreFinalized(_ sender: MyData)
}
class MyData {
var dataA: DataA? {
didSet {
delegate?.myData(self, updatedA: dataA)
}
}
var dataB: DataB? {
didSet {
delegate?.myData(self, updatedB: dataB)
}
}
var dataC: DataC? {
didSet {
delegate?.myData(self, updatedC: dataC)
}
}
weak var delegate: MyDataDelegate?
func finalize() {
delegate?.myDataAreFinalized(self)
}
}
And now your page controller can use it:
var myData: MyData = {
let data = MyData()
data.delegate = self
return data
}()
and delegates:
func myData(_ sender: MyData, updatedA: DataA?) {
}
func myData(_ sender: MyData, updatedB: DataB?) {
}
func myData(_ sender: MyData, updatedC: DataC?) {
}
func myDataAreFinalized(_ sender: MyData) {
dismiss(animated: true, completion: nil)
}
Q
There are several communication patterns and there is no best way to go for , it depends on what are you going to do, or how you implement it.
I suggest reading to this answer.
I'm creating an app and I have all the logic done, but I want to do a Code refactoring and create MVC pattern. But I dealing with some asynchronous informations, that came from API.
/MenuViewController
Alamofire.request(.GET, Urls.menu).responseJSON { request in
if let json = request.result.value {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
let data = JSON(json)
var product: [Product] = []
for (_, subJson): (String, JSON) in data {
product += [Product(id: subJson["id"].int!, name: subJson["name"].string!, description: subJson["description"].string!, price: subJson["price"].doubleValue)]
}
dispatch_async(dispatch_get_main_queue()) {
self.products += product
self.tableView.reloadData()
}
}
}
}
This is my code, already working. But I want to create a Model that will handle this and just return the array of Products to my MenuViewController.
Model/Menu
class Menu {
var products: [Product] = []
init() {
Alamofire.request(.GET, Urls.menu).responseJSON { request in
if let json = request.result.value {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)) {
let data = JSON(json)
var product: [Product] = []
for (_, subJson): (String, JSON) in data {
product += [Product(id: subJson["id"].int!, name: subJson["name"].string!, description: subJson["description"].string!, price: subJson["price"].doubleValue)]
}
dispatch_async(dispatch_get_main_queue()) {
self.products += product
}
}
}
}
}
func totalOfProducts() -> Int {
return self.products.count
}
func getProducts() -> [Product]? {
return self.products
}
func getProductFromIndex(index: Int) -> Product {
return self.products[index]
}
}
But I got my self thinking, how I gonna get the main_queue to another class?
So I tried something like this:
class MenuViewControlvar: UITableViewController {
var products: [Product] = []
let menu: Menu = Menu()
// MARK: View Controller Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
if let products = menu.getProducts() {
self.tableView.reloadData()
}
// rest of the code
But didn't worked. My TableView is never updated.
I was wondering if I can do this, or I've to keep my Alamofire code in my viewDidLoad() from my MenuViewController
Thank you.
I am just giving you a direction with the step I would follow (Not writing the code thinking you can work it out):
First, write a networking class that accepts network request along with a competition block. Completion block shall be executed as soon as networking is done. This is a wrapper class and can be used across classes.
Second, write a model class that has all the parameters necessary for view controller's functionalities/view drawing.
Third, from view controller, call the networking class. In completion block, pass the model setting, table reload code and any code to remove loading overlay/indicator. This block should get executed on main queue.
Fourth, add code to show loading overlay/indicator before you trigger networking.
Delegation is an ideal solution for this problem of updating your model data and your view based on an asynchronous network call and it’s pretty much the same technique that is implemented throughout the iOS SDK to solve the same problem. There are many benefits of delegation over observation, another viable solution.
First, move your networking code to a separate class
class NetworkingController {
Create a protocol that view controllers can conform to. This provides the loose coupling between your network operations and your views to effectively maintain separation between the MVC layers.
#protocol NetworkingControllerDelegate: class {
func menuDataDidUpdate()
}
Have the networking controller support a property for its delegate
weak var delegate: NetworkingControllerDelegate?
In summary your networking class now looks something like this:
#protocol NetworkingControllerDelegate: class {
func menuDataDidUpdate()
}
class NetworkingController {
weak var delegate: NetworkingControllerDelegate?
// Insert networking functions here.
}
Then, have your view controller conform to this protocol like so
class MenuViewController: NetworkingControllerDelegate {
and create a new network controller in your view controller
var myNetworkController = NetworkController()
and set the delegate of your network controller instance to be your view controller
myNetworkController.delegate = self
Then in your networking code, when the network request has completed and your model has been updated, make a call to the networking controller's delegate.
delegate.menuDidUpdate()
Create the implementation for this method in your view controller since it is now the delegate for your networking code.
func menuDidUpdate() {
// Update your menu.
}
This makes your view controller look something like:
class MenuViewController: NetworkingControllerDelegate {
var myNetworkController = NetworkController()
override func viewDidLoad() {
myNetworkController.delegate = self
}
// MARK: NetworkingControllerDelegate
func menuDidUpdate() {
// Update your menu.
}
}
This is just the outline of the implementation to give you the necessary information about how to proceed. Fully adapting this to your problem is up to you.
Let's consider the following case:
I have a tab bar application where tapping each tab bar item takes user to other view that is handled by different view controller(typical pattern).
In one of my controllers I have method that downloads the important data and I want them to be global for whole application. What design pattern should I use?
One way to do that is to store this data using persistence such as core data, but is it the only way to make data visible for all view controllers? Maybe app delegate is able to perform such actions?
How in general you solve such situation where you have some data or variable which should be visible for all view controllers in your project?
Note that I'm not asking about persisting data across launches of the app, I just wonder how to make some data global in terms of the whole project.
Dont (emphasize DON'T) use following:
Singletons
AppDelegate (just another Singleton)
NSUserDefaults
Rather Don't:
Core Data
Do:
pass in either during instantiation or via properties
Why?
The DON'Ts messes up your memory
the Rather Don't messes with several principals of SOLID.
How would you do it correctly:
Create a base view controller that has a property that takes your data, make all your view controller inherit from it.
subclass UITabBarController
if a new view controller is selected, set the data to the view controller
the implementation is a bit tricky, this is from a real world app
class ContentTabBarController : UITabBarController {
private var kvoSelectedViewControllerContext: UInt8 = 1
required init(coder aDecoder: NSCoder) {
self.addObserver(self, forKeyPath: "selectedViewController", options: .New | .Old | .Initial , context: &kvoSelectedViewControllerContext)
}
deinit{
self.removeObserver(self, forKeyPath: "selectedViewController")
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if context == &kvoSelectedViewControllerContext {
var targetVC : UIViewController?
if let viewController = change["new"] as? UIViewController{
if let oldViewController = change["old"] as? UIViewController{
if viewController != oldViewController {
targetVC = viewController
}
}
} else {
targetVC = self.viewControllers![0] as? UIViewController
}
self.configureTargetViewController(targetVC)
}
}
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.translucent = false
}
func configureTargetViewController(viewController: UIViewController?){
//setup data
}
}
How does the tab bar controller get the data.
Well, that is up to you. It could fetch core data, it could actually pass a fetching manager as data. It could read from disc or network. But for a sane design it should not do it itself but use another class for it. and an instance of this fetcher class should be set from outside the tab bar controller, i.e. from the App Delegate.
One easy way would be to make a struct and make it hold variables. Then, you can edit it anytime you would want to. For example:
struct Variables {
static var number = 4
}
Then you can edit the data inside Variables in any view controller you want by doing this code.
Variables.number = 6 //or any other number you want
A cleaner and efficient, although not necessarily different, way to do this is to create a singleton class, e.g. AppData, which you can access in a variety of ways, and which would be available to all your other classes. It has the benefit of separating your app-specific stuff from the app delegate stuff. You might define the class this way:
#interface AppData : NSObject
// Perhaps you'll declare some class methods here & objects...
#end
you can define ivars for the AppData class, and then manage a singleton instance of AppData. Use a class method, e.g. +sharedInstance, to get a handle to the singleton on which you could then call mehods. For example,
[[AppData sharedInstance] someMethod:myArgument];
Your implementation of +sharedInstance can be where you manage the actual creation of the singleton, which the method ultimately returns.
Try this simple method,
1) Create a variable in appdelegate.swift that could be visible to all viewcontroller.
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
...
...
var Check:String!="" // to pass string
...
...
}
2) Create appdelegate instance in any viewcontroller
viewcontroller1.swift
import UIKit
class ViewController: UIViewController {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
...
...
override func viewDidLoad() {
super.viewDidLoad()
...
var tmp = "\(appDelegate.Check)"
appDelegate.Check="Modified"
}
}
Viewcontroller2.swift
import UIKit
class ViewController: UIViewController {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
...
...
override func viewDidLoad() {
super.viewDidLoad()
...
var getdata = "\(appDelegate.Check)"
println("Check data : \(getdata)") // Output : Check data : Modified
}
}