Modified button images lost during seque - ios

I have three scenes (viewControllers) linked through modal segues (I don't want a navigation controller). The view has a series of buttons, each initially with a red-light image. When clicked they go to the next scene, whose action may affect the image on the preceding scene by making it a yellow or green light image.
If the button image is changed programmatically via setImage, and later if the button is clicked again, the button image always reverts to the initial red-light image. Similarly if I change the button images on scene 2, go back to scene 1, and then back to scene 2 again, the buttons are back to default, rather than retaining the previous button image state.
So, the two basic questions are:
How to prevent the button image from changing back to default when clicked.
How to have an visited scene remember it's previous settings rather than go back to initial settings when visited a second time.
Thanks,
Ray

If you're using modal transitions, you need to use dismissViewControllerAnimated:completion: to get back. I'm guessing that instead you're pushing from VC1 to VC2, and then pushing a new copy of VC1 in order to return, which is wrong.

1 question: Please, show your code for changing images, mb problem in it.
2 question: My answer for second question is make a manager for your button states, for example like below:
.h
NS_ENUM(NSInteger, LightState) {
LightStateRed,
LightStateYellow,
LightStateGreen
};
#interface LightManager : NSObject
+ (instancetype)sharedManager;
- (NSString *)imageForButtonNumber:(int)buttonNumber;
- (void)changeImageForButtonNumber:(int)buttonNumber;
#end
.m
#implementation LightManager {
NSMutableArray *buttonStates;
NSArray *images;
}
+ (instancetype)sharedManager {
static LightManager *sharedManager;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[LightManager alloc] init];
});
return sharedManager;
}
- (id)init {
self = [super init];
if (self) {
images = #[#"red_light", #"yellow_light", #"green_light"]; // Or whatever images have names in your app
buttonStates = [[NSMutableArray alloc] initWithCapacity:9000]; // Number of all buttons
for (int index = 0; index < 9000; index++) {
[buttonStates addObject:#(LightStateRed)]; // Initalize with default value
}
}
return self;
}
- (NSString *)imageForButtonNumber:(int)buttonNumber { // For restoring previous state
return images[[buttonStates[buttonNumber] integerValue]];
}
- (void)changeImageForButtonNumber:(int)buttonNumber { // On button click
if ([buttonStates[buttonNumber] integerValue] == LightStateGreen) { // If you want looping
buttonStates[buttonNumber] = #(LightStateRed);
}
else {
buttonStates[buttonNumber] = #([buttonStates[buttonNumber] integerValue] + 1);
}
}
#end
Or make manager from your first controller and pass states to modal controller through -initWithParams: and save back in first controller via delegate methods or whatever.

Related

Obtaining ViewController Identifier for "Back" UIButton no UINavigationController

I am very new to Xcode and Objective-C as a whole. I have begun my first coding project/app this past week and I am running into a quick problem.
When creating my app, I started by dragging ViewControllers from the right Utilities menu section into my workspace area. I then added UIButtons from the same menu and Control+dragged a modal connection between the two (when the button is pressed, the view changes from one controller to the other?). My experience has been largely UI based and very little coding.
My Issue:
I have 5 ViewControllers each individually dragged from the Utilities menu. Four of them are linked by UIButtons titled "Next" and "Prev" according to the order I want them navigated. If at any time, a button present on all for ViewControllers is pressed on any given ViewController, you are presented with a separate "Menu" ViewController that has a "Back" UIButton. When this button is pressed, I want the ViewController to be switched back to whichever of the 4 VC's I was previously on when I pressed "Menu." All of the ViewControllers are in class ViewController.
Logically, this can be accomplished through setting a couple integers:
#implementation ViewController
int buttonPressedHere = 0;
- (IBAction)view1menu:(id)sender{
buttonPressedHere = 1;
}
- (IBAction)view2menu:(id)sender{
buttonPressedHere = 2;
}
- (IBAction)view3menu:(id)sender{
buttonPressedHere = 3;
}
- (IBAction)view4menu:(id)sender{
buttonPressedHere = 4;
}
- (void)setPrevious{
if(buttonPressedHere == 1)
//insert code to return to previous ViewController 1
if(buttonPressedHere == 2)
//insert code to return to previous ViewController 2
if(buttonPressedHere == 3)
//insert code to return to previous ViewController 3
if(buttonPressedHere == 4)
//insert code to return to previous ViewController 4
}
#end
The issue here is, I don't know how to get the identifier of the ViewController I want to switch to. Is there an easier way? Help!
First, define class for previous ViewControllers in Xib or Storyboard:
This is usually needed, you will custom the previous four views' activities further. The custom view controller class should inherit from UIViewController.
Then, you are able to control the pop action by following code:
- (void)setPrevious {
if (buttonPressedHere == 1) {
[self popToPreviousViewControllerWithClass:[FirstViewController class]];
}
else if (buttonPressedHere == 2) {
[self popToPreviousViewControllerWithClass:[SecondViewController class]];
}
else if (buttonPressedHere == 3) {
[self popToPreviousViewControllerWithClass:[ThirdViewController class]];
}
else if (buttonPressedHere == 4) {
[self popToPreviousViewControllerWithClass:[FourthViewController class]];
}
}
- (void)popToPreviousViewControllerWithClass:(Class)class {
for (UIViewController *previousViewController in self.navigationController.viewControllers) {
if ([previousViewController isKindOfClass:class]) {
[self.navigationController popToViewController:previousViewController animated:YES];
break;
}
}
}

Only sometimes get EXC_BAD_ACCESS when setting uitabviewcontroller viewcontrollers

I'm trying to create and set VCs for my tab view controller. There is a next button in each VC that either opens the next tab or loads a new set of tabs when it reaches the end. Inside both Type1.m and Type2.m:
- (IBAction)next:(id)sender {
if (self.tabBarController.selectedIndex < [self.tabBarController.viewControllers count] - 1) {
self.tabBarController.selectedIndex += 1;
}
else {
// load nextData
NSLog(#"nextData: %#", nextData);
if (nextData == nil) {
// essentially stop the next button from working
return;
}
self.tabBarController.selectedIndex = 0U;
[self.tabBarController setTitle:[nextData display_name]];
[self.tabBarController setViewControllers:[nextData loadTabs]];
}
}
loadTabs is a method of the Data class (which is a Core Data NSManagedObject subclass with a category).
Inside ParentData+Helpers.m It looks like:
- (NSArray *)loadTabs {
NSMutableArray *mut = [NSMutableArray new];
for (SubData *d in self.datapoints) {
if ([d.field_x isEqual:#YES]) {
[mut addType1:d];
else {
[mut addType2:d];
}
return mut;
}
addType1 and addType2 are nearly identical in their fields, but have different implementations. They look like:
- (TypeVC *)addType1:(SubData *)data {
TypeVC *vc = [[TypeVC alloc] init];
vc.datapoint = data;
return vc;
}
where TypeVC is either Type1 or Type2
So now I have a table view and each cell has an associated ParentData which has an array of SubData objects. When selected, it loads a UITabViewController with Type1 and Type2 VCs.
inside TableVC.m:
- (void)- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self performSegueWithIdentifier:#"toTabs" sender:self];
}
where toTabs leads to a UITabBarController in the storyboard.
In prepareForSegue:
UITabBarController presentedTabBarController = [segue destinationViewController];
[presentedTabBarController setTitle:self.selectedTitleName];
[presentedTabBarController setViewControllers:[self.selectedParentData loadTabs]];
So all that works fine.
So I run my app, get to the UITabBarController and the thing that should be there is. The first set of tabs has 2 tabs, so I hit next and it goes to the second one. I hit next, and it loads the next set of tabs and opens to the correct tab. I hit next, alls working good. I hit next four or five times, each time it works correctly. It doesnt matter if its Type1 or Type2, it works.
But then suddenly, I get to a specific one that has 2 tabs. It loads the first one (the one at index 0) and then I hit next. And suddenly I have EXC_BAD_ACCESS(code=1, address=0xWHATEVER) at the line self.tabBarController.selectedIndex += 1;. I don't understand why this would happen.

Xcode - Saving button click sequence and replaying the actions in saved order?

I am trying to save the order in which the buttons are pressed, and then replay that order and run the actions assigned to the buttons in the order they were originally pressed? Can anyone please help me with this?
Each UIControl element has a tag which you can use to be able to identify between the various buttons that are going to be tapped. As each button is tapped, the method (selector) associated with that button will be called (you can even have a single selector be called for all the buttons and differentiate between them via their tags).
As each button is tapped, keep track of which button is tapped by adding the tag of each button to a queue (or in Objective-C: NSMutableArray). Then to replay the actions you can merely read the tag values from the queue and call the corresponding selector.
An example to illustrate:
#property (nonatomic, strong) NSMutableArray *taskArray;
// in your init or viewDidLoad:
_taskArray = [NSMutableArray new];
// in the selector that is called by *all* buttons
-(IBAction) buttonTapped:(id)sender {
[_taskArray addObject:[NSNumber numberWithInteger:sender.tag]];
[self executeActionWithTag:sender.tag];
}
-(void) executeActionWithTag:(NSUInteger)tag {
if(tag == 1) {
// perform specific action 1 ...
} else if (tag == 2) {
// perform specific action 2 ...
}
// ...
}
-(void) replayButtonActions {
for (NSNumber *tag in _taskArray) {
[self executeActionWithTag:[tag integerValue]];
}
}

Using the same view controller multiple times

I'm new to Objective-C and have a question. Did the search multiple times but I couldn't find what I was looking for.
I'm using storyboard for this app. On the homescreen you've got some buttons with labels above them. Those labels should tell a number. When pushing the button you go to a new viewController where you have input that (after 'save') goes back to the homescreen and updates the label with the correct number. All that works great for one button and I'm very happy about it.
The problems are:
1. Since I have multiple buttons with labels, I want to use the same viewController to give input over and over again. I tried connecting every button to slide to the viewController under the identifier "AddData", but Xcode doesn't allow the same identifiers twice or more in storyboard. So I would need something else for this. Any idea?
2. Currently I use the following code to bring back the data to the homescreen:
homeScreenViewController
- (IBAction)unwindToHomeScreen:(UIStoryboardSegue *)segue;
{
inputDataViewController *source = [segue sourceViewController];
self.logoOneLabel.text = source.endTotalNumber;
}
inputDataViewController:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if (sender != self.saveButton) {
return;
} else {
if (endTotalLabelNumber > 0) {
self.endTotalNumber = [NSString stringWithFormat:#"%.0f", totalLabelNumber + endTotalLabelNumber];
} else if (endTotalLabelNumber == 0 && totalLabelNumber == 0){
self.endTotalNumber = 0;
} else {
self.endTotalNumber = [NSString stringWithFormat:#"%.0f", totalLabelNumber + endTotalLabelNumber];
}
}
}
This works great for the one button, but how to use this with multiple? I heard about Delegates to use the same viewController multiple time and get data back to different places, but I just don't get it. Any help?
You shouldn't need delegates.
What you will need is a property on the view controller that handles input to it knows which button it is handling input for.
When you segue to the input controller, set this property, based on which button was pushed. When you unwind back, fetch this property to know which label to modify.
For example, in your input view controller's .h file, add a property like this:
#property (nonatomic,assign) NSInteger handlingTag;
Or something, whatever name makes sense to you.
Now you need to implement your home screen view controller's prepareForSegue:sender:.
Use the sender argument to determine which button was pushed, and based on that, set the input view controller's new handlingTag property based on the button in a way that you will know what to do with it when we unwind.
Now in the unwind method:
switch (source.handlingTag)
Create a switch structure based on the source's handlingTag property, and set the appropriate label based on this value.
As Jeff points out in the comments, it'd be a really good idea to define an NS_ENUM to use here for the property rather than an NSInteger. The NS_ENUM would allow you to name the values you're using.
There is a few different way to implement what you need. But i think most common its a delegate.
This is how your inputDataViewController looks like:
#import <UIKit/UIKit.h>
#protocol inputDataDelegate;
#interface inputDataViewController : UIViewController
#property (weak) id<inputDataDelegate> delegate;
#property (strong, nonatomic) NSNumber *buttonTag;
#end
#protocol inputDataDelegate <NSObject>
-(void) inputDataViewControllerDismissed:(id)data;
#end
Then in #implementation, you should in "save" button action, message to you delegate method :
[self inputDataViewControllerDismissed:#{#"buttonTag":buttonTag,#"endTotalNumber":endTotalNumber}
Next in homeScreenViewController connect delegate :
#interface homeScreenViewController : UIViewController<inputDataDelegate>
After that in #implementation:
-(void)inputDataViewControllerDismissed:(id)data
{
// if you use modal
[self dismissViewControllerAnimated:YES completion:nil];
// or if you use push
//[self.navigationController popViewControllerAnimated:YES];
switch (data[#"buttonTag"]) {
case 1:
self.lableWtiTagOne = data[#"endTotalNumber"];
break;
case 2:
self.lableWtiTagTwo = data[#"endTotalNumber"];
break;
// number of cases depend how many buttons you have
}
Also, most important, thing didn't forget send self to our delegate:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"inputDataController"])
{
inputDataViewController *inputCtrl = [segue destinationViewController];
inputCtrl.delegate = self;
inputCtrl.buttonTag = sender.tag
}
}

Programmatically switch view in iOS

I'm new to iOS development and I'm trying to make a simple arithmetic game. It has two views, a kind of start screen with a button to start playing the game and a label with the high score on it, and the view to play the actual game. Here is a screenshot of it:
The code for the game's ViewController looks like this:
#import "AdditionController.h"
#import "ViewController.h"
#interface AdditionController ()
//properties and outlets here
#end
#implementation AdditionController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
[self setBackground];
self.difficulty = 20;
[self setupGame];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)setBackground
{
...
}
-(void)setupGame
{
...
}
-(void)writeQuestion
{
...
}
- (void)advanceTimer:(NSTimer *)timer
{
...
}
- (IBAction)checkAnswer:(id)sender {
...
}
- (IBAction)quit:(id)sender {
[self gameOver];
}
- (void)gameOver{
NSString *goodbye = [NSString stringWithFormat:#"You scored %d.", self.score_val];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
// to store
NSNumber *aNumber = [defaults objectForKey:#"hiScore"];
NSInteger anInt = [aNumber intValue];
if (anInt < self.score_val) {
[defaults setObject:[NSNumber numberWithInt:self.score_val] forKey:#"hiScore"];
[defaults synchronize];
goodbye = [goodbye stringByAppendingString:#"\nThat's a new high score!"];
}
UIAlertView *message = [[UIAlertView alloc] initWithTitle:#"Game Over!" message:goodbye delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil];
[message show];
// I have been using this method to go back to the start screen.
// But this method doesn't update the new highscore
[self.navigationController popViewControllerAnimated:YES];
// However I should be able to do this if I can setup an ID for the start screen
ViewController *svc = [storyboard instantiateViewControllerWithIdentifier:#"StartViewController"];
}
#end
However when I go to View -> Utilities -> Show Identity Inspector, there is no option to set the story board ID, only one to set the restoration ID.
I was wondering if there was anyway to set the storyboard ID, or if I was missing something obvious? I'm using Xcode Version 5.0.2 (5A3005).
Also just wanted to add that I don't seem to be able to select UIViewController from the drop down list!
First, your screenshot clearly shows that you have the UIView selected in the storyboard, not the UIViewController, which is what you can set a storyboard ID on. So if you want to set an ID, you have to make sure you have the actual UIViewController selected.
Second, what you're trying to do by setting a storyboard ID is to create another instance of your first view controller, which is not what you want to do. You still have an instance of this view controller, you just want to get that instance and update the high score on it. You can do this in multiple ways:
You could create a delegate protocol on your second view controller, where it has a delegate method for updating the high score. Then you would set your first view controller as the delegate of the second, implement the delegate protocol method(s) in the first view controller, and call these at the appropriate time from the second view controller.
You could pass a block to the second view controller from the first that can be called with a high score (and any other important information) to let the first view controller know what to update.
You could use the UINavigationController to get the first view controller. If it is the root view controller of the navigation controller, then it'll be easy to get. Then you'll have your reference to the already existing instance and can update the high score however you were already planning on doing so. This one is the least ideal choice, however, because it requires that the second view controller have knowledge of the type of the first view controller, when really that shouldn't be necessary.
More just to add to Gavin's answer,
Whenever I've built simple games within an iOS app before i've created separate UIViews from the object library in storyboard, then I've put them outside the view controller's main view (i.e. move them below 'First Responder' in the document outline view). Then you can make these views properties of your viewController, and swap them in/out whenever you want with something like this when the start button is pressed to reveal self.gameView underneath:
[self.startView removeFromSuperview];
and the same would apply to reveal a self.finishView

Resources