📜 ⬆️ ⬇️

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 causal mention that it was still in Delphi of ancient versions) .

The basics


First we need some self-made UIView heir to get Xcode to render it in Interface Builder. To do this, you need to mark it with the IB_DESIGNABLE attribute (technically, this is a macro in Objective-C, 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 having assembled the project):


Now it would be great to edit the properties of a component, affecting its appearance, directly 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 Runtime Attributes:


')
Currently supported property types:

NSNumber, UIEdgeInsets, NSRange types, and enumeration types (for now?) Are not supported. Runtime Attributes support NSRange, some system components allow you to edit 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 exactly like this).

Problems


In the ideal world, the described actions are enough to add support for live rendering. In the real world, there may be some non-obvious difficulties.

Access to application bundle


In the early beta versions of Xcode 6 for live rendering, the UIView descendant should 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.

The point is this: Xcode builds your application with special defaults, the simulator loads the application as a dynamic library and instantiates your successor to UIView to render it, and sends the results back to Xcode via XPC.

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

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

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

Now imagine how many places + mainBundle is used implicitly? Yes, in any place where nil can be specified as the bundle argument.

The solution is to do the global XXXApplicationBundle function (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 successor UILabel, which formats phone numbers.

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

Features of the view life cycle during 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 get involved in the partial instantiation of the nib (there is still a lot of things besides your view), and the instance is created via -initWithFrame :. For views that are uploaded to xib, -initWithFrame: is often not implemented, or is implemented and consists of some assert to drop the program and remind the hapless user that view is intended exclusively for downloading from xib. In fact, nothing prevents us from implementing -initWithFrame: in such cases, "as it should," 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 many people have a category for loading a view from xib, so I’ll not go into the details of the + xxx_viewFromNib implementation (don’t forget to specify the correct bundle). I must note that in Swift such a trick will not work (since initializers are similar to constructors in Java or C ++, that is, they cannot replace the object being initialized with another).

After instantiation, the -prepareForIntefaceBuilder method (if one is implemented) will be called on the view. In it, you can set the values ​​of properties so that your component looks intelligent by default. When uploading 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 marked as IB_DESIGNABLE, it will be rendered even when editing its own xib. Here is such a recursion. I do not even know if it is a bug.

Problem diagnosis


Sometimes live rendering simply will not work, giving the message that "ibtool crashed" without special details. Faced a similar one, debugging the mentioned problem with loading resources from the wrong bundle: the font registration code just fell, dropping the simulator along with it. But I learned this only by examining the logs in Console.app, and finding the crash log of a simulator with a sane stektrays.
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, go to Console.app and look for a crash log.

the end


Despite the described pitfalls, I consider live rendering to be an excellent way to speed up prototyping, developing and debugging custom view. Especially cool is that when live rendering, layout constraints and the intrinsic content size of your view are taken into account, so autolayout works honestly, without constraints-stubs.

Bonus: Editing properties through the Assistant Editor also works for non-visual objects (that is, arbitrary objects added to the 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 view.

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

Source: https://habr.com/ru/post/239257/


All Articles