With MVVM and ReactiveCocoa, how to handle the delegate pattern in iOS? - ios

A common situation is to have a View Controller A, and it has some information which will be sent to View Controller B; and B will edit the information, when B finishes editing the information, B will call the delegate method to update A, and pop itself from the navigation controller.
How to handle this problem with MVVM and ReactiveCocoa?

Heavy use of ReactiveCocoa, in general, will start pushing you away from the delegate pattern. However, since much of the code you've already written and all of the code you'll encounter in the iOS standard libraries use it, being able to interact with it is still important.
You'll want to use the -[NSObject rac_signalForSelector:] category, that will return a signal that receives a RACTuple value of the arguments to a method each time it is invoked, and completes when the object sending the signal is deallocated.
Let's say you have a UIViewController to display that contains a list of checkboxes a user can select, with a continue button at the bottom. Since the selections change over time, you could represent it as an RACSignal of NSIndexSet values. For the purposes of this example, let's say you must use this class as is, and it currently declares a delegate pattern that contains the following:
#class BSSelectionListViewController;
#protocol BSSelectionListViewControllerDelegate <NSObject>
- (void)listChangedSelections:(BSSelectionListViewController*)list;
- (void)listContinueTouched:(BSSelectionListViewController*)list;
#end
When you present the view controller from elsewhere (like a UIViewController at the top of the navigation stack), you'll create the view controller and assign self as the delegate. It might look something like
BSSelectionListViewController* listVC = [[BSSelectionListViewController alloc] initWithQuestion:question listChoices:choices selections:idxSet];
listVC.delegate = self;
[self.navigationController pushViewController:listVC];
Before pushing this UIViewController on the stack, you'll want to create signals for the delegate methods that it could call:
RACSignal* continueTouched = [[[self rac_signalForSelector:#selector(listContinueTouched:)]
takeUntil:list.rac_willDeallocSignal]
filter:^BOOL(RACTuple* vcTuple)
{
return vcTuple.first == listVC;
}];
RACSignal* selections = [[[[self rac_signalForSelector:#selector(listChangedSelections:)]
takeUntil:list.rac_willDeallocSignal]
filter:^BOOL(RACTuple* vcTuple)
{
return vcTuple.first == listVC;
}]
map:^id(RACTuple* vcTuple)
{
return [vcTuple.first selections];
}];
You can then subscribe to these signals to do whatever side effects you need. Maybe something like:
RAC(self, firstChoiceSelected) = [selections map:^id(NSIndexSet* selections)
{
return #([selections containsIndex:0]);
}];
and
#weakify(self)
[continueTouched subscribeNext:^(id x)
{
#strongify(self)
[self.navigationController popToViewController:self];
}];
Because it's possible that you might have several of these screens that you're the delegate of, you want to make sure that you are filtering down to just this one in your RACSignals.
ReactiveCocoa will actually implement these methods (the ones in the delegate protocol) for you. However, to keep the compiler happy, you should add stubs.
- (void)listChangedSelections:(BSSelectionListViewController *)list {}
- (void)listContinueTouched:(BSSelectionListViewController*)list {}
This is, IMO, an improvement over the standard delegate pattern, where you would need to declare an instance variable to hold the selection view controller, and check in the delegate methods which controller is calling you. ReactiveCocoa's rac_signalForSelector method can reduce the scope of that state (this view controller comes and goes over time) in to a local variable instead of an instance variable. It also allows you to be explicit about dealing with the changes to the selections.

Related

How to pass data between views. When should I use what?

I have a View-Hierarchy like this:
UIViewController (SingleEventViewController)
UIScrollView (EventScrollView)
UIView (contentView)
3xUITableView (SurePeopleTV, MaybePeopleTV, NopePeopleTV (all inherited from the same UITableView)), & all other UI-Elements
The SingleEventViewController stores one Event (passed within the initializer). (All Events are stored in Core-Data).
The three UITableViews are there for displaying the users which are participating (or not or maybe) at the Event. My question is, what are the possibilities to fill the tableViews with the data and what would you recommend in which situation.
Currently I have a property parentVC: SingleEventViewController in all Subviews and get the data like this:
override func loadUsers() {
//class SurePeopleTV
guard let parentController = parentVC else { return }
users = (parentController.thisEvent.eventSureParticipants?.allObjects as! [User])
finishedLoading = true
super.loadUsers()
}
.
func applyDefaultValues() {
//class EventScrollView
guard let parent = parentVC else { return }
titleLabel.text = parent.eventName
}
I'm new to programming but I got a feeling that I should not create a parentVC reference in all of my classes.
An object should not (ideally) know about its parent - if it does they are "tightly coupled". If you change the object's parent, your code may break. In your case, your parent object must have a thisEvent property.
You want your objects to be "loosely coupled", so the object doesn't know about a specific parent object.
In Swift, the usual ways to pass information "back up the chain" is to use the delegate design pattern… https://developer.apple.com/documentation/swift/cocoa_design_patterns or to use closures.
See also https://www.andrewcbancroft.com/2015/04/08/how-delegation-works-a-swift-developer-guide/ for info on delegation
First of all, if you create a reference to the parent ViewController make sure it is weak, otherwise you can run into memory management issues.
Edit: As Ashley Mills said, delegates the way to handle this
The recommended way to pass data between ViewControllers is using something like this
Every time a segue is performed from the view controller this function is in this function is called. This code first checks what identifier the segue has, and if it is the one that you want, you can access a reference to the next view controller and pass data to it.

Swift, how to tell a controller that another controller is its delegate

I'm learning Swift and I'm studying the delegation pattern.
I think I understand exactly what is delegation and how it works, but I have a question.
I have a situation where Controller A is the delegate for Controller B.
In controller B I define a delegate protocol.
In controller B I set a variable delegate (optional)
In controller B I send message when something happens to the delegate
Controller A must adopt method of my protocol to become a delegate
I cannot understand if every delegate controller (in this case A) listens for messages sent by controller B or If I have to tell to controller B that A is now his delegate.
I notice that someone use this code (in controller A)
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "Example" {
let navigationController = segue.destinationViewController as UINavigationController
let controller = navigationController.topViewController as AddItemViewController
controller.delegate = self
}
}
Is this the only way to tell a delegator who is his delegate?
I believe, you need to tell a deligator who is its delegate upon creation of that it. Now, the delegator can be created programatically or through storyboard. So, based on that you have two options, you can tell it who is its delegator programatically like you showed in the code or from IB.
The key here is upon creation. Let's me explain myself. Take the case of a UIView. Say, you want a Custom UIView object(CustomView). So, you drag and drop a UIView in your View Controller and in the identity inspector, you assign its class as of your CustomView's class. So, basically, as soon as the controller is created, your custom view will also be created. Now, you can either say it that the View Controller in which it is created is its delegate or You can go to the IB and connect the view's delegate to the View Controller.
Now, let's assume that you wanted the custom view to be created in your ViewController programatically. In that case, you would probably call the -initWithFrame: method to create the view and upon creation you tell that delegator that who is its delegate like-
myCustomView.delegate = self;
same goes with a View Controller.
controller.delegate = self;
So, basically to tell a delegator who is its delegate, you first need that delegator to be created. At least, that's what I think.
I think one of the best example of delegation is UITableView.
Whenever you want the control of various properties of a tableView e.g. rowHeight etc, you set your controller to be the delegate of your tableview. To set the delegate of your tableView you need to have tableView created obviously as pointed out by #natasha.
So in your case, you can set delegate of your delegator when you create it or when you find a need for the controller to be delegate of your delegator but you definitely need your delegator to be present to set its property.
You can set your controller as delegate at any time when you require control.
I'm sure you want your UIViewController to act like described, but here is a simpler example how to use the delegation pattern with custom classes:
protocol ControllerBDelegate: class {
func somethingHappendInControllerB(value: String)
/* not optional here and passes a value from B to A*/
/* forces you to implement the function */
}
class ControllerB {
var delegate: ControllerBDelegate?
private func someFunctionThatDoSomethingWhenThisControllerIsAlive() {
/* did some magic here and now I want to tell it to my delegate */
self.delegate?.somethingHappendInControllerB(value: "hey there, I'm a magician")
}
func doSomething() {
/* do something here */
self.someFunctionThatDoSomethingWhenThisControllerIsAlive()
/* call the function so the magic can really happen in this example */
}
}
class ControllerA: ControllerBDelegate {
let controllerB = ControllerB()
init() {
self.controllerB.delegate = self /* lets say we add here our delegate*/
self.controllerB.doSomething() /* tell your controller B to do something */
}
func somethingHappendInControllerB(value: String) {
print(value) /* should print "hey there, I'm a magician" */
}
}
I wrote the code from my mind and not testet it yet, but you should get the idea how to use such a pattern.

Get topmost view controller in stack without looping through

I can't loop through my view controllers because I need to only call one method if the current view controller is one in particular. If I loop through, then all methods will get called. I've been using this code:
if let viewControllers = navigationController?.viewControllers {
for viewController in viewControllers {
// some process
if viewController.isKindOfClass(MyViewController) {
println("\(viewController) yes it is")
} else {
self.navigationController?.popViewControllerAnimated(true)
}
}
}
Essentially, I don't want to popViewControllerAnimated if the current view is on a particular view controller. However because of the loop, it's getting called anyway. How can I just return the current view controller without making this loop?
You can get first and last with following Obj-C methods:
[self.navigationController.viewControllers lastObject];
[self.navigationController.viewControllers firstObject];
the methods are not available in Swift but easily doable by extending Array so that you can reuse it in the project.
extension Array {
var lastObject: T {
return self[self.endIndex - 1]
}
}
So since I needed to see if the current, or top most view controller matched a particular view controller, I converted self.navigationController?.visibleViewController to a string and checked if .rangeofString() contained the name of the view controller I was looking for. Worked great.
Update: Although this method worked for me, #Gandalf's suggestion below is probably a much safer bet. That works for me too.

Objective-C category calling method of a custom class

I have some classes, lets call them A, B and C, being inherited from UIViewController and some category of UIViewController that has some methods in it that have to be common for all the classes.
Category's methods have to call another methods on ViewControllers, but. Not all methods will be implemented in VCs (because they never be called in that context).
If I write method definition in category, I get an error, that method is not declared, but it is declared in VC or not declared and will never be used in that context.
Is there any ideas how to override this and share huge part of code (Navigation code) between multiple ViewControllers?
- (void)homeButtonPressed {
NSLog(#"Home button pressed, ViewController");
}
- (void)changeFloorButtonPressed {
[self changeFloor];
NSLog(#"Change Floor button pressed, ViewController");
}
- (void)QRButtonPressed {
NSLog(#"QR button pressed, ViewController");
}
Here [self changeFloor]; is method, which is specific only to one ViewController.
Why note create your base class of type UIViewController, then create classes that inherit from your new base class. Each child can then implement the methods they need.
MyBaseViewController: UIViewController
ViewAViewController: MyBaseViewController
ViewBViewController: MyBaseViewController
etc
I use this type of approach for universal apps, my child class can override the implementation from the parent or add to it.
// common elements
HomeViewController: UIViewController
// iPhone specific implementation
HomeViewController_iPhone : HomeViewController
// iPhone specific implementation
HomeViewController_iPad: HomeViewController
Create an objective-c protocol which declares an #optional method for -(void)changeFloor;
Then, in your category, conform to that protocol. You will be able to call the changeFloor method without getting a compile error.
Because the method is optional, you must check if:
[self respondsToSelector:#selector(changeFloor)] before calling changeFloor.
edit: there is probably a way to architect your code so that you don't need to do this.

Three20 URL routing with objects as parameters

We just added Three20 to our existing project and are having some trouble to get along with its routing system.
What we have right now is a transition from a view controller A to a view controller B, having A as the delegate of view controller B, to allow me to trigger an action on A when a specific action occurs on B.
Let's say I have a button on A that calls B like the following:
- (IBAction)buttonAction:(id)sender {
id bvc = [[[BViewController alloc] initWithDelegate:self] autorelease];
[self.navigationController pushViewController:bvc animated:YES];
}
Now, instead of the button, we want to use a link in a TTStyledText for the same purpose. The problem is we don't know how to pass the A view controller instance to B's initWithDelegate: method. We discovered how to pass strings as parameters for the TTURLMap, but that won't work for us.
Maybe we need a more thorough design change here. Any thoughts?
Here are 2 options:
you set all your object in a kind an object container and put it in a global cache where you can get an id for that container which you encode into a string in the url. This would add a bit of overhead in managing the lifecycle of these object :(
There is another useful way to invoke three 20 controller using urls which involves a bit different way of coupling the paramters:
Your target controller would have another init method like this:
-(id)initWithNavigatorURL:(NSURL *)URL query:(NSDictionary *)query {
self = [super initWithNavigatorURL:URL query:query];
if (self) {
self.parameter = [query objectForKey:#"[YOUR PARAMETER NAME]"];
}
return self;
}
you invoke it by calling the controller like this:
TTURLAction *action = [[[TTURLAction actionWithURLPath:#"yourapp://yourViewController"]
applyAnimated:YES]
applyQuery:[NSDictionary dictionaryWithObject:[YOUR OBJECT] forKey:#"[YOUR PARAMETER NAME]"]];
[[TTNavigator navigator] openURLAction:action];
Now all you are left with is mapping the links inside the label to somehow invoke that fancy TTURLAction.
You do this by changing the TTURLMap in your controller that hosts the styled label.
three20 has the following way to add a mapping to a specific class and selector:
- (void)from:(NSString*)URL toObject:(id)object selector:(SEL)selector;
so in that view controller that hosts the label add this:
TTNavigator* navigator = [TTNavigator navigator];
TTURLMap* map = navigator.URLMap;
[map from:#"yourapp://someaction" toObject:self selector:#selector(userpressedlink)];
inside that userpressedlink method call the fancy TTURLAction
A few thing to remember:
you need to remove that mapping ( I suggest viewWillAppear to add the mapping and viewWillDisappear to remove them)
each link should have it's own mapping so you can distinguish the links and map them to differen selectors.

Resources