Passing Self ViewController as a Parameter? - ios

I have a TableView inside my app and i populate it from a JSON, which is downloaded inside a DataService class. This DataService class simply does the download & save data to array operation.
Inside my ViewController class, i also have an array for this data that is initialized empty at the beginning, filled with the data from DataService afterwards. Since i need to fill the data array (the one inside ViewController class) once the download operation is completed, DataService class needs to reach this array. So i decided to pass the ViewController object itself to DataService class so that it can fill VC's array with downloaded data and also reload its tableView data.
So my DataService class looks like this:
var dataArray: [EtkinlikCellData] = []
func fetchData(senderVC: EtkinliklerVC) {
// Download & fill dataArray operations
senderVC.dataArray = self.dataArray // setting sender VC's array
senderVC.tableView.reloadData() // reloading sender VC's table data
}
And how's it used inside ViewController class:
var dataArray = [EtkinlikCellData]()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
DataService.instance.fetchData(senderVC: self)
}
I know that if i did the fetch operation directly inside the ViewController class, there would be no problems since fetchData() method would reach the dataArray and tableView object itself. But since it is in another class, would it be wrong or harmful to pass a ViewController object as a parameter? Thanks.

The only threat is retain cycles and you can avoid it with weakly reference the object like
Option 1: // problem model shouldn't have any UI content
weak var delgate:EtkinliklerVC?
fetchData(senderVC: EtkinliklerVC) {
self.delegate = senderVC
....
self.delegate?.dataArray = self.dataArray
self.delegate?.tableView.reloadData()
Option 2 :
Establish a completion ( Recommended )
func fetchData(completion:#escaping:([String] -> ())) {
completion(arr)
}
then use it
DataService.instance.fetchData() { [weak self] arr in
// set it and reload
}
Plus:
Say you left your current code as it's , since the singleton alive which means it's strongly retained and you pass the vc object to it , user opens the vc and before data returns he pressed back button in your vc , since there is no weak reference between the singleton and the vc , then the vc will still be in memory while it should be de allocated

Related

Passing data from one view controller to the third directly

Is there any way of passing data from 1st view controller to (say) 3rd view controller without passing the data through the 2nd view controller?
I actually have a final submit button on the 4th view controller which gathers all the data right from the 1st view controller.
I want the data of each view controller to be directly transferred to the 4th view controller where the submit button is, without going through the view controllers to reach there.
I have already tried passing data through view controllers think there can be a more clear way of directly transferring data specially images as these are the main part of my data.
You could use a "Model" for this purpose with a delegate pattern.
A model is a class (or struct) which can be accessible by several VCs.
The delegate is going to be used to "notify" that a property value has changed.
/// Should be implemented by your VC
protocol MyModelDelegate: AnyObject {
func dataToShareChanged(to data: dataToShare)
}
/// Use the same instance for the VC1 and VC4
final class MyModel {
weak var delegate: MyModelDelegate?
var dataToShare: Foo {
didSet { delegate?.dataToShareChanged(to: dataToShare) }
}
}
In your case by the 1th and the 4th. Each of those VC should have the same instance of the model. You can achive this by giving the model object to the VCs if you initialize them.
If you are working with storyboards, you have to assging the models in the "viewDidLoad" for instance.
So you VC would look like:
class MyController: UIViewController, MyModelDelegate {
var model: MyModel?
func viewDidLoad() {
...
model.delegate = self
}
// Implementation of the delegate function.
func dataToShareChanged(to data: dataToShare) {
/// assign the new data value here
}
}
If you use this approach, you would not need to pass data though the VCs at all. Simple assign the new value in the model and the other VC is going to receive those data changes through the model delegate function.
Passing data forward from one view controller to the next isn't necessarily a bad thing. However when dealing with large amounts of data especially images you can easily run into memory pressure via this method.
Delegate way looks promising if all you needed was to inform the current viewcontroller neighbour (forward or backward) about data change.
Let me suggest an alternative set of solutions.
First off, don't manage image objects in memory. If you don't need it for anything else, write it to your apps temporary directory, keep hold of the URL and let go of the UIImage object. The snippet below lets you save your UIImage object to NSTemporaryDirectory with a name and return a URL object.
func saveImageToTempDirectory(image: UIImage, withName: String) -> URL? {
let url = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
.appendingPathComponent(withName, isDirectory: false)
.appendingPathExtension("jpg")
// Then write to disk
if let data = image.jpegData(compressionQuality: 0.8) {
do {
try data.write(to: url)
return url
} catch {
//handle error
return nil
}
}
return nil
}
You can choose to pass the URL from this method from one view controller to the other. Have this method in a Util class for better organization.
Method 1
Write the image urls from specific viewcontrollers into some local storage. You could use UserDefaults for this as its the easiest. You could also create separate folders for each viewcontroller while saving temp directory.
Method 2
Singletons. While singletons are frowned upon since they always hold state and becomes hard to test and/or debug, you could make use of a Singleton class that holds all your local URLs as part of arrays.
final class ImagePathManager {
static let shared = ImagePathManager()
var firstViewControllerImages: [URL] = []
//Initializer access level change now
private init(){}
}
You can append urls from first viewcontroller to ImagePathManager.shared.firstViewControllerImages and access them the same way from anywhere else in your application.
That being said, Singleton pattern usage is a slippery slope and you should always be very careful while using it in your apps.

I need to reorganize my solution for my iOS app due to redundant data

I realized I'm not approaching this correctly when I have 3 copies of the same object, so I need some guidance towards structuring this problem. I'll do my best to explain the model at the moment. For simplicity VC = View Controller
Tl;dr:
MainVC -> MapVC -> DetailsVC <- FavoritesVC
Tab1 Tab2
MyClass objects consist of a 'favorite' bool flag and a unique id String
MainVC makes array of MyClass, gives array to MapVC after construction
MapVC constructs dict mapping Pins to MyClass, user selects pin, corresponding
MyClass sent to DetailsVC in segue
DetailsVC gets copy of MyClass object, displays its details, can mark as favorite
Since marking as favorite occurs on the copied object, MapVC doesn't realize it's
marked as favorite
It starts with the MainVC. This has a container view, which switches between a MapVC and a TableVC (we'll focus on the MapVC). The MainVC uses an XML parser object to sort through a response and create custom MyClass objects and appends them to
myList //(an array of MyClass)
The MapVC also has an array of MyClass called mapList. When MainVC is done updating myList, it also sets
mapPage.mapList = myList
MapVC also has a dictionary that maps Pins to MyClass, so that when a user selects a pin I can see which MyClass it corresponds to.
The map sends the selected MyClass to the DetailsVC in a segue as such
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let dest = segue.destinationViewController as? DetailsTableViewController{
dest.myobject = mapList[selectedRow]
// selectedRow is correctly set elsewhere, no bugs
}
}
The DetailsVC (which only knows about the single selected myObject) displays the passed (or rather copied) object's details. You can then mark it as a favorite, which sets the object's favorite flag to true. To save favorites, I store a dictionary of MyClass objects that have been marked as favorite to UserDefaults. I use a dictionary instead of an array so I can look up and remove myObjects when a user unfavorites them
var faveArray = [String : MyClass]()
if let unarchivedObject = NSUserDefaults.standardUserDefaults().objectForKey(Constants.PreferenceKeys.favorites) as? NSData {
faveArray = NSKeyedUnarchiver.unarchiveObjectWithData(unarchivedObject) as! [String : MyClass]
}
if isFavorite{
faveArray[myobject.id] = myobject
}
else{
faveArray.removeValueForKey(myobject.id)
}
defaults.setObject(NSKeyedArchiver.archivedDataWithRootObject(faveArray as NSDictionary), forKey: Constants.PreferenceKeys.favorites)
The FavoritesVC loads the UserDefault dictionary and shows each myObject in a table view. You can also select a myObject here, which will segue to the same DetailsVC where they can unfavorite/refavorite continuously (the favorite button updates instantly, so if you favorite, it turns into unfavorite).
The problem is that when they unfavorite something from the FavoritesVC, it's a different copy MyClass object than the one sitting in the MapVC (same object, different copies), so when you go back to the map it still says "Unfavorite". The broader issue is that I have 3 of the same MyClass lists, 2 dictionaries, and 3 copies of the same MyClass object (1 in MapVC, 1 in ListVC, 1 in FavoritesVC) and I realize this isn't good
I'm thinking of moving all of the data objects to a static class (is that even possible?) so that all controllers have access to the same MyClass array and objects. Is there a better or standard approach?
What you want is a singleton. This is an instance of a class, that is initialized once and, when accessed, is the same instance (stored in the same block of memory, and the equivalent of a static instance). There are multiple singleton patterns, and as I mentioned in my comment, if you want to not accept my answer and simply ask about singletons in Swift, that is okay! It's an open question. Here is an example of the singleton pattern I use, for a MyClass, class:
private let mySingleton = MyClass()
class MyClass {
class var shared : MyClass {
return mySingleton
}
}
Add properties, etc... to this class, of course. To access the SAME instance of this class in any VC, use the following code:
let globalMyClass = MyClass.shared
That's it! Obviously, name your class and var whatever you like. And again, there is another solid approach to global vars in Swift. Use this for now, but learn when you have time. Good luck!

Automatically update an UILabel at var update in swift

At some point of my project I got an UILabel displaying the number of element in an array.
I want to update automatically the label text when I modifying the array.
I can make my UILabel global and access it with a "didSet" on the array, but I don't want to make all my UILabel global (I got 8 UILabel for 8 different evolutive var).
Here is a way to update the UILabel content like a pointer or a reference on a specific variable ?
Edit
Some code example :
I got a global which is a dictionary,
let eg:[string : [SomeClass]]!
In a ViewController I got a UILabel, with
label.text = eg["key"].count
I want to automatically update the value display if I do something like that
eg["key"].append(something)
Since you are using global data, one approach you might try is using NSNotificationCenter. If you were to encapsulate your global data inside an object, the object could post a notification every time the data is updated.
This approach will allow multiple observers the opportunity to act on changes to global state "automatically". It will also add the benefit of keeping your UIKit elements from being exposed.
Inside the userInfo property of the posted notification, you would place the key and associated count value.
Make your view controller an observer of this notification. When it receives the notification, it can update its UILabels itself, using the data received inside userInfo.
A simple implementation might look like this:
class SomeClass {
// ...
}
class MyGlobalObject {
var eg:[String : [SomeClass]]!
static let sharedInstance = MyGlobalObject()
private init() {
eg = ["someKey":[], "someOtherKey":[]]
}
func appendTo(key:String, value:SomeClass) {
eg[key]?.append(value)
NSNotificationCenter.defaultCenter().postNotification(NSNotification(name:"ValueChanged", object: nil, userInfo: ["Key":key, "Count" : (eg[key]?.count)!]))
}
}
class ViewController: UIViewController {
// Define labels, etc....
override func viewDidLoad() {
super.viewDidLoad()
// Put this wherever makes the most sense. viewWillAppear() works too. Don't forget to remove yourself as an observer when you are done.
NSNotificationCenter.defaultCenter().addObserver(self, selector:"didUpdate:", name: "ValueChanged", object: nil)
}
func didUpdate(notif:NSNotification) {
print("Received Notification!")
let userInfo = notif.userInfo
// Update appropriate label with the data
}
}
Then anywhere in your app, you could do this:
MyGlobalObject.sharedInstance.appendTo("someKey", value: SomeClass())

How to access all objects in one class from a different swift file

I'm trying to change the properties of a few IBOutlets from a different swift file and class. Ex: run function in class B (type UIView) to alter the alpha of IBOutlet in class A (type UIViewController). I've tried many different ways of accessing them (extensions,superclasses) but I still get nil values for these outlets. I am aware that this is because the view is allocated for but not loaded, but I cannot find a way to store and access class "A"'s outlets correctly to avoid this. Here is my code from class "B" :
let mainView = MainViewController() //Code in B
var changeAlpha = mainView.changeText() //Code in B
func changeAlpha() { //Code in class MainViewController
self.hotLabel.alpha = 0
}
Thanks in advance!
Don't try to allocate the MainViewController again.
Try to access the existing MainViewController instead.
var parentResponder: UIResponder? = self
while parentResponder != nil {
parentResponder = parentResponder!.nextResponder()
if let mainView = parentResponder as? MainViewController {
var changeAlpha = mainView.changeText()
}
}
When you try to create an object of MainViewController class (which is already includes your data), the values that you have stored earlier will not be accessible because the reference will be different for these two class objects
You have to pass the existing MainViewController class into the B class or try to access the existing MainViewController or implement the messaging concepts.
The only way to solve these kind of issues is, try to study the OOPS basic concepts (especially object life cycle, allocation etc..) before writing the code.

Accessing application delegate variable delays view loading in swift

I am a newbie to Swift and i have started my new project with Swift. I am facing a delay issue while loading a viewcontroller.
On the application delegate i have a variable
var allTerms: [Dictionary<String, AnyObject>]?
This allTerms is populated with data from a local json file of 900Kb. The total json data count is 800.
So far i have a home screen and a second view. From the home screen when i navigate to second screen i need to access this allTerms from the application delegate. Referring to great tutorials,i was able to access the allTerms variable from the application delegate
let appDelegate = UIApplication.sharedApplication().delegate as AppDelegate!
self.tableData = (appDelegate.allTerms! as NSArray) as? Array
However doing so this is causing a noticeable delay in loading the secondview , which doesnot happen if i comment the line
self.tableData = (appDelegate.allTerms! as NSArray) as? Array
Appreciate any suggestions!
You might want to create a separate data manager class instead of storing it in the app delegate. You could use something like this:
class DataManager {
var allTerms: [[String:AnyObject]]?
class var sharedInstance: DataManager {
struct Singleton {
static let instance = DataManager()
}
return Singleton.instance
}
// You can access allTerms by calling DataManager.sharedInstance.allTerms
}
This probably won't solve your lag, but it's a good practice to make a DataManager class to store things. I also rewrote your allTerms declaration to use the short form for the dictionary.

Resources