Variables and Transferring Data between View Controllers - ios

I know that there are tutorials everywhere, but I can't figure this out for some reason. I have a tab bar controller. Each tab links to a navigation controller, which is segued to a view controller. So, 2 main view controllers (StatusVC and TransactionsVC).
In StatusVC, I have a text field. In TransVC, I have a table view. A person adds a cell to the table. Math is done behind the scenes. The cell values are added together (numbers). This information is sent back to StatVC for calculations and displaying of the data. I've already got the math part down. My question: how do I transfer the data between view controllers, and better yet, how do I store this data so that it doesn't get deleted on quit (NSUserDefaults probably)?
This can be broken down I suppose, the transferring of data, the saving of data, and the displaying of data when the tab is pressed and view is shown.
I'm hoping this is making sense. Anyway, here's the code I've got. You're looking at TranVC. User enters data into the table with an alert view. You are looking at part of the Alert View delegate methods. This is when the user enters data into a cell (presses done). Look for key areas with the ******* comments.
StatusViewController *statVC = [[StatusViewController alloc]init]; //*******init
// Set the amount left in the budget
NSString *amountToSpend = statVC.amountLeftInBudget.text;
double budgetLabel = [amountToSpend doubleValue];
NSString *lastItem = [transactions objectAtIndex:0];
double lastLabel = [lastItem doubleValue];
double totalValue = budgetLabel - lastLabel;
NSString *amountToSpendTotal = [NSString stringWithFormat: #"%.2f", totalValue];
statVC.amountLeftInBudget.text = amountToSpendTotal; //*******set text (but not save), either way, this doesn't work
// Set the amount spent
NSString *sum = [transactions valueForKeyPath:#"#sum.self"];
double sumLabel = [sum doubleValue];
NSString *finalSum = [NSString stringWithFormat:#"%.2f", sumLabel];
//Set the amountSpent label
statVC.amountSpent.text = finalSum; //*******set text (but not save), either way, this doesn't work
// The maxed out budget section
if ([statVC.amountLeftInBudget.text isEqualToString: #"0.00"]) //*******set color (but not save), either way, this doesn't work
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor redColor];
} else if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedAscending)
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor redColor];
} else if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedDescending)
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor colorWithRed:23.0/255.0 green:143.0/255.0 blue:9.0/255.0 alpha:1.0];
}
if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedAscending)
{
// Create our Installation query
UIAlertView *exceed;
exceed = [[UIAlertView alloc]
initWithTitle: #"Budget Exceeded"
message: #"You have exceeded your budget amount"
delegate: self
cancelButtonTitle: #"Okay"
otherButtonTitles: nil];
[exceed show];
}
Any help with this would be amazing.

This is indeed a common question.
There are various solutions. The one I recommend is to use a data container singleton. Do a google search on the singleton design pattern in Objective C. You'll even find examples of it here on SO.
Create a singleton with properties for the values that you want to share. Then teach your singleton to save it's data. You can use user defaults, you can use NSCoding, you can extract the data to a dictionary and save it to a plist file in your documents directory, or various other schemes as well.

Like Duncan suggested, a Singleton pattern might be the best route to go. If you place the shared data into a model class, you can create a class method that can be used to acquire a singleton object.
MyModel.m
#implementation MyObject
- (id) init
{
return nil; // We force the use of a singleton. Probably bad practice?
}
// Private initializer used by the singleton; not included in the header file.
- (id)initAsSingleton {
self = [super init];
if (self) {
// Initialize your singleton instance here.
}
return self;
}
+ (MyModel *)sharedMyModel {
static MyModel *myModel = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
myModel = [[MyModel alloc] initAsSingleton];
});
return myModel;
}
MyModel.h
#interface MyModel : NSObject
+ (MyModel *)sharedMyModel; // Singleton instance.
#end
This does not protect against you using [[MyModel alloc] init];. It returns a nil object which is probably poor programming on my end, but it does force you to use the singleton object instead. To use in each one of your view controllers, you just use the following line to grab the singleton instance.
MyModel *model = [MyModel sharedMyModel];
Store the data into it, and return to your other view controller and grab the singleton again. You'll have all of your data.
After thinking about it, you could also force the default initializer to just return your singleton instance like:
- (id)init {
return [MyModel sharedMyModel];
}

