Where to put array of data - ios

I am trying to best follow MVC. Where is the 'correct' place to put my array of data? I am talking about the people array below. Should it be placed in the view controller below, or should it be part of a data service/data store class that is instantiated in the view controller (through dependency injection)? I have seen it done both places.
class ViewController: UIViewController {
var people: [Person] = []
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let context = appDelegate.persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
people = try? context.fetch(fetchRequest)
}
}

It's totally fine to store it inside viewController according to MVC. Although, you can move it to some separate source object, or dataProvider, but most of the time it's ok, and from your code it seems for me as it's okay.
I have few comments for you:
1) It's better to move out CoreData logic from AppDelegate - whether to separate singleton, or to service object (preferably, cause singletons are hard to test)
2) context.fetch is better to wrap inside do {} catch {} block most of times
3) You don't have to wait until viewWillAppear fires to fetch your data, at least from provided code
4) It's better to move logic of fetching inside some service with method like fetchPersons

Related

How do I constantly send data from parent ViewController to ContainerViewController

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
}
}

Why is selector being sent to previous view controller?

I have an app with a data model class that declares a protocol, and two view controllers embedded in a navigation controller. The data model class is a shared instance. Both view controllers are delegates of the data model. The second view controller has a UITableView.
On start, calls to data model functions from the first view controller work as expected. When I segue from the first view controller to the second, calls to data model functions work as expected as well.
However, when I navigate back to the first view controller and a data model function is called, the app crashes with this error:
2017-04-03 09:48:12.623027 CoreDataTest[3207:1368182]
-[CoreDataTest.PracticesViewController noDupeWithResult:]: unrecognized selector sent to instance 0x15fe136e0
That PracticesViewController is the second view controller. I don't understand why a selector is being sent to what I am thinking of as the previous view controller. My expectation is that the selector should be sent to the first view controller that has just been navigated back to,
I am self-taught, so I presume there is something basic that I am missing, but I don't know what I don't know. Can someone explain why the crash is happening?
Code for the data model
import Foundation
import CoreData
import UIKit
#objc protocol PracticesDataDelegate {
#objc optional func practicesLoadError(headline:String,message:String)
#objc optional func practicesLoaded(practices:[NSManagedObject])
#objc optional func practiceStored()
#objc optional func practiceDeleted()
#objc optional func noDupe(result:String)
}
class PracticesDataModel {
static let sharedInstance = PracticesDataModel()
private init () {}
var delegate: PracticesDataDelegate?
var practices: [NSManagedObject] = []
// some code omitted . . .
/// Check for a duplicate exercise
func checkForDupe(title:String,ngroup:String,bowing:String,key:String){
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Practice")
let predicate = NSPredicate(format: "exTitle == %# AND notegroup == %# AND bowing == %# AND key == %#", title, ngroup, bowing, key)
fetchRequest.predicate = predicate
do {
practices = try managedContext.fetch(fetchRequest)
if practices.count == 0 {
self.delegate?.noDupe!(result:"none") // exception happens here
} else {
self.delegate?.noDupe!(result:"dupe")
}
} catch let error as NSError {
// to come
}
}
The top of the first view controller
import UIKit
class galamianSelection: UIViewController, ExcerciseDataModelDelegate, PracticesDataDelegate {
let exerciseModel = ExerciseDataModel.sharedInstance
let pModel = PracticesDataModel.sharedInstance
// some code omitted . . .
//// THE VIEW ////
override func viewDidLoad() {
super.viewDidLoad()
exerciseModel.delegate = self
pModel.delegate = self
exerciseModel.loadExercises()
}
//// RESPOND TO PRACTICE DATA MODEL ////
func practiceStored() {
print("exercise stored")
}
func noDupe(result: String) {
if result == "none" {
let d = Date()
pModel.storePractice(date: d, exTitle: theExerciseTitle.text!, notegroup: theNoteGroup.text!, bowing: theBowings.text!, rythem: theRhythms.text!, key: theKey.text!, notes: "")
} else {
print("dupe")
}
}
The top of the second view controller
import UIKit
class PracticesViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, PracticesDataDelegate {
#IBOutlet weak var tableView: UITableView!
let pModel = PracticesDataModel.sharedInstance
// some code omitted . . .
//// THE VIEW ////
override func viewDidLoad() {
super.viewDidLoad()
pModel.delegate = self
pModel.getPractices()
}
delegate is properly set to self in both view controllers.
I am happy to provide more code is needed, but I suspect someone who knows can diagnose from what I've provided.
There are a few issues here.
You are trying to reuse a delegate but only setting it in viewDidLoad. viewDidLoad is only called when, as the name implies, the view initially loads. That means that if you have VC1 in a navigation controller, then you push to VC2, then pop back to VC1, viewDidLoad will not be called a second time. The view has already loaded. If you want a piece of code to be called every time a view controller comes back into focus, you should put it into viewWillAppear: or viewDidAppear:
You are force unwrapping an optional protocol method which you haven't actually implemented (a bug which you're seeing as a result of the first issue, but it's still an issue on its own). Change the force unwrap to an optional self.delegate?.noDupe?(result:"none")
You also declared your delegate like this var delegate: PracticesDataDelegate?. This makes your class retain its delegate and is generally not the right behavior. In your case it actually causes a retain cycle (until you change the delegate). You should change this declaration to weak var delegate: PracticesDataDelegate?
What appears to be happening is:
In PracticesViewController you set the model's delegate to self, as in pModel.delegate = self.
You navigate back to your first view controller. This means that PracticesViewController gets deallocated. But the model's delegate has not been changed, so it's still pointing to the memory location where the view controller used to be.
Later (but soon), your model tries to call a method on its delegate, but it can't because it was deallocated. This crashes your app.
You could reassign the value of the delegate, for example in viewDidAppear. Or you could have your model check whether its delegate implements a method before attempting to call it. That's a standard practice for optional protocol methods-- since they don't have to be implemented, you check first before calling them.
In general, don't use ! in Swift unless you want your code to crash there if something goes wrong.

Using a variable to type cast in Swift 3

I have two functions that are nearly identical and I want to merge them into one but I cannot find out how to handle the type casting in my if-let statement. There are only two solutions that I can think of but I cannot execute either of them.
Here are the two functions (there's a lot more to them but this is the only part that is causing me trouble in the merge):
func loadNextEventViewController() {
if let nextEventViewController = storyboard?.instantiateViewController(withIdentifier: "EventViewController") as? EventViewController {
// Executed code in here
}
}
func loadFinishViewController() {
if let finishViewController = storyboard?.instantiateViewController(withIdentifier: "FinishViewController") as? FinishViewController {
// Executed code in here
}
}
My first attempt was to make a generic parameter that could accept either the EventViewController OR FinalViewController, but as far as I can tell, there is no logical OR for generic parameters, only logical AND.
My second attempt was to create a computer variable but this didn't work either.
How can I take an argument in my function call that I could cast to be either class type in my if-let block?
Example:
func loadViewController(identifier: String, viewControllerType: UIViewController)
I've solved this issue in a very clunky way by using an in-else statement but I'd like to find a more elegant way of solving this problem.
You can do it this way
func load(_ viewController: UIViewController) {
if viewController is EventViewController {
//do stuff
}
if viewController is FinishViewController {
//do stuff
}
//do stuff that applies to all types of view controllers
}
Then call it like this:
let eventVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("EventViewController")
load(eventVC)
If you want to avoid using if statements you can use a switch statement instead, like so:
func load(_ viewController: UIViewController) {
switch viewController {
case is EventViewController:
//do stuff for EventViewController
case is FinishViewController:
//do stuff for FinishViewControllers
default:
//do stuff for other types, or break
}
//do stuff that applies to all viewControllers
}
This is not a problem of casting.. in your application you have two options, move to one vc or the other. There could be alot of logic that makes this decision or not. but either way, a decision HAS to be made at some point on what VC to show. You just need to decide on the best place to make this decision.
based on what you've shown so far, you could just do:
func showController(identifier: String) {
if let vc = storyboard?.instantiateViewController(withIdentifier: identifier) as? UIViewController {
// Executed code in here
}
}
I presume though, that each route has a different actions that require handling differently. The most common approach here is to use a segue, then you can use prepareForSegue to capture the segue and set properties as required based on the segue identifier. If you can provide some more information I can provide a better answer.
The main thing you need to consider here are.. whats actually different between the two. determine this and you can refactor your code to reduce repetition
You can do it like this
func loadViewController(identifier: String) {
let vc = storyboard?.instantiateViewController(withIdentifier: identifier) as? ViewController
if let eventVC = vc as? EventViewController {
// Executed code in here
} else if let finishVC = vc as? FinishViewController {
// Executed code in here
}
}

How to update UITabBarController from a Helper class?

I have a help class like this:
class NotificationHelper {
var managedObjectContext: NSManagedObjectContext!
init(context: NSManagedObjectContext) {
//super.init()
managedObjectContext = context
}
//functions related to Core Data
//update Badge
func updateBadge() {
var count = 1
let currentCount = self.tabBarController?.tabBar.items?[3].badgeValue
if currentCount != nil {
count = Int(currentCount!)! + 1
}
self.tabBarController?.tabBar.items?[3].badgeValue = String(count)
}
}
I'm just not sure how to get a reference to tabBarController so I can update it. I tried making the class inherit from UIViewController, but I think I was going down the wrong path there.
Also, am I correct in passing managedObjectContext like this? So that this class will be able to access Core Data.
Solved.
Instead of trying to inherit from somewhere, I decided to pass the UITabBarController as a parameter when needed:
func updateTabBarBadge(tabBarController: UITabBarController) {
It just means I have to call updateTabBarBadge every time I want to update it, instead of having other functions update it for me.

How to make data visible for all view controllers?

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
}
}

Resources