Your Second iOS App: Edit one object in two different views - iphone

I am going through the tutorials provided by Apple and tried to improve "My Second iOS App", the app for bird sightings.
(There is a MasterView where all entered sightings are listed. If you click one, you are directed to a DetailView of the sighting. You can add a sighting and are asked to enter a name and location.)
I want to sepperate the views for entering the birds name and location.
So I have two views (one for entering the name and one for entering the location) and one object I want to store.
In the file BirdSighting.m I added the following methods
-(id)initWithNameOnly:(NSString *)name date:(NSDate *)date
{
self = [super init];
if (self) {
_name = name;
_date = date;
return self;
}
return nil;
}
and
-(id)setLocation:(NSString *)location
{
if (self) {
_location = location;
return self;
}
return nil;
}
In the AddSightingNameViewController.m I implemented the following code
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"ToLocation"])
{
if ([self.birdNameInput.text length])
{
BirdSighting *sighting;
NSDate *today = [NSDate date];
sighting = [[BirdSighting alloc] initWithNameOnly:self.birdNameInput.text date:today];
self.birdSighting = sighting;
}
}
}
The view for entering the name leads with a push segue to the location-view. There has'nt been changed much else.
Now how do I pass the object generated in the first view to the second? And how do I call the setLocation method on this specific object in AddSightingLocationViewController.m? Do I have to define different properties? And how do I finally display the object in the MasterView with the correct data after I entered the location?
As this code is not working yet, I don't even know if it is working, what I am trying to do. So please be gentle, if this is crappy code.

This is the method I have been using:
First you will need to add a property in your destination view controller, to hold the object you want to pass:
#property (strong, nonatomic) BirdSighting *newSighting;
Then change the prepareForSegue method in your first view controller to the following:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"ToLocation"])
{
if ([self.birdNameInput.text length])
{
BirdSighting *sighting;
NSDate *today = [NSDate date];
sighting = [[BirdSighting alloc] initWithNameOnly:self.birdNameInput.text date:today];
self.birdSighting = sighting;
// Get destination view
YourDestinationViewController *vc = (YourDestinationViewController *)segue.destinationViewController;
// Pass birdSighting object to your destination view controller
[vc setNewSighting:self.birdSighting];
}
}
}
I think I originally got this method from this question
It is also worth noting that the BirdSighting class has a location #property in it's .h file & you will notice the #synthesize line in the .m file.
The #synthesize directive automatically creates accessor methods for you:
#property (nonatomic, copy) NSString *location;
Has the following methods automatically generated (but not visible in the file):
- (NSString *)location;
- (void)setValue:(NSString *)location;
Therefore it was unnecessary for you to override the setter method for location in the BirdSighting.m file with:
-(id)setLocation:(NSString *)location
If you remove that method (note that it should return void not id) you should now be able to access the location variable in a BirdSighting object in the following way:
// In this example we are accessing a BirdSighting #property (hence the use of self.sighting)
// #property (strong, nonatomic) BirdSighting *sighting;
// Getter - returns (NSString *)location of the BirdSighting object
[self.sighting location];
// Setter - sets the location property of the BirdSighting object to 'newLocation'
[self.sighting setLocation:newLocation];
Hope this clears some things up for you!

Related

iPhone: How do I share information between my UITabBarViewController and the individual Tabs?

