Пробуем Xcode Live Rendering

    Как вы знаете, в Xcode 6 и iOS 8 SDK Apple добавила возможность рендеринга кастомных компонентов и редактирования их свойств прямо в стандартном Interface Builder (здесь должно быть едкое упоминание о том, что это было еще в Delphi древних версий).

    Основы


    Для начала нам понадобится какой-то самодельный наследник UIView, чтобы заставить Xcode рендерить его в Interface Builder. Для этого его нужно пометить атрибутом IB_DESIGNABLE (технически в Objective-C это макрос, ну раз Apple называет это атрибутом, и в Swift это атрибут, так тому и быть):
    IB_DESIGNABLE
    @interface XXXStaticPriceView : UIView
    
    @property (nonatomic, copy) IBInspectable NSNumber *price;
    @property (nonatomic) IBInspectable NSUInteger amount;
    @property (nonatomic) IBInspectable NSNumberFormatterRoundingMode roundingMode;
    
    @property (nonatomic, getter = isHighlighted) IBInspectable BOOL highlighted;
    
    @property (nonatomic, copy) IBInspectable UIColor *textColor;
    @property (nonatomic, copy) IBInspectable UIColor *outlineColor;
    
    @end
    

    Теперь можно создать storyboard (или xib) и разместить там наш view, и Xcode должен будет его успешно отобразить (предварительно собрав проект):


    Теперь было бы здорово редактировать свойства компонента, влияющие на его внешний вид, прямо из IB. Для этого нужно пометить соответствующие свойства атрибутом IBInspectable. Вот результат:


    Все редактируемые в Assistant Editor свойства дублируются в Runtime Attributes:



    Поддерживаемые на данный момент типы свойств:
    • целочисленные типы (кроме enum)
    • float/double/CGFloat (Float/Double/CGFloat в Swift)
    • NSString (Strnig в Swift)
    • BOOL (Bool в Swift)
    • CGPoint
    • CGSize
    • CGRect
    • UIColor
    • UIImage

    Типы NSNumber, UIEdgeInsets, NSRange, а также типы-перечисления (пока?) не поддерживаются. Runtime Attributes поддерживают NSRange, некоторые системные компоненты позволяют редактировать UIEdgeInsets- и enum-свойства, поэтому есть надежда на их поддержку в будущем. NSNumber также можно задать через Runtime Attributes (см. скриншот выше, свойство price задано именно так).

    Проблемы


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

    Доступ к application bundle


    В ранних бета-версиях Xcode 6 для live rendering наследник UIView должен был находиться в отдельном модуле (фреймворке). Позже это ограничение убрали, однако для понимания процесса взаимодействия Xcode, iOS Simulator и вашего приложения в ходе live rendering это полезно знать.

    Суть такова: Xcode собирает ваше приложение с указанием специальных дефайнов, симулятор загружает приложение как динамическую библиотеку и инстанциирует ваш наследник UIView, чтобы его отрендерить, и передает результаты обратно в Xcode через XPC.

    Важно! Точка входа вашего приложения не вызывается, соответственно не создается и application delegate. Так что если там у вас расположен какой-то важный код (например, настройка UIAppearance), имейте это в виду.

    В части «загружает приложение как динамическую библиотеку» и кроется дьявол: бандл вашего приложения более не является главным бандлом, и вызов +[NSBundle mainBundle] вернёт не его, а что-то вроде:

    po [NSBundle mainBundle]
    NSBundle </Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Overlays> (loaded)
    

    А теперь представьте, в скольких местах +mainBundle используется неявно? Да в любом месте, где в качестве аргумента bundle можно указать nil.

    Решение такое: делаем глобальную функцию XXXApplicationBundle (или категорию NSBundle с методом), где используем +[NSBundle bundleForClass:<какой-нибудь класс, который гарантированно в вашем бандле>], и используем ее вместо +mainBundle или nil.

    Но этой проблеме подвержен не только ваш код, но и код используемых библиотек. Например, libPhoneNumber-iOS обращается к своим ресурсам именно через +mainBundle. Упс, никакого live rendering для нашего наследника UILabel, форматирующего телефонные номера.

    Нет, не тяните руки к Objective-C runtime, не надо swizzle'ить +mainBundle, неизвестно, что при этом поломается. Да и CoreFoundation API для доступа к бандлам нам не подменить при всём желании.

    Особенности жизненного цикла view при live rendering


    Наивный iOS-разработчик может подумать, что создаваться view должен с помощью -initWithCoder:, он же в xib! Но не всё так просто, Apple решили не связываться с частичным инстанциированием nib (там помимо вашего view еще много всего может быть), и инстанс создается через -initWithFrame:. Для view, которые свёрстаны в xib, -initWithFrame: часто не реализуется, или реализуется и состоит из какого-нибудь assert, чтоб уронить программу и напомнить незадачливому пользователю, что view предназначен исключительно для загрузки из xib. На самом же деле ничего не мешает нам реализовать -initWithFrame: в таких случаях «как надо», и просто грузить view из xib и возвращать:

    - (instancetype)initWithFrame:(CGRect)frame
    {
        self = [self.class xxx_viewFromNib];
        self.frame = frame;
        return self;
    }
    

    Думаю, категория для загрузки view из xib есть у многих, поэтому в подробности реализации +xxx_viewFromNib вдаваться не буду (не забываем указывать правильный bundle). Должен заметить, что в Swift такой трюк не пройдёт (так как там initializers похожи на конструкторы в Java или C++, то есть не могут подменить инициализируемый объект другим).

    После инстанциирования у view будет вызван метод -prepareForIntefaceBuilder (если таковой реализован). В нём можно задать значения свойств, чтоб по умолчанию ваш компонент выглядел осмысленно. Загружая картинки и другие ресурсы в этом методе не забывайте про правильный bundle.

    Yo dawg, we heard u like live rendering


    Если ваш view создается из xib и помечен как IB_DESIGNABLE, он будет рендериться даже при редактировании его собственного xib. Вот такая вот рекурсия. Даже не знаю, баг ли это.

    Диагностика проблем


    Иногда live rendering просто не будет работать, выдавая сообщение о том, что «ibtool crashed» без особых подробностей. Столкнулся с подобным, отлаживая упомянутую проблему с загрузкой ресурсов из неправильного бандла: код регистрации шрифта просто падал, роняя вместе с собой симулятор. Но узнал я это только изучив логи в Console.app, и обнаружив крэшлог симулятора с вменяемым стектрейсом.
    stacktrace
    Application Specific Information:
    *** CFRelease() called with NULL ***
    
    Thread 0 Crashed:
    0   com.apple.CoreFoundation        0x0000000112f0ef6f CFRelease + 1183
    1   com.company.XXXCoreTestHost     0x000000021e206dd7 LoadFonts + 455 (XXXCore.m:38)
    2   dyld_sim                        0x000000010f8a9867 ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 265
    3   dyld_sim                        0x000000010f8a99f4 ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 40
    4   dyld_sim                        0x000000010f8a65a5 ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 305
    5   dyld_sim                        0x000000010f8a642c ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 138
    6   dyld_sim                        0x000000010f8a669d ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 75
    7   dyld_sim                        0x000000010f89e352 dyld::runInitializers(ImageLoader*) + 89
    8   dyld_sim                        0x000000010f8a2be7 dlopen + 951
    9   libdyld.dylib                   0x000000011666d3df dlopen + 59
    10  com.apple.dt.IBFoundation       0x00000001115419a3 -[IBAbstractInterfaceBuilderTool _resultByLoadingUnloadedBundleInstance:] + 154
    11  com.apple.dt.IBFoundation       0x0000000111541f6e -[IBAbstractInterfaceBuilderTool loadBuiltLiveViewBundleInstances:] + 607
    12  com.apple.dt.IBFoundation       0x0000000111540e42 __80-[IBMessageReceiveChannel deliverMessage:toTarget:withArguments:context:result:]_block_invoke + 278
    13  com.apple.dt.IBFoundation       0x0000000111540c66 -[IBMessageReceiveChannel deliverMessage:toTarget:withArguments:context:result:] + 441
    14  com.apple.dt.IBFoundation       0x0000000111540930 __88-[IBMessageReceiveChannel runBlockingReceiveLoopNotifyingQueue:notifyingTarget:context:]_block_invoke + 97
    15  libdispatch.dylib               0x000000011663daf4 _dispatch_client_callout + 8
    16  libdispatch.dylib               0x000000011662aeb2 _dispatch_barrier_sync_f_slow_invoke + 51
    17  libdispatch.dylib               0x000000011663daf4 _dispatch_client_callout + 8
    18  libdispatch.dylib               0x00000001166292e9 _dispatch_main_queue_callback_4CF + 490
    19  com.apple.CoreFoundation        0x0000000112f9f569 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
    20  com.apple.CoreFoundation        0x0000000112f6246b __CFRunLoopRun + 2043
    21  com.apple.CoreFoundation        0x0000000112f61a06 CFRunLoopRunSpecific + 470
    22  com.apple.Foundation            0x00000001118dd862 -[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 275
    23  com.apple.dt.IBFoundation       0x0000000111520745 -[IBAbstractPlatformTool startServingReceiveChannel:] + 322
    24  com.apple.dt.IBFoundation       0x000000011152081f -[IBAbstractPlatformTool startServingSocket:] + 106
    25  com.apple.dt.IBFoundation       0x0000000111520ae2 +[IBAbstractPlatformTool main] + 220
    26  IBDesignablesAgentCocoaTouch    0x000000010f7eafe0 main + 34
    27  libdyld.dylib                   0x000000011666e145 start + 1
    

    Поэтому в любой непонятной ситуации следуйте в Console.app и ищите крэшлог.

    Конец


    Несмотря на описанные подводные камни, считаю live rendering отличным способом ускорения прототипирования, разработки и отладки кастомных view. Особенно круто, что при live rendering учитываются layout constraints и intrinsic content size вашего view, поэтому autolayout работает по-честному, без констрейнтов-заглушек.

    Бонус: редактирование свойств через Assistant Editor работает и для невизуальных объектов (то есть произвольных объектов, добавленных в xib или storyboard), просто используйте IBInspectable без IB_DESIGNABLE: www.merowing.info/2014/06/behaviours-and-xcode-6.

    Надеюсь, что мой опыт будет кому-то полезен и сэкономит некоторое количество времени при реализации live rendering для ваших view.

    Полезные ссылки:
    1. Creating a Custom View That Renders in Interface Builder
    2. WWDC 2014 Session 411 – What's New in Interface Builder
    3. Небольшой туториал с жизненным циклом UIView
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 18
    • 0
      Спасибо, что поделились реальным опытом использования. Live rendering сыроват местами, но пользоваться можно. Думаю, в XCode 7 будет намного лучше :)
      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          Эти плагины с Cocoa Touch не дружили, если мне не изменяет память.
        • 0
          А что на счет упомянутой iOS 8? На проекте с таргетом 7+ в Xcode 6 не будет работать?
          • 0
            Рендериться будет в iOS 8-симуляторе в любом случае, deployment target безразличен, насколько я понял.

            У меня вообще проект с deployment target 6.0.
            • 0
              Это вы вот про это?
              Суть такова: Xcode собирает ваше приложение с указанием специальных дефайнов, симулятор загружает приложение как динамическую библиотеку и инстанциирует ваш наследник UIView, чтобы его отрендерить, и передает результаты обратно в Xcode через XPC.


              Надо попробовать будет. Спасибо за опыт.
              • 0
                Да, вьюшка рендерится в невидимом симуляторе, который отдает в Interface Builder картинку.
          • 0
            - (instancetype)initWithFrame:(CGRect)frame
            {
                self = [self.class xxx_viewFromNib];
                self.frame = frame;
                return self;
            }
            


            А вот здесь у вас не будет двойного выделения памяти?
            Ведь перед вызовом initWithFrame: разработчики вызывают alloc, а далее у Вас внутри вызывается метод класса [self.class xxx_viewFromNib], который возвращает новый объект. В результате изначальный alloc не имеет смысла.
            • 0
              Да, будет двойное выделение памяти, изначально выделенный участок памяти будет тут же освобожден. Учитывая, что UIView — достаточно легковесный объект (действительно «тяжелой» его частью является только backing store связанного CALayer, но его не может существовать до первой отрисовки, а тем более до вызова -init...), я бы не стал из-за этого беспокоиться, пока это действительно не станет бутылочным горлышком.

              В общем-то все, что реализовано по паттерну class cluster в Cocoa как-то так и работает. Буду рад ошибаться.
              • 0
                Хотя нет, с class cluster можно организовать более эффективную схему, переопределив +allocWithZone: у корневого класса, чтоб он возвращал всегда один и тот же объект-плейсхолдер, тут так не получится.
                • 0
                  Все равно это мягко говоря «костыльно». По сути здесь наблюдается таже проблема, что и раньше: нельзя ложить в xib вью, которая реализована в другой xib.
                  Решалось это двумя способами:
                  1. Не использовать xib для вашей вью;
                  2. Добавлять вашу вью в коде, а не в xib.

                  IB_DESIGNABLE компоненты должны быть реализованы в коде, без использования xib.
                  • 0
                    3. использовать storyboard embed segue и UIVIewController вместо UIView ;)

                    Если уж так не нравится «костыль», можно обернуть его в #ifdef/#endif, чтоб -initWithFrame: присутствовал только в режиме live rendering.

                    IB_DESIGNABLE компоненты должны быть реализованы в коде, без использования xib.

                    Это где-то прямым текстом в докуентации Apple написано?
                    • 0
                      Не знаю, написано ли там это. Я сделал такой вывод исходя из того, что Вы написали в статье.
                      Возможно я выразился слишком обобщенно. Нужно было сказать так:
                      «Исходя из информации в статье IB_DESIGNABLE компоненты нужно реализовать в коде, без использования xib, иначе будет выполняться двойное выделение памяти, что само по себе неправильно. Неважно 1 байт это или 1 мегабайт.»

                      По поводу #ifdef/#endif, не понял как это поможет решить проблему в runtime.
              • 0
                А нельзя как то IB_DESIGNABLE обернуть в #ifdef? А то получается какая то ерунда, бандл меняется, вообще лучше бы не было этих аттрибутов… Или это только в симуляторе? Вообще я так и не понял, зачем что то передавать из симулятора в ХCode, ведь этот аттрибут нужен только для отображения в IB…
                • 0
                  IB_DESIGNABLE влияет только на live rendering, и вся эта чехарда с бандлами тоже актальна только при live rendering. Просто так у вас в коде ничего не сломается.

                  Поясню про «передавать из симулятора в Xcode»:
                  1. Вы открываете в IB xib/storyboard, который содержит view, поддерживающий live rendering
                  2. Xcode собирает ваш проект в отдельную папочку
                  3. Xcode запускает в фоне симулятор iOS и просит его отрендерить view такого-то класса (который можно найти в бинарнике, полученном в п. 2) с такими-то параметрами
                  4. Симулятор рендерит вьюшку и отправляет обратно в Xcode изображение
                  • 0
                    По п. 3 еще поясню: собственно там и происходит «магия» с бандлами, так как ваше приложение не запускается как обычно, а загружается как динамическая библиотека.
                    • 0
                      Спасибо за пояснения, теперь понятно. Ну в таком случае вроде как ничего страшного нету.
                • 0
                  Вот хорошая статья про то же самое на Swift
                  justabeech.com/2014/07/27/xcode-6-live-rendering-from-nib/

                  Там обходят проблему Swift
                  Должен заметить, что в Swift такой трюк не пройдёт (так как там initializers похожи на конструкторы в Java или C++, то есть не могут подменить инициализируемый объект другим).

                  путем двойного вкладывания
                  CustomView
                      CustomView
                          UIImageView
                          UILabel
                  

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