How to best design my list-detail viewcontrollers? - ios

My app has a list of clients in a table view. When you click on a client it takes you to a detailed view controller using a standard navigation controller.
The list view controller allows the user to swipe to delete a client. The detailed view controller has a button to delete a client.
When deleting a client I want to present an action sheet with a several choices.
THE QUESTION: I don't want to duplicate code in both of my view controllers for presenting the action sheet and handling the results of the action sheet. As both view controllers are deleting a client, the code is identical in both instances. Is there a design pattern that is considered best practice in this case?
Thanks for any help.

I guess you can't really use one-set of code for UIActionSheet in both viewControllers. But for a very similar situation, my approach was as follows. I hope it helps.
I create my own custom class, e.g. MySortingClass (in my case, it was sorting options, e.g. date ascending / descending, name ascending / descending). This class is a subclass of NSObject.
In my custom class I declare various methods that will return, for example, an array of options title to show to the user, an array of NSSortDescriptors, etc.
In any of my viewControllers that I need to present a list of sorting options to user, I would import MySortingClass, alloc, init, and get an array of options, show them via actionSheet, and send back the response as an index to the MySortingClass and get the appropriate NSSortDescriptor back and re-sort.
...
// In MySortingClass
- (NSArray *)arrayOfOptions;
- (NSSortDescriptor *)sortDescriptorForSortingOptionIndex:(NSInteger)index;
...
...
// In a ViewController
MySortingClass *msc = [MySortingClass alloc] init];
...
- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex
{
...
[anArray sortedArrayUsingDescriptors:[NSArray arrayWithObject:[msc sortDescriptorForSortingOptionIndex:buttonIndex]]];
...
}

Related

Xcode UITabBarController: Make Two tabs point to same ViewController

I built a simple app to view movie listing from rotten tomatoes(as a part of learning ios development)https://raw.githubusercontent.com/koldkoder/movie_listing/master/rotten_tomatoes.gif
I am trying to add tab control. One tab would list current box office movies, and second tab to list movies out on DVD recently. Both Views are exactly same, just they get data from different api endpoint. I want to use UITabBarController to implement this functionality. But i dont want to create two duplicate Viewcontrollers, instead use one for both the purpose. What is the right way of doing this, using storyboard, and code.
This is pretty straight forward. Create one UIViewController that takes a view type param in initializer like this:
typedef NS_ENUM (NSInteger, MediaViewType) {
MediaViewTypeBoxOffice = 0,
MediaViewTypeDVD
};
- (id)initWithViewType:(MediaViewType)iViewType;
In the implementation file, handle your view & functionality based on passed view type. Then add your view controller's instances to UITabBarController:
MyMediaViewController *vc1 = [MyMediaViewController alloc] initWithViewType: MediaViewTypeBoxOffice];
MyMediaViewController *vc2 = [MyMediaViewController alloc] initWithViewType: MediaViewTypeDVD];
self.tabBarController.viewControllers = #[vc1, vc2];

Xcode - Retrieving value from textfield in different viewController

