My app has two views managed by a Tab Bar Controller. One of the views is Google Map (GMSMapView using their SDK) and the other is a TableView showing a list of the same data. The markers on the map are the same data in the TableView (just alternate presentations of the same data).
I fetch the data from an NSURLSessionDataTask. I'm wondering what is the best way to share that data between the two views. Obviously, I don't want to fetch the data twice for each view. But I'm not sure what is the best practice for making that shared data available/synched between the two views.
A similar question was asked but not answered here.
You can create a model class which holds the map related data in an array/dictionary/custom class objects. You can make this model class as a singleton(can be initialized only once). Both view controllers (i.e the map and table view) can refer to this model to populate date in different views now.
Model Class
-----------
#property (strong, nonatomic) MyCustomDataRepresentationObj *data;
+ (id)sharedModel {
static MyModelClass *sharedModel = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedModel = [[self alloc] init];
});
return sharedModel;
}
-(void)fetchMapDataWithCompletionBlock:(void(^)(id response, NSError *error)onComplete
{
// Check if data is available.
// Note: You can add a refresh data method which will fetch data from remote servers again.
if (!data) {
__weak MyModelClass *weakSelf = self;
// Make HTTP calls here, assume obj is returned value.
// Convert network response to your data structure
MyCustomDataRepresentationObj *objData = [MyCustomDataRepresentationObj alloc] initWith:obj];
// Now hold on to that obj in a property
weakSelf.data = objData;
// Return back the data
onComplete(objData, error);
} else {
onComplete(objData, nil); // Return pre fetched data;
}
}
Now in view controllers you would have to call the model class method which will inturn make the network call(if needed) and returns data in completion block.
View Controller 1
-----------------
-(void)viewDidLoad
{
// This is where the trick is, it returns the same object everytime.
// Hence your data is temporarily saved while your app is running.
// Another trick is that this can be accessed from other places too !
// Like in next view controller.
MyModel *myModelObj = [MyModel sharedModel];
// You can call where ever data is needed.
[myModelObj fetchMapDataWithCompletionBlock:^(id response, NSError *error){
if (!error) {
// No Error ! do whats needed to populate view
}
}];
}
Do the same in other view controller.
View Controller 2
-----------------
-(void)viewDidLoad
{
// Gets the same instance which was used by previous view controller.
// Hence gets the same data.
MyModel *myModelObj = [MyModel sharedModel];
// Call where ever data is needed.
[myModelObj fetchMapDataWithCompletionBlock:^(id response, NSError *error){
if (!error) {
// No Error ! do whats needed to populate view
}
}];
}
Note: I have just jotted down these lines of code here, there might be syntax errors. Its just to get the basic idea.
A UITabBarController act as a Container.
So from your 2 child ViewControllers, you can access the TabBarViewController with the property parentViewController.
So if you want to share the same data with your 2 child ViewControllers, you can fetch and store your data in your UITabBarController. And, from your UIViewControllers, you can access it like this
MyCustomTabBarController *tabBar = (MyCustomTabBarController*)self.parentViewController;
id data = tabBar.myCustomData;
Use Singleton Patterns create a singleton class and initialize singleton instance in your AppDelegate.m this way you can access your singleton class instance from your AppDelegate by using
How about a data fetching object? Make a new class that makes requests for your data bits and stores the results internally.
You then could get the data into your ViewController with a number of different methods:
Direct Reference Associate this object with each ViewController as a property on the ViewControllers before setting the viewControllers property on the Tab Bar Controller.
Your interface to this new class could include the set of fetched results, as well as a method (with a callback when the request finished perhaps) to tell the object to fetch more results.
Notification Center Your object could post notifications when it has more data, and just include a method to start requesting more data.
Delegate + Registration You could create a protocol for objects that want to get told about changes to the data set, make sure all of your necessary ViewControllers conform, and have a delegates NSArray property on your data fetching object. This is far more manual than Notification Center, but it's slightly easier if you need a very robust interface.
Needless to say, there are a lot of ways to handle this, but they all start with designating a class to do the specific task of fetching/storing your data.
Related
My situation is a bit more complex than what I've seen here before posting, and I'm not really good with memory management.
I have a custom UITableViewCell (that we will call MyCell here) and I pass its pointer to an UITableViewController (MyController here) when clicking on it. I pass the pointer because I want to call a method of this cell and the reference is only made by copy in Objective-C so it doesn't call the method on the right cell. I have made this:
MyController.h
#interface MyController : UITableViewController {
MyCell * __autoreleasing *_cell;
}
-(instancetype)initWithCell:(MyCell * __autoreleasing *)cell;
#end
MyController.m
- (instancetype)initWithCell:(MyCell **)cell {
if (self = [super init]) {
_cell = cell;
// Checkpoint 1
}
}
Then I want to use this variable later in my code, for example to define the number of sections:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Checkpoint 2
return (*_cell).contents.count; // contents being an NSArray property of the custom cell
}
The issue: At the "checkpoints" marked here, I have an NSLog(#"%ld", (unsigned long)(*_cell).contents.count);, however it shows 2 (the right number) in the first checkpoint, but 0 in the second checkpoint, so basically when I click on the cell an empty table view is shown.
I used to pass the cell by copy by storing it in a nonatomic, strong property and everything worked well, but by changing the calls from self.cell to _cell because of the pointer reference, the view is now empty as I said. It is likely a memory management issue, but I have no clue on how to solve it (fairly new to Objective-C, first app).
N.B.: I tried to change the __autoreleasing by a __strong, but this lead to a crash at every access of a property of the _cell. I have also tried to use a nonatomic, assign property to store it instead of using a ivar but it didn't solve my problem.
Thanks!
Edit: Forgot to mention that I call the view controller by using
[self.navigationController pushViewController:[[MyController alloc] initWithCell:(MyCell **)&cell] animated:YES];
in my previous view controller, in the tableView:didSelectRowAtIndexPath: method.
A few points:
The * __autoreleasing * pattern serves a very specific purpose, namely where a called method creates an object and needs to update the caller’s pointer to reference this new object. For example, consider an example from the regular expression documentation:
NSError *error = NULL;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"\\b(a|b)(c|d)\\b"
options:NSRegularExpressionCaseInsensitive
error:&error];
So, error is a NSError pointer, and the caller is
supplying the address to that pointer so that if
regularExpressionWithPattern instantiates a
NSError object, it can update the caller’s reference to point to it.
The only time you need to employ this “pointer to a pointer” pattern
is when the called routine is instantiating an object and wants to
update a pointer of the calling routine. We generally only have to
use that pattern when you want to return pointers to more than one
object (e.g. in the case of regularExpressionWithPattern, it will
return a NSRegularExpression * pointer, but optionally may also
want to update the NSError * pointer, too).
You said:
I pass the pointer because I want to call a method of this cell and the reference is only made by copy in Objective-C so it doesn't call the method on the right cell.
That logic is not quite right. MyCell * is a pointer to that
original object, not a copy of it. You can access it without
resorting to the “pointer to a pointer” pattern. In your case, if
you would just use MyCell *, not MyCell * *.
You should not pass a cell reference to this view controller at all ... one view controller should not be reaching into the view hierarchy of another view controller;
Table views have all sorts of optimizations associated with cell reuse ... one shouldn’t use a cell beyond interaction in its own table view data source and delegate methods; and
One should not use a cell (a “view” object) to store “model” data (what is currently stored in the contents property). Do not conflate the “model” (the data) with the “view” (the UI).
So, I would suggest a simple pointer (not a pointer to a pointer) to a model object:
#interface MyController : UITableViewController
#property (nonatomic, strong) NSArray <ModelObject *> *contents; // or whatever you previously stored in cell `contents`
#end
And then:
MyController *controller = [[MyController alloc] init]; // usually we'd instantiate it using a storyboard reference, but I'm gather you're building your view hierarchy manually
controller.contents = self.contents[indexPath.row]; // note, not `cell.contents`, but rather refer to this current view controller’s model, not the cell (the “view”)
[self.navigationController pushViewController:controller animated:YES];
And for passing data to and from view controllers, see Passing data between view controllers. But, as a general rule, one view controller shouldn’t be accessing the view objects of another.
I'm creating an user setup account with 5 steps using storyboard. Each step have a ViewController: 1º)Input for Name, contact etc, 2º) Import photos, 3º)Input, etc 4º)more inputs 5º)Confirmation Page, if the user click "confirm" -> Get all the inputs and upload to Parse.
The only solution i get when i search online for this, is to create a func "Prepare for Segue" and pass the information...But for me, this doesnt make any sense:
If i had 1000 viewcontrollers, the first viewcontroller information will be passsed through all the 1000 viewcontrollers? Why not the nº1000 view controller getting all the information that was left behind? e.g: The viewcontroller nº50 dont need the information about the viewcontroller nº1... This is a nonsense.
Is there any other way to implement this?
Maybe there is a solution since i'm using Parse like when i do:
ParseClass["Name"] = textfield.text
It sends information to Parse and they hold it until i do something like SaveInBackground().
I'm trying to find a solution like this using viewcontrollers. Each viewcontroller send information to parse and hold the information until the "saveinbackground()" happens on Viewcontroller nº5.
Possible? thank you
Yes it is possible. You can use NSUserDefaults for that which will store your info into memory and once it is stored you can use it anywhere in your project.
Like you can store value with it this way:
NSUserDefaults.standardUserDefaults().setObject(yourObject, forKey: "yourKey")
Now you can retrive this info from any where with yourKey like this:
let yourInstance = NSUserDefaults.standardUserDefaults().objectForKey("yourKey")
And you can cast it's type as per your need.
For more Info read Apple Document for NSUserDefaults.
One more way to pass value from one view to another view without segue by using Class or Struct.
You can read more about Classes and Structures from Apple Documentation.
If our inputs are finite, create a model class with properties for it ex:
#interface UserDataInput : NSObject
#property (nonatomic)NSString *name;
#property (nonatomic)NSString *contactNumber;
....
blah blah bla
....
#end
then make it as a singleton class
#implementation UserDataInput
+ (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static UserDataInput *sharedInstance = nil;
dispatch_once(&onceToken, ^{
sharedInstance = [[[self class] alloc] init];
});
return sharedInstance;
}
#end
Then set properties from a view controller on leaving that view controller like,
UserDataInput *sharedInput = [UserDataInput sharedInstance];
sharedInput.name = self.nameField.text;
etc....
In final view controller you can access these properties to upload to parse.
Try it.
let yourInstance = NSUserDefaults.standardUserDefaults().objectForKey("yourKey")
NSUserDefaults.standardUserDefaults().synchronize()
I have a simple app that:
Returns data from a server on load
Contains a UITabBarController with 2 items ViewController and TableViewController. The view is the first tab a user sees.
I have a model I am creating with data by calling this:
self.tide = [[TideModel alloc] initWithJSON:userLocationAsString];
I pass a longitude and latitude and it returns json. I have my view then accessing that data and displaying on the screen with no issues. What I am trying to solve now is how to get that data passed on to my TableViewController ?
TableViewController.m: (returns null
- (void)viewDidLoad
{
[super viewDidLoad];
self.tide = [[TideModel alloc] init];
NSLog(#"%#", self.tide.tideSummary);
}
TableViewController.h
#property (strong, nonatomic) TideModel *tide;
Few things
I am initializing my model with initWithJSON
When a user views the tableview the model is already populated with data, so I dont need to resend the JSON (that would be over kill).
Would a segue be needed if I am using data from a single model
initWithJSON: below:
-(id)initWithJSON:(NSString *)location {
self = [super init];
if(self) {
NSString *locationQueryURL = [NSString stringWithFormat:#"http://x/location/%#", location];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:locationQueryURL parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
// Extra code cut out to save space
..................
..................
..................
self.maxheight = [NSString stringWithFormat:#"%#", [dctOfTideSummaryStats valueForKey: #"maxheight"]];
self.tideSummary = [responseObject valueForKeyPath:#"tide.tideSummary"];
return self;
}
What is the best way to use that same data created and initialized from initWithJSON to be used within my TableViewController?
Thoughts?
I wouldn't pass data between two different view controllers in a tab bar controller. They're siblings so they shouldn't need to be tightly coupled to each other.
When you get the data from the server I would persist it somewhere like Core Data.
Then you could pass the NSManagedObjectContext to each view controller and they can access the data from Core Data individually. In a standard view controller you could use a NSFetchRequest to retrieve the data. In the table view controller you could use an NSFetchedResultsController to access the data.
If you use Core Data I would recommend looking at the Master-Detail Xcode project template. Make sure to select 'Use Core Data'. To get started look at the Core Data Programming Guide and the Core Data related sample code on Apple's developer site. Marcus Zarra's Core Data book is also excellent and there's good info on Core Data Libraries and Utilities here.
There are alternatives to Core Data as well. You could read/write to a plist file, you could use NSKeyedArchiver. There are also frameworks to make working with models easier. There are a number of discussions available to help decide which persistence approach to choose.
But with any of these choices I would recommend accessing the model data individually from each VC and not passing it between them because they're siblings. If you have a parent/child VC setup like if you wanted to edit a value in a table view then it would be correct pass the model object from the parent to the child.
I am still relatively new to iOS programming. Here is a question that confused me for a long time.
So in one of the view controllers, before this view controller is pushed into the navigation item, I am passing one parameter, say userId, to it in the prepareForSegue from previous view controller. And when this view controller is loading (initialising) based on the userId from the previous view controller, I am making a network call to fetch a list of information that's related to this user and then populating this information to the model of the current view controller.
Where should I put the logic of this data preparation?
Using viewDidLoad: should be fine for common storyboard use because the storyboard does not reuse view controller. Anyway, for the completeness of my view controller usage scenario, I tend to use this pattern:
Start loading remote data asynchronously in viewWillAppear:
Stop loading remote data in viewWillDisappear:
This make sure that your data will be always updated to the current userId because the ID might be changed after viewDidLoad, e.g. in case of view controller reuse or accessing .view property before setting userId.
You should also track if your data has been loaded. For example, you could make a private boolean field named _isDataLoaded, set it to true when finish loading data and set it to false when cancelling loading data or setting new userId.
To sum it up, the pattern in my idea should be something like this:
#interface UserViewControler : UIViewController {
bool _isDataLoaded;
NSURLConnection _dataConnection;
}
#implementation UserViewController
-(void) setUserId:(int)userId {
if (_userId != userId) {
_userId = userId;
_isDataLoaded = false;
}
}
-(void) viewWillAppear:(BOOL)animated {
if (!_isDataLoaded) {
_dataConnection = // init data connection here
_dataConnection.delegate = self;
[_dataConnection start];
}
}
-(void) viewWillDisappear:(BOOL)animated {
if (_dataConnection) {
[_dataConnection cancel];
_dataConnection = nil;
_isDataLoaded = false;
}
}
// NSURLConnection call this when finish
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
_isDataLoaded = true;
_dataConnection = nil;
}
// NSURLConnection call this when fail to load data
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
_isDataLoaded = false;
_dataConnection = nil;
}
It depends on what framework you use to retrieve data from remote server, but the pattern should be like this. This will ensure that:
You will load data only when the view appear.
View controller will not loading more data after disappear.
In case of same userId, data would not be downloaded again.
Support view controller reuse.
- (void) viewDidLoad {
[super viewDidLoad];
// initialize stuff
}
Although, it may be better to do the network call and gather all this information into a custom class that contains all the information, and then perform the segue. Then all you have to do in the new view controller is pulled data out of the object (which would still be done in viewDidLoad).
Arguably, this method might be better because if there's a problem with the network, you can display an error message and then not perform the segue, giving the user an easier way to reattempt the same action, or at least they'll be on the page to reattempt the same action after leaving app to check network settings and coming back.
Of course, you could just segue forward always, and segue backward if there's a network error, but I think this looks sloppier.
Also, it's worth noting that if you're presenting the information with a UICollectionView or a UITableView, the presenting logic can (should) be moved out of viewDidLoad and into the collection/table data source methods.
What I have done in the past is make custom initializers.
+(instancetype)initWithUserID:(NSString)userID;
Here is an example of the implementation.
+(instancetype)initWithUserID:(NSString *)userID {
return [[self alloc] initWithUserID:userID];
}
-(id)initWithUserID:(NSString *)userID {
self = [self initWithNibName:#"TheNameOfTheNib" bundle:nil];
if(self) {
_userID = userID;
}
//do something with _userID here.
//example: start loading content from API
return self;
}
-(void)viewDidLoad {
//or do something with userID here instead.
}
The other thing I would suggest is make a custom class that loads data and uses blocks.
Then you can do something like this
[API loadDataForUserID:userID withCompletionBlock^(NSArray *blockArray) {
//in this case I changed initWithUserID to initWithUsers
[self.navigationController pushViewController:[NextController initWithUsers:blockArray] animated:YES];
}
I have a very complex situation (well for me) I am trying to resolve but thus far am having trouble with it.
I will outline the structure of the application now and then explain the problem I am having.
The names I am using are made up due to sensitivity of the data I am using.
secondToLastViewController // is a UITableView on the navigation stack
lastViewController // is just a normal UIView that i want to push onto the navigation stack
RequestClass // this class dose requests to my database and passed the data back to correct classes
getInfoClass // class is used for this specific request stores the information correctly and passes it back to secondToLastViewController
So as the user initiates didSelectRowAtIndexPath inside secondToLastViewController I make a request for the data using the RequestClass
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//..
[RequestClass Getinfo:storedInfoPram];
}
now the thread shoots off to my RequestClass, which in turn queries the DB for some data which is then received and this data is passed off to my getInfoClass the reason I have done this is because there are dozens and dozens of different calls in RequestClass all doing different things, this particular request brings back alot of data I have to sort into correct object types so have created this class to do that for me.
anyway inside getInfoClass I sort everything into their correct types etc and pass this data back to secondToLastViewController in a method called recivedData, this is also where I think things are going wrong... as I create a new instance of secondToLastViewController the thing is I dont know how to pass the data back to the same secondToLastViewController that is already on the stack and was where the original request came from.
- (void) recivedData {
// do some stuff then pass data back to secondToLastViewController
SecondToLastViewController *sec = [[SecondToLastViewController alloc] init];
[sec sendGetSeriesArrays:pram1 Pram2:pram2 Pram3:pram3 Pram4:pram4 Pram5:pram5];
}
Now going back into SecondToLastViewController the thread lands in this method
- (void)sendGetSeriesArrays:pram1 Pram2:pram2 Pram3:pram3 Pram4:pram4 Pram5:pram5{
// call detailed view onto the stack
lastViewController *last = [[lastViewController alloc] initWithNibName:#"lastViewController" bundle:nil];
[self.navigationController pushViewController:last animated:YES];
}
after the thread reaches this point nothing happens... all the data is there and ready to be sent but the new view is never pushed to the controller stack.. and I think it is due to me declaring another version of secondToLastViewController when I am inside getInfoClass
what I would like to know firstly is how do I pass the recived data in sendGetSeriesArrays to the final view and secondly how do i even load the lastview onto the navigation stack?
Your observation is correct you are creating the secondToLastViewController instance again inside the getInfoClass. Dont do like that you have to use delegate/protocol approach for passing the data back to the secondToLastViewController.
Do like this
Define a protocol in getInfo class
getInfoClass.h
#protocol GetInfoClassProtocol <NSObject>
//delegate method calling after getting data
// I dont know the argument types give it properly
- (void)sendGetSeriesArrays:pram1 Pram2:pram2 Pram3:pram3 Pram4:pram4 Pram5:pram5;
#end
// declare the delegate property
#property (assign, nonatomic)id<GetInfoClassProtocol>delegate;
getInfoClass.m
- (void) recivedData {
// do some stuff then pass data back to secondToLastViewController
if ([self.delegate respondsToSelector:#selector(sendGetSeriesArrays: param2:)])
{
[self.delegate sendGetSeriesArrays:pram1 Pram2:pram2 Pram3:pram3 Pram4:pram4 Pram5:pram5];
}
}
secondToLastViewController.m
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
//..
RequestClass.delegate = self;
[RequestClass Getinfo:storedInfoPram];
}
Your secondToLastViewController should conform to the GetInfoClassProtocol
There are lots of ways you can accomplish this. In your revivedData function, instead of creating a new instance, you could:
1) Maintain a pointer to the navigation controller in getInfoClass, then you can get the last view controller from the view controllers on the navigation stack and use that. This will be the active instance of the view controller. There are ways to recover this from the window object, but those seem fragile and I would not recommend that approach.
2) You can pass a pointer to self from secondToLastViewController to your RequestClass getInfo call, then hold on to that and pass it back. This is probably a pain depending on the amount of code you have already.
3) You can maintain a static instance of the class if you will never have more than one secondToLastViewController. See How do I declare class-level properties in Objective-C?