I'm using ReactiveCocoa framework at my app for the power using MVVM design pattern.
So for every Controller, I have a ViewModel. and the Controller is binded to his ViewModel.
UIButton binding will look like so:
#implementation HomeController
-(void) bindViewModel {
self.viewHeader.buttonSide.rac_command = self.viewModel.execiteFullPortfolio;
}
It all works well, But when i would like to pass parameters to the ViewModel, I'm not sure what is the right way to do so...
Say I have a UICollectionView of Stocks, and every click on a specific stock, I would like to navigate to thats stocks profile page.
That logic should be done at the ViewModel, But how do i get the stock passed with the RACCommand?
What I'm currently doing is :
#implementation HomeController
-(void) bindViewModel {
__unsafe_unretained HomeController *weakSelf = self;
self.viewPortfolioPusherView.scrollGainView.blockSelect = ^ (STStock *stock){
weakSelf.viewModel.selectedStock = stock;
[weakSelf.viewModel.executeGoToStock execute:[RACSignal empty]];
};
}
#implementation HomeViewModel
-(void) initialization {
self.executeGoToStock = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf moveToSelectedStock];
});
return [RACSignal empty];
}];
}
-(void) moveToSelectedStock {
[self stockProfileControllerLazy];
self.stockProfileController.stock = self.selectedStock;
[Navigator pushController:self.stockProfileController fromController:[self.delegate controller]];
}
I'm sure this is not best practice! So I'm asking, What is??
Thanks .
Why not just pass the STStock instance into the call to execute on the command, rather than an empty signal?
[weakSelf.viewModel.executeGoToStock execute:stock];
Then:
self.executeGoToStock = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(STStock *stock) {
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf moveToSelectedStock:stock];
});
return [RACSignal empty];
}];
You obviously need to modify moveToSelectedStock to take a parameter as well. However, I'd go a bit further an implement an RACCommand on your Navigator that does that. Furthermore, I'd make a separate view model for an instance of STStock rather than a collection. So, when you select a stock, it might look something a little more like this:
StockViewModel *viewModel = [[StockViewModel alloc] initWithStock:stock];
[[Navigator pushViewModel] execute:viewModel];
This obviously omits a few details. For example, my navigator maps view model classes to controller classes. When a view model is pushed, it creates the corresponding controller, and binds the view model to it.
Related
I'm using ReactiveCocoa and am trying to apply MVVM. I have a fairly typical UITableView scenario with a refresh control for reloading data.
I've omitted the the UITableDataSource/Delegate methods as these are straight forward. The code below illustrates how I've designed the ViewModel and the ViewController to fit together.
ViewModel.h
#property (strong, nonatomic, readonly) RACCommand *getItemsCommand;
#property (strong, nonatomic, readonly) NSArray *items;
ViewModel.m
- (instancetype)init {
self = [super init];
if (!self) return nil;
#weakify(self);
self.getItemsCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [[ItemsDataSource getItems]
doNext:^(NSArray *items) {
#strongify(self);
// I actually do a little extra work here such as sorting
// the items appropriately for the view.
self.items = items;
}];
}];
return self;
}
ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
[self.tableView addSubview:self.refreshControl];
RACSignal *refreshSignals = [RACSignal merge:#[
[self.refreshControl rac_signalForControlEvents:UIControlEventValueChanged],
[RACSignal return:nil]
]];
[refreshSignals
subscribeNext:^(id x) {
[self.viewModel.getItemsCommand execute:nil];
}];
[RACObserve(self.viewModel, items)
subscribeNext:^(NSArray *items) {
[self.tableView reloadData];
} completed:^{
[self.refreshControl endRefreshing];
}];
}
Questions/Problems
The completed block where I call endRefreshing never gets executed and for the life of me I can't figure out why.
Would it be better to use a public method - (RACSignal *)getItems instead of the getItems RACCommand?
Is my usage of doNext: in the ViewModel correct in order to apply side effects (i.e. the sorting of the items array) without causing an additional subscription?
I suggest making getItemsCommand use -map: to sort and process the items array. Leave any other side effect work to be done in a separate -doNext:. Once you have your command following this pattern (which is more compositional in RAC), then you can use the RAC() macro to assign the command's finished product, the sorted array, to the items property.
RAC(self, items) = [self.getItemsCommand.executionSignals concat];
RAC has a built-in command support for UIRefreshControl that will start/stop the refresh control along with the start/stop of the command. You should find that you can reduce your UIRefreshControl code to:
self.refreshControl.rac_command = self.getItemsCommand;
For table reloading, you can do:
[RACObserve(self, items) subscribeNext:^(id _) {
#strongify(self);
[self.tableView reloadData];
}];
Hope that helps.
1) Well, let's look at the signal:
RACObserve(self.viewModel, items)
When will that complete? Only when self.viewModel or self is deallocated, just like any other RACObserve. As long as those objects are around, it'll keep on nexting any time you set self.items.
It appears that you want it to endRefreshing once the getItemsCommand finishes executing, and you have this sort of implicit expectation that, since you know that command sets self.viewModel.items, that completion will somehow propagate -- but this isn't the case. To see when the command completes, you have to subscribe to the command's returned signal.
2) The advantage of RACCommand is the auto enabling/disabling behavior, which you aren't really taking advantage of here. The more canonical thing to do would be self.refreshControl.rac_command = self.viewModel.getItemsCommand;. That'll handle the endRefreshing stuff for you, saving you the headache from part 1.
3) ...sort of. The do* family of methods injects side effects for every subscription to the signal. So if you subscribe to a signal twice, and it sends a next, any doNext block it has will be invoked twice. An explicit subscription is more clear, since you want to execute this exactly once per next, regardless of how many times it's subscribed to.
#weakify(self);
self.getItemsCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
RACSignal *itemsSignal = [ItemsDataSource getItems];
[itemsSignal subscribeNext:^(NSArray *items) {
#strongify(self);
// Do stuff...
self.items = items;
}];
return itemsSignal;
}];
I want to inject a view into my view controller so that I can inject a mock view in my unit tests (WPDependencyInjectorImplementation is my TyphoonAssembly subclass).
My ideal loadView method would look like the following:
- (void)loadView {
WPDependencyInjectorImplementation *injectorAssembly = (WPDependencyInjectorImplementation *) [TyphoonAssembly defaultAssembly];
self.view = [injectorAssembly someView];
}
I'm not sure what the definition for this would look like or whether it's possible, given that the code for creating a view from a xib is the following:
NSArray *views = [[NSBundle mainBundle] loadNibNamed:#"WPSomeView" owner:nil options:nil];
return [views firstObject];
That's right you just override loadView, rather than looking up the view from the container, as you've shown, you should provide the view via an initializer or property setter. And then in loadView, set it to the injected view as follows:
- (void)loadView
{
[self.view = _myInjectedView]; //Do this rather than looking it up via the TyphoonFactory - easier to test.
}
If you do it this way:
You can refer to the view via it's actual type, rather than down-casting from UIView
It'll be really simple to mock-out in a pure unit test. (No need for TyphoonPatcher, swizzling, etc).
Here's an example:
- (id)userDetailsController
{
return [TyphoonDefinition withClass:[UserDetailsController class] initialization:^(TyphoonInitializer* initializer)
{
initializer.selector = #selector(initWithSession:userDetailsView:);
[initializer injectWithDefinition:[self session]];
[initializer injectWithDefinition:[self userDetailsView]];
}];
}
- (id)userDetailsView
{
return [TyphoonDefinition withClass:[UserDetailsView class]
properties:^(TyphoonDefinition* definition)
{
//circular dependency. Can also be set within VC.
[definition injectProperty:#selector(delegate)
withDefinition:[self userDetailsController]];
[definition injectProperty:#selector(sideMargin)
withValueAsText:#"${view.field.default.side.margin}"];
}];
}
Injecting From a Xib
We don't actually have a Xib factory we can provide you yet. It should be a quick job to define one using a similar pattern to the object that emits a theme here, so you'll have a component in the DI container for each of your Xib-based views, and just inject that directly.
Alternatively you could use our new TyphoonFactoryProvider.
If you get stuck, please let us know and one of us will find some time to create and push that Xib-view-factory for you.
I am writing an app which is a sort of dictionary - it presents the user with a list of terms, and when clicked on, pops up a dialog box containing the definition. The definition itself may also contain terms, which in turn the user can click on to launch another definition popup.
My main app is stored in 'myViewController.m'. It calls a custom UIView class, 'CustomUIView.m' to display the definition (this is the dialog box that pops up). This all works fine.
The text links from the CustomUIView then should be able to launch more definitions. When text is tapped in my CustomUIView, it launches another CustomUIView. The problem is, that this new CustomUIView doesn't have access to the hash map which contains all my dictionary's terms and definitions; this is only available to my main app, 'myViewController.m'.
Somehow, I need to make my hash map, dictionaryHashMap, visible to every instance of the CustomUIView class. dictionaryHashMap is created in myViewController.m when the app opens and doesn't change thereafter.
I don't wish to limit the number of CustomUIViews that can be opened at the same time (I have my reasons for doing this!), so it would be a little resource intensive to send a copy of the dictionaryHashMap to every instance of the CustomUIView. Presumably, the solution is to make dictionaryHashMap a global variable.
Some of my code:
From myViewController.m:
- (void)viewDidLoad
{
self.dictionaryHashMap = [[NSMutableDictionary alloc] init]; // initialise the dictionary hash map
//... {Code to populate dictionaryHashMap}
}
// Method to pop up a definition dialog
- (void)displayDefinition:(NSString *) term
{
NSArray* definition = [self.dictionaryHashMap objectForKey:term]; // get the definition that corresponds to the term
CustomUIView* definitionPopup = [[[CustomUIView alloc] init] autorelease]; // initialise a custom popup
[definitionPopup setTitle: term];
[definitionPopup setMessage: definition];
[definitionPopup show];
}
// Delegation for sending URL presses in CustomUIView to popupDefinition
#pragma mark - CustomUIViewDelegate
+ (void)termTextClickedOn:(CustomUIView *)customView didSelectTerm:(NSString *)term
{
myViewController *t = [[myViewController alloc] init]; // TODO: This instance has no idea what the NSDictionary is
[t displayDefinition:term];
}
From CustomUIView.m:
// Intercept clicks on links in UIWebView object
- (BOOL)webView: (UIWebView*)webView shouldStartLoadWithRequest: (NSURLRequest*)request navigationType: (UIWebViewNavigationType)navigationType {
if ( navigationType == UIWebViewNavigationTypeLinkClicked ) {
[myViewController termTextClickedOn:self didSelectTerm:request];
return NO;
}
return YES;
}
Any tips on how to make the dictionaryHashMap visible to CustomUIView would be much appreciated.
I have tried making the dictionaryHashMap global by doing the following:
Changing all instances of 'self.dictionaryHashMap' to 'dictionaryHashMap'
Adding the line 'extern NSMutableDictionary *dictionaryHashMap;' to CustomUIView.h
Adding the following outside of my implementation in myViewController.m: 'NSMutableDictionary *dictionaryHashMap = nil;'
However, the dictionaryHashMap remains invisible to CustomUIView. As far as I can tell, it actually remains a variable which is local to myViewController...
It's not resource-intensive to pass around the reference (pointer) to dictionaryHashMap. A pointer to an object is only 4 bytes. You could just pass it from your view controller to your view.
But I don't know why you even need to do that. Your view is sending a message (termTextClickedOn:didSelectTerm:) to the view controller when a term is clicked. And the view controller already has a reference to the dictionary, so it can handle the lookup. Why does the view also need a reference to the dictionary?
Anyway, if you want to make the dictionary a global, it would be more appropriate to initialize it in your app delegate, in application:didFinishLaunchingWithOptions:. You could even make the dictionary be a property of your app delegate and initialize it lazily.
UPDATE
I didn't notice until your comment that termTextClickedOn:didSelectTerm: is a class method. I assumed it was an instance method because myViewController starts with a lower-case letter, and the convention in iOS programming is that classes start with capital letters. (You make it easier to get good help when you follow the conventions!)
Here's what I'd recommend. First, rename myViewController to MyViewController (or better, DefinitionViewController).
Give it a property that references the dictionary. Whatever code creates a new instance of MyViewController is responsible for setting this property.
Give CustomUIView properties for a target and an action:
#property (nonatomic, weak) id target;
#property (nonatomic) SEL action;
Set those properties when you create the view:
- (void)displayDefinition:(NSString *)term {
NSArray* definition = [self.dictionaryHashMap objectForKey:term];
CustomUIView* definitionPopup = [[[CustomUIView alloc] init] autorelease]; // initialise a custom popup
definitionPopup.target = self;
definitionPopup.action = #selector(termWasClicked:);
...
In the view's webView:shouldStartLoadWithRequest: method, extract the term from the URL request and send it to the target/action:
- (BOOL)webView: (UIWebView*)webView shouldStartLoadWithRequest: (NSURLRequest*)request navigationType: (UIWebViewNavigationType)navigationType {
if ( navigationType == UIWebViewNavigationTypeLinkClicked ) {
NSString *term = termForURLRequest(request);
[self.target performSelector:self.action withObject:term];
return NO;
}
return YES;
}
In the view controller's termWasClicked: method, create the new view controller and set its dictionary property:
- (void)termWasClicked:(NSString *)term {
MyViewController *t = [[MyViewController alloc] init];
t.dictionary = self.dictionary;
[t displayDefinition:term];
}
Create a class that will be used as singleton. Example.
You Should always keep your data in separate class as the mvc pattern suggest and that could be achieved by using a singleton class for all your dictionary terms and accesing them from every custom view when needed.
I'm wondering if my method to setup my UIViewController is optimal or just plain stupid.
I have typedef'ed an enum with some categories. Let's say 6 different categories.
So depending on which category is the selected one. My UIViewController have a switch which will call different method to setup my UIView according to the selected category.
Just wondering if this is a good method to do this, or should I consider creating 6 different UIViewControllers?
A discussion with pro and cons is very much appreciated.
Thanks.
They are basically the same.
Sample code:
switch (self.category) {
case vegetables:
recipe = [[[WebServices sharedInstance].recipeDictionary objectForKey:self.chosenCategory] objectAtIndex:4]; //Needs to be made random
descriptionText.text = recipe.recipeDescription;
[self setupText];
[self setupVegetablesView];
break;
case dairy:
recipe = [[[WebServices sharedInstance].recipeDictionary objectForKey:self.chosenCategory] objectAtIndex:4]; //Needs to be made random
descriptionText.text = recipe.recipeDescription;
[self setupText];
[self setupDairyProductsView];
break;
- (void)setupVegetablesView
{
descriptionText.textColor = [UIColor colorWithRed:0/255.0 green:103/255.0 blue:55/255.0 alpha:1];
background.image = imageBackgroundVegetables;
topBar.image = topBarForVegetables;
subtitle.image = subtitleImageVegetables;
subtitleLink.image = subtitleLinkBarVegetables;
...
}
Depends on your situation. If the view controllers are similar, than this makes sense. But if they are completely different from each other, use separate subclasses.
I would implement it as following
• i would several UIView derived class each one for the type of UIView that i need
For example, i would have VegatableView and DiaryView
• each one of these view will have the same base class of for example MyBaseView
• MyBaseView will have a function called setup this function will need to be implemented in each of my derived classes (vegetable and diary)
• depending on your enum i would create one of these concrete classes and call the setup function
Example:
switch (self.category) {
MyBaseView recipe;
case vegetables:
//Create an instance of VegetableView
recipe = [[VegetableView alloc] init];
break;
case dairy:
//Create an instance of DiaryView
recipe = [[VegetableView alloc] init];
break;
}
//Call setup for the created view
[recipe setup];
//Setup function in vegetableView.m
- (void)setup
{
//Do some vegetable setup stuff
}
//Setup function in diaryView.m
- (void)setup
{
//Do some diary setup stuff
}
In this way, i would minimize the different code, i would make the parameter equal for both the types of view
Also adding new views will be rather easy, just subclass MyBaseView and implement a setup function that is specialized for your new view
Hence increase the objects decoupling and reducing complexity
Is there a way to identify which object is calling the draw method.
Creation:
joint.model = [[Box alloc] init];
The calling code:
[joint.model draw];
The draw method (within Box class):
-(void)draw
{
glBindVertexArrayOES(_boxVAO);
glDrawArrays(GL_TRIANGLES, 0, 7055*3);
}
How can i receive the joint object in my draw method?
If more class info is necessary i can attach, but i did not assume since theres not much more.
The model object needs to have a pointer back to joint in order to use it in the -draw method. So you need to either modify the Box class to have a pointer to whatever type joint is, or if Box is defined by a framework you're using, you need to subclass it. So you could do either:
#class Box {
Model* model; // Or whatever type model is.
}
Or if that's not an option, you could do this:
#class BetterBox : Box {
Model* model; // Or whatever type model is.
}
And make sure that model.joint is created like this:
model.joint = [[BetterBox alloc] init]; // or [[Box alloc] init] if you modified the Box class
[model.joint setModel:model];
Then in your draw method, you can simply access model like this:
- (void)draw
{
[model someMethod];
//... etc. ...
}