I have 2 viewControllers (NewTicket1Controller and NewTicket2Controller). View 1 has a text field named 'ticket' and view 2 wants to access that value.
Here is my code in View 2.
NewTicket1Controller *screen1 = [[NewTicket1Controller alloc] init];
NSLog(#"%#", screen1.ticket.text);
My NSLog statement above returns null for the ticket textfield value. But it's not null. If I switch back to view 1 I can see that there is a value in that field.
Can I not retrieve a variable like this?
When you instantiate a new NewTicket1Controller, all of it's properties are initialized to their default values. This does not give you a reference to any existing NewTicket1Controller objects. If you are using storyboards, you can pass it to the other view controller in prepareForSegue, or if you are not using storyboards, you can programatically pass it to the new view controller when it is created, assuming it is created from the NewTicket1Controller.
No need to initialize you view to access it. If you want to send text contained in textfield to another view..You need to store that ticket text field value into a string of other view controller..
At NewTicket2Controller take property with NSString with name ticketString and synthesize it...Then you can use that reference for storing value of ticket at NewTicket1Controller
At button action while switching to NewTicket2Controller..Put some code at NewTicket1Controller
NewTicket2Controller *screen2 = [[NewTicket2Controller alloc] initWithNibName:#"NewTicket2Controller" bundle:nil];
screen2.ticketString=self.ticket.text;//if you propertise the ticket textfield
NSLog(#"%#", screen2.ticketString);
Hope it helps you..
You just created and inited screen1. Any and all values will be nil or initialized.
A few things:
You want to try to keep the data (model) separate from your views and controller when possible.
You're going to have to provide some connection between the 2 view controllers.
One approach is to set up one as a weak link to the other.
Example for NewTicket2Controller
#property (nonatomic, weak) NewTicket1Controller *delegate;
And then when you create NewTicket2Controller you would assign self.delegate = screen1
After that you could access methods using the delegate.
(Ideally you'd setup a protocol for the delegate)
Others have given you partial answers. Let me state everything at once.
First, never try to manipulate another view controller's view objects directly. That is serious violation of the "encapsulation" principle of object oriented programming. You should treat a view controller's views as private, and add properties or methods to communicate between view controllers.
Second, you are creating a brand new instance of your view controller and expecting to be able to use that new view controller instance to alter settings in an existing view controller. This is like buying a new car that is a perfect match for your existing car, setting the radio station on that new car, and then wondering why the radio station on the old car doesn't change. They are different cars! They are different objects! They may be feature-for-feature identical, but they are different instances of the same object. If you had an identical twin, it is still a different person, right?
You need a way to get a pointer to your existing NewTicket1Controller object. How you do that depends on how your program is set up. Post some info on how you are setting up your view controllers. Are you using a navigation controller? Is NewTicket1Controller your root view controller? How are you getting from view controller 1 to view controller 2? A segue? (assuming you're using storyboards. Tell us if you're not.)

Objective-C: what design patterns are there to hook up a model with views that are selected from a property list?

I am trying to build a MVC app with Objective-C. It is supposed to be a questionnaire app. I have all the questions and multiple choice answers stored into a property list, because I have different of questionnaires that I want to be able to load with this app. The idea is that the main model will keep track which item it should read of the property list, and select the corresponding view and viewController.
So schematically I have the following problem.
The RootView shows the start menu, that selects which questionnaire you will be able to take.
The RootViewController is the first controller called by the app delegate. It is supposed to instantiate the model and show the RootView. It furthermore controls the buttons of the RootView.
The model is supposed to wrap the items of the property list into a fitting datastructure, and supply it to the view controllers that need it.
The SelectedViewController is a controller that is a template specifically made for a type of question. The question could be a multiple choice, an open question, a 3, 5 or 7 choice likert scale kind of question, anything really. The template name that these view controllers will really get is ViewController.
The SelectedView is a tailor made view to the question type and will get the same name format as all the selected view controllers.
Here are my ideas.
My initial hunch is to use the delegate pattern, and set the model as a delegate to any SelectedViewController.
I could also use the delegate pattern to the RootViewController, and let him monitor if the SelectedViewController should be destroyed (via a delegate message). In that case, I can implement a prepareForSegue in the RootViewController to the SelectedViewController.
Since it is a questionnaire from a plist I could also add a prepare for segue to
every selected viewcontroller, but that will probably be a problem,
since there are at least 15 different ways of displaying the
questions.
Apparently there is also something like Key-Value Observing, according to this question. So that's also something I could use.
I think there is a definite way to deal with this, because the design patterns in iOS are pretty wel described, so there should be a few options for this really (or only just one). At the moment I am leaning towards setting the RootViewController as a delegate to the SelectedViewController and let the RootViewController handle the model. In this way I am extending the RootViewController to also hold all common functionality that every SelectedViewController should have.
But I am really not sure if this is the way to go, because my knowledge on design patterns is limited. My question is: what is the right option to choice in this specific situation (e.g. views and view controllers selected via a .plist file)?
There is no need for a specific pattern - you can deal with accessing an instance of a model object by name, i.e. in the same exact way that you deal with making a specific view and the view controller.
Let's say you are looking to connect the QuizQuestionViewController4MC and its QuizQuestionView4MC to their model. Let's assume that the model class is called QuizQuestionModel4MC, and that it needs to be configured with an object that you get from a key #"4MC" in the plist. Since your code learns the name of the model class only at runtime, you can create an instance using reflection:
NSDictionary *dataFromPlist = ... // Someone passes this to you
NSString *identifier = dataFromPlist[#"identifier"]; // Returns #"4MC"
NSString *ctrlName = [NSString stringWithFormat:#"QuestionViewController%#", identifier];
NSString *modelClassName = [NSString stringWithFormat:#"QuizQuestionModel%#", identifier];
id model = [[NSClassFromString(modelClassName) alloc] init];
// Configure the model with the data from plist
[model setPlistData:dataFromPlist];
// The model is ready to be used - give it to the view controller
MyBaseViewController *ctrl = [storyboard – instantiateViewControllerWithIdentifier:ctrlName];
// Give the view controller its model - now it is ready to be used
[ctrl setModel:model];
Note the class of the view controller - MyBaseViewController. This is not your root view controller, it's a base class for all your specific view controllers. It is this view controller that knows about a model, but it does not know the specific subclass in the model hierarchy. Each subclass of the view controller knows about its specific model subclass, so it can access the information from the model class directly, without going through selectors or KVP.
Of course it is up to the designer of the app to "wire up" correct view controllers to the correct models. In terms of the above example, QuizQuestionViewController4MC needs to know the structure of the QuizQuestionModel4MC in order to avoid sending unrecognized selectors to an incorrect class.

Storing contextual information in UIActionSheet

When creating a UIActionSheet to prompt a user to delete an item from a list, I currently have to maintain the deleted item (or at least its index in the list) as an instance variable in my view controller:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if ([indexPath row] == 4) {
// Delete button pressed
_deletingItemIndex = [indexPath section];
UIActionSheet actionSheet = ...
}
}
Then when the UIActionSheet closes, if the user decided to delete the item, I have to reference that _deletingItemIndex variable, then set it to -1 or some other nil value.
What I would like to do, is maintain either the deleting item, or its index, in the actual UIActionSheet without needing to subclass UIActionSheet.
I find it strange that the delegate method for UIActionSheet provides the sheet to the delegate, but you can't store any contextual information (not even a dictionary) in the sheet itself.
There are quite a few categories out there for adding block-based dismiss handlers to UIActionSheet and UIAlertView now. I personally use Mugunth Kumar's UIKitCategoryAdditions.
This would let you do the following...
[UIActionSheet actionSheetWithTitle:#"Hooray" message:#"Blocks Are Awesome!" buttons:#[...] showInView:self onDismiss:^(int buttonIndex) {
//Now you have access to all your local variables here!
} onCancel:^{
//And here!
}];
You should use objc_setAssociatedObject()
Have a look at http://darkdust.net/writings/objective-c/attaching-objects for some code.
UIActionSheet is a system object which should only be concerned with presenting a user with choices and getting a button index out.
Using an instance variable of view controller also doesn't make sense, because this isn't really a state of view controller, but rather of a current action which started existing the moment user started deleting.
The correct pattern, thus, is to create a new object, save information there, and use as a delegate:
MyDeletionAction *action = [MyDeletionAction actionWithIndex:[indexPath section]];
UIActionSheet actionSheet = ...
actionSheet.delegate = action;
...
and somewhere else, define MyDeletionAction as a class implementing the delegate protocol.
An added bonus of this approach is that you can take the code out of your view controller into separate classes, which is a good thing. Moreover, it's likely that your MyDeletionAction, MyInsertionAction etc. will share some common code.
Perhaps even the presence of action sheet should be an implementation detail of your action. For example, what if you provide an option to delete without confirmation?
Note also that in this approach someone should retain an action object, for example by object retaining itself until the action is fully completed, or by using a VC instance variable for this purpose:
self.lastAction = [MyDeletionAction actionWithIndex:[indexPath section]];
[self.lastAction start];
This also allows you to remember the state of last action for possible postprocessing.
start here appears because my actions usually have this kind of inheritance: MyDeletionAction <- MyAction <- NSOperation. Your mileage may vary.

Pass data between UITabBarController views

I have searched for an entire day for a simple example on this and haven't found it yet. I am working on an app and I want to make an API call on the initial load and populate some variables that will be accessible in any of the tab views.
Example: I want to make a single API call to get my data which will include data relating to alerts for the alerts tab. I want to use this data on the initial load to update the "Alerts" tab badge. However, I want to also use that information on the alerts page once the person goes to that tab without having to make another API call to get the same information again.
I have read a ton of explanations that do not fit my requirements, so I am looking for a good example to help me out.
Use your UITabBarViewController's viewControllers property to access the array of view controllers in your tab bar controller. Typecast them according to your architecture of tab bar controller.
Then get a pointer to any of view controller using that array.
For example, say your tab bar controller has 5 tabs, each tab having a UINavigationController which has particular UIViewController as root view controllers. Say you want to set badge value of 3rd tab to your web response array count. You can do that as
[[[self.tabviewController viewControllers] objectAtIndex:2]
setBadgeValue:[NSString stringWithFormat:#"%d",[myArray count]];
You can also get to particular view controller's property by typecasting the view controllers. For example
MyViewController *myVc = (MyViewController*) [[(UINavigationController*)[[self.tabviewController viewControllers] objectAtIndex:2] viewControllers] objectAtIndex:0];
[myVc setPropertyName:propertyValue];
I had this question typed up since yesterday and made sure to search before posting. There was no question similar that I found that had the answer, and it may be very straight forward or maybe this is not the way to do it but here is how I solved this issue: using NSUserDefaults and the code example on this page
Put the data in your app delegate object. You can access it from anywhere in your app by (MyAppDelegate *)[[UIApplication sharedApplication] delegate], or you can give each of your view controllers an explicit link to it.
NSUserDefaults isn't really meant for sharing data globally in your app, although it would get the job done. It also has the benefit that the information sticks around if your app can't connect to the server next time. Is that a good thing or a bad thing?
Put all of those variables in a single class and access a shared instance of it whenever you want.
Add
+ (YourClass *)sharedObject
{
static YourClass *sharedClassObject = nil;
if (sharedClassObject == nil) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedClassObject = [[YourClass alloc] init];
//Initialise it here if necessary
});
}
return sharedClassObject;
}
To access the shared instance, simply use [YourClass sharedObject].
You should use NSNotificationCenter to post the notification that new data arrived and your new data as an object.
Each of the viewControllers that need that object should just subscribe to that notification and just consume the new data.

Resources