I have a navigation controller with 2 view controllers, A and B.
A and B both have a property
#property (strong, nonatomic) NSString *string;
string is created in controller A and passed to controller B
ViewControllerB.string = self.string;
[self.navigationController pushViewController:ViewControllerB];
In View Controller B, string is modified and when I pop to View Controller A, the value of string has not changed.
Since they both hold a strong reference to it, shouldn't string be changed in both View Controllers?
Am I missing something?
In View Controller B, string is modified
This is impossible since you're using NSString, not NSMutableString. If you replace it within ViewControllerB:
self.string = #"some other string";
Then you are setting ViewControllerB's string property to reference that new string, but other references to the original string are not affected.
If you really want both view controllers to modify the same string, use NSMutableString. But shared mutable state is probably bad. Instead, consider using an intermediary class (like a data source), or the delegate pattern for ViewControllerB to hand ViewControllerA an updated string.
Related
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.
I've added a view controller as child like this:
UIViewController *sendViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"send"];
[self addChildViewController:sendViewController];
In the sendViewController I got this property:
#property (strong, nonatomic) IBOutlet StringInputTableViewCell *nameCell;
and in that class I can do something like self.nameCell.textField.text = #"test"; to set the textField to test
But how can I get this text thats filled in the textField in the parent class?
You are mixing model and view, thus breaking the MVC design pattern.
You should not try to read what is the content of a UI element. Rather you should have all the data (i.e. model) and the view (i.e. the UI, such as text fields) managed by a controller.
There are (easy) ways to get to this information, but I strongly advise you not to go down that road!
Basic inheritance between the parent and child class should allow you to pass the property forward.
You'll need to create a child object of the class say obj. Then to get the text value of the field you'll use (in the parent class)
id obj = [[ChildClassName alloc] init];
NSString *myChildsText = obj.nameCell.textField.text; // will get the value #"test" as assigned in the childclass.
Or of course, you can create a getter and setter in the Child Class for your #property. For example ::
- (IBOutlet)nameCell {
// returns the value
}
- (IBOutlet)setNameCell :(StringInputTableViewCell)newValue {
//set the value to the #synth value hereā¦
}
then you can call the child objects getters/setters as below ::
NSString *text = [obj nameCell]; //etc etc
You can use 4 approaches in here
a) keep reference to you childviewcontroller -> get the value directly
b) use delegate method ( best for observing changes in your textfield )
c) send NSNotification -> this comes with overhead, well at certain points it can help you a lot
d) KVO