I have a TableViewController that segues into a TabBarViewController.
I know how to pass my object via a segue, but not by a relationship like the TabBarViewController and it's tab share.
How can I do this? From the TabView is there a way to access the TabBarViewControllers member variables?
Update:
This is how I've solved the problem so far, but I'm not crazy about using the AppDelegate to do this...
Add the Following to WhateverYouNamedYourAppDelegate.h
#class myObjectIWantToPass;
#property (strong, nonatomic) myObjectIWantToPass *object;
Then add the following to the View Class file you have your data in that you want to pass on. I'm going to assume you know how to set up your object already in this file if your planning on passing it to another view.
WhateverYouNamedYourAppDelegate *appDelegate =
(WhateverYouNamedYourAppDelegate *)[[UIApplication sharedApplication] delegate];
appDelegate.object = object;
Then you do some similar work to retrieve the object back from the appDelegate in your destination View Class.
WhateverYouNamedYourAppDelegate *appDelegate = (WhateverYouNamedYourAppDelegate *)[[UIApplication sharedApplication] delegate];
object = appDelegate.object;
You can make singleton classes, so that all of the controllers can access those variables in the Singleton. See Code below
SingletonClass.h
#interface SingletonClass : NSObject {
NSString *someString;
}
#property(nonatomic, retain)NSString *someString;
+(id)shared;
#end
SingletonClass.m
#import "SingletonClass.h"
static SingletonClass *aShared;
#implementation LibShared
#synthesize someString;
+(id)shared
{
if (aShared == nil) {
aShared = [[self alloc] init];
}
return aShared;
}
-(id)init
{
if (self = [super init]) {
}
}
-(void)dealloc
{
[someString releease];
[super dealloc];
}
In the tabbar you can set the variable on SingletonClass:
[[SingletonClass shared] setSomeString:#"Value_Set"];
On the tableViewController, you can get the property of the someString variable on the SingletonClass:
NSString *string = [[SingletonClass shared] someString];
There's no need for a singleton pattern here. Instead, you can send the data-object forwards in - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender just as you do normally, you just need to find the correct viewController in the UITabBarController's viewControllers property.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"MyTabBarSegue]) {
UITabBarController *tabBarController = segue.destinationViewController;
// Either set the index here, if you know for sure which viewController is which, or
// Enumerate the viewControllers for isKindOfClass:[MYCustomViewController class] to be robust and change-proof
MYCustomViewController *myVC = [[tabBarController viewControllers] objectAtIndex:0];
myVC.dataObject = self.dataObject;
}
}

Passing an int to a new ViewController

Ive spent a month trying to pass my int to a new view. My int is called score and is connected to a label called scorelab. The user increases the score and once the they run out of time game switches views to game over screen and i would like the users score to be displayed on the game over screen in a label. The game play controller is called GamePlayViewController and the gave over controller is called GameDidEndViewController. I'm tired of trying to get this to work. I have looked at every tutorial possible but nothing seems to work for my case. Please help me i would appreciate it so so much, this is the very last part of game that I need to finish. Thank you in advance!
Sorry here is the code:
GamePlayViewController.h
#import "GameDidEndViewController.h"
int score;
IBOutlet UILabel *scorelab;
GamePlayViewController.m
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"GameOver"])
{
GameDidEndViewController *vc = [segue destinationViewController];
[vc setscore2: score];
}
}
GameDidEndViewController.h
#import "GamePlayViewController.h"
IBOutlet UILabel *scorelab2;
int score2;
}
#property (nonatomic , assign) int score2;
#property (nonatomic , assign) UILabel *scorelab2;
GameDidEndViewController.m
#synthesize score2 , scorelab2;
-(void)viewWillAppear:(BOOL)animated {
scorelab2.text = [NSString stringWithFormat: #"%d", score2];
[super viewWillAppear: animated];
}
What you do is create a property in GameDidEndViewController to hold the passed in score value.
#interface GameDidEndViewController : UIViewController {
....
}
#property(nonatomic,assign) int score;
#end
Make sure you #synthesize score in the #implementation(.m).
Now when you init and show the GameDidEndViewController you set the score, likely something like this.
GameDidEndViewController *end = .....// init the view.
end.score = score; // assuming you're doing this from GamePlayViewController where it has an int named `score`
Hope this helps !
Here is some tutorial for passing data between ViewControllers using Storyboard:
Storyboards Segue Tutorial: Pass Data Between View Controllers
Tutorial on How-To Pass Data Between Two View Controllers
or use something like this:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:...]) {
MyViewController *controller = (MyViewController *)segue.destinationViewController;
controller.score = score;
}
}
Hope this helps.

endless loop adding a subview

