I'm having a bit of trouble with this one! I have definitely looked in to a lot of posts, but can't get it to work. Previously in my app, I have managed to pass data between classes by assigning it to a property. I'm not sure where I'm going wrong here so any help would be appreciated. I know the following code isn't great, and I'm looking in to dictionaries to help, but for now I would like to get this working.
- (IBAction)checkAnswers:(id)sender
{
// The following if statements check user input and checks to see if it is the right answer.
// The .text parts are uitextfield properties and then shows an image of a tick if correct.
// It then assigns one to a variable and sums them up at the end (blag)
if ([eyepiece.text isEqualToString:#"Eyepiece"]) {
UIImage *image = [UIImage imageNamed:#"Tick.png"];
[eyepieceTick setImage:image];
a = 1;
}
if ([focussingKnobs.text isEqualToString:#"Focussing knobs"]) {
UIImage *image = [UIImage imageNamed:#"Tick.png"];
[focussingTick setImage:image];
b = 1;
}
if ([objectiveLens.text isEqualToString:#"Objective lenses"]) {
UIImage *image = [UIImage imageNamed:#"Tick.png"];
[objectiveTick setImage:image];
c = 1;
}
if ([stage.text isEqualToString:#"Stage"]) {
UIImage *image = [UIImage imageNamed:#"Tick.png"];
[stageTick setImage:image];
d = 1;
}
if ([mirror.text isEqualToString:#"Mirror"]) {
UIImage *image = [UIImage imageNamed:#"Tick.png"];
[mirrorTick setImage:image];
e = 1;
}
blag = a + b + c + d + e;
// Here I update a label with the score
finalScore = [[NSString alloc] initWithFormat:#"%D", blag];
[score setText:finalScore];
// This is probably where I'm going wrong. I'm allocating a model class called
// Level_3_Brain and trying to assign a new property in that class (cellsLevelThree)
// with the score.
// Level_3_Brain *level = [[Level_3_Brain alloc] init];
// level.cellsLevelThree = blag;
// Updated to
[Level_3_Brain sharedInstanceOfLevel3].cellsLevelThree = blag;
// I then set them all back to zero so that the score doesn't go above 5
a = 0, b = 0, c = 0, d = 0, e = 0;
blag = 0;
}
My model class .h now has:
#interface Level_3_Brain : NSObject
+ (id)sharedInstanceOfLevel3;
#property (nonatomic) int cellsLevelThree;
#end
My .m now has this code taken from the Singleton article:
#implementation Level_3_Brain
#synthesize cellsLevelThree;
static Level_3_Brain *sharedInstanceOfLevel3 = nil;
// Get the shared instance and create it if necessary.
+ (Level_3_Brain *)sharedInstanceOfLevel3 {
if (sharedInstanceOfLevel3 == nil)
{
sharedInstanceOfLevel3 = [[super allocWithZone:NULL] init];
}
return sharedInstanceOfLevel3;
}
// We can still have a regular init method, that will get called the first time the Singleton is used.
- (id)init
{
self = [super init];
if (self) {
// Work your initialising magic here as you normally would
}
NSLog(#"%#", cellsLevelThree);
return self;
}
// Your dealloc method will never be called, as the singleton survives for the duration of your app.
// However, I like to include it so I know what memory I'm using (and incase, one day, I convert away from Singleton).
-(void)dealloc
{
// I'm never called!
// [super dealloc];
}
/ We don't want to allocate a new instance, so return the current one.
+ (id)allocWithZone:(NSZone*)zone {
return [self sharedInstanceOfLevel3];
}
// Equally, we don't want to generate multiple copies of the singleton.
- (id)copyWithZone:(NSZone *)zone {
return self;
}
#end
Unfortunately, I'm now getting the error "Property 'cellsLevelThree' not found on object of type id. Please help!!
You correctly identified the place where you are "going wrong" in your comment: the problem is not in that you are setting the value incorrectly, but in that you are setting it on a brand-new instance that is local to the method, and is promptly discarded upon the exit from that method.
In general, your model class(es) should be created when your application starts up, and remain available during the entire lifetime of your application. This is often accomplished using singletons.
Related
I've created a class to control an NSOutlineView and that class comunicate with my AppDelegate using notification. This class that controls the behavior of the outlineview is initialized using awakefromnib so that header is added immediately (my will), while later by calling a method of this class is populated by children. Everything works ok, but when is the moment to create a notification for my AppleDelegate I discover that instance variable is null while at the time of the initial call was ok.
#interface MyClass : NSObject <NSApplicationDelegate, NSTextViewDelegate, NSTextFieldDelegate, NSOutlineViewDelegate, NSOutlineViewDataSource>
{
NSString * _plistPath;
}
#implementation MyClass
- (void)awakeFromNib {
static dispatch_once_t once;
dispatch_once(& once, ^{
self.Outline.delegate = self;
self.Outline.dataSource = self;
self.Outline.floatsGroupRows = NO;
[_treeController addObject: #{#"title": #"Model list", #"isLeaf": #(NO)}.mutableCopy];
[self.Outline expandItem:[self.Outline itemAtRow:0]];
[self.Outline selectRowIndexes:[NSIndexSet indexSetWithIndex:1] byExtendingSelection:NO];
// Enable Drag and Drop
[self.Outline registerForDraggedTypes: [NSArray arrayWithObject: #"public.text"]];
});
}
- (void)loadPlist:(NSDictionary *)dict path:(NSString *)path
{
_plistPath = path;
NSLog(#"at loadPlist _plistPath = %#, self is = %#", _plistPath, self); // here is ok!
}
// Here all the NSOutlineViewDelegate methods
// .....
// finally after editing value in the interface using the outline view
// and some field attacched to the tree controller..
// I need to notificate AppleDelegate for the changes made to the plist,
//reindicating the path stored in _plistPath variable..but:
- (void)update
{
NSMutableArray *List = [NSMutableArray array];
NSInteger rows = [_Outline numberOfRows];
NSInteger i;
i = 0;
while (i != rows) {
id obj = [_Outline itemAtRow:i];
if (i != 0) {
Tree *entry = (Tree *)(((NSTreeNode *)obj).representedObject);
if (entry.title.length > 0 && ![entry.title isEqualToString:kNullString]
&& entry.size.length > 0 && ![entry.size isEqualToString:kNullString]
&& entry.model.length > 0 && ![entry.model isEqualToString:kNullString]
&& entry.year.length > 0 && ![entry.year isEqualToString:kNullString]) {
NSMutableDictionary *childDict = [NSMutableDictionary dictionary];
[childDict setObject:entry.title forKey:#"Name"];
[childDict setObject:entry.size forKey:#"Size"];
[childDict setObject:entry.model forKey:#"Model"];
[childDict setObject:entry.year forKey:#"Year"];
[List addObject:childDict];
}
}
i++;
}
NSMutableDictionary *uf = [NSMutableDictionary dictionary];
NSLog(#"at update _plistPath = %#, self is = %#", _plistPath, self); // this time here is nil....
[uf setObject:_plistPath forKey:#"path"]; // here crash because _plistPath is nil
[uf setObject:List forKey:#"dictionary"];
NSNotification *note = [NSNotification notificationWithName:#"UpdatePlist" object:_Outline userInfo:uf];
[[NSNotificationCenter defaultCenter] postNotification:note];
}
basically at the second time I need "_plistPath" value is nil (I have some very specific reasons for sending to MyClass the original path and then return it back to the sender (ie AppleDelegate)) and I have more instance variables declared in MyClass that works well..so I can't understand why. Any suggestion?
EDIT
As requested by #matt I change the NSLog including "self"
then the output is that:
2015-05-11 22:06:36.433 TestApp[24990:56140] at loadPlist _plistPath = /Volumes/DATI/test.plist, self is = <MyClass: 0x600000127760>
2015-05-11 22:07:08.552 TestApp[24990:56140] at update _plistPath = (null), self is = <MyClass: 0x608000125c80>
Your log shows the problem:
at loadPlist _plistPath = /Volumes/DATI/test.plist,
self is = <MyClass: 0x600000127760>
at update _plistPath = (null),
self is = <MyClass: 0x608000125c80>
These are two difference instances of MyClass. But plistPath is an instance variable - so it can perfectly reasonably have a value in one instance and be nil in another instance.
I’m trying to create a nib for a generic SectionHeader for a TableView as I need several similar SectionHeaders. I’m trying to follow this SO post:
How to create multiple windows with the same nib file in xcode
The view defined in my nib file is assigned a base class of BaseSectionHeader. Here’s its initializer:
BaseSectionHeader.m
- (id) initWithController:(MasterViewController*)ctlr;
{
self = [[[UINib nibWithNibName:#"SectionHeader" bundle:nil] instantiateWithOwner:ctlr options:nil] objectAtIndex:0];
if (self)
{
_controller = ctlr;
}
return self;
}
Here’s the initializer of a subclass I’d like to derive:
SectionHeader.h
#interface SectionHeader : BaseSectionHeader <UIAlertViewDelegate>
…
#end
SectionHeader.m
- (id) initWithController:(MasterViewController*)ctlr
{
if (self = [super initWithController:ctlr])
{
_deleteConfirmButtonWidth = 70.0;
}
return self;
}
And here’s how I instantiate a section header:
MasterViewController.m
…
SectionHeader* hdr = [[SectionHeader alloc] initWithController:self];
…
The problem is hdr is returned as a BaseSectionHeader, not a SectionHeader. This works correctly if I don't use the nib and construct BaseSectionHeader manually in code. But I’d like to use IB to construct the BaseSectionHeader if I can.
Why is hdr a BaseSectionHeader instead of a SectionHeader when I use a nib? Is there a way use the nib and get the subclass I want for hdr?
FWIW here’s my manual code:
BaseSectionHeader.m
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame])
{
_label = [[UILabel alloc] initWithFrame:CGRectMake(10.0, 0.0, 275.0, 40.0)];
[_label setTextColor:TanColor];
[_label setNumberOfLines:2];
[_label setLineBreakMode:NSLineBreakByTruncatingTail];
[_label setAdjustsFontSizeToFitWidth:YES];
[self addSubview:_label];
}
return self;
}
SectionHeader.m
- (id)initWithFrame:(CGRect)frame Controller:(MasterViewController*)ctlr
{
if (self = [super initWithFrame:frame])
{
_controller = ctlr;
_deleteConfirmButtonWidth = 70.0;
_titleButton = [UIButton buttonWithType:UIButtonTypeCustom];
_titleButton.frame = CGRectMake(10.0, 0.0, 275.0, 40.0);
_titleButton.alpha = 0.3;
[self addSubview:_titleButton];
}
return self;
}
Thanks
Typically, a nib is used to instantiate an object whose code stands alone. The nib for objectX is not used within the code of objectX.
When SectionHeader is allocated, and its init called, it passes itself to the BaseSectionHeader version of the same method. But BaseSectionHeader drops that and creates a new object to put in self. Thus a SectionHeader is replaced with a BaseSectionHeader.
The UINib calls to instantiate should be made in MasterViewController.
I have a problem with the PIn color mapView when a refresh is done.
In my I app i display some point with two color in order to identify if a service is available.
On the first start, no problems appear. The code is the follower:
- (void)viewDidLoad
{
[super viewDidLoad];
[self dowloadPoint]; // here I exucte the first start
}
- (void)dowloadPoint{
NSURL *url1 =[NSURL URLWithString:#"http:MYUSRL"];
NSData *datos1 =[[NSData alloc] initWithContentsOfURL:url1];
[self plotBarPosition:datos_string1]; //Here I call the plotBarPosition method
}
- (void)plotBarPosition:(NSString *)datos_string1 {
for (id<MKAnnotation> annotation in _mapView.annotations) {
[_mapView removeAnnotation:annotation];
}
// Parse the string into JSON
NSDictionary *json = [(NSDictionary*)[datos_string1 JSONValue]objectForKey:#"features"];
// Get the objects you want, e.g. output the second item's client id
NSArray *items_properties = [json valueForKeyPath:#"properties"];
NSArray *items_geo = [json valueForKeyPath:#"geometry"];
for (int i = 0; i < [json count]; i++){
NSString *nomprePunto =[[items_properties objectAtIndex:i] objectForKey:#"title"];
NSNumber *lat =[[[items_geo objectAtIndex:i] objectForKey:#"coordinates"] objectAtIndex:0];
NSNumber *lon =[[[items_geo objectAtIndex:i] objectForKey:#"coordinates"] objectAtIndex:1];
CLLocationCoordinate2D coordinate;
coordinate.latitude = lat.doubleValue;
coordinate.longitude = lon.doubleValue;
//ESTADO
NSString *description = [[items_properties objectAtIndex:i] objectForKey:#"description"];
NSString *estado_punto = [[NSString alloc]init];
if ([description rangeOfString:#"Averiado"].location == NSNotFound) {
estado_punto = #"Available";
} else {
estado_punto = #"NOt Available";
averiados ++;
}
NSString *averiadosStr = [NSString stringWithFormat:#"%d",averiados];
averiadosLabel.text = averiadosStr;
MyLocation *location =[[MyLocation alloc] initWithName:nomprePunto coordinate:coordinate estado:estado_punto];
[_mapView addAnnotation:location];
}
}
- (MKPinAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(MyLocation *)annotation {
static NSString *identifier = #"MyLocation";
if ([annotation isKindOfClass:[MyLocation class]]) {
MKPinAnnotationView *annotationView = (MKPinAnnotationView *) [_mapView dequeueReusableAnnotationViewWithIdentifier:identifier];
if (annotationView == nil) {
annotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:identifier];
annotationView.enabled = YES;
annotationView.canShowCallout = YES;
if([[annotation estado] isEqualToString:#"En Servicio"])
annotationView.pinColor = MKPinAnnotationColorGreen;
} else {
annotationView.annotation = annotation;
}
return annotationView;
}
return nil;
}
But whe I add a refres button that is function is simply a refreshcalling the dowloadPoint once again,
- (IBAction)refresh{
[self dowloadPoint];
}
the color of pins change in a "random manner", not corrisponding with the real state of point.
Any ideas about what is happening? Thanks in advance.
EDIT: It seemps pproblems is due to:
for (id<MKAnnotation> annotation in _mapView.annotations) {
[_mapView removeAnnotation:annotation];
}
erasing it, the app work properly but pins area drown abow the previous ones...:S
The default color of the pin is red. You set it to green if the estado property of your MyLocation object is equal to #"En Servicio". I understand that sometimes the color is red, when your estado property is equal to #"En Servicio", or sometimes green when it is not.
One reason could be that your MyLocation object simply does no longer exist when you press the refresh button. In this case, you might still have a pointer to the memory location where it once existed, but this location may have been overwritten by anything, causing a random color.
This can happen e.g. if your MyLocation object has been created as an autorelease object that has been released when you returned to the main event loop, i.e. to handle user interactions.
This should not be the case if you are using ARC.
I am having problems finding any other information than the docs for how to save the tab order for my UITabBarController, so that the user's customization is saved for next app launch. I have searched online, but have been unable to find any blog posts or articles that goes through the proper code for doing this.
I realize I have to use the delegate methods for the UITabBarController (didEndCustomizingViewControllers:) but I am not sure how I best approach persistance in terms of saving the state of the order the user wants the tabs in.
Can someone post some code, point me in the right direction or perhaps you have a link for something saved? :)
Thanks
As far as you've asked for some sample code I will simply post here how I dealt with the same task in my app.
Quick intro: I was using a NIB file for storing initial UITabBarController state and to differ my tabs one from another I simply defined tag variables for UITabBarItem objects assigned to each UIViewController stuffed in my UITabBarController. To be able to accurately track last selected tab (including the 'More' one) I've implemented following methods for UITabBarControllerDelegate of my UITabBarController and UINavigationControllerDelegate of its moreNavigationController. Here they are:
#pragma mark UINavigationControllerDelegate
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
[[NSUserDefaults standardUserDefaults] setInteger:mainTabBarController.selectedIndex forKey:#"mainTabBarControllerSelectedIndex"];
}
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
[[NSUserDefaults standardUserDefaults] setInteger:mainTabBarController.selectedIndex forKey:#"mainTabBarControllerSelectedIndex"];
}
#pragma mark UITabBarControllerDelegate
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController {
[[NSUserDefaults standardUserDefaults] setInteger:tabBarController.selectedIndex forKey:#"mainTabBarControllerSelectedIndex"];
}
And here's the code for saving the tabs order:
#pragma mark UITabBarControllerDelegate
- (void)tabBarController:(UITabBarController *)tabBarController didEndCustomizingViewControllers:(NSArray *)viewControllers changed:(BOOL)changed {
int count = mainTabBarController.viewControllers.count;
NSMutableArray *savedTabsOrderArray = [[NSMutableArray alloc] initWithCapacity:count];
for (int i = 0; i < count; i ++) {
[savedTabsOrderArray addObject:[NSNumber numberWithInt:[[[mainTabBarController.viewControllers objectAtIndex:i] tabBarItem] tag]]];
}
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithArray:savedTabsOrderArray] forKey:#"tabBarTabsOrder"];
[savedTabsOrderArray release];
}
As you can see I've been storing the order of tabs' indexes in an array in NSUserDefaults.
On app's launch in applicationDidFinishLaunching: method I reordered the UIViewControllers using following code:
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
mainTabBarController.delegate = self;
int count = mainTabBarController.viewControllers.count;
NSArray *savedTabsOrderArray = [[userDefaults arrayForKey:#"tabBarTabsOrder"] retain];
if (savedTabsOrderArray.count == count) {
BOOL needsReordering = NO;
NSMutableDictionary *tabsOrderDictionary = [[NSMutableDictionary alloc] initWithCapacity:count];
for (int i = 0; i < count; i ++) {
NSNumber *tag = [[NSNumber alloc] initWithInt:[[[mainTabBarController.viewControllers objectAtIndex:i] tabBarItem] tag]];
[tabsOrderDictionary setObject:[NSNumber numberWithInt:i] forKey:[tag stringValue]];
if (!needsReordering && ![(NSNumber *)[savedTabsOrderArray objectAtIndex:i] isEqualToNumber:tag]) {
needsReordering = YES;
}
}
if (needsReordering) {
NSMutableArray *tabsViewControllers = [[NSMutableArray alloc] initWithCapacity:count];
for (int i = 0; i < count; i ++) {
[tabsViewControllers addObject:[mainTabBarController.viewControllers objectAtIndex:
[(NSNumber *)[tabsOrderDictionary objectForKey:
[(NSNumber *)[savedTabsOrderArray objectAtIndex:i] stringValue]] intValue]]];
}
[tabsOrderDictionary release];
mainTabBarController.viewControllers = [NSArray arrayWithArray:tabsViewControllers];
[tabsViewControllers release];
}
}
[savedTabsOrderArray release];
if ([userDefaults integerForKey:#"mainTabBarControllerSelectedIndex"]) {
if ([userDefaults integerForKey:#"mainTabBarControllerSelectedIndex"] == 2147483647) {
mainTabBarController.selectedViewController = mainTabBarController.moreNavigationController;
}
else {
mainTabBarController.selectedIndex = [userDefaults integerForKey:#"mainTabBarControllerSelectedIndex"];
}
}
mainTabBarController.moreNavigationController.delegate = self;
[window addSubview:mainTabBarController.view];
}
It's quite tricky and may seem strange, but don't forget that my UITabBarController was fully created in a nib file. If you construct it programmatically you may simply do the same but following the saved order.
P.S.: and don't forget to synchronize NSUserDefaults when your app terminates.
- (void)applicationWillTerminate:(UIApplication *)application {
[[NSUserDefaults standardUserDefaults] synchronize];
}
I hope this will help. If something is not clear please do comment and ask.
First I voted up the previous answer, but then I noticed how ridiculously complex it is. It can and should be simplified.
- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSArray *initialViewControllers = [NSArray arrayWithArray:self.tabBarController.viewControllers];
NSArray *tabBarOrder = [[AppDelegate sharedSettingsService] tabBarOrder];
if (tabBarOrder) {
NSMutableArray *newViewControllers = [NSMutableArray arrayWithCapacity:initialViewControllers.count];
for (NSNumber *tabBarNumber in tabBarOrder) {
NSUInteger tabBarIndex = [tabBarNumber unsignedIntegerValue];
[newViewControllers addObject:[initialViewControllers objectAtIndex:tabBarIndex]];
}
self.tabBarController.viewControllers = newViewControllers;
}
NSInteger tabBarSelectedIndex = [[AppDelegate sharedSettingsService] tabBarSelectedIndex];
if (NSIntegerMax == tabBarSelectedIndex) {
self.tabBarController.selectedViewController = self.tabBarController.moreNavigationController;
} else {
self.tabBarController.selectedIndex = tabBarSelectedIndex;
}
/* Add the tab bar controller's current view as a subview of the window. */
[self.window addSubview:self.tabBarController.view];
}
- (void)applicationWillTerminate:(UIApplication *)application {
NSInteger tabBarSelectedIndex = self.tabBarController.selectedIndex;
[[AppDelegate sharedSettingsService] setTabBarSelectedIndex:tabBarSelectedIndex];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (void)tabBarController:(UITabBarController *)tabBarController didEndCustomizingViewControllers:(NSArray *)viewControllers changed:(BOOL)changed {
NSUInteger count = tabBarController.viewControllers.count;
NSMutableArray *tabOrderArray = [[NSMutableArray alloc] initWithCapacity:count];
for (UIViewController *viewController in viewControllers) {
NSInteger tag = viewController.tabBarItem.tag;
[tabOrderArray addObject:[NSNumber numberWithInteger:tag]];
}
[[AppDelegate sharedSettingsService] setTabBarOrder:[NSArray arrayWithArray:tabOrderArray]];
[tabOrderArray release];
}
All this happens in AppDelegate. You set UITabBarController's delegate to AppDelegate instance in Interface Builder. sharedSettingsService is what persists the data for me. Basically it can be a NSUserDefaults front-end or anything you like (CoreData for example). So everything is simple, Interface Builder helps here, not makes things more complex.
Perhaps late to the game, but been learning Swift for less than two months at school and sat perhaps more than fifteen hours with this because I couldn't find a decent explanation on the interwebz.
Here is a solution in Swift
Give all your tabItem's a tag, starting at 1. You do this in each separate view. If you got six views, their tabItems will in that case have a unique number each ranging between 1 and 6.
Add UITabBarControllerDelegate to all the ViewControllers' classes so you can use the function explained later in point 5.
class FirstViewController: UIViewController, UITabBarControllerDelegate {
Add the following variable globally (right after the code above, as an example) so you can save variables locally on the phone from any function within the class.
let defaults = NSUserDefaults.standardUserDefaults()
Delegate the tabBarController to the view so the view can update any changes to the tabBarController. Put the following into your viewDidLoad().
tabBarController!.delegate = self
Implement the following code. This one will activate when the user is editing the tab view. What the code does is taking the [ViewControllers]'s tags in the order they are in (after the user changed it) and saves it locally on the phone. The first viewController's tag is saved as an integer in the variable "0", the second tag in a variable called "1", and so on.
func tabBarController(tabBarController: UITabBarController, didEndCustomizingViewControllers viewControllers: [UIViewController], changed: Bool) {
if (changed) {
print("New tab order:")
for (var i=0; i<viewControllers.count; i++) {
defaults.setInteger(viewControllers[i].tabBarItem.tag, forKey: String(i))
print("\(i): \(viewControllers[i].tabBarItem.title!) (\(viewControllers[i].tabBarItem.tag))")
}
}
}
The prints tell you the new order of the tabs. Nothing you will need, but I think it's nice to see what's happening in the background. All this was only to save the order of the tabs. You will now have to retrieve them when the program is starting.
Switch from your UIViewControl file to AppDelegate.swift.
Finally, write the following in func application(application: UIApplication, didFinishLaunchingWithOptions... that you can find at the top of AppDelegate.swift.
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Let you read and write to local variables on your phone
let defaults = NSUserDefaults.standardUserDefaults()
// Getting access to your tabBarController
let tabBar: UITabBarController = self.window?.rootViewController as! UITabBarController
var junkViewControllers = [UIViewController]()
// returns 0 if not set, hence having the tabItem's tags starting at 1.
var tagNumber : Int = defaults.integerForKey("0")
if (tagNumber != 0) {
for (var i=0; i<tabBar.viewControllers?.count; i++) {
// the tags are between 1-6 but the order of the
// viewControllers in the array are between 0-5
// hence the "-1" below.
tagNumber = defaults.integerForKey( String(i) ) - 1
junkViewControllers.append(tabBar.viewControllers![tagNumber])
}
tabBar.viewControllers = junkViewControllers
}
}
What is good to know is that all views that a tabBarController contains is stored as an array in tabBarController.viewControllers.
This code basically creates an array called junkViewControllers. The for-loop then adds the existing UIViewControllers from the program in the order from the previous stored variables, based on the UIViewControllers' tags. When all this is done, the tabBarController shortened to tabBar is overwritten with the array junkViewController.
This is what did the trick for me, building on the answers from Rickard & Jesper. All of the code goes into the main TabBarController.
import UIKit
class TabBarController: UITabBarController, UITabBarControllerDelegate {
let tabOrderKey = "customTabBarOrder"
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
loadCustomTabOrder()
}
func loadCustomTabOrder() {
let defaults = NSUserDefaults.standardUserDefaults()
let standardOrderChanged = defaults.boolForKey(tabOrderKey)
if standardOrderChanged {
print("Standard Order has changed")
var VCArray = [UIViewController]()
var tagNumber = 0
let tabBar = self as UITabBarController
if let countVC = tabBar.viewControllers?.count {
print("\(countVC) VCs in total")
for var x = 0; x < countVC; x++ {
tagNumber = defaults.integerForKey("tabPosition\(x)")
for VC in tabBar.viewControllers! {
if tagNumber == VC.tabBarItem.tag {
VCArray.append(VC)
print("Position \(x): \(VCArray[x].tabBarItem.title!) VC (tag \(tagNumber))")
}
}
}
}
tabBar.viewControllers = VCArray
}
}
func tabBarController(tabBarController: UITabBarController, didEndCustomizingViewControllers viewControllers: [UIViewController], changed: Bool) {
print("Change func called")
if changed {
print("Order has changed")
let defaults = NSUserDefaults.standardUserDefaults()
for var x = 0; x < viewControllers.count; x++ {
defaults.setInteger(viewControllers[x].tabBarItem.tag, forKey: "tabPosition\(x)")
print("\(viewControllers[x].tabBarItem.title!) VC (with tag: \(viewControllers[x].tabBarItem.tag)) is now in position \(x)")
}
defaults.setBool(true, forKey: tabOrderKey)
} else {
print("Nothing has changed")
}
}
}
I will explain how to do this programmatically. NOTE: This is using ARC, so you may have to insert retain/release calls as needed.
You use the tag property of the UITabBarItem for sorting. For every UIViewController that you are adding to the UITabBarController, make sure that each has a unique tag.
- (id)init
{
self = [super init];
if (self) {
self.tabBarItem.tag = 0;
self.tabBarItem.image = <image>;
self.tabBarItem.title = <title>;
}
return self;
}
Presumably you would just use their default sorting order for their tags, so whatever you have as your original first view controller would be 0, followed by 1, 2, 3, etc.
Set up your UIViewControllers in the AppDelegate's didFinishLaunchingWithOptions as you normally would, making sure that you are instantiating them in their "default order". As you do so, add them to an instance of a NSMutableArray.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.tabBarController = [[UITabBarController alloc] init];
self.tabBarController.delegate = self;
NSMutableArray *unsortedControllers = [NSMutableArray array];
UIViewController *viewOne = [[UIViewController alloc] init];
[unsortedControllers addObject:viewOne];
UIViewController *viewTwo = [[UIViewController alloc] init];
[unsortedControllers addObject:viewTwo];
...
After they are all instantiated and added to the array, you will check to see if the user has customized their order by querying NSUserDefaults. In the defaults, you will store an array of the user's customized tab bar order. This will be an array of NSNumbers (how this is created is explain in the last code snippet). Use these to create a new "sorted" array of view controllers and pass that to the tab bar controller. If they haven't customized the order, the default will return nil and you can simply used the unsorted array.
...
NSArray *tabBarOrder = [[NSUserDefaults standardUserDefaults] arrayForKey:#"tabBarOrder"];
if (tabBarOrder)
{
NSMutableArray *sortedControllers = [NSMutableArray array];
for (NSNumber *sortNumber in tabBarOrder)
{
[sortedControllers addObject:[unsortedControllers objectAtIndex:[sortNumber intValue]]];
}
self.tabBarController.viewControllers = sortedControllers;
} else {
self.tabBarController.viewControllers = unsortedControllers;
}
[self.window setRootViewController:self.tabBarController];
[self.window makeKeyAndVisible];
return YES;
}
To create to customized sort order, use the UITabBarController's delegate method:
- (void)tabBarController:(UITabBarController *)tabBarController didEndCustomizingViewControllers:(NSArray *)viewControllers changed:(BOOL)changed
{
NSMutableArray *tabOrderArray = [[NSMutableArray alloc] init];
for (UIViewController *vc in self.tabBarController.viewControllers)
{
[tabOrderArray addObject:[NSNumber numberWithInt:[[vc tabBarItem] tag]]];
}
[[NSUserDefaults standardUserDefaults] setObject:[NSArray arrayWithArray:tabOrderArray] forKey:#"tabBarOrder"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
Simplified Rickard Elimää answer even further. "Swift" solution to saving and loading Customized ViewControllers using the delegate function of tabBarController CustomizingViewControllers.
This is how I did it.
class TabBarController: UITabBarController, UITabBarControllerDelegate {
let kOrder = "customOrder"
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
loadCustomizedViews()
}
func loadCustomizedViews(){
let defaults = NSUserDefaults.standardUserDefaults()
// returns 0 if not set, hence having the tabItem's tags starting at 1.
let changed : Bool = defaults.boolForKey(kOrder)
if changed {
var customViewControllers = [UIViewController]()
var tagNumber: Int = 0
for (var i=0; i<self.viewControllers?.count; i++) {
// the tags are between 0-6 and the
// viewControllers in the array are between 0-6
// so we swap them to match the custom order
tagNumber = defaults.integerForKey( String(i) )
//print("TabBar re arrange i = \(i), tagNumber = \(tagNumber), viewControllers.count = \(self.viewControllers?.count) ")
customViewControllers.append(self.viewControllers![tagNumber])
}
self.viewControllers = customViewControllers
}
}
func tabBarController(tabBarController: UITabBarController, didEndCustomizingViewControllers viewControllers: [UIViewController], changed: Bool){
if (changed) {
let defaults = NSUserDefaults.standardUserDefaults()
//print("New tab order:")
for (var i=0; i<viewControllers.count; i++) {
defaults.setInteger(viewControllers[i].tabBarItem.tag, forKey: String(i))
//print("\(i): \(viewControllers[i].tabBarItem.title!) (\(viewControllers[i].tabBarItem.tag))")
}
defaults.setBool(changed, forKey: kOrder)
}
}
}
Updated Answer for Swift 2.0
`
let tabBarOrderKey = "tabBarOrderKey"
extension TabBarButtonsController: UITabBarControllerDelegate {
// Saves new tab bar custom order
func tabBarController(tabBarController: UITabBarController, didEndCustomizingViewControllers viewControllers: [UIViewController], changed: Bool) {
var orderedTagItems = [Int]()
if changed {
for viewController in viewControllers {
let tag = viewController.tabBarItem.tag
orderedTagItems.append(tag)
}
NSUserDefaults.standardUserDefaults().setObject(orderedTagItems, forKey: tabBarOrderKey)
}
}
// set up tag to compare with when pulling from defaults and for saving initial tab bar change
func setUpTabBarItemTags() {
var tag = 0
if let viewControllers = viewControllers {
for view in viewControllers {
view.tabBarItem.tag = tag
tag += 1
}
}
}
// Get Saved Tab Bar Order from defaults
func getSavedTabBarItemsOrder() {
var newViewControllerOrder = [UIViewController]()
if let initialViewControllers = viewControllers {
if let tabBarOrder = NSUserDefaults.standardUserDefaults().objectForKey(tabBarOrderKey) as? [Int] {
for tag in tabBarOrder {
newViewControllerOrder.append(initialViewControllers[tag])
}
setViewControllers(newViewControllerOrder, animated: false)
}
}
}
}
`
Remember to set the delegate and call these methods in the view did load
I'd like to share the code I have been working on for nearly 3 days now trying all sorts of combinations with many errors and failure - the general life of coding! ha ha. Anyway the code below works with Swift 5.
It's the extraction of How to: Save order of tabs when customizing tabs in UITabBarController , Sam's Code, but with the for loop update for Swift 5.
I have got this working great, all in the TabBarViewController.swift file.
So if you just paste this into your Swift file, make sure your Tab Bar Items have a tag number from the Attributes Inspector and your good to go!
Thanks again to Sam for the Code in the first place.
import UIKit
class TabBarController: UITabBarController, UITabBarControllerDelegate {
let tabOrderKey = "customTabBarOrder"
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
loadCustomTabOrder()
}
func loadCustomTabOrder() {
let defaults = UserDefaults.standard
let standardOrderChanged = defaults.bool(forKey: tabOrderKey)
if standardOrderChanged {
print("Standard Order has changed")
var VCArray = [UIViewController]()
var tagNumber = 0
let tabBar = self as UITabBarController
if let countVC = tabBar.viewControllers?.count {
print("\(countVC) VCs in total")
for x in 0..<countVC {
tagNumber = defaults.integer(forKey: "tabPosition\(x)")
for VC in tabBar.viewControllers! {
if tagNumber == VC.tabBarItem.tag {
VCArray.append(VC)
print("Position \(x): \(VCArray[x].tabBarItem.title!) VC (tag \(tagNumber))")
}
}
}
}
tabBar.viewControllers = VCArray
}
}
func tabBarController(_ tabBarController: UITabBarController, didEndCustomizing viewControllers: [UIViewController], changed: Bool) {
print("Change func called")
if changed {
print("Order has changed")
let defaults = UserDefaults.standard
for x in 0..<(viewControllers.count) {
defaults.set(viewControllers[x].tabBarItem.tag, forKey: "tabPosition\(x)")
print("\(viewControllers[x].tabBarItem.title!) VC (with tag: \(viewControllers[x].tabBarItem.tag)) is now in position \(x)")
}
defaults.set(true, forKey: tabOrderKey)
} else {
print("Nothing has changed")
}
}
}
I'm working on a species ID app and would like to populate a layer with sprites based on which animal you select on the main layer. I've made each animal a menu item, and can get my info layer to appear when pressing the button, but how can I set it up so the layer shows the right data depending on which animal you select? The info layer is not a full screen layer, but rather an overlaying layer that only fills about 75% of the screen, which is why I'm going with a layer rather than a scene. I know I can create a new layer for each animal (approx 50) and code it so each button calls its own layer, but I think populating based on which button is pressed would make for cleaner code. If flamingoButton is pressed, sprite is filled with flamingo.png and label is populated with flamingo information. How do I get my info layer to listen to the buttons on the main layer?
MainLayer.m code:
-(id) init
{
if( (self=[super init]))
{
CCMenuItemImage *flamingoButton = [CCMenuItemImage itemFromNormalImage:#"Explore-sign.png" selectedImage:#"Explore-sign.png" target:self selector:#selector(showSecondLayer:)];
flamingoButton.position = CGPointMake(0, 60);
flamingoButton.tag = 101;
CCMenu *menu = [CCMenu menuWithItems:flamingoButton, nil];
[self addChild:menu];
}
return self;
}
-(void) showSecondLayer: (id) sender
{
CCMenuItemImage *item = (CCMenuItemImage *) sender;
int itemID = item.tag;
secondLayer = [SecondLayer node];
secondLayer.position = CGPointMake(0, 700);
[self addChild:secondLayer];
CCMoveTo *moveLayer = [CCMoveTo actionWithDuration:1.0 position:CGPointMake(0, 0)];
[secondLayer runAction:moveLayer];
}
SecondLayer.m (the info layer)
-(id) init
{
if( (self=[super init]))
{
//Change this sprite image based on button from main layer. I don't have it coded in yet, but I understand the concept of putting a variable in the file string using %# or %d
CCSprite *infoCard = [CCSprite spriteWithFile:#"species1.png"];
infoCard.anchorPoint = CGPointMake(0.5, 0);
infoCard.position = CGPointMake(512, 0);
[self addChild:infoCard];
}
return self;
}
Ok, this might work:
//MainLayer:
-(id) init
{
if( (self=[super init]))
{
CCMenuItem *flamingoButton = [CCMenuItemImage itemFromNormalImage:#"Explore-sign.png"
selectedImage:#"Explore-sign.png"
target:self
selector:#selector(showSecondLayer:)];
flamingoButton.position = ccp(0, 60);
flamingoButton.tag = 1;
CCMenu *menu = [CCMenu menuWithItems:flamingoButton, nil];
[self addChild:menu];
}
return self;
}
-(void) showSecondLayer: (CCMenuItem*) sender
{
secondLayer = [SecondLayer layerWithTag:[sender tag]];
secondLayer.position = ccp(0, 700);
[self addChild:secondLayer];
CCMoveTo *moveLayer = [CCMoveTo actionWithDuration:1.0 position:ccp(0, 0)];
[secondLayer runAction:moveLayer];
}
//Second Layer.h
+(id)layerWithTag:(NSInteger)aTag;
-(id) initWithTag:(NSInteger)aTag;
//Second Layer.m:
+(id)layerWithTag:(NSInteger)aTag {
return [[[SecondLayer alloc] initWithTag:aTag] autorelease];
}
-(id) initWithTag:(NSInteger)aTag
{
if( (self=[super init]))
{
//Change this sprite image based on button from main layer. I don't have it coded in yet, but I understand the concept of putting a variable in the file string using %# or %d
CCSprite *infoCard = [CCSprite spriteWithFile:[NSString stringWithFormat:#"species%d.png", aTag]];
infoCard.anchorPoint = ccp(0.5, 0);
infoCard.position = ccp(512, 0);
[self addChild:infoCard];
}
return self;
}
EDIT:
Even though the previous solution works, it's not intuitive, and I feel I am breaking some OOP concepts. Most importantly, it is only useable given that your info about the animal can be retrieved using a single int! .. Using it this way is a BIT better, it's totally up to you to decide:
Ehm, so, I would suggest you set up an Entity Class first:
//AnimalResources.h
#import "Blahblahblah"
//Give it a good name, I was always bad at Science:
#interface AnimalResources {
//load all your properties:
NSString* info;
CCSprite* sprite;
...
}
//set the properties as needed:
//Make sure you properly manage this!! It is retained!
#property (nonatomic, retain) CCSprite* sprite;
...
//method prototype (signature.. am not sure)
//Now, we shall build on the fact that it will be easy for you to map an integer to the right resources:
+(id)animalResourcesWithTag:(NSInteger)aTag;
-(id)initAnimalResourcesWithTag:(NSInteger)aTag;
//AnimalResources.m:'
#synthesize sprite, ... ;
+(id)animalResourcesWithTag:(NSInteger)aTag {
[[[AnimalResources alloc] initAnimalResourcesWithTag:aTag] autorelease];
}
-(id)initAnimalResourcesWithTag:(NSInteger)aTag {
if ((self = [super init])) {
//use tag to retrieve the resources:
//might use the stringFormat + %d approach, or have a dictionary/array plist, that maps an int to a dictionary of resource keys.
//string way of doing things:
self.sprite = [CCSprite spriteWithFile:[NSString stringWithFormat:#"species%d.png", aTag]];
...
//Dictionary: dict/array is an NSDictionary/NSArray read from disk sometime. Don't read it here, since it
//will read the file from disk many times if you do --> BAD. I could explain a rough way to do that if you
//need help
animalDict = [dict objectForKey:[NSString stringWithFormat:#"species%d.png", aTag]];
//OR...
animalDict = [array objectAtIndex:aTag];
//better to have #"spriteNameKey" defined in a macro somewhere: #define kAnimalResourceKeySprite #"SpriteKey"
self.sprite = [CCSprite spriteWithFile:[animalDict objectForKey:#"SpriteNameKey"]];
....
}
return self;
}
Phew! Then .. you guessed it!
-(void) showSecondLayer: (CCMenuItem*) sender
{
secondLayer = [SecondLayer layerWithAnimalResources:[AnimalResources animalResourcesWithTag:[sender tag]]];
secondLayer.position = ccp(0, 700);
[self addChild:secondLayer];
CCMoveTo *moveLayer = [CCMoveTo actionWithDuration:1.0 position:ccp(0, 0)];
[secondLayer runAction:moveLayer];
}
//Second Layer.h
+(id)layerWithAnimalResources:(AnimalResources*)resource;
-(id)initWithAnimalResources:(AnimalResources*)resource;
//Second Layer.m:
+(id)layerWithAnimalResources:(AnimalResources*)resource {
return [[[SecondLayer alloc] initWithAnimalResources:aTag] autorelease];
}
-(id) initWithAnimalResources:(AnimalResources*)resource
{
if( (self=[super init]))
{
//Change this sprite image based on button from main layer. I don't have it coded in yet, but I understand the concept of putting a variable in the file string using %# or %d
CCSprite *infoCard = [resource sprite];
infoCard.anchorPoint = ccp(0.5, 0);
infoCard.position = ccp(512, 0);
[self addChild:infoCard];
}
return self;
}
Give each menu item a unique id. In the method which you invoke on the tap of the button, you can reference the id of the sender. Use this id to populate the new layer with the unique information.
- (void) buttonPressed: (id) sender
{
MenuItem* item = (MenuItem*) sender;
int itemID = item.tag;
// Get unique data based on itemID and add new layer
}
EDIT: Per your code updates
-(void) showSecondLayer: (id) sender
{
CCMenuItemImage *item = (CCMenuItemImage *) sender;
int itemID = item.tag;
secondLayer = [SecondLayer node];
[secondLayer setItem: itemID]; // ADDED
secondLayer.position = CGPointMake(0, 700);
[self addChild:secondLayer];
CCMoveTo *moveLayer = [CCMoveTo actionWithDuration:1.0 position:CGPointMake(0, 0)];
[secondLayer runAction:moveLayer];
}
SecondLayer.m (the info layer)
-(id) init
{
if( (self=[super init]))
{
// Removed
}
return self;
}
-(void) setItem: (int) item
{
CCSprite *infoCard = [CCSprite spriteWithFile:[NSString stringWithFormat:#"species%d", item]];
infoCard.anchorPoint = CGPointMake(0.5, 0);
infoCard.position = CGPointMake(512, 0);
[self addChild:infoCard];
}