При чтении различных исходников сталкиваюсь с проектами, реализованными с использованием xib файлов. Мне самому больше нравится использование xib, вместо storyboard (не холивара ради пишу, storyboard тоже хорош), однако часто изучение навигации между экранами превращается в пытку. И поэтому хотелось бы поделится собственным опытом.



Чем так хорош storyboard? В первую очередь тем, что он позволяет собрать всю навигацию и отобразить визуально большинство переходов.
Да, используя xib для каждого экрана мы лишаемся возможности визуально увидеть все переходы (ну и еще пары возможностей), однако мы получаем немного своих плюсов. Я не стану явно описывать плюсы и минусы использования одного и другого во избежании холивара, только лишь покажу как можно собрать всю навигацию, используя xib файлы, избавиться от лишнего использования singleton'ов, а так же как устранить связность между контроллерами.

Подход очень простой. Используем Router объекты для связи между экранами. Разделяем Router на пользовательские истории. Взаимодействуем, используя callback.

Мини демонстрация на практике


  • Экран с таблицей и кнопкой добавления записи
  • Экран создания записи
  • Экран детального просмотра записи

Первоначальная настройка


Создадим роутер и отобразим пустой экран
RXAppDelegate.m
#import "RXAppDelegate.h"
#import "RXRouter.h"


@implementation RXAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.rootViewController = [[RXRouter alloc] initRouter];
    [self.window makeKeyAndVisible];
    
    return YES;
}

@end
RXRouter.h
#import <UIKit/UIKit.h>


@interface RXRouter : UINavigationController

- (instancetype)initRouter;

@end
RXRouter.m
#import "RXRouter.h"


@implementation RXRouter

- (instancetype)initRouter {
    UIViewController *rootViewController = [self createRootViewController];
    self = [super initWithRootViewController:rootViewController];
    if (self != nil) {
        self.interactivePopGestureRecognizer.enabled = NO;
    }
    return self;
}

- (UIViewController *)createRootViewController {
    UIViewController *controller = [[UIViewController alloc] init];
    return controller;
}

@end

Реализация


Реализуем создание контроллера, который будет показывать записи. Так же сразу свяжем этот экран с другими экранами и взаимодействие между ними.
RXRoute.m
#import "RXRouter.h"
#import "RXNoteListViewController.h"
#import "RXCreateNoteViewController.h"
#import "RXDetailNoteViewController.h"


@implementation RXRouter

- (instancetype)initRouter {
    UIViewController *rootViewController = [self createRootViewController];
    self = [super initWithRootViewController:rootViewController];
    if (self != nil) {
        self.interactivePopGestureRecognizer.enabled = NO;
    }
    return self;
}

- (UIViewController *)createRootViewController {
    RXNoteListViewController *noteListController = [[RXNoteListViewController alloc] init];
    __weak RXRouter *weakSelf = self;
    __weak RXNoteListViewController *weakNoteListController = noteListController;
    noteListController.createNoteBlock = ^{
        RXCreateNoteViewController *createNoteViewController = [weakSelf createNoteViewController];
        createNoteViewController.createNoteBlock = ^(RXNote *note){
            [weakNoteListController addNote:note];
            [weakSelf popViewControllerAnimated:YES];
        };
        [weakSelf pushViewController:createNoteViewController animated:YES];
    };
    noteListController.detailNoteBlock = ^(RXNote *note){
        RXDetailNoteViewController *detailNoteViewController = [weakSelf createDetailNoteViewControllerWithNote:note];
        [weakSelf pushViewController:detailNoteViewController animated:YES];
    };
    return noteListController;
}

- (RXCreateNoteViewController *)createNoteViewController {
    return [[RXCreateNoteViewController alloc] init];
}

- (RXDetailNoteViewController *)createDetailNoteViewControllerWithNote:(RXNote *)note {
    RXDetailNoteViewController *controller = [[RXDetailNoteViewController alloc] init];
    [controller showNote:note];
    return controller;
}

@end
RXNoteListViewController.h
#import <UIKit/UIKit.h>


@class RXNote;