I am a little confused, and after countless attempts and read several articles I decided to write.
my problem is that if you call a method from a class (xml) and it is aimed at viewcontroller all goes well
but if I might add [self.view add...] it back to the top reloading the viewDidLoad of the viewController class entering into an endless loop.
this is what I do
class (ViewController)
.h
#import <UIKit/UIKit.h>
#class XMLStuff;
#interface skiSpeedViewController : UIViewController {
}
#property (nonatomic, retain) XMLStuff *xml;
.m
- (void)viewDidLoad
{
[super viewDidLoad];
xml.skiSpeedC = self;
GpsStuff *gps = [GpsStuff alloc];
[gps init];
}
gps.m
- (id)init
{
self = [super init];
if (self) {
xml = [XMLStuff alloc];
}
}
-(void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
[xml lon:newLocation.coordinate.longitude lat:newLocation.coordinate.latitude];
xml.h
#import "skiSpeedViewController.h"
#class skiSpeedViewController;
#interface XMLStuff : NSObject <NSXMLParserDelegate> {
}
#property (retain, nonatomic) skiSpeedViewController *skiSpeedC;
.m
#synthesize skiSpeedC;
- (void) parserDidEndDocument:(NSXMLParser *)parser {
NSLog(#"--%#", self.skiSpeedC); // Return (null)
[self.skiSpeedC riceviDic:datiMeteo];
}
ViewController.m
-(void)riceviDic:(NSMutableDictionary *)dictMeteo {
datiMeteo = [[NSMutableDictionary alloc]initWithDictionary:dictMeteo];
}
}
- (void) parserDidEndDocument:(NSXMLParser *)parser {
classViewController *skiSpeedC = [classViewController alloc];
[skiSpeedC riceviDic:datiMeteo];
}
You are creating a new instance of classViewController every time. Your "xml" class (XMLStuff?) should have a pointer to the view controller and be calling the riceviDic method on that instance.
You're getting an infinite loop because when you allocate the XML object in viewDidLoad, it too starts parsing the XML, then creates more XML objects, which then create more viewControllers...
So, add a property to XMLStuff of type classViewController, and when you create it in viewDidLoad:
xml.skiSpeedC = self;
Then, in parserDidEndDocument:
- (void) parserDidEndDocument:(NSXMLParser *)parser {
[self.skiSpeedC riceviDic:datiMeteo];
}
UPDATE
OK, after your edit things look very different - you seem to have introduced a new class - GpsStuff, which has its own instance of XMLStuff (and a dodgy looking init method which I assume you haven't copied in properly?). Which one is actually parsing your document? XMLStuff in your view controller, or in GPSStufF? I'm guessing the one in GPSStuff, which you haven't set the skiSpeedC property for. I was previously assuming that you were calling everything from your view controller.
Why not remove the creation of a new XMLStuff object from GPSStuff, and when you create GPSStuff in your view controller, pass the xml object you've created into it:
- (void)viewDidLoad
{
[super viewDidLoad];
GpsStuff *gps = [[GpsStuff alloc] init];
XMLStuff *xml = [[XMLStuff alloc] init];
xml.skiSpeedC = self;
gps.xml = xml;
[xml release];
}
Also, the skiSpeedC property should probably not be retain, since it is essentially a delegate assignment and the view controller is not going to be released before you release the xml parser.
As a note, by convention you should be initializing objects like this:
GPSStuff *gps = [[GPSStuff alloc] init];
Not on two lines. You want what is returned from init to be assigned to your variable.

How to copy text from one ViewController to another?

I'm writing a little quiz application. So I have a first view, where I ask a person to enter his/her name and a button which is the IBAction to go the next view with the first question. In the end, after a person has answered all questions, comes the congratulations view, where I want to copy the name, that was entered on the first view. Say there will be a phrase "Well done, John!" How can I do it?
On the first view(FirstViewController) I made a textfield
IBOutlet UITextField *text1;
And on the last view in .h:
IBOutlet UITextField *text2;
and
-(IBAction)copy:(id)sender;
And in .m:
- (IBAction)copy:(id)sender
{
[text2 setText:[text1 text]];
}
and
#import "FirstViewController.h"
But it says identifier text1 is undefined
I don't know what else to do...
You can override the
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
method of last view controller with something like:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil andUserName:(NSString*)userName
(don't forget to declare this method in your .h file.)
Now while initializing your last view controller, you can pass the name of the user to last view controller.
In the implementation of the this overriden method in last view controller, copy this name in some local variable. and use it.
Hope this will solve your problem.
In your firstView add NSString with its property
Something like this in .h file
NSString *username;
and
#property (nonatomic , retain) NSString *username;
in .m file
username = text1.text;
In LastView
im .m file
#import "FirstView.h"
And where ever you want username try this
FirstView *firstview = [[FirstView alloc] init];
text2 = firstview.username;
Hope it works..!!
Override the init method of SecondViewController as follows
-(id)initWithUsername:(NSString *)username
{
//create instance and then do
text2.text = username ;
}
And in the FirstViewController , where you create the instance of SecondViewController init it using the above
SecondViewController *second = [[SecondViewController alloc] initWithUsername:text1.text];
For passing value from one VC to other you have to do following code :
First declare local variable in next view where you want to move like.
#property(nonatomic, strong) NSString *personName;
Now prepare object of second VC in the first VC.m file on button click action. But before that must set storyboard identifier of that second VC in storyboard.
SecondViewController *obj = (SecondViewController *) [self.storyboard instantiateViewControllerWithIdentifier:#"SecondViewController"];
obj.personName = self.txtUserName.text;
Now go on there on second VC and do whatever you want.
Note : Here you don't need to used userdefaults, OR prepared init methods with parameters, OR using delegates etc.
The best way to do this would be taking the variables you need on various views and handling them on the delegate since the delegate has access to everything. Making a Global variable is never advisable that is what the delegate is for.
You can refer to this question that treats the same issue: Passing data between view controllers
You can add a NSString member (along with a property) in the application delegate class.
/// AppDelegate.h, or whatever it's called in your application
#interface AppDelegate : NSObject <UIApplicationDelegate> {
NSString *username;
// <rest of the members>
}
...
#property (nonatomic, retain) NSString *username;
#end
/// AppDelegate.m
#implementation AppDelegate
#synthesize username;
#end
/// FirstViewController.m
-(void)viewDidDissappear:(BOOL)animated{
[super viewDidDissappear:animated];
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
appDelegate.username = text1.text;
}
/// LastViewController.m (or whatever it's called)
-(void)viewDidLoad{
[super viewDidLoad];
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[text2 setText:[NSString stringWithFormat:#"Well done %#", appDelegate.username]];
}
Ok, so for everyone to be happy, you can (and are suggested to) create your own singleton class in which you can hold non-persistent global variables, and use in the code above instead of the application delegate.
/// SharedData.h
#interface SharedData: NSObject {
NSString *username; /// or instead you can have a NSMutableDictionary to hold all shared variables at different keys
}
+(SharedData *)sharedInstance;
#property(nonatomic, retain) NSString *username;
#end
/// SharedData.m
#include "SharedData.h"
static SharedData *_sharedInstance=nil;
#implementation SharedData
#synthesize username;
+(SharedData)sharedInstance{
if(!_sharedInstance){
_sharedInstance = [[SharedData alloc] init];
}
return _sharedInstance;
}
#end
I would make use of NSUserDefaults to set the user's name when they enter it. Then, in your last view controller, just query NSUserDefaults again for the name you want. Granted, this is still global state, but NSUserDefaults is meant for this sort of purpose:
// In the first view controller
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:#"John Doe" forKey:#"XXUserNameKey"]; // This should actually be an extern for your app to use globally
// In the last view controller
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSString *userName = [userDefaults stringForKey:#"XXUserNameKey"];
Adding state to your app delegate is an abuse of the design pattern.
Also, I should clarify that NSUserDefaults should not be used as a general-purpose storage container. Its intent is for common, shared data like user names, preferences, etc. Something like the name a user enters at the beginning of a game seems to fall within the usage pattern.

Method call in Objective-C

I'm new to Objective-C and iPhone SDK development. I want to call a method in the same class:
- (void) setFilePath:(NSString *) p
{
[self methodCall];
}
- (void) methodCall
{
fileContent.text = #"Test"; //fileContent is a UITextView
}
If the property "filePath" is set, the method "setFilePath" is called. Then the UITextView, created in IB, should display the text. But that doesn't work ...
If I call the method directly via button in IB, then the UITextView changes his content successfully:
- (IBAction) clickButton
{
fileContent.text = #"Test";
}
What could be the problem?
Thanks for your answers!
EDIT 2: I solved the problem by setting "filePath" after pushing the view:
- (IBAction) showFileContent {
FileContentsViewController *fileContentsViewController = [[FileContentsViewController alloc] init];
[self.navigationController pushViewController:fileContentsViewController animated:YES];
fileContentsViewController.filePath = self.filePath;
fileContentsViewController.title = [NSString stringWithFormat:#"Content from von %#", [filePath lastPathComponent]];
[fileContentsViewController release];
}
EDIT 1: Here's the code of my interface:
#interface FileContentsViewController : UIViewController {
NSString *filePath;
UITextView *fileContent;
}
- (void) methodCall;
#property (nonatomic, retain) NSString *filePath;
#property (nonatomic, retain) IBOutlet UITextView *fileContent;
#end
... and here's the code of the implementation:
#import "FileContentsViewController.h"
#implementation FileContentsViewController
#synthesize filePath;
#synthesize fileContent;
- (void) setFilePath:(NSString *) p
{
NSLog(#"setFilePath executed!");
[self methodCall];
}
- (void) methodCall
{
fileContent.text = #"Test"; // UITextView
}
// some standard methods
#end
... and finally the code of the method that sets "filePath":
- (IBAction) showFileContent {
FileContentsViewController *fileContentsViewController = [[FileContentsViewController alloc] init];
fileContentsViewController.filePath = self.filePath;
fileContentsViewController.title = [NSString stringWithFormat:#"Content from von %#", [filePath lastPathComponent]];
[self.navigationController pushViewController:fileContentsViewController animated:YES];
[fileContentsViewController release];
}
What it looks like is that the fileContentsViewController created in -showFileContent doesn't have anything assigned to its FileContentsViewController.fileContent (or, at least, fileContent doesn't point to a UITextView that gets displayed) when fileContentsViewController.filePath is set.
You set filePath immediately after creating fileContentsViewController. If FileContentsViewController's -init doesn't create an appropriate fileContent, then when -setFilePath: is called from -showFileContent, there's no fileContent to set the text of. If fileContentsViewController is a typical view controller, fileContent won't exist until fileContentsViewController is loaded, which (I believe) happens during -pushViewController:animated.
One fix is to override -setFileContent to set fileContent.text as appropriate:
-(void)setFileContent:(UITextView*)fileContentView {
if (fileContent != fileContentView) {
[fileContent release];
fileContent = [fileContentView retain];
if (self.filePath) { // if file path is not nil
fileContent.text = ...;
}
}
}
Another other fix is to ensure you only set filePath when fileContent exists, but this is more brittle. A third is to set filePath after you push fileContentsViewController.
The way you would discover the cause during debugging is to check two things: execution ("Is the code I'm expecting to be executed ever reached?") and data ("Do the variables hold the values I expect?"). Set breakpoints in -showFileContent and -methodCall so you know that the methods are being called (which would be one reason for failure). If execution makes it into -methodCall, the problem must be something else. From there, examine the values of the variables used in -methodCall and you'll discover fileContent is either nil or not the same fileContent that shows up later.
Have you checked that fileContent has been set up at the time setFilePath is called? If you're trying to set things up at start up then it's possible that you're making calls before the views have been loaded (which the OS delays until the last possible moment).
You can force views to load by calling [self view] just before you try to access any of your Interface Builder views (NB don't call loadView - that doesn't do what you'd think).
If the problem is that setFilePath: is not called that I would guess that your code looks like
filePath = #"some value";
when it should be
self.filePath = #"some value";
When using #property you need to use self.filePath to call the methods, otherwise you will just access the ivar directly.
How have you define filePath property ?
I think that it is the problem...