Try Xcode Live Rendering

    As you know, in Xcode 6 and iOS 8 SDK, Apple added the ability to render custom components and edit their properties directly in the standard Interface Builder (there should be a caustic mention that this was in Delphi of ancient versions) .

    The basics


    To get started, we'll need some makeshift descendant of UIView to get Xcode to render it in Interface Builder. To do this, you need to mark it with the IB_DESIGNABLE attribute (technically, in Objective-C, this is a macro, well, since Apple calls it an attribute, and in Swift it is an attribute, so be it):
    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
    

    Now you can create a storyboard (or xib) and place our view there, and Xcode will have to display it successfully (after building the project):


    Now it would be great to edit the properties of the component that affect its appearance, right from IB. To do this, mark the corresponding properties with the IBInspectable attribute. Here is the result:


    All properties edited in the Assistant Editor are duplicated in the Runtime Attributes:



    Currently supported property types:
    • integer types (except enum)
    • float / double / CGFloat (Float / Double / CGFloat in Swift)
    • NSString (Strnig in Swift)
    • BOOL (Bool in Swift)
    • Cgpoint
    • Cgsize
    • Cgect
    • Uicolor
    • UIImage

    The types NSNumber, UIEdgeInsets, NSRange, and enumeration types (yet?) Are not supported. Runtime Attributes support NSRange, some system components allow editing UIEdgeInsets and enum properties, so there is hope for their support in the future. NSNumber can also be set via Runtime Attributes (see the screenshot above, the price property is set that way).

    Problems


    In an ideal world, the steps described are enough to add support for live rendering. In the real world, some unobvious difficulties may arise.

    Access Application Bundle


    In the early beta versions of Xcode 6 for live rendering, the UIView descendant was supposed to be in a separate module (framework). Later this restriction was removed, however, to understand the process of interaction between Xcode, iOS Simulator and your application during live rendering, it is useful to know this.

    The bottom line is: Xcode collects your application with special defines, the simulator loads the application as a dynamic library and instantiates your UIView descendant to render it, and transfers the results back to Xcode via XPC.

    Important! The entry point of your application is not called, and accordingly the application delegate is not created. So if you have some important code located there (for example, setting UIAppearance), keep this in mind.

    In the “loads the application as a dynamic library” part, the devil is hiding: the bundle of your application is no longer the main bundle, and calling + [NSBundle mainBundle] will return not it, but something like:

    po [NSBundle mainBundle]
    NSBundle  (loaded)
    

    Now imagine how many places + mainBundle is used implicitly? Yes, anywhere where you can specify nil as the bundle argument.

    The solution is this: we make the global function XXXApplicationBundle (or the NSBundle category with the method), where we use + [NSBundle bundleForClass: <some class that is guaranteed in your bundle>], and use it instead of + mainBundle or nil.

    But this problem affects not only your code, but also the code of the libraries used. For example, libPhoneNumber-iOS accesses its resources through + mainBundle. Oops, no live rendering for our UILabel heir formatting phone numbers.

    No, do not pull your hands to the Objective-C runtime, do not swizzle + mainBundle, it is not known what will break. Yes, and CoreFoundation API for access to bundles we can not replace with all our desire.

    View lifecycle features for live rendering


    A naive iOS developer might think that a view should be created using -initWithCoder :, it's in xib! But not everything is so simple, Apple decided not to mess with the partial nib instance (there can be a lot of things besides your view), and the instance is created via -initWithFrame :. For a view that is compiled into xib, -initWithFrame: is often not implemented, or is implemented and consists of some kind of assert to drop the program and remind the unlucky user that view is intended solely for loading from xib. In fact, nothing prevents us from implementing -initWithFrame: in such cases "as needed", and just load the view from xib and return:

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

    I think that many people have a category for loading view from xib, so I won’t go into details of the implementation + xxx_viewFromNib (do not forget to specify the correct bundle). I should note that such a trick will not work in Swift (since initializers there are similar to constructors in Java or C ++, that is, they cannot replace the initialized object with another).

    After instantiation, view will call the -prepareForIntefaceBuilder method (if one is implemented). You can set property values ​​in it, so that by default your component looks meaningful. When downloading pictures and other resources in this method, do not forget about the correct bundle.

    Yo dawg, we heard u like live rendering


    If your view is created from xib and is marked as IB_DESIGNABLE, it will render even when editing its own xib. Here is such a recursion. I don’t even know if this is a bug.

    Diagnosis of problems


    Sometimes, live rendering simply will not work, giving a message saying that “ibtool crashed” without any particular details. Faced this, debugging the mentioned problem with loading resources from the wrong bundle: the font registration code simply crashed, dropping the simulator along with it. But I found out only by examining the logs in Console.app, and discovering a simulator crash log with a sane stack trace.
    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
    

    Therefore, in any incomprehensible situation, follow in Console.app and look for a crashlog.

    the end


    Despite the pitfalls described, I think live rendering is a great way to speed up prototyping, development, and debugging custom views. It’s especially cool that live rendering takes into account the layout constraints and intrinsic content size of your view, so autolayout works honestly, without any stubs.

    Bonus: editing properties through the Assistant Editor also works for non-visual objects (that is, arbitrary objects added to xib or storyboard), just use IBInspectable without IB_DESIGNABLE: www.merowing.info/2014/06/behaviours-and-xcode-6 .

    I hope that my experience will be useful to someone and will save some amount of time when implementing live rendering for your views.

    Useful links:
    1. Creating a Custom View That Renders in Interface Builder
    2. WWDC 2014 Session 411 - What's New in Interface Builder
    3. A small tutorial with a UIView life cycle

    Also popular now: