Передача данных обратно из модального представления в WatchKit

При модальном представлении или отправке контроллера интерфейса мы можем указать параметр context для передачи некоторых данных новому контроллеру следующим образом.

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

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

Мой вопрос в том, как мы можем сделать обратное?

Скажем, мы представляем контроллер модально для пользователя, чтобы он мог выбрать элемент из списка, и мы возвращаемся к основному контроллеру, как мы можем получить выбранный элемент?


person BytesGuy    schedule 19.11.2014    source источник
comment
Вы пытались использовать делегата? -> Создайте протокол, который реализует ваш BackController. Таким образом, вы сможете вызывать функцию из этого протокола, передавая ваши значения в ваш ModalController.   -  person Pintouch    schedule 19.11.2014


Ответы (6)


Я написал полный пример, который использует делегирование в WatchKit, передает экземпляр делегата в контексте и вызывает функцию делегата из модального окна: Вот полный пример проекта на GitHub

Вот основные классы примера:

InterfaceController.swift

Это основной Контроллер, на его просмотре есть метка и кнопка. Когда вы нажимаете кнопку, вызывается presentItemChooser, и он представляет ModalView (ModalInterfaceController). Я передаю экземпляр InterfaceController в контексте модального окна. Важно, что этот контроллер реализует функции ModalItemChooserDelegate (определение протокола находится в модальном файле).

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

Это класс моего модального контроллера. Я храню ссылку на мой предыдущий контроллер (self.delegate = context as? InterfaceController). Когда строка выбрана, я вызываю функцию делегата didSelectItem(selectedItem), прежде чем закрыть ее.

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()
        }


    }

Вот как я передаю данные моему предыдущему контроллеру. Если это лучший способ, дайте мне знать, я возьму его. :)

person Pintouch    schedule 19.11.2014
comment
Спасибо за ответ Пинтач. Было бы полезно иметь некоторый код на случай, если другие наткнутся на вопрос при работе с WatchKit. - person BytesGuy; 20.11.2014
comment
Поскольку WatchKit отправляет объект контекста при отправке или использовании переходов, в отличие от iOS на iPhone или iPad, где есть prepareForSegue, в которой доступен реальный представленный VC, как на самом деле установить делегата представленного контроллера представления на представляющий контроллер представления? В вопросе OP нет представленного объекта контроллера представления, доступного для представляющего контроллера представления, и я не нашел подходящего метода в документации. - person ghr; 23.11.2014
comment
Ссылка для айфона, но вопрос про WatchKit. - person TruMan1; 27.04.2015
comment
Apple использует одни и те же шаблоны для iOS и Watchkit. Поэтому, учитывая дату, когда я публикую свой ответ, я поместил документ iPhone, чтобы показать пример шаблона делегирования, потому что документы Watchkit не были полными, когда я отвечал на него. - person Pintouch; 27.04.2015
comment
Проблема в том, специально для watchkit, как вы перехватываете нажатие на верхний левый угол [кнопка закрытия] модального представления? - person dchappelle; 08.05.2015
comment
@dchappelle Зачем вам нужно перехватывать действия пользователя в модальном режиме? - person Pintouch; 13.05.2015
comment
@Pintouch Хотя вы на самом деле не пишете это в своем коде, а просто цитируете Apple Docs (что здесь неверно, потому что вы ссылаетесь на iOS, а не на документацию WatchKit), вы говорите что-то опасное в тексте своего ответа: вы НЕ ДОЛЖЕН заставлять контроллер интерфейса представления закрывать модальный диалог. В WatchKit нет способа удалить представленный интерфейсный контроллер, который он должен удалить сам. Отказ от представления контроллера интерфейса делает именно это, если это корневой контроллер интерфейса, он окажется в каком-то неподключенном состоянии и больше не будет корректно обновляться. - person coolio; 26.05.2015
comment
@coolio Я удалю справочный документ по iOs, это была первая версия ответа, когда документы WatchKit еще не были полностью выпущены. Я не отклоняю модальный диалог в представленном контроллере интерфейса, я отклоняю его от самого контроллера модального диалога: self.dismissController() в функции func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int), может быть, я неправильно понял ваш комментарий? - person Pintouch; 26.05.2015
comment
@Pintouch, нет, как я уже писал, твой код в порядке. У вас просто есть эта цитата в вашем ответе, и это почти первое, что вы читаете в своем ответе: когда приходит время отклонить представленный контроллер представления, предпочтительный подход - позволить представляемому контроллеру представления отклонить его. Это не правильно. Вы делаете это прямо в коде, но если кто-то просто читает текст вашего ответа, это вводит в заблуждение. Вы должны удалить эту цитату. У меня были некоторые проблемы с одновременным отклонением стека IC, и я попробовал это, и у меня начались проблемы, которые трудно диагностировать, поэтому людям НЕ следует отклонять родителя здесь. - person coolio; 27.05.2015
comment
Должен ли делегат быть слабым? Может быть, это может привести к сохранению цикла? - person Hossam Ghareeb; 28.06.2015
comment
Прохладно. Большое тебе спасибо. Классно все объяснил. Отличная работа - person Abdul Yasin; 21.12.2017
comment
Круто, спасибо, это работает хорошо. Возможно, следует рассмотреть 2 небольших улучшения: 1. Сделать переменную делегата слабой (чтобы избежать потенциального цикла удержания). 2. Рассмотрите возможность сделать тип делегата переменной ModalItemChooserDelegate (чтобы делегатом мог быть любой объект, реализующий протокол ModalItemChooserDelegate) - person Bocaxica; 12.09.2018
comment
Протокол — лучший выбор для один-к-одному viewController. Теперь, если вы хотите отправлять данные для нескольких интерфейсных контроллеров, вы должны использовать NotificationCenter. - person Pablo Ruan; 07.03.2019

