Using storyboards you have no easy access to the first view controller in appDelegate (though once you do prepareForSegue makes it easy to pass the ManagedObjectContext down the navigation stack.
I've settled on giving each view controller (or superclass of each view controller) requiring Core Data access a moc member:
#synthesize moc = _moc;
#property (nonatomic) __weak NSManagedObjectContext *moc;
I'm uneasy about it because it doesn't seem a very elegant way to do it - too much code. But assigning directly requires specifying absolute indexes into the viewControllers arrays and changing appDelegate every time the requirement for ManagedObjectContexts change
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController;
// rootView gets a tab bar controller
for(UINavigationController *navController in tabBarController.viewControllers) {
for(UIViewController *viewController in navController.viewControllers) {
if([viewController respondsToSelector:#selector(setMoc:)]) {
[viewController performSelector:#selector(setMoc:) withObject:self.managedObjectContext];
NSLog(#"Passed moc to %#", [viewController description]);
}
}
}
return YES;
}
What are the pitfalls of this approach and is there a better way? Is it better to try and be more generic:
- (void)assignManagedObjectContextIfResponds:(UIViewController *)viewController {
if([viewController respondsToSelector:#selector(setMoc:)]) {
[viewController performSelector:#selector(setMoc:) withObject:self.managedObjectContext];
NSLog(#"Passed moc to %#", [viewController description]);
}
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSMutableArray *viewControllers = [NSMutableArray array];
UIViewController *firstLevelViewController = self.window.rootViewController;
if([firstLevelViewController respondsToSelector:#selector(viewControllers)]) {
NSArray *firstLevelViewControllers = [firstLevelViewController performSelector:#selector(viewControllers)];
for(UIViewController *secondLevelViewController in firstLevelViewControllers) {
if([secondLevelViewController respondsToSelector:#selector(viewControllers)]) {
NSArray *secondLevelViewControllers = [secondLevelViewController performSelector:#selector(viewControllers)];
for(UIViewController *thirdLevelViewController in secondLevelViewControllers) {
[viewControllers addObject:thirdLevelViewController];
}
} else {
[viewControllers addObject:secondLevelViewController];
}
}
} else {
// this is the simple case, just one view controller as root
[viewControllers addObject:firstLevelViewController];
}
// iterate over all the collected top-level view controllers and assign moc to them if they respond
for(UIViewController *viewController in viewControllers) {
[self assignManagedObjectContextIfResponds:viewController];
}
return YES;
}
Don't know if I understood properly, but why don't you left the managed object context directly in AppDelegate class and leave there all the logic for instantiate. And from then you can ask for it.
#property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
then you can recall it anytime from anywhere.
NSManagedObjectContext *moc = [(YourApplicationDelegate*)[[UIApplication sharedApplication] delegate] managedObjectContext];
For convenience I declared a define for it:
#define MOC [(YourApplicationDelegate*)[[UIApplication sharedApplication] delegate] managedObjectContext]
Therefore this become:
[MOC save:&error];
You can take this everywhere you like.
Just try to have a look at the auto generated code for a CoreData application in Xcode, you will see that many accessors with CoreData are in there, and the CoreData itself is lazily initialized at first request.
Adam,
Whilst I was exploring storyboards I pretty much did it the same way you did except I made each of my view controllers that had a MOC property conform to a protocol.
There's nothing significantly different there, so I'll move on.
I think the point is Storyboards, IMO, are half-baked. Coming from a .Net background what is obviously missing is an object builder framework coupled with an IoC container.
When Apple add that Storyboards will be awesome. When the storyboard framework can look at the destinationViewController, determine it's dependencies and resolve those from a container life will be great. For now, all it can really do is look at the destinationViewController and init you a generic one, which is of limited use.
Unfortunately, because it's a half-baked solution I'm sticking with the traditional approach for now so all my view controllers are alloc'd and init'd manually and more importantly I've added a method to each view controller to initWithMOC:(MOC *)moc;
The architect in me is telling me this code is more robust, I guess it's a matter of opinion as to whether it's worth the trade-off.
Anyone else come up with a better way?
CA.
Related
I'm reading the Big Nerd Ranch book and I'm at the 10th chapter about the navigation controller.
In the main controller, there is a TableView ( https://github.com/bignerdranch/iOS3eSolutions/blob/master/11.%20Homepwner/Homepwner/Homepwner/ItemsViewController.m ) where there are two methods that interact with the detail view :
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[[self tableView] reloadData];
}
- (void)tableView:(UITableView *)aTableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
DetailViewController *detailViewController = [[DetailViewController alloc] init];
NSArray *items = [[BNRItemStore sharedStore] allItems];
BNRItem *selectedItem = [items objectAtIndex:[indexPath row]];
// Give detail view controller a pointer to the item object in row
[detailViewController setItem:selectedItem];
// Push it onto the top of the navigation controller's stack
[[self navigationController] pushViewController:detailViewController
animated:YES];
}
In the detail view controller (https://github.com/bignerdranch/iOS3eSolutions/blob/master/11.%20Homepwner/Homepwner/Homepwner/DetailViewController.m), there is a method that "saves" the BNRItem being changed :
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// Clear first responder
[[self view] endEditing:YES];
// "Save" changes to item
[item setItemName:[nameField text]];
[item setSerialNumber:[serialNumberField text]];
[item setValueInDollars:[[valueField text] intValue]];
}
The code works well :
I understand how the main controller set the object to edit but I don't understand how the main view controller knows that the BNRItem has been changed and then set it back to the tableview ?
I was excepting the author to write a setter in the main controller (ItemsViewController.m ) that could be called by the detail view controller (DetailViewController.m) giving the new BNRItem.
But this part works "automatically".
Thank you.
As you are pointing to the same memory chunk in both controllers, changed made in the DetailViewController are obviously reported in ItemsViewController.m.
I am trying to set up an app so that there is a background task where the locationManager receives updates that are then stored using Core Data. Here is the code I am using to run the locationManager as a background task:
(void)viewDidLoad
{
[super viewDidLoad];
timerAndMap = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endDate];
}];
if(nil == locationManager) {
locationManager = [[CLLocationManager alloc] init];
}
locationManager.delegate = self;
locationManager.distanceFilter = 500;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
[locationManager startUpdatingLocation];
[[UIApplication sharedApplication] endBackgroundTask:timerAndMap];
}
However, the problem I am having is figuring out how to make the delegate methods part of the background thread. When the locationManager calls didUpdateToLocation I want to save the longitude, latitude, and date of the new location as attributes of an entity in a temporary managedObjectContext that is then merged with my established ManagedObjectContext on the main thread (as to my understanding while the app is running in the background new Core Data entities cannot be stored to my main thread ManagedObjectContext).
Locations are updated every second or so, create a NSMutableArray with the coordinates, keep a list of the coordinates, and when it reaches a point, create an NSArray with the coordinates from the mutable array, clear the NSMutableArray, pass the NSArray instance to the background thread, and perform the save.
Edit: based on the new information: CLLocationManager's method are asynchronous. What you can do is, if this is what you are trying to accomplish, to dispatch a background block and create your entity within the block on your delegate callback. You can even manage your own own serial dispatch_queue to do this.
Take into consideration thou, that just because you are doing something it the background that does not make it efficient. You will still be calling save every time you get a new location (which could be every 1-2 seconds). Writing into the persistent store is expensive.
I am currently using the new iOS 5 Storyboard approach to creating my Tabbed Application with Monotouch. I have developed two of my tab views in Xcode with Storyboard and linked them appropriately to the Tab Bar Controller. I also want to develop (in Xcode) a third tab view that would be shared among two additional tabs. I want to reuse the same layout, but display different data depending on which tab is selected (think something like a "Popular" and a "Recent" that would have the same layout but different data).
To do this, I figured I could add the tab manually twice after the Storyboard-driven tabs are added. How do I do this with the Storyboard approach? I'm not sure where in the code to do this since the loading of the Storyboard seems pretty transparent (i.e. no code in AppDelegate that I see). Or, is there another (easier/better) way to share a view between two tabs using the Storyboard approach?
I don't know Monotouch, but here's how I did it in Objective-c. I didn't find anything about this topic, so if something is wrong, people please comment :) By the way, I'm using ARC, so I don't manually manage memory! What I needed to achieve was like you, having a tab bar, loading the same viewController, but loading different data for each tab.
AppDelegate.m
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UITabBarController *root = (UITabBarController*)self.window.rootViewController;
UIStoryboard *mainStoryboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle: nil];
TeamViewController *home = [[mainStoryboard instantiateViewControllerWithIdentifier:#"Team"] initHome];
TeamViewController *visitor = [[mainStoryboard instantiateViewControllerWithIdentifier:#"Team"] initVisitor];
[root setViewControllers:[NSArray arrayWithObjects:home, visitor, nil] animated:NO];
UITabBar *tabs = root.tabBar;
UITabBarItem *homeTab = [tabs.items objectAtIndex:0];
UITabBarItem *visitorTab = [tabs.items objectAtIndex:1];
homeTab.title = #"Home team";
visitorTab.title = #"Visitor team";
return YES;
}
You can see I call initHome and initVisitor when I load my two TeamViewController, here is the code about it.
TeamViewController.h
#interface TeamViewController : UIViewController
{
enum
{
HOME,
VISITOR
};
int team;
}
TeamViewController.m
- (id)initHome
{
team = HOME;
return self;
}
- (id)initVisitor
{
team = VISITOR;
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
if(team == HOME)
{
label.text = #"home data";
}
else if(team == VISITOR)
{
label.text = #"visitor data";
}
}
I don't know how well you can translate that to your project, but I hope you get the big picture of it :)
If you need to read a bit about how to access the first view controller using the storyboard: http://developer.apple.com/library/ios/#releasenotes/Miscellaneous/RN-AdoptingStoryboards/_index.html#//apple_ref/doc/uid/TP40011297
There is a section called "Accessing the First View Controller"
I followed this tutorial http://bytedissident.theconspiracy5.com/2010/11/24/uiwebview-tutorial/ and when I run the simulation in Xcode, all that is displayed is a blank white screen. I'm wondering what could be wrong. Is there some sort of code connection to the internet that i'm missing? I really don't know what could be wrong.
WebPageViewController.m
#import "WebPageViewController.h"
#implementation WebPageViewController
#synthesize wView;
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
[super viewDidLoad];
//a URL string
NSString *urlAddress = #"http://www.nd.edu";
//create URL object from string
NSURL *url = [NSURL URLWithString:urlAddress];
//URL Request Object created from your URL object
NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];
//Load the request in the UIWebView
[wView loadRequest:requestObj];
//scale page to the device - can also be done in IB if preferred.
//wView.scalesPageToFit = YES;
}
Did you connect the .xib to the WebPageViewController class you created? You can check by selecting the .xib in the editor, and use identity inspector on 'File's Owner' in the Class field under Custom Class
My application is in landscape mod.When i call MFMailComposeViewController through Present model view, it comes in landscape mod.I rotate device and MFMailComposeViewController view goes to portrait mod i want to restrict this rotation it should always be in landscape mod only.Is there any way to do it..
Subclass the MFMailComposeViewController class, so that you can override its shouldAutorotateToInterfaceOrientation to display it however you like:
#interface MailCompose : MFMailComposeViewController {
}
#end
#implementation MailCompose
// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
// Return YES for supported orientations
return (interfaceOrientation == UIInterfaceOrientationLandscapeLeft);
}
#end
Specify the new class in place of MFMailComposeViewController:
MailCompose *controller = [[MailCompose alloc] init];
controller.mailComposeDelegate = self;
[controller setSubject:#"In app email..."];
[controller setMessageBody:#"...email body." isHTML:NO];
[self presentModalViewController:controller animated:YES];
[controller release];
I found that a simple category for MFMailComposeViewController also worked. Since I like my app to rotate to any angle, I created & linked in MFMailComposeViewController+Rotate.h:
#import <Foundation/Foundation.h>
#import <MessageUI/MFMailComposeViewController.h>
#interface MFMailComposeViewController (Rotate)
#end
and MFMailComposeViewController+Rotate.m
#import "MFMailComposeViewController+Rotate.h"
#implementation MFMailComposeViewController (Rotate)
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
// Return YES for supported orientations
return YES;
}
- (NSUInteger)supportedInterfaceOrientations {
// return the desired orientation mask from http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UIApplication_Class/Reference/Reference.html
return /*mask*/;
}
#end
For my base line testing purposes (iOS 3.1.3), I don't seem to need to put it into the root view controller.
It is worth noting that BEFORE I did this, after my app would load MFMailComposeViewController, it would then stop rotating to any other orientation, even after MFMailComposeViewController was dismissed! Now my app remains freely rotatable.
Henry
This worked for me, perfectly, with one minor change:
MailCompose *controller = [[MailCompose alloc]
initWithRootViewController:self.navigationController];
...
And I was convinced that I had to call the base-class initWithRootViewController method. Otherwise, how would the MFMailComposeViewController know about it? But it turns out the example above that simply calls [[MailCompose alloc] init] is enough. The underlying MFMailComposeViewController "just knows" how to display itself.
I love happy surprises like this. I posted this in case it is equally illuminating for anyone else.
Create a new controller and inherit it from MFMailComposeViewController.In this controller just write one function shouldautorotate thing. Create instance of this.Now it will be working fine.