Навигация между экранами с использованием xib файлов

    При чтении различных исходников сталкиваюсь с проектами, реализованными с использованием 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'ов.

    Ссылка на проект
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

          Отделили view от логики переходов, как минимум.
        • +1
          Не очень понятно, почему логика роутера (логика переходов) определена вместе с контейнером для самих контроллеров. При таком подходе для каждого варианта контейнера придется создавать свой базовый роутер. Более логично смотрится вариант с отделением логики переходов от контроллеров в отдельном объекте. Конечно, тогда контроллер будет вынужден что-то знать о роутере, но это нормально: контроллер должен быть главным объектом и, соответственно, общим связующим звеном.
          • +2
            А, собсна, при чем тут навигация в приложении и xib'ы?
            Xib'ы — это разметка, Storyboard — это навигация. Если нравится верстать в отдельных файлах (лично я 90% верстаю в отдельных xib'ах), то вполне себе можно накидать экраны и переходы в сториборде, а затем у контроллеров в сториборде удалить корневой View.
            По умолчанию, если в сториборде у контроллера нет View, то будет выполнен поиск xib-файла с таким же названием как и у контроллера.
            Итого имеем: верстка в xib'е, навигация в storyboard, все довольны.
            • 0
              При таком подходе надо быть осторожным: можно получить ошибку Missing proxy for identifier UIStoryboardPlaceholder. Решения есть (например), но прежде всего надо быть внимательным, потому что используется не поведение по умолчанию, а некий недокументированный хак, который загружает view для контроллера из другого файла (xib'а).

              P.S. Если это не хак, а документированное поведение, — дайте знать.
              • +2
                Storybords могут быть и без навигации.
              • 0
                        self.interactivePopGestureRecognizer.enabled = NO;
                

                А зачем было блочить swipe-to-back?
                • 0
                  извиняюсь, кусок скопированного кода (
                • 0
                  А вот этот код работает в надежде, что рантайм всегда будет создавать self заранее?
                  UIViewController *rootViewController = [self createRootViewController];
                  self = [super initWithRootViewController:rootViewController];
                  

                  Ну и про две буквы в префиксе своего класса уже сто раз проговорено — так нельзя, надо минимум три.
                  • 0
                    память под объект уже выделена в момент вызова, без всякой надежды
                    как минимум метод initRouter вызван у объекта (который и будет self) — подробнее
                    максимум возможного — [super initWithRootViewController:rootViewController] вернет nil и rootViewController будет уничтожен
                    • 0
                      Вопрос скорее про моветон, нежели про особенности текущей реализации рантайма.
                      Скажите, пожалуйста, для вас приведённый код не «пахнёт»?
                      • 0
                        нет, для меня этот кусок кода приемлемый
                        1) я использую возможность языка
                        2) мне не нужно делать лишние манипуляции с initWithRootViewController
                        3) метод createRootViewController использует self только для связи. Можно было сперва просто создать контроллер, потом вызвать initWithRootViewController и только потом связать, но если это делается проще, почему не использовать
                        4) если речь про подмену объекта в init, то сам этот момент кейс «пахнет»

                        это ведь не конструктор, если в пример хочется привести swift

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.