Related

How to add KVO to synchronized class?

In my app I have the Restaurant class that you can see below. I'd like to attach a KVOController to it. But I'm having no luck. When I attach it with the code below, it crashes.
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;
[self.KVOController observe:self keyPath:#"[Restaurant current].name.asString" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
DDLogDebug(#"Restaurant changed");
}];
What's the best way to add KVO to a class like this?
#implementation Restaurant
static Restaurant *current = nil;
+ (Restaurant *)current {
#synchronized(self) {
if (current == nil) {
current = [[Restaurant alloc] initWithId:0];
}
}
return current;
}
- (id)initWithId:(NSInteger)number {
self = [super init];
if (self)
{
...
}
return self;
}
#end
The problem is not #synchronized. There are several issues with your code:
Do you want to observe when the current restaurant changes? Or when the current restaurant's name changes (without +[Restaurant current] pointing to a different restaurant instance). Or any kind of name change, whether triggered by a change of current or a change of name?
Depending on the answer, you'll either want to observe observe:[Restaurant class] or observe:[Restaurant instance], but definitely not observe:self (unless you're setting this up inside the Restaurant class implementation, in which case [self class] would be an alternative to [Restaurant class]).
For any change to be observable, you must ensure that the class is implemented in a KVO-compliant way. This goes both for changes to +[Restaurant current] as well as for changes to -[Restaurant name], depending on what you want to be able to observe.
[Restaurant current].name.asString is not a valid key path. Valid key paths may only contain property names (ASCII, begin with a lowercase letter, no whitespace) and dots to separate them (see Key-value coding for details). Once you're telling the KVOController to observe:[Restaurant class], all that remains for the key path is current.name.asString.
What is name if not a string? Do you really need to convert it to a string for observing it? If your intention is to watch for name changes, observing current.name is probably sufficient.
You'll likely end up with one of the following two options:
FBKVOController *kvoController = [FBKVOController controllerWithObserver:self];
[kvoController observe:[Restaurant class] keyPath:#"current.name" ...];`
// or
[kvoController observe:[Restaurant current] keyPath:#"name" ...];`
And again, for any changes to be observable, they need to be KVO-compliant.

How to save and load again data on views when back many times in iOS

I'm working on a project that has many view controllers. Suppose that they are:
A -> B -> C -> D -> E ->F ->G -> H. Each of them has a back and a next button to switch to another view and has many text fields.
I typed text into every textfield. From H view, I can go back to previous views by popviewcontroller and review typed data. but when I click on next button again, all of data on the view were lost. I need to back/next continuous without losing data. How can I do that?
Create a Singleton class.
Give in Singleton class a property like Form *form;
If you start your first ViewController create a new Form
[Singleton sharedInstance].form = [[Form alloc] init];
On leave first ViewController set property from TextField
[Singleton sharedInstance].form.name = textField.text
On leave second ViewController set property
[Singleton sharedInstance].form.mail = textField.text
In each ViewController in viewWillAppear method set stored text
self.textField.text = [Singleton sharedInstance].form.name
or
self.textField.text = [Singleton sharedInstance].form.mail
It's a simple example, but hope it helps to understand what is to do :)
What about using a NSMutableDictionary to keep the models for each view controller as a key value pair. And Each View Controller initialized with this NSMutableDictionary
- (id) initWithDataDictionary:(NSMutableDictionary *)aDataDictionary
{
self = [super init];
_myDataModel = (MyDataModel*)[aDictionary valueForKey:#"MyKeyName"];
if(_myDataModel == nil)
{
_myDataModel = [MyDataModel alloc] init];
aDataDictionary setValue:_myDataModel forKey:#"MyKeyName"];
}
return self;
}
- (void) viewDidLoad
{
[super viewDidLoad];
[self displayData];
}
- (void) displayData
{
self.text1.text = _myDataModel.name;
}
You may also able to keep a key-value pair for setting focus to a last focused textfield.
I see two options here:
If you use storyboards unwind segues are very good option.
Else you can create own delegate.

Reset property within lazy getter / after each access

I feel like there is a more regulation way to do what I am doing in, either by some iOS specific thing, or pattern I'm aware of. I'm trying to create an NSMutableArray variable, that essentially acts as temporary storage for a logger class. Each time the array is accessed, I want to either lazily instantiate it, or set it to nil. The way I am thinking of doing it seems a little hacky and I'm looking for some input?
- (NSMutableArray)myArray {
if (!_myArray) {
_myArray = [[NSMutableArray alloc] init];
} else {
_myArray = nil;
}
return _myArray;
}
The effect I'm hoping to achieve is using a logger that is logging details about network requests - http codes, URLs, repsonse times, etc. I want the logger to amalgamate all this output in this storage array. Later on, when I'm hitting an API, I want to take the contents of this array, and send it up to the API, and I also want the array to reset (so the array is essentially a log of network request data since the last time the app hits the API, versus a log of what has happened since the app launched.
I realise that I could do this manually by niling the array when I access it, but I'm trying to do this in a more of a plug and play way, where it you don't need to worry if someone forgets to nil the array etc
The effect that you are trying to achieve is perfectly legitimate, but you shouldn't try to achieve it with a getter alone: the very fact that a simple getter could reset something back to nil would be counter-intuitive to your readers.
Instead, you should make two methods - one to prepare the array, and another one to harvest it, and replace with a fresh nil:
- (NSMutableArray*)myArray {
if (!_myArray) {
_myArray = [[NSMutableArray alloc] init];
}
return _myArray;
}
- (NSMutableArray*)resetArray{
NSMutableArray *res = _myArray;
_myArray = nil;
return res;
}
Now the sequence of operations becomes intuitively clear: you get myArray as many times as you wish, add as many items as you need, and then when you are done call resetArray. This would get you a complete array with all the data, and reset the object to be ready for the next call:
for (int col = 0 ; col != 10 ; col++) {
[log.myArray addObject:[self getDataForIndex:col]];
}
NSMutableArray* logArray = [log resetArray];
What you're doing doesn't make any sense to me.
Creating it empty if it doesn't exist makes sense.
Setting it to nil if it does exist does not make sense.
The usual pattern for lazy loading is to use a property with a getter:
#property (nonatomic, retain) NSMutableArray * myArray;
and then the implementation:
//Custom getter
-(NSMutableArray*) myArray;
{
if (!_myArray)
_myArray = [[NSMutableArray alloc] init];
return _myArray;
}
Then you ALWAYS refer to the array using the getter. If it hasn't yet been created, the getter creates and returns the empty array. If it does exist, it returns the existing one.

Can you send a message from one class to trigger a 'self' in another?

I am adapting a program from a book course that I have been following and have three classes: a view controller, a view and a singleton that houses a variable that is shared by the other two.
I am trying to write some code within the view controller that will subsequently trigger the view to send a [self setNeedsDisplay] message. However, when I set this up as an instance method, the compiler throws a 'no class method' exception. When I change the method to a class method, the 'self' property no longer works.
I'll be honest, I'm still getting my head around a lot of the distinctions of OOP in general, so I may not have a full grasp of the situation. But I was wondering, is it possible to send a message from one class to another that triggers the second class to initiate a 'self' action?
Here is the code in the view controller that would send the message (with my current coding attempt):
- (IBAction)indexChanged:(UISegmentedControl *)sender
{
switch (sender.selectedSegmentIndex)
{
case 0:
{
UIColor *selectedColor = [UIColor colorWithRed:1.0
green:0.0
blue:0.0
alpha:1.0];
NSLog(#"Red button pressed");
[BNRHypnosisView setCircleColor:selectedColor];
break;
}
...
Here is the code in the view class that would receive the message:
- (void)setCircleColor:(UIColor *)circleColor
{
[[CDFSingleton colorPicker] changeCircleColor:circleColor];
[self setNeedsDisplay];
}
Not sure how relevant it is, but here is my singleton class:
#implementation CDFSingleton
+ (CDFSingleton *) colorPicker
{
static CDFSingleton *colorPicker = nil;
if (!colorPicker) {
colorPicker = [[super allocWithZone:nil] init];
}
return colorPicker;
}
+ (id)allocWithZone:(struct _NSZone *)zone
{
return [self colorPicker];
}
- (id)init
{
self = [super init];
if (self) {
//In this case, set the default circleColor to light gray
_circleColor = [UIColor lightGrayColor];
NSLog(#"Initial colour set to light gray (%#)", _circleColor);
}
return self;
}
- (void)changeCircleColor:(UIColor *)color
{
_circleColor = color;
NSLog(#"Circle colour changed to %#", color);
NSLog(#"_circleColor currently set to %#", _circleColor);
}
#end
If anyone has any thoughts and/or opinions on this, I would love to hear them!
OK, so, when you have two strings — say, #"kitten" and #"puppy" — if you wanted to get the length of a particular string, you would ask that string for its length. For example, you might write [#"kitten" length] and get the answer 6. You wouldn't write [NSString length], because you don't want the length of the NSString class — you want the length of that specific NSString that contains the word "kitten".
Your own classes work the same way. Just like you wouldn't talk to the NSString class when you want to know something about a particular NSString, you don't want to talk to the BNRHypnosisView class to affect the view being displayed in your app. Instead, you need to pass your view controller a reference to the specific BNRHypnosisView that is being displayed.

A ViewModel pattern for iOS apps with ReactiveCocoa

I'm working on integrating RAC into my project with the goal of creating a ViewModel layer that will allow easy caching/prefetching from the network (plus all of the other benefits of MVVM). I'm not especially familiar with MVVM or FRP yet, and I'm trying to develop a nice, reusable pattern for iOS development. I have a couple of questions about this.
First, this is sort of how I've added a ViewModel to one of my views, just to try it out. (I want this here to reference later).
In ViewController viewDidLoad:
#weakify(self)
//Setup signals
RAC(self.navigationItem.title) = self.viewModel.nameSignal;
RAC(self.specialtyLabel.text) = self.viewModel.specialtySignal;
RAC(self.bioButton.hidden) = self.viewModel.hiddenBioSignal;
RAC(self.bioTextView.text) = self.viewModel.bioSignal;
RAC(self.profileImageView.hidden) = self.viewModel.hiddenProfileImageSignal;
[self.profileImageView rac_liftSelector:#selector(setImageWithContentsOfURL:placeholderImage:) withObjectsFromArray:#[self.viewModel.profileImageSignal, [RACTupleNil tupleNil]]];
[self.viewModel.hasOfficesSignal subscribeNext:^(NSArray *offices) {
self.callActionSheet = [[UIActionSheet alloc] initWithTitle:#"Choose Office" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
self.directionsActionSheet = [[UIActionSheet alloc] initWithTitle:#"Choose Office" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
self.callActionSheet.delegate = self;
self.directionsActionSheet.delegate = self;
}];
[self.viewModel.officesSignal subscribeNext:^(NSArray *offices){
#strongify(self)
for (LMOffice *office in offices) {
[self.callActionSheet addButtonWithTitle: office.name ? office.name : office.address1];
[self.directionsActionSheet addButtonWithTitle: office.name ? office.name : office.address1];
//add offices to maps
CLLocationCoordinate2D coordinate = {office.latitude.doubleValue, office.longitude.doubleValue};
MKPointAnnotation *point = [[MKPointAnnotation alloc] init];
point.coordinate = coordinate;
[self.mapView addAnnotation:point];
}
//zoom to include all offices
MKMapRect zoomRect = MKMapRectNull;
for (id <MKAnnotation> annotation in self.mapView.annotations)
{
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.2, 0.2);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
[self.mapView setVisibleMapRect:zoomRect animated:YES];
}];
[self.viewModel.openingsSignal subscribeNext:^(NSArray *openings) {
#strongify(self)
if (openings && openings.count > 0) {
[self.openingsTable reloadData];
}
}];
ViewModel.h
#property (nonatomic, strong) LMProvider *doctor;
#property (nonatomic, strong) RACSubject *fetchDoctorSubject;
- (RACSignal *)nameSignal;
- (RACSignal *)specialtySignal;
- (RACSignal *)bioSignal;
- (RACSignal *)profileImageSignal;
- (RACSignal *)openingsSignal;
- (RACSignal *)officesSignal;
- (RACSignal *)hiddenBioSignal;
- (RACSignal *)hiddenProfileImageSignal;
- (RACSignal *)hasOfficesSignal;
ViewModel.m
- (id)init {
self = [super init];
if (self) {
_fetchDoctorSubject = [RACSubject subject];
//fetch doctor details when signalled
#weakify(self)
[self.fetchDoctorSubject subscribeNext:^(id shouldFetch) {
#strongify(self)
if ([shouldFetch boolValue]) {
[self.doctor fetchWithCompletion:^(NSError *error){
if (error) {
//TODO: display error message
NSLog(#"Error fetching single doctor info: %#", error);
}
}];
}
}];
}
return self;
}
- (RACSignal *)nameSignal {
return [RACAbleWithStart(self.doctor.displayName) distinctUntilChanged];
}
- (RACSignal *)specialtySignal {
return [RACAbleWithStart(self.doctor.primarySpecialty.name) distinctUntilChanged];
}
- (RACSignal *)bioSignal {
return [RACAbleWithStart(self.doctor.bio) distinctUntilChanged];
}
- (RACSignal *)profileImageSignal {
return [[[RACAbleWithStart(self.doctor.profilePhotoURL) distinctUntilChanged]
map:^id(NSURL *url){
if (url && ![url.absoluteString hasPrefix:#"https:"]) {
url = [NSURL URLWithString:[NSString stringWithFormat:#"https:%#", url.absoluteString]];
}
return url;
}]
filter:^BOOL(NSURL *url){
return (url != nil && ![url.absoluteString isEqualToString:#""]);
}];
}
- (RACSignal *)openingsSignal {
return [RACAbleWithStart(self.doctor.openings) distinctUntilChanged];
}
- (RACSignal *)officesSignal {
return [RACAbleWithStart(self.doctor.offices) distinctUntilChanged];
}
- (RACSignal *)hiddenBioSignal {
return [[self bioSignal] map:^id(NSString *bioString) {
return #(bioString == nil || [bioString isEqualToString:#""]);
}];
}
- (RACSignal *)hiddenProfileImageSignal {
return [[self profileImageSignal] map:^id(NSURL *url) {
return #(url == nil || [url.absoluteString isEqualToString:#""]);
}];
}
- (RACSignal *)hasOfficesSignal {
return [[self officesSignal] map:^id(NSArray *array) {
return #(array.count > 0);
}];
}
Am I right in the way I'm using signals? Specifically, does it make sense to have bioSignal to update the data as well as a hiddenBioSignal to directly bind to the hidden property of a textView?
My primary question comes with moving concerns that would have been handled by delegates into the ViewModel (hopefully). Delegates are so common in iOS world that I'd like to figure out the best, or even just a moderately workable, solution to this.
For a UITableView, for example, we need to provide both a delegate and a dataSource. Should I have a property on my controller NSUInteger numberOfRowsInTable and bind it to a signal on the ViewModel? And I'm really unclear on how to use RAC to provide my TableView with cells in tableView: cellForRowAtIndexPath:. Do I just need to do these the "traditional" way or is it possible to have some sort of signal provider for the cells? Or maybe it's best to leave it how it is, because a ViewModel shouldn't really be concerned with building the views, just modifying the source of the views?
Further, is there a better approach than my use of a subject (fetchDoctorSubject)?
Any other comments would be appreciated as well. The goal of this work is to make a prefetching/caching ViewModel layer that can be signalled whenever needed to load data in the background, and thus reduce wait times on the device. If anything reusable comes out of this (other than a pattern) it will of course be open source.
Edit: And another question: It looks like according to the documentation, I should be using properties for all of the signals in my ViewModel instead of methods? I think I should configure them in init? Or should I leave it as-is so that getters return new signals?
Should I have an active property as in the ViewModel example in ReactiveCocoa's github account?
The view model should model the view. Which is to say, it shouldn't dictate any view appearance itself, but the logic behind whatever the view appearance is. It shouldn't know anything about the view directly. That's the general guiding principle.
On to some specifics.
It looks like according to the documentation, I should be using properties for all of the signals in my ViewModel instead of methods? I think I should configure them in init? Or should I leave it as-is so that getters return new signals?
Yes, we typically just use properties that mirror their model properties. We'd configure them in -init kinda like:
- (id)init {
self = [super init];
if (self == nil) return nil;
RAC(self.title) = RACAbleWithStart(self.model.title);
return self;
}
Remember that view models are just models for a specific use. Plain old objects with plain old properties.
Am I right in the way I'm using signals? Specifically, does it make sense to have bioSignal to update the data as well as a hiddenBioSignal to directly bind to the hidden property of a textView?
If the bio signal's hiddenness is driven by some specific model logic, it'd make sense to expose it as a property on the view model. But try not to think of it in view terms like hiddenness. Maybe it's more about validness, loading, etc. Something not tied to specifically how it's presented.
For a UITableView, for example, we need to provide both a delegate and a dataSource. Should I have a property on my controller NSUInteger numberOfRowsInTable and bind it to a signal on the ViewModel? And I'm really unclear on how to use RAC to provide my TableView with cells in tableView: cellForRowAtIndexPath:. Do I just need to do these the "traditional" way or is it possible to have some sort of signal provider for the cells? Or maybe it's best to leave it how it is, because a ViewModel shouldn't really be concerned with building the views, just modifying the source of the views?
That last line is exactly right. Your view model should give the view controller the data to display (an array, set, whatever), but your view controller is still the table view's delegate and data source. The view controller creates cells, but the cells are populated by data from the view model. You could even have a cell view model if your cells are relatively complex.
Further, is there a better approach than my use of a subject (fetchDoctorSubject)?
Consider using a RACCommand here instead. It'll give you a nicer way of handling concurrent requests, errors, and thread-safety. Commands are a pretty typical way of communicating from the view to the view model.
Should I have an active property as in the ViewModel example in ReactiveCocoa's github account?
It just depends on whether you need it. On iOS it's probably less commonly needed than OS X, where you could have multiple views and view models allocated but not "active" at once.
Hopefully this has been helpful. It looks like you're heading in the right direction generally!
For a UITableView, for example, we need to provide both a delegate and
a dataSource. Should I have a property on my controller NSUInteger
numberOfRowsInTable and bind it to a signal on the ViewModel?
The standard approach, as described by joshaber above is to manually implement the datasource and delegate within your view controller, with the view model simply exposing an array of items each of which represents a view model which backs a table view cell.
However, this results in a lot of boiler-plate in your otherwise elegant view controller.
I have created a simple binding helper that allows you to bind an NSArray of view models to a table view with just a few lines of code:
// create a cell template
UINib *nib = [UINib nibWithNibName:#"CETweetTableViewCell" bundle:nil];
// bind the ViewModels 'searchResults' property to a table view
[CETableViewBindingHelper bindingHelperForTableView:self.searchResultsTable
sourceSignal:RACObserve(self.viewModel, searchResults)
templateCell:nib];
It also handles selection, executing a command when a row is selected. The complete code is over on my blog. Hope this helps!

Resources