Вы можете передать информацию обратно через протоколы, передав self в контексте:

InterfaceController.m

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

//...

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

И установка делегата так:

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"];
    }
}

Не забудьте объявить свой протокол:

PictureSelectionController.h

@protocol PictureSelectionControllerDelegate <NSObject>

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

@end

Затем вы можете вызвать этот метод из PictureSelectionController.m:

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

И получить его в методе делегата в InterfaceController.m:

- (void)selectedPicture:(UIImage *)picture {
    NSLog(@"Got me a cat picture! %@", picture);
}
person Stunner    schedule 24.03.2015
comment
Вы сэкономили так много моего времени - person Soheil; 22.08.2015

Как говорит ghr, это требует немного больше пояснений. Самый простой (хотя и хакерский) способ — сделать представляющий контроллер частью контекста, который вы передаете в представленный контроллер. Таким образом, вы можете перезвонить представившему контроллеру, когда вам это нужно. Один из способов сделать это — использовать NSDictionary в качестве контекста и сохранить специальный ключ со ссылкой на представляющий контроллер. Надеюсь это поможет.

person Adam    schedule 24.11.2014

Я тестировал передачу self контроллерам (модальным или нет) и использование didDeactivate как способ вызова методов делегата, но загвоздка в том, что он вызывается всякий раз, когда экран закрывается или когда представляется новое представление. Я только начинаю работать с WatchKit, поэтому могу ошибаться.

Мой делегат

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

Мой корневой контроллер

@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.");
}

Мой дочерний контроллер

@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];
}
person Mattio    schedule 20.03.2015

Передача данных из interfaceController watchOS с помощью блока и перехода

Передача данных туда и обратно между интерфейсными контроллерами не так проста. В WatchKit есть процесс перехода, но первая проблема заключается в том, что нет prepareForSegue, и вы не можете получить доступ к targetViewController перехода, поэтому вы не можете легко вводить данные в новый контроллер (WatchOS 3-4). В обратном направлении выхода нет, поэтому вы не можете добраться до разматывающего перехода.

Еще одна проблема заключается в том, что эти решения пытаются обновить данные и пользовательский интерфейс первого interfaceController в методе willActivate, который запускается каждый раз, когда просыпается экран часов — довольно часто — и это может вызвать проблемы и усложнить.

Практика программирования в основном использует делегата и внедряет self с использованием контекста перехода, как описано в приведенных выше ответах.

Но использование делегата немного сложнее, поэтому я использую блоки, которые более современны и, как мне кажется, лучше и элегантнее.

Давайте посмотрим, как это сделать:

Сначала давайте подготовим переход в конструкторе интерфейсов раскадровки Apple Watch, просто соедините кнопку с другим интерфейсным контроллером, нажав кнопку Ctrl, и назовите переход.

InterfaceBuilder для раскадровки Apple Watch

затем в файле .h исходного interfaceController давайте назовем его SourceInterfaceController.h и объявим свойство для блока:

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

затем используйте contextForSegueWithIdentifier:, чтобы передать блок или любые другие данные в интерфейсный контроллер назначения, используя segueIdentifier, если у вас больше переходов.

Этот метод Apple фактически использует контекст (id) в качестве возвращаемого объекта, который может быть любым объектом, и метод awakeWithContext:(id)context целевого интерфейсаController будет использовать его при запуске interfaceController.

Итак, давайте объявим блок в SourceInterfaceController.m, а затем передадим его в контекст:

- (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;
    }

}

Если вы хотите передать больше данных, чем просто блок с контекстом, в интерфейсный контроллер назначения, просто оберните их в NSDictionary.

В целевом интерфейсном контроллере с именем DestinationInterfaceController.h давайте объявим другое свойство для хранения блока, используя любое имя, но то же объявление переменной

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

затем извлеките блок из контекста в DestinationInterfaceController.m:

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

    self.initNewSessionBlock = context;
}

Позже в DestinationInterfaceController.m просто активируйте блок, например, в методе действия с кнопкой:

- (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];
    }

}

Блок будет выполняться любым методом исходного интерфейсного контроллера, используя данные в области действия целевого интерфейсного контроллера, поэтому вы можете отправить данные обратно в целевой исходный контроллер. Вы можете вытолкнуть интерфейсный контроллер с помощью popController, если все в порядке и блок возвращает yes как BOOL.

Примечание. Конечно, вы можете использовать любой переход, будь то push или modal, а также вы можете использовать pushControllerWithName:context: для запуска segue, и вы можете использовать контекст этого метода таким же образом.

person BootMaker    schedule 26.09.2017

возможно, есть какие-то другие способы, но я предпочитаю использовать метод pushControllerWithName:.

Корневой контроллер:

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

Дочерний контроллер:

- (IBAction)BackToRootControllerButton {
    [self pushControllerWithName:@"TableInterfaceController" context:@"pass some data back to root controller here..."];
}
person jibz    schedule 12.05.2015
comment
Это не отвечает на вопрос и фактически создает большой недостаток памяти. OP прямо заявляет, что он может передавать данные вперед, но не назад. - person Schemetrical; 12.05.2015
comment
Это создает еще один контроллер - person Amrut; 29.01.2016