In my app I use two table views that are bound to each their NSArrayController and the array controllers are set to use Core Data entities. When data is generated, an NSObject is created and values are stored in it with obj.setValue(_:forKey:). After this the object is simply added to the array controller with ac.addObject().
Shouldn't this suffice to have Core Data taking care of persistent storage of the data?
In any case, if I try to save the data by calling saveAction() it tells me that the MOC has no changes (moc.hasChanges = false) so it doesn't even begin to save the data with this method.
What else do I need to take care of to make Core Data store the data properly and acknowledge changes?
The array controllers are set in Interface Builder as follows:
Mode: Entity Name
Entity Name: 'name of entity in data model'
Prepares Content is checked
They are also correctly bound to the managed object context.
Simplified, relevant code from my app:
/* Clear existing data. */
let range:NSRange = NSMakeRange(0, arrayController.arrangedObjects.count);
let indexSet:NSIndexSet = NSIndexSet(indexesInRange: range);
arrayController.removeObjectsAtArrangedObjectIndexes(indexSet);
let array = generateData();
/* Generate data. */
for i in 0 ..< array.count
{
let data = array[i];
/* Create new data object. */
var obj:NSObject = arrayController.newObject() as! NSObject;
obj.setValue(data.name, forKey: "name");
obj.setValue(data.type, forKey: "type");
obj.setValue(data.category, forKey: "category");
/* Add it to the array controller's contentArray. */
arrayController.addObject(obj);
}
UPDATE:
It looks like my app is instantiating four MOCs when it launches. I suspect that the way how I add them in the Storyboard for the two array controllers is wrong. I added an NSObject to the two table view controllers (which also contain their array controllers) and set their base classes to be my CoreDataDelegate (which is my class for the core data code that is normally in AppDelegate). I suspect this is where the multiple instances of CoreDataDelegate are created. The question is: How should I do this right so that the array controllers can reach my CoreDataDelegate class?
I made my core data delegate class a Singleton which solved the problem for me...
class CoreDataDelegate : NSObject
{
static let instance = CoreDataDelegate();
...
}
Then I have a reference to it in my AppDelegate (which is also a singleton) ...
#NSApplicationMain
class AppDelegate : NSObject, NSApplicationDelegate
{
static let instance = AppDelegate();
let coreData:CoreDataDelegate;
override init()
{
coreData = CoreDataDelegate.instance;
super.init();
}
...
}
Then in the storyboard I added an NSObject to my two table view controllers and set the base class to AppDelegate. And then bound the ArrayControllers to the moc via AppDelegate/coreData.moc.
Now only one instance of CoreDataDelegate is created (and therefore only one moc) and I can happily report that saving works now!
Related
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 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!
I'd like to use a variable of type Any in order to pass different classes to a child view controller. For example, I might have Table, Chair and Plate objects. In my child view controller, I'd like to change the value of one of their properties (e.g. Table.legs was 4, change that to 3), and for the parent view controller to be able to read that from the child VC. I'll use a Protocol to update the parent VC that can pop the child after reading the updated object.
In order to work out how the passing of generics might work, I wrote this code in a playground:
class Table {
var legs: Int
var material: String
init(legs: Int, material: String) {
self.legs = legs
self.material = material
}
}
var anObject: Any?
// set up the Table
let aTable = Table(legs: 4, material: "Oak")
// set anObject to be the Table
anObject = aTable
// get the object and change it
let bTable = anObject as! Table
bTable.legs = 3
// get the original object and cast it as a Table
let cTable = anObject as! Table
print(cTable.legs) // prints 3
I believe from this, I should be able to do what I describe above without any issues, because the original object reference (anObject) is updated whenever I update a variable referencing it.
My question is this - are there any pitfalls I should be aware of when adopting this approach? It appears that rather than creating a copy of an object, swift will always create a pointer to the original object; are there any situations when that does not hold true?
Appologies if this is seen as a fairly basic question, but this is all fairly new to me - many thanks in advance!
Class are reference types as you noticed, if you assign an instance of the class to a variable, it keep the reference (the pointer in memory) to the instance and not the value copy.
Struct are value types, if you copy the instance of the structure to another variable, it's just copied to the variable.
When using Objective-C I would pass a NSMutableArray from one view controller VC_A to another VC_B by simply assigning a property in VC_B as
VC_B.list = self.list
where self is VC_A
It allows the changes done in VC_B on the list to be seen in the list in VC_A when the view controller was say popped off the navigation stack.
However in Swift as arrays are passed by value, assigning as above does not work so I am stuck how to solve this. What would be the correct way to handle this now?
You can still do this in Swift by making the property an NSMutableArray, just as before. Foundation types still exist if you ask for them. But this is bad design in both ObjC and Swift. It creates spooky action at a distance (when things magically change values that were not part of the call) and likely breaks MVC (if the data is persistent, it should live in the model, not in the view controllers).
There are two common patterns in Cocoa: store the data in the model, or pass it via delegation.
If this array represents some kind of persistent state (such as a list of items in the system), then that belongs in the model layer, and both view controllers should read and manipulate it there rather than by communicating with each other. (See Model-View-Controller.)
If this array is a transient piece of data (such as selections from a list), then the calling VC should set itself as the delegate to the receiving VC, and when the receiving VC finishes, it should pass the data back to its delegate. (See Delegates and Data Sources.)
If you use the standard Swift Array which is a value type you have to use a wrapper or a untyped NSArray.
// a generic wrapper class
class Reference<T> {
var value: T
init(_ val: T) { value = val }
}
// Usage
class ViewController1 {
static var list = Reference<[Int]>([])
}
class ViewController2 {
static var list = Reference([3, 5, 7, 9, 11])
func passToOtherVC() {
ViewController1.list = self.list
}
}
If you want to mutate the array you should always change the value property of the Reference object.
In Swift, objects are automatically passed by reference. NSArray is an Objective C class (pass by reference), where as Array is a struct (pass by value).
So if you are working with NSMutableArray the array is already being passed by reference.
Just as a potential proof of concept that complements my comment on the question - it is possible to use the Objective-C NSMutableArray to accomplish this task:
class A {
var x: NSMutableArray = NSMutableArray(capacity: 12)
}
class B {
var y: NSMutableArray!
}
let a = A()
let b = B()
b.y = a.x
b.y[0] = 123
assert(a.x[0] === b.y[0])
Still, this is approach is not following the Swift style of handling data structures IMO.
When using Objective-C I would pass a NSMutableArray from one view controller VC_A to another VC_B by simply assigning a property in VC_B as
VC_B.list = self.list
where self is VC_A
It allows the changes done in VC_B on the list to be seen in the list in VC_A when the view controller was say popped off the navigation stack.
However in Swift as arrays are passed by value, assigning as above does not work so I am stuck how to solve this. What would be the correct way to handle this now?
You can still do this in Swift by making the property an NSMutableArray, just as before. Foundation types still exist if you ask for them. But this is bad design in both ObjC and Swift. It creates spooky action at a distance (when things magically change values that were not part of the call) and likely breaks MVC (if the data is persistent, it should live in the model, not in the view controllers).
There are two common patterns in Cocoa: store the data in the model, or pass it via delegation.
If this array represents some kind of persistent state (such as a list of items in the system), then that belongs in the model layer, and both view controllers should read and manipulate it there rather than by communicating with each other. (See Model-View-Controller.)
If this array is a transient piece of data (such as selections from a list), then the calling VC should set itself as the delegate to the receiving VC, and when the receiving VC finishes, it should pass the data back to its delegate. (See Delegates and Data Sources.)
If you use the standard Swift Array which is a value type you have to use a wrapper or a untyped NSArray.
// a generic wrapper class
class Reference<T> {
var value: T
init(_ val: T) { value = val }
}
// Usage
class ViewController1 {
static var list = Reference<[Int]>([])
}
class ViewController2 {
static var list = Reference([3, 5, 7, 9, 11])
func passToOtherVC() {
ViewController1.list = self.list
}
}
If you want to mutate the array you should always change the value property of the Reference object.
In Swift, objects are automatically passed by reference. NSArray is an Objective C class (pass by reference), where as Array is a struct (pass by value).
So if you are working with NSMutableArray the array is already being passed by reference.
Just as a potential proof of concept that complements my comment on the question - it is possible to use the Objective-C NSMutableArray to accomplish this task:
class A {
var x: NSMutableArray = NSMutableArray(capacity: 12)
}
class B {
var y: NSMutableArray!
}
let a = A()
let b = B()
b.y = a.x
b.y[0] = 123
assert(a.x[0] === b.y[0])
Still, this is approach is not following the Swift style of handling data structures IMO.