33

When modally presenting, or pushing, an interface controller we can specify the context parameter to pass some data to the new controller as follows.

// Push
[self pushControllerWithName:@"MyController" context:[NSDictionary dictionaryWithObjectsAndKeys:someObject, @"someKey", ..., nil]]; 

// Modal
[self presentControllerWithName:@"MyController" context:[NSDictionary dictionaryWithObjectsAndKeys:someObject, @"someKey", ..., nil]]; 

My question is, how can we do the reverse?

Say we present a controller modally for the user to pick an item from a list and we return to the main controller, how can we get the item that has been picked?

progrmr
  • 75,956
  • 16
  • 112
  • 147
BytesGuy
  • 4,097
  • 6
  • 36
  • 55
  • 2
    Did you try using a delegate? -> Create a protocol that your BackController implements. So you will be able to call a function from that protocol passing your values in your ModalController. – Pintouch Nov 19 '14 at 16:21

6 Answers6

31

I wrote a full example that uses Delegation in WatchKit, passing the delegate instance in the context, and calling delegate function from the modal : Here is the full project example on GitHub

Here is the principale classes of the example :

InterfaceController.swift

This is the main Controller, there are a label and a button on his view. When you press the button, the presentItemChooser get called and it present the ModalView (ModalInterfaceController). I pass the instance of InterfaceController in the context to the modal. Important this controller implements `ModalItemChooserDelegate' functions (the protocol definition is in the modal file)

class InterfaceController: WKInterfaceController, ModalItemChooserDelegate {

    @IBOutlet weak var itemSelected: WKInterfaceLabel!
    var item = "No Item"

    override func awakeWithContext(context: AnyObject?) {
        super.awakeWithContext(context)

        // Configure interface objects here.

    }

    override func willActivate() {
        // This method is called when watch view controller is about to be visible to user
        itemSelected.setText(item)
        super.willActivate()

    }

    override func didDeactivate() {
        // This method is called when watch view controller is no longer visible
        super.didDeactivate()
    }

    func didSelectItem(itemSelected: String) {
        self.item = itemSelected
    }

    @IBAction func presentItemChooser() {

        self.presentControllerWithName("ModalInterfaceController", context: self)

    }
}

ModalInterfaceController.swift

This is the class of my modal controller. I hold the reference of my previous controller (self.delegate = context as? InterfaceController). When a row is selected, I call my delegate function didSelectItem(selectedItem) before dismissing it.

protocol ModalItemChooserDelegate {
        func didSelectItem(itemSelected:String)
    }

    class ModalInterfaceController: WKInterfaceController {

        let rowId = "CustomTableRowController"

        let items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5"]

        var delegate: InterfaceController?

        @IBOutlet weak var customTable: WKInterfaceTable!

        override func awakeWithContext(context: AnyObject?) {
            super.awakeWithContext(context)
            self.delegate = context as? InterfaceController
            // Configure interface objects here.
            println(delegate)
            loadTableData()
        }

        override func willActivate() {
            // This method is called when watch view controller is about to be visible to user

            super.willActivate()
        }

        override func didDeactivate() {
            // This method is called when watch view controller is no longer visible
            super.didDeactivate()
        }

        private func loadTableData(){
            customTable.setNumberOfRows(items.count, withRowType: rowId)
            for(i, itemName) in enumerate(items){
                let row = customTable.rowControllerAtIndex(i) as! TableRowController
                row.fillRow(itemName)

            }

        }

        override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
            let selectedItem = items[rowIndex]
            self.delegate?.didSelectItem(selectedItem)
            self.dismissController()
        }


    }

This is how I pass data back to my previous Controller. If is a better way let me know, I'll take it. :)

Pintouch
  • 2,630
  • 1
  • 15
  • 22
  • 5
    Thanks for the answer Pintouch. It would be helpful to have some code in case others stumble upon the question when working with WatchKit. – BytesGuy Nov 19 '14 at 21:20
  • 3
    Since WatchKit sends a context object when pushing or using segues, unlike iOS on iPhone or iPad where there is prepareForSegue in which the actual presented VC is available, how does one actually set the presented view controller's delegate to the presenting view controller? In the OP's question, there is no presented view controller object available to the presenting view controller and I haven't found an suitable method in the documentation. – ghr Nov 23 '14 at 12:24
  • The link is for iPhone but the question is about WatchKit. – TruMan1 Apr 27 '15 at 04:53
  • 1
    Apple uses the same patterns for iOS and for Watchkit. So considering the date when I post my answer, I put iPhone doc to show the delegation pattern example, because Watchkit docs weren't complete about it when I answer it. – Pintouch Apr 27 '15 at 05:58
  • The issue is - specifically for watchkit - how do you intercept the tapping of the upper left corner [close button] of the modal view? – dchappelle May 07 '15 at 23:02
  • @dchappelle Why do you need to intercept a close user action on a modal view ? – Pintouch May 13 '15 at 10:14
  • @Pintouch While you are not actually writing it in your code and you are just quoting the Apple Docs (which are wrong here because you are linking to the iOS, not WatchKit documentation) you are saying something dangerous in the text of your answer: You should NOT make the presenting interface controller dismiss the modal dialog. There is no way in WatchKit to remove a presented interface controller it has to remove itself. Dismissing the presenting interface controller does just that, if it's a root interface controller it will end up in some unconnected state and no longer update correctly. – coolio May 26 '15 at 15:46
  • @coolio I will remove the iOs reference doc, it was the first answer version when WatchKit docs was not yet full released. I'm not dismissing the modal dialog in the presenting interface controller, I'm dismissing it from the modal dialog controller itself : `self.dismissController()` in the function `func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int)`, maybe I misunderstood your comment? – Pintouch May 26 '15 at 16:02
  • @Pintouch, no, as I wrote, your code is OK. You just have that quote in your answer and it's pretty much the first thing one reads in your answer: "When it comes time to dismiss a presented view controller, the preferred approach is to let the presenting view controller dismiss it." This is wrong. You do it right in the code but if one just reads the text of your answer it's misleading. You should remove that quote. I had some trouble getting a stack of ICs dismissed simultaneously and tried it and started to have difficult-to-diagnose issues, so people should NOT dismiss the parent here. – coolio May 27 '15 at 00:40
  • 2
    Should the delegate be weak? Maybe your could may cause retain cycle? – Hossam Ghareeb Jun 28 '15 at 08:35
  • Cool. Thank you very much. Nicely explained everything. Good job – Abdul Yasin Dec 21 '17 at 06:52
  • Awesome, thanks, this works well. Maybe 2 small improvements to consider: 1. Make the delegate variable weak (to avoid potential retain cycle). 2. Consider to make the type of the delegate variable ```ModalItemChooserDelegate``` (so the delegate can be any object that implements the ```ModalItemChooserDelegate``` protocol) – Bocaxica Sep 12 '18 at 09:26
  • Protocol is best choice for one-to-one viewController, Now if you want send data for multiples InterfaceController, you should use NotificationCenter. – Pablo Ruan Mar 07 '19 at 18:51
13

You can transfer back information via Protocols by passing self within the context:

InterfaceController.m

// don't forget to conform to the protocol!
@interface InterfaceController() <PictureSelectionControllerDelegate>

//...

// in some method
[self pushControllerWithName:@"PictureSelectionController" 
                     context:@{@"delegate" : self}];

And setting the delegate like so:

PictureSelectionController.m

@property (nonatomic, unsafe_unretained) id<PictureSelectionControllerDelegate> delegate;

// ...

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];

    // Configure interface objects here.
    if ([context isKindOfClass:[NSDictionary class]]) {
        self.delegate = [context objectForKey:@"delegate"];
    }
}

Don't forget to declare your protocol:

PictureSelectionController.h

@protocol PictureSelectionControllerDelegate <NSObject>

- (void)selectedPicture:(UIImage *)picture;

@end

Then you can call that method from PictureSelectionController.m:

- (IBAction)buttonTapped {
    // get image
    UIImage *someCrazyKatPicture = //...
    [self.delegate seletedPicture:someCrazyKatPicture];
}

And receive it in the delegate method within InterfaceController.m:

- (void)selectedPicture:(UIImage *)picture {
    NSLog(@"Got me a cat picture! %@", picture);
}
Stunner
  • 12,025
  • 12
  • 86
  • 145
2

As ghr says, this requires a bit more explanation. The easy (if hacky) way is to make the presenting controller be part of the context that you are passing into the presented controller. That way, you can call back to the presenting controller when you need to. One way to do this is to use an NSDictionary as your context, and store a special key with a reference to the presenting controller. Hope this helps.

Adam
  • 76
  • 4
1

I've been testing passing self to the controllers (modal or not) and using didDeactivate as a way to invoke the delegate methods, but the catch is that it's called whenever the screen is dismissed or when a new view is presented. I'm just getting started with WatchKit so I could be totally wrong here.

My delegate

@class Item;
@class ItemController;
@protocol AddItemDelegate <NSObject>
- (void)didAddItem:(ItemController *)controller withItem:(Item *)item;

My root controller

@interface ListController() <AddItemDelegate>
...
- (void)table:(WKInterfaceTable *)table didSelectRowAtIndex:(NSInteger)rowIndex {
    // TODO: How do we pass data back? Delegates? Something else?
    if ([self.items[rowIndex] isEqualToString:@"Item 1"]) {
        // TODO: Do I really want to pass along a single object here?
        [self pushControllerWithName:@"Item" context:self];
    }
}
...
#pragma mark - AddItemDelegate
- (void)didAddItem:(ItemController *)controller withItem:(Item *)item {
    NSLog(@"didAddItem:withItem: delegate called.");
}

My child controller

@property (nonatomic, strong) Item *item;
@property (nonatomic, weak) id<AddItemDelegate> delegate;
...
- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];

    // TODO: Check that this conforms to the protocol first.
    self.delegate = context;
}
...
- (void)didDeactivate {
    [super didDeactivate];

    [self.delegate didAddItem:self withItem:self.item];
}
Mattio
  • 2,014
  • 3
  • 24
  • 37
1

Passing data back from watchOS interfaceController using block and segue

Passing data back and forth between interfaceControllers is not so simple. There is segue process in WatchKit but the first problem is that there is no prepareForSegue and you couldn't reach segue's destinationViewController, so you couldn't inject stuffs easily to the new controller (WatchOS 3 - 4). In the backward direction there is no exit so you couldn't reach the unwind segue.

Another problem is that these solutions try to update the data and the user interface of the of the first interfaceController in the willActivate method which is fired any time the watch screen awake - so quite frequently - and this could cause problems and it's complicate.

The programming practice is mainly using delegate and injecting self using the context of the segue, as the above answers describe.

But using delegate is a little bit complicate so I use blocks which is more contemporary and I think better and more elegant.

Let's see how:

First let's prepare the segue in the Interface Builder of the Apple Watch's storyboard, just connect a button with another interfaceController pushing Ctrl button and name the segue.

InterfaceBuilder for Apple Watch storyboard

then in the .h file of the source interfaceController lets's name it SourceInterfaceController.h declare a property for the block:

@property (nonatomic, strong) BOOL (^initNewSessionBlock)(NSDictionary *realTimeDict, NSError *error);

then use contextForSegueWithIdentifier: to transfer the block or any other data to the destination interfaceController using the segueIdentifier if you have more segues.

This Apple method actually use a (id)context as a return object which could be any object and the destination interfaceController's awakeWithContext:(id)context method will use it when the the interfaceController launches.

So let's declare the block in SourceInterfaceController.m then pass it to the context:

- (id)contextForSegueWithIdentifier:(NSString *)segueIdentifier {

    __unsafe_unretained typeof(self) weakSelf = self;

    if ([segueIdentifier isEqualToString:@"MySegue"]) {

        self.initNewSessionBlock =  ^BOOL (NSDictionary *mySegueDict, NSError *error)
        {
            [weakSelf initNewSession];
            NSLog(@"message from destination IC: %@", realTimeDict[@"messageBack"]);
            return YES;
        };

        return self.initNewSessionBlock;
    }
    else if ([segueIdentifier isEqualToString:@"MyOtherSegue"]) {

        self.otherBlock =  ^BOOL (NSString *myText, NSError *error)
        {
            //Do what you like
            return YES;
        };

        return self.otherBlock;

    }
    else {
        return nil;
    }

}

If you'd like to transfer any more data than just the block with the context to the destination interfaceController, just wrap them in a NSDictionary.

In the destination interfaceController name it DestinationInterfaceController.h let's declare another property to store the block using any name but the same variable declaration

@property (copy) BOOL (^initNewSessionBlock)(NSDictionary *realTimeDict, NSError *error);

then fetch the block from the context in DestinationInterfaceController.m:

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];

    self.initNewSessionBlock = context;
}

Later in DestinationInterfaceController.m just trigger the block, for example in an action method with a button:

- (IBAction)initNewSessionAction:(id)sender {

    NSError *error = nil;
    NSDictionary *realTimeDict = @{@"messageBack" : @"Greetings from the destination interfaceController"};

    BOOL success = self.initNewSessionBlock(realTimeDict, error);
    if (success) {
        [self popController];
    }

}

The block will be executed any method of the source interfaceController using the data in the scope of the destination interfaceController, so you can send data back to the destination sourceController. You can pop the interfaceController using popController if everything is ok and the block return yes as a BOOL.

Note: Of course you can use any kind of segue whether it's a push or modal and you can use pushControllerWithName:context: too to trigger the segue, and you can use this method's context in the same way.

BootMaker
  • 1,639
  • 1
  • 22
  • 24
-4

maybe there is some other ways but i prefer to use pushControllerWithName: method.

Root controller:

- (IBAction)GoToChildControllerButton {
    [self pushControllerWithName:@"TableInterfaceController" context:@"pass some data to child controller here..."];
}

Child controller:

- (IBAction)BackToRootControllerButton {
    [self pushControllerWithName:@"TableInterfaceController" context:@"pass some data back to root controller here..."];
}
jibz
  • 1