I'm working on integrating RAC into my project with the goal of creating a ViewModel layer that will allow easy caching/prefetching from the network (plus all of the other benefits of MVVM). I'm not especially familiar with MVVM or FRP yet, and I'm trying to develop a nice, reusable pattern for iOS development. I have a couple of questions about this.
First, this is sort of how I've added a ViewModel to one of my views, just to try it out. (I want this here to reference later).
In ViewController viewDidLoad:
#weakify(self)
//Setup signals
RAC(self.navigationItem.title) = self.viewModel.nameSignal;
RAC(self.specialtyLabel.text) = self.viewModel.specialtySignal;
RAC(self.bioButton.hidden) = self.viewModel.hiddenBioSignal;
RAC(self.bioTextView.text) = self.viewModel.bioSignal;
RAC(self.profileImageView.hidden) = self.viewModel.hiddenProfileImageSignal;
[self.profileImageView rac_liftSelector:#selector(setImageWithContentsOfURL:placeholderImage:) withObjectsFromArray:#[self.viewModel.profileImageSignal, [RACTupleNil tupleNil]]];
[self.viewModel.hasOfficesSignal subscribeNext:^(NSArray *offices) {
self.callActionSheet = [[UIActionSheet alloc] initWithTitle:#"Choose Office" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
self.directionsActionSheet = [[UIActionSheet alloc] initWithTitle:#"Choose Office" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
self.callActionSheet.delegate = self;
self.directionsActionSheet.delegate = self;
}];
[self.viewModel.officesSignal subscribeNext:^(NSArray *offices){
#strongify(self)
for (LMOffice *office in offices) {
[self.callActionSheet addButtonWithTitle: office.name ? office.name : office.address1];
[self.directionsActionSheet addButtonWithTitle: office.name ? office.name : office.address1];
//add offices to maps
CLLocationCoordinate2D coordinate = {office.latitude.doubleValue, office.longitude.doubleValue};
MKPointAnnotation *point = [[MKPointAnnotation alloc] init];
point.coordinate = coordinate;
[self.mapView addAnnotation:point];
}
//zoom to include all offices
MKMapRect zoomRect = MKMapRectNull;
for (id <MKAnnotation> annotation in self.mapView.annotations)
{
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.2, 0.2);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
[self.mapView setVisibleMapRect:zoomRect animated:YES];
}];
[self.viewModel.openingsSignal subscribeNext:^(NSArray *openings) {
#strongify(self)
if (openings && openings.count > 0) {
[self.openingsTable reloadData];
}
}];
ViewModel.h
#property (nonatomic, strong) LMProvider *doctor;
#property (nonatomic, strong) RACSubject *fetchDoctorSubject;
- (RACSignal *)nameSignal;
- (RACSignal *)specialtySignal;
- (RACSignal *)bioSignal;
- (RACSignal *)profileImageSignal;
- (RACSignal *)openingsSignal;
- (RACSignal *)officesSignal;
- (RACSignal *)hiddenBioSignal;
- (RACSignal *)hiddenProfileImageSignal;
- (RACSignal *)hasOfficesSignal;
ViewModel.m
- (id)init {
self = [super init];
if (self) {
_fetchDoctorSubject = [RACSubject subject];
//fetch doctor details when signalled
#weakify(self)
[self.fetchDoctorSubject subscribeNext:^(id shouldFetch) {
#strongify(self)
if ([shouldFetch boolValue]) {
[self.doctor fetchWithCompletion:^(NSError *error){
if (error) {
//TODO: display error message
NSLog(#"Error fetching single doctor info: %#", error);
}
}];
}
}];
}
return self;
}
- (RACSignal *)nameSignal {
return [RACAbleWithStart(self.doctor.displayName) distinctUntilChanged];
}
- (RACSignal *)specialtySignal {
return [RACAbleWithStart(self.doctor.primarySpecialty.name) distinctUntilChanged];
}
- (RACSignal *)bioSignal {
return [RACAbleWithStart(self.doctor.bio) distinctUntilChanged];
}
- (RACSignal *)profileImageSignal {
return [[[RACAbleWithStart(self.doctor.profilePhotoURL) distinctUntilChanged]
map:^id(NSURL *url){
if (url && ![url.absoluteString hasPrefix:#"https:"]) {
url = [NSURL URLWithString:[NSString stringWithFormat:#"https:%#", url.absoluteString]];
}
return url;
}]
filter:^BOOL(NSURL *url){
return (url != nil && ![url.absoluteString isEqualToString:#""]);
}];
}
- (RACSignal *)openingsSignal {
return [RACAbleWithStart(self.doctor.openings) distinctUntilChanged];
}
- (RACSignal *)officesSignal {
return [RACAbleWithStart(self.doctor.offices) distinctUntilChanged];
}
- (RACSignal *)hiddenBioSignal {
return [[self bioSignal] map:^id(NSString *bioString) {
return #(bioString == nil || [bioString isEqualToString:#""]);
}];
}
- (RACSignal *)hiddenProfileImageSignal {
return [[self profileImageSignal] map:^id(NSURL *url) {
return #(url == nil || [url.absoluteString isEqualToString:#""]);
}];
}
- (RACSignal *)hasOfficesSignal {
return [[self officesSignal] map:^id(NSArray *array) {
return #(array.count > 0);
}];
}
Am I right in the way I'm using signals? Specifically, does it make sense to have bioSignal to update the data as well as a hiddenBioSignal to directly bind to the hidden property of a textView?
My primary question comes with moving concerns that would have been handled by delegates into the ViewModel (hopefully). Delegates are so common in iOS world that I'd like to figure out the best, or even just a moderately workable, solution to this.
For a UITableView, for example, we need to provide both a delegate and a dataSource. Should I have a property on my controller NSUInteger numberOfRowsInTable and bind it to a signal on the ViewModel? And I'm really unclear on how to use RAC to provide my TableView with cells in tableView: cellForRowAtIndexPath:. Do I just need to do these the "traditional" way or is it possible to have some sort of signal provider for the cells? Or maybe it's best to leave it how it is, because a ViewModel shouldn't really be concerned with building the views, just modifying the source of the views?
Further, is there a better approach than my use of a subject (fetchDoctorSubject)?
Any other comments would be appreciated as well. The goal of this work is to make a prefetching/caching ViewModel layer that can be signalled whenever needed to load data in the background, and thus reduce wait times on the device. If anything reusable comes out of this (other than a pattern) it will of course be open source.
Edit: And another question: It looks like according to the documentation, I should be using properties for all of the signals in my ViewModel instead of methods? I think I should configure them in init? Or should I leave it as-is so that getters return new signals?
Should I have an active property as in the ViewModel example in ReactiveCocoa's github account?
The view model should model the view. Which is to say, it shouldn't dictate any view appearance itself, but the logic behind whatever the view appearance is. It shouldn't know anything about the view directly. That's the general guiding principle.
On to some specifics.
It looks like according to the documentation, I should be using properties for all of the signals in my ViewModel instead of methods? I think I should configure them in init? Or should I leave it as-is so that getters return new signals?
Yes, we typically just use properties that mirror their model properties. We'd configure them in -init kinda like:
- (id)init {
self = [super init];
if (self == nil) return nil;
RAC(self.title) = RACAbleWithStart(self.model.title);
return self;
}
Remember that view models are just models for a specific use. Plain old objects with plain old properties.
Am I right in the way I'm using signals? Specifically, does it make sense to have bioSignal to update the data as well as a hiddenBioSignal to directly bind to the hidden property of a textView?
If the bio signal's hiddenness is driven by some specific model logic, it'd make sense to expose it as a property on the view model. But try not to think of it in view terms like hiddenness. Maybe it's more about validness, loading, etc. Something not tied to specifically how it's presented.
For a UITableView, for example, we need to provide both a delegate and a dataSource. Should I have a property on my controller NSUInteger numberOfRowsInTable and bind it to a signal on the ViewModel? And I'm really unclear on how to use RAC to provide my TableView with cells in tableView: cellForRowAtIndexPath:. Do I just need to do these the "traditional" way or is it possible to have some sort of signal provider for the cells? Or maybe it's best to leave it how it is, because a ViewModel shouldn't really be concerned with building the views, just modifying the source of the views?
That last line is exactly right. Your view model should give the view controller the data to display (an array, set, whatever), but your view controller is still the table view's delegate and data source. The view controller creates cells, but the cells are populated by data from the view model. You could even have a cell view model if your cells are relatively complex.
Further, is there a better approach than my use of a subject (fetchDoctorSubject)?
Consider using a RACCommand here instead. It'll give you a nicer way of handling concurrent requests, errors, and thread-safety. Commands are a pretty typical way of communicating from the view to the view model.
Should I have an active property as in the ViewModel example in ReactiveCocoa's github account?
It just depends on whether you need it. On iOS it's probably less commonly needed than OS X, where you could have multiple views and view models allocated but not "active" at once.
Hopefully this has been helpful. It looks like you're heading in the right direction generally!
For a UITableView, for example, we need to provide both a delegate and
a dataSource. Should I have a property on my controller NSUInteger
numberOfRowsInTable and bind it to a signal on the ViewModel?
The standard approach, as described by joshaber above is to manually implement the datasource and delegate within your view controller, with the view model simply exposing an array of items each of which represents a view model which backs a table view cell.
However, this results in a lot of boiler-plate in your otherwise elegant view controller.
I have created a simple binding helper that allows you to bind an NSArray of view models to a table view with just a few lines of code:
// create a cell template
UINib *nib = [UINib nibWithNibName:#"CETweetTableViewCell" bundle:nil];
// bind the ViewModels 'searchResults' property to a table view
[CETableViewBindingHelper bindingHelperForTableView:self.searchResultsTable
sourceSignal:RACObserve(self.viewModel, searchResults)
templateCell:nib];
It also handles selection, executing a command when a row is selected. The complete code is over on my blog. Hope this helps!
Related
I've just started updating my ReactiveCocoa app to use the MVVM pattern and have a few questions regarding the boundary between the ViewController and ViewModel and how dumb the ViewController should be.
The first part of the app I am updating is the login flow, which behaves as follows.
User enters an email address, password and touches the login button
A successful response contains one or more User models
These User models are displayed along with a logout button
A User model must be selected for the session before the login view is closed and the main view is presented.
Before MVVM
LoginViewController directly handles the LoginButton command
LoginButton command talks directly to the SessionManager
LoginViewController displays a UIActionSheet for selecting a User model or logging out
The user selection and logout functions of the LoginViewController talk directly to the SessionManager
After MVVM
LoginViewModel exposes a login command and user selection and logout methods
LoginViewModel user selection and logout methods talk directly to the SessionManager
LoginViewController reacts to the login command of the LoginViewModel
LoginViewController displays a UIActionSheet for selecting a User model or logging out
The user selection and logout functions of the LoginViewController talk to the LoginViewModel
LoginViewModel.h
#interface LoginViewModel : RVMViewModel
#property (strong, nonatomic, readonly) RACCommand *loginCommand;
#property (strong, nonatomic, readonly) RACSignal *checkingSessionSignal;
#property (strong, nonatomic, readonly) NSArray *users;
#property (strong, nonatomic) NSString *email;
#property (strong, nonatomic) NSString *password;
- (void)logout;
- (void)switchToUserAtIndex:(NSUInteger)index;
#end
LoginViewModel.m
#implementation LoginViewModel
- (instancetype)init {
self = [super init];
if (self) {
#weakify(self);
// Set up the login command
self.loginCommand = [[RACCommand alloc] initWithEnabled:[self loginEnabled]
signalBlock:^RACSignal *(id input) {
#strongify(self);
[[[SessionManager sharedInstance] loginWithEmail:self.email
password:self.password]
subscribeNext:^(NSArray *users) {
self.users = users;
}];
return [RACSignal empty];
}];
// Observe the execution state of the login command
self.loggingIn = [[self.loginCommand.executing first] boolValue];
}
return self;
}
- (void)logout {
[[SessionManager sharedInstance] logout];
}
- (void)switchToUserAtIndex:(NSUInteger)index {
if (index < [self.users count]) {
[[SessionManager sharedInstance] switchToUser:self.users[index]];
}
}
- (RACSignal *)loginEnabled {
return [RACSignal
combineLatest:#[
RACObserve(self, email),
RACObserve(self, password),
RACObserve(self, loggingIn)
]
reduce:^(NSString *email, NSString *password, NSNumber *loggingIn) {
return #([email length] > 0 &&
[password length] > 0 &&
![loggingIn boolValue]);
}];
}
#end
LoginViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
#weakify(self);
// Bind to the view model
RAC(self.controlsContainerView, hidden) = self.viewModel.checkingSessionSignal;
RAC(self.viewModel, email) = self.emailField.rac_textSignal;
RAC(self.viewModel, password) = self.passwordField.rac_textSignal;
self.loginButton.rac_command = self.viewModel.loginCommand;
self.forgotPasswordButton.rac_command = self.viewModel.forgotPasswordCommand;
// Respond to the login command execution
[[RACObserve(self.viewModel, users)
skip:1]
subscribeNext:^(NSArray *users) {
#strongify(self);
if ([users count] == 0) {
[Utils presentMessage:#"Sorry, there appears to be a problem with your account."
withTitle:#"Login Error"
level:MessageLevelError];
} else if ([users count] == 1) {
[self.viewModel switchToUserAtIndex:0];
} else {
[self showUsersList:users];
}
}];
// Respond to errors from the login command
[self.viewModel.loginCommand.errors
subscribeNext:^(id x) {
[Utils presentMessage:#"Sorry, your login credentials are incorrect."
withTitle:#"Login Error"
level:MessageLevelError];
}];
}
- (void)showUsersList:(NSArray *)users {
CCActionSheet *sheet = [[CCActionSheet alloc] initWithTitle:#"Select Organization"];
// Add buttons for each of the users
[users eachWithIndex:^(User *user, NSUInteger index) {
[sheet addButtonWithTitle:user.organisationName block:^{
[self.viewModel switchToUserAtIndex:index];
}];
}];
// Add a button for cancelling/logging out
[sheet addCancelButtonWithTitle:#"Logout" block:^{
[self.viewModel logout];
}];
// Display the action sheet
[sheet showInView:self.view];
}
#end
Questions
Creating the additional ViewModel layer means I need to proxy the SessionManager calls. I guess the benefit of decoupling the LoginViewController from the SessionManager outweighs the additional code and function calls of the ViewModel layer?
The LoginViewController has knowledge of the User model in order to display a list of users which can be selected. This breaks the MVVM pattern and certainly doesn't feel right. Should the LoginViewModel extract only the necessary properties of a User model required by the LoginViewController and add them to a dictionary, an array of which is returned to the LoginViewController? Or would it be better to have a method on the LoginViewModel which returns the name of a user given an index, allowing the LoginViewController to display this name? I understand the ViewModel is responsible for bridging the gap between the model and the view however this does feel like double handling. As per my hunch in the first question I guess the benefits of separating these concerns far outweigh what feels like a slightly laborious mapping process.
If the LoginViewModel calls all functionality contained within the SessionManager is it enough to write tests against the LoginViewModel only or should tests also be written specifically against the SessionManager?
This is pretty late, and I'm sure you've moved on.
1) moving program logic out of a view/control is always worth the extra few lines of cone that you need to write to proxy. The point of MVVM is to encourage the separation of concerns and to provide a clear channel of data between the View/Controller and the Model via the ViewModel.
From the perspective of the View/Controller, your View Models should perform the following function:
Act as a black-box of data which your View/Controller can leverage without performing any business rules and always assume the data is correct.
Act as a conduit for user-input processing which takes that user-input without having to perform any business rules.
2) In my implementations of MVVM I try and follow this paradigm: A view/controller containing a CollectionView/TableView is a parent view, and the cells are child views. Thus, you should have a parent ViewModel who's job is to initialize and manage child ViewModels.
In your case, you're not using a Collection/Table view, but the concept is the same. You should be asking your parent View Model for a list of child ViewModels which you can pass into another view to leverage. Following the point in answer #1, the parent View Model should ensure that the Child ViewModels are initialized correctly so that the child View doesn't need to worry about any data validation.
3) When testing your View Model's data validation/rules, you can stub out the Session Manager entirely and only test the View Model. What I do is create assertions that the stubbed/mocked Session Manager functions are called appropriately in my unit test.
I know that there are tutorials everywhere, but I can't figure this out for some reason. I have a tab bar controller. Each tab links to a navigation controller, which is segued to a view controller. So, 2 main view controllers (StatusVC and TransactionsVC).
In StatusVC, I have a text field. In TransVC, I have a table view. A person adds a cell to the table. Math is done behind the scenes. The cell values are added together (numbers). This information is sent back to StatVC for calculations and displaying of the data. I've already got the math part down. My question: how do I transfer the data between view controllers, and better yet, how do I store this data so that it doesn't get deleted on quit (NSUserDefaults probably)?
This can be broken down I suppose, the transferring of data, the saving of data, and the displaying of data when the tab is pressed and view is shown.
I'm hoping this is making sense. Anyway, here's the code I've got. You're looking at TranVC. User enters data into the table with an alert view. You are looking at part of the Alert View delegate methods. This is when the user enters data into a cell (presses done). Look for key areas with the ******* comments.
StatusViewController *statVC = [[StatusViewController alloc]init]; //*******init
// Set the amount left in the budget
NSString *amountToSpend = statVC.amountLeftInBudget.text;
double budgetLabel = [amountToSpend doubleValue];
NSString *lastItem = [transactions objectAtIndex:0];
double lastLabel = [lastItem doubleValue];
double totalValue = budgetLabel - lastLabel;
NSString *amountToSpendTotal = [NSString stringWithFormat: #"%.2f", totalValue];
statVC.amountLeftInBudget.text = amountToSpendTotal; //*******set text (but not save), either way, this doesn't work
// Set the amount spent
NSString *sum = [transactions valueForKeyPath:#"#sum.self"];
double sumLabel = [sum doubleValue];
NSString *finalSum = [NSString stringWithFormat:#"%.2f", sumLabel];
//Set the amountSpent label
statVC.amountSpent.text = finalSum; //*******set text (but not save), either way, this doesn't work
// The maxed out budget section
if ([statVC.amountLeftInBudget.text isEqualToString: #"0.00"]) //*******set color (but not save), either way, this doesn't work
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor redColor];
} else if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedAscending)
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor redColor];
} else if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedDescending)
{
statVC.amountLeftInBudget.textColor = statVC.currencyLabel.textColor = [UIColor colorWithRed:23.0/255.0 green:143.0/255.0 blue:9.0/255.0 alpha:1.0];
}
if ([statVC.amountLeftInBudget.text compare:#"0.00"] == NSOrderedAscending)
{
// Create our Installation query
UIAlertView *exceed;
exceed = [[UIAlertView alloc]
initWithTitle: #"Budget Exceeded"
message: #"You have exceeded your budget amount"
delegate: self
cancelButtonTitle: #"Okay"
otherButtonTitles: nil];
[exceed show];
}
Any help with this would be amazing.
This is indeed a common question.
There are various solutions. The one I recommend is to use a data container singleton. Do a google search on the singleton design pattern in Objective C. You'll even find examples of it here on SO.
Create a singleton with properties for the values that you want to share. Then teach your singleton to save it's data. You can use user defaults, you can use NSCoding, you can extract the data to a dictionary and save it to a plist file in your documents directory, or various other schemes as well.
Like Duncan suggested, a Singleton pattern might be the best route to go. If you place the shared data into a model class, you can create a class method that can be used to acquire a singleton object.
MyModel.m
#implementation MyObject
- (id) init
{
return nil; // We force the use of a singleton. Probably bad practice?
}
// Private initializer used by the singleton; not included in the header file.
- (id)initAsSingleton {
self = [super init];
if (self) {
// Initialize your singleton instance here.
}
return self;
}
+ (MyModel *)sharedMyModel {
static MyModel *myModel = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
myModel = [[MyModel alloc] initAsSingleton];
});
return myModel;
}
MyModel.h
#interface MyModel : NSObject
+ (MyModel *)sharedMyModel; // Singleton instance.
#end
This does not protect against you using [[MyModel alloc] init];. It returns a nil object which is probably poor programming on my end, but it does force you to use the singleton object instead. To use in each one of your view controllers, you just use the following line to grab the singleton instance.
MyModel *model = [MyModel sharedMyModel];
Store the data into it, and return to your other view controller and grab the singleton again. You'll have all of your data.
After thinking about it, you could also force the default initializer to just return your singleton instance like:
- (id)init {
return [MyModel sharedMyModel];
}
I am still relatively new to iOS programming. Here is a question that confused me for a long time.
So in one of the view controllers, before this view controller is pushed into the navigation item, I am passing one parameter, say userId, to it in the prepareForSegue from previous view controller. And when this view controller is loading (initialising) based on the userId from the previous view controller, I am making a network call to fetch a list of information that's related to this user and then populating this information to the model of the current view controller.
Where should I put the logic of this data preparation?
Using viewDidLoad: should be fine for common storyboard use because the storyboard does not reuse view controller. Anyway, for the completeness of my view controller usage scenario, I tend to use this pattern:
Start loading remote data asynchronously in viewWillAppear:
Stop loading remote data in viewWillDisappear:
This make sure that your data will be always updated to the current userId because the ID might be changed after viewDidLoad, e.g. in case of view controller reuse or accessing .view property before setting userId.
You should also track if your data has been loaded. For example, you could make a private boolean field named _isDataLoaded, set it to true when finish loading data and set it to false when cancelling loading data or setting new userId.
To sum it up, the pattern in my idea should be something like this:
#interface UserViewControler : UIViewController {
bool _isDataLoaded;
NSURLConnection _dataConnection;
}
#implementation UserViewController
-(void) setUserId:(int)userId {
if (_userId != userId) {
_userId = userId;
_isDataLoaded = false;
}
}
-(void) viewWillAppear:(BOOL)animated {
if (!_isDataLoaded) {
_dataConnection = // init data connection here
_dataConnection.delegate = self;
[_dataConnection start];
}
}
-(void) viewWillDisappear:(BOOL)animated {
if (_dataConnection) {
[_dataConnection cancel];
_dataConnection = nil;
_isDataLoaded = false;
}
}
// NSURLConnection call this when finish
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
_isDataLoaded = true;
_dataConnection = nil;
}
// NSURLConnection call this when fail to load data
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
_isDataLoaded = false;
_dataConnection = nil;
}
It depends on what framework you use to retrieve data from remote server, but the pattern should be like this. This will ensure that:
You will load data only when the view appear.
View controller will not loading more data after disappear.
In case of same userId, data would not be downloaded again.
Support view controller reuse.
- (void) viewDidLoad {
[super viewDidLoad];
// initialize stuff
}
Although, it may be better to do the network call and gather all this information into a custom class that contains all the information, and then perform the segue. Then all you have to do in the new view controller is pulled data out of the object (which would still be done in viewDidLoad).
Arguably, this method might be better because if there's a problem with the network, you can display an error message and then not perform the segue, giving the user an easier way to reattempt the same action, or at least they'll be on the page to reattempt the same action after leaving app to check network settings and coming back.
Of course, you could just segue forward always, and segue backward if there's a network error, but I think this looks sloppier.
Also, it's worth noting that if you're presenting the information with a UICollectionView or a UITableView, the presenting logic can (should) be moved out of viewDidLoad and into the collection/table data source methods.
What I have done in the past is make custom initializers.
+(instancetype)initWithUserID:(NSString)userID;
Here is an example of the implementation.
+(instancetype)initWithUserID:(NSString *)userID {
return [[self alloc] initWithUserID:userID];
}
-(id)initWithUserID:(NSString *)userID {
self = [self initWithNibName:#"TheNameOfTheNib" bundle:nil];
if(self) {
_userID = userID;
}
//do something with _userID here.
//example: start loading content from API
return self;
}
-(void)viewDidLoad {
//or do something with userID here instead.
}
The other thing I would suggest is make a custom class that loads data and uses blocks.
Then you can do something like this
[API loadDataForUserID:userID withCompletionBlock^(NSArray *blockArray) {
//in this case I changed initWithUserID to initWithUsers
[self.navigationController pushViewController:[NextController initWithUsers:blockArray] animated:YES];
}
I have several popovers in my application and I am having difficulty in determining which popover was dismissed. Is there a "tag" feature equivalent for UIPopOvers?
I can NSLog the popoverController in the popoverContorllerDidDismissPopover method and see the memory reference of each one but that doesn't help.
#pragma mark - Popover controller delegates
- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController {
NSLog(#"Popover dismised %#", popoverController);
}
An extract from here:
If I understand the question, then basically, no - and it's maddening.
On the one hand you're told that only one popover should be showing at
any one moment. On the other hand you don't automatically get a
reference to that popover. Thus it is up to you to store a reference,
manually, to the current popover controller at the time it shows its
popover, so that you can talk to it later in order to dismiss it.
Popover controller management can thus get really elaborate and
clumsy; you're doing all kinds of work that the system should just be
doing for you.
iOS is funny this way. I'm reminded of how there's no call in iOS 4
that tells you current first responder. Obviously the system knows
what the first responder is, so why won't it tell you? It's kind of
dumb. This is similar; the system clearly knows useful stuff it won't
share with you. m.
There are many ways how to distinguish between popovers. I will list few of them:
You are asking about tag. Note that every popover has a content view controller and this controller has a view that can be tagged. However, using magic integer tags to distinguish between views is arguable in general.
Store the type of the popover into a variable/property in your controller, e.g. as an enum. This is the simplest way.
Add the neccessary information to the popover, but be clever about it, e.g.
#interface MyPopoverController : UIPopoverController
#property (nonatomic, copy, readwrite) void (^dissmissHandler)(void);
#end
#implementation MyPopoverController
- (id)initWithContentViewController:(UIViewController*)contentView {
self = [super initWithContentViewController:contentView];
if (!self) {
return nil;
}
self.delegate = self;
return self;
}
- (void)popoverControllerDidDismissPopover:(UIPopoverController*)popover {
assert(popover == self);
if (self.dissmissHandler) {
self.dissmissHandler();
}
}
#end
MyPopoverController* popover = [MyPopoverController alloc] initWithContentViewController:...];
popover.dissmissHandler = ^{
...
};
As #Anoop stated, you can usually only have one popover showing at a time.
One possible solution is to check the contentViewController property on the pop over. If you are storing a reference of each view controller you could do something like:
- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController {
if ( popoverController.contentViewController == self.someUIViewController ) {
// do stuff
}
else if ( popoverController.contentViewController == someoTherViewController ) {
//
}
NSLog(#"Popover dismised %#", popoverController);
}
If storing a reference to each content view controller is not possible (or maybe just not a good idea), you could always check its type:
- (void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController {
if ( [popoverController.contentViewController isKindOfClass:[MyAwesomeViewController class]] ) {
// do stuff
}
else if ( [popoverController.contentViewController isKindOfClass:[MyOtherViewController class]] ) {
//
}
NSLog(#"Popover dismised %#", popoverController);
}
Another possible solution, which is probably better from a design stand point of view, would be to pass in a delegate to the view controller contained in the pop over. More here. This way, the view controller displayed can send data back to your main view controller.
I have my main app delegate
I have a few UIViewController derived instances driven by a Storyboard
Say I'd like to provide a centralized persistence layer for my application - perhaps Core Data of SQLite. Where would I put those objects? I'm missing some centrally accessible "Application" class you can access from all the UIViewController instances.
Is there a pattern to follow here?
you should check the singleton pattern:
In software engineering, the singleton pattern is a design pattern
that restricts the instantiation of a class to one object. This is
useful when exactly one object is needed to coordinate actions across
the system. The concept is sometimes generalized to systems that
operate more efficiently when only one object exists, or that restrict
the instantiation to a certain number of objects. The term comes from
the mathematical concept of a singleton.
here is a source for a example implementation: What should my Objective-C singleton look like?
and here is the direct link for the modern solution:
https://stackoverflow.com/a/145395/644629
What you're describing is your model layer. There are two main ways to manage the model:
At application startup, create the main model object and hand it to the first view controller.
Make the main model object a Singleton.
The "main model object" in both cases is generally some kind of object manager. It could be a document, or it could be a PersonManager if you have a bunch of Person objects. This object will vend model objects from your persistence store (generally Core Data).
The advantage of a Singleton here is that it's a little easier to implement and you don't have to pass around the manager. The advantage of a non-Singleton is that it's easier to have more than one (for a document-based system), and it's easier to test and reason about non-singletons than singletons. That said, probably 80% of my projects use a singleton model manager.
As a side note, that you appear to already understand: never store the model in the application delegate, and never use the application delegate as a "rendezvous point" to get to the model. That is, never have a sharedModel method on the application delegate. If you find yourself calling [[UIApplication sharedApplication] delegate] anywhere in your code, you're almost always doing something wrong. Hanging data on the application delegate makes code reuse extremely difficult.
Go with a singleton pattern, which has scope of application lifetime.
#interface DataManager ()
#end
#pragma mark -
#implementation DataManager
#pragma mark - Shared Instance
static DataManager* sharedInstance = nil;
#pragma mark - Singleton Methods
- (id)init
{
self = [super init];
if (self) {
// Initialization code here.
}
return self;
}
+ (DataManager*)sharedInstance
{
#synchronized([DataManager class])
{
if (!sharedInstance) {
//[[self alloc] init];
sharedInstance = [[DataManager alloc] init];
}
return sharedInstance;
}
return nil;
}
+ (id)alloc
{
#synchronized([DataManager class])
{
NSAssert(sharedInstance == nil, #"Attempted to allocate a second instance \
of a singleton.");
sharedInstance = [super alloc];
return sharedInstance;
}
return nil;
}
#end
Declare your properties in .h file and synthesize them here in .m file.
To use that property just call:
// set value
[[DataManager sharedInstance] setSharedProperty:#"ABC"]; // If its a string
// get Value
NSLog(#"value : %#", [[DataManager sharedInstance] sharedProperty]);
Hope this is what you required.
Enjoy Coding :)