Пробуем 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 (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 0×0000000112f0ef6f CFRelease + 1183 1 com.company.XXXCoreTestHost 0×000000021e206dd7 LoadFonts + 455 (XXXCore.m:38) 2 dyld_sim 0×000000010f8a9867 ImageLoaderMachO: doModInitFunctions (ImageLoader: LinkContext const&) + 265 3 dyld_sim 0×000000010f8a99f4 ImageLoaderMachO: doInitialization (ImageLoader: LinkContext const&) + 40 4 dyld_sim 0×000000010f8a65a5 ImageLoader: recursiveInitialization (ImageLoader: LinkContext const&, unsigned int, ImageLoader: InitializerTimingList&, ImageLoader: UninitedUpwards&) + 305 5 dyld_sim 0×000000010f8a642c ImageLoader: processInitializers (ImageLoader: LinkContext const&, unsigned int, ImageLoader: InitializerTimingList&, ImageLoader: UninitedUpwards&) + 138 6 dyld_sim 0×000000010f8a669d ImageLoader: runInitializers (ImageLoader: LinkContext const&, ImageLoader: InitializerTimingList&) + 75 7 dyld_sim 0×000000010f89e352 dyld: runInitializers (ImageLoader*) + 89 8 dyld_sim 0×000000010f8a2be7 dlopen + 951 9 libdyld.dylib 0×000000011666d3df dlopen + 59 10 com.apple.dt.IBFoundation 0×00000001115419a3 -[IBAbstractInterfaceBuilderTool _resultByLoadingUnloadedBundleInstance:] + 154 11 com.apple.dt.IBFoundation 0×0000000111541f6e -[IBAbstractInterfaceBuilderTool loadBuiltLiveViewBundleInstances:] + 607 12 com.apple.dt.IBFoundation 0×0000000111540e42 __80-[IBMessageReceiveChannel deliverMessage: toTarget: withArguments: context: result:]_block_invoke + 278 13 com.apple.dt.IBFoundation 0×0000000111540c66 -[IBMessageReceiveChannel deliverMessage: toTarget: withArguments: context: result:] + 441 14 com.apple.dt.IBFoundation 0×0000000111540930 __88-[IBMessageReceiveChannel runBlockingReceiveLoopNotifyingQueue: notifyingTarget: context:]_block_invoke + 97 15 libdispatch.dylib 0×000000011663daf4 _dispatch_client_callout + 8 16 libdispatch.dylib 0×000000011662aeb2 _dispatch_barrier_sync_f_slow_invoke + 51 17 libdispatch.dylib 0×000000011663daf4 _dispatch_client_callout + 8 18 libdispatch.dylib 0×00000001166292e9 _dispatch_main_queue_callback_4CF + 490 19 com.apple.CoreFoundation 0×0000000112f9f569 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9 20 com.apple.CoreFoundation 0×0000000112f6246b __CFRunLoopRun + 2043 21 com.apple.CoreFoundation 0×0000000112f61a06 CFRunLoopRunSpecific + 470 22 com.apple.Foundation 0×00000001118dd862 -[NSRunLoop (NSRunLoop) runMode: beforeDate:] + 275 23 com.apple.dt.IBFoundation 0×0000000111520745 -[IBAbstractPlatformTool startServingReceiveChannel:] + 322 24 com.apple.dt.IBFoundation 0×000000011152081f -[IBAbstractPlatformTool startServingSocket:] + 106 25 com.apple.dt.IBFoundation 0×0000000111520ae2 +[IBAbstractPlatformTool main] + 220 26 IBDesignablesAgentCocoaTouch 0×000000010f7eafe0 main + 34 27 libdyld.dylib 0×000000011666e145 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.
Полезные ссылки:
Creating a Custom View That Renders in Interface Builder WWDC 2014 Session 411 — What’s New in Interface Builder Небольшой туториал с жизненным циклом UIView