typedef void (^RXNoteListViewControllerCreateNoteBlock)();
typedef void (^RXNoteListViewControllerDetailNoteBlock)(RXNote *note);


@interface RXNoteListViewController : UIViewController

@property (copy, nonatomic) RXNoteListViewControllerCreateNoteBlock createNoteBlock;
@property (copy, nonatomic) RXNoteListViewControllerDetailNoteBlock detailNoteBlock;

- (void)addNote:(RXNote *)note;

@end
RXCreateNoteViewController.h
#import <UIKit/UIKit.h>


@class RXNote;
typedef void (^RXCreateNoteViewControllerCreateNoteBlock)(RXNote *note);


@interface RXCreateNoteViewController : UIViewController

@property (copy, nonatomic) RXCreateNoteViewControllerCreateNoteBlock createNoteBlock;

@end
RXDetailNoteViewController.h
#import <UIKit/UIKit.h>


@class RXNote;
typedef void (^RXDetailNoteViewControllerDoneBlock)();


@interface RXDetailNoteViewController : UIViewController

- (void)showNote:(RXNote *)note;

@end

Таким образом мы сразу видим навигацию между экранами, а каждый экран ничего не знает о других экранах. Более того, используя блоки, удалось устранить необходимость информации экрана о роутере.
Так же мы можем без проблем передавать данные из экрана в экран и уменьшить использование singleton'ов.

Ссылка на гитхаб

Комментарии (8)


  1. itruf
    30.08.2015 19:34

    Эм… Если честно, то немного не понял какую проблему вы решили. Чем вам не понравился UINavigationController, который для этого вполне себе ок?

    И зачем вам было нужно делать синглтоны для передачи данных между контролерами.

    Так же мы можем без проблем передавать данные из экрана в экран и уменьшить использование singleton'ов.


    1. ajjnix
      30.08.2015 19:49

      Собрать переходы между экранами, изолировать контроллеры.
      А singleton'ы, к примеру сервисы. Здесь мы можем передать экземпляр.
      Точно так же как мы открываем сторибоард и смотрим навигацию, открываем роутер и смотрим навигацию.


    1. corristo
      31.08.2015 01:46
      +1

      >какую проблему

      Отделили view от логики переходов, как минимум.


  1. Brain89
    31.08.2015 02:35
    +1

    Не очень понятно, почему логика роутера (логика переходов) определена вместе с контейнером для самих контроллеров. При таком подходе для каждого варианта контейнера придется создавать свой базовый роутер. Более логично смотрится вариант с отделением логики переходов от контроллеров в отдельном объекте. Конечно, тогда контроллер будет вынужден что-то знать о роутере, но это нормально: контроллер должен быть главным объектом и, соответственно, общим связующим звеном.


  1. Woit
    31.08.2015 08:28
    +2

    А, собсна, при чем тут навигация в приложении и xib'ы?
    Xib'ы — это разметка, Storyboard — это навигация. Если нравится верстать в отдельных файлах (лично я 90% верстаю в отдельных xib'ах), то вполне себе можно накидать экраны и переходы в сториборде, а затем у контроллеров в сториборде удалить корневой View.
    По умолчанию, если в сториборде у контроллера нет View, то будет выполнен поиск xib-файла с таким же названием как и у контроллера.
    Итого имеем: верстка в xib'е, навигация в storyboard, все довольны.


    1. Brain89
      31.08.2015 08:54

      При таком подходе надо быть осторожным: можно получить ошибку Missing proxy for identifier UIStoryboardPlaceholder. Решения есть (например), но прежде всего надо быть внимательным, потому что используется не поведение по умолчанию, а некий недокументированный хак, который загружает view для контроллера из другого файла (xib'а).

      P.S. Если это не хак, а документированное поведение, — дайте знать.


    1. corristo
      31.08.2015 13:40
      +2

      Storybords могут быть и без навигации.


  1. IgorFedorchuk
    31.08.2015 08:47

            self.interactivePopGestureRecognizer.enabled = NO;
    

    А зачем было блочить swipe-to-back?