Tutorial: Building your own custom IBDesignable view: a UITextView with placeholder - Digital Leaves
861
post-template-default,single,single-post,postid-861,single-format-standard,qode-social-login-1.0,qode-restaurant-1.0,ajax_fade,page_not_loaded,,select-theme-ver-4.1,wpb-js-composer js-comp-ver-5.2,vc_responsive

Tutorial: Building your own custom IBDesignable view: a UITextView with placeholder

One of the more powerful features introduced with XCode 6 and iOS 8 is the possibility to design, build and integrate custom controls directly in the Interface Builder. Previously, we were able to subclass UIView or any of the UIKit control classes, but we couldn’t really see the results of our customizations until runtime. Now, thanks to IBDesignable controls, we are able to see our custom controls in Interface Builder exactly as they are going to be rendered live, and this really helps a lot when implementing a concrete design for an App.

IBDesignable

Making our class IB friendly is surprisingly easy. All we have to do is include the keyword @IBDesignable prior to our class sentence, like this:

The IBDesignable attribute tells XCode that MyView will be customizable and designable in Interface Builder.

IBInspectable

IBInspectableBut what’s the point of having an IB-customizable view or control if we cannot modify any of its properties? That’s what IBInspectable is for. When applied to a variable, IBInspectable will tell XCode that the value of the variable should be available for modification in the Attribute Inspector of the control in IB. XCode will set up a textfield or selection popup for the control. Currently, the following swift types (and their corresponding Objective-C types) are inspectable: UIImage, UIColor, String, Int, Double, CGFloat, Bool, CGPoint, CGSize and CGRect. These types allow a wide range of customization for your views. Let’s see an example:

Here, we are defining a variable called cornerRadius, and linking it with the layer’s corner radius. As a result, every time we set the cornerRadius attribute in the Attribute Inspector of IB, we will see the view change it’s corner radius attribute.

The most useful use case for IBInspectable, and maybe the first that comes to mind, is being able to change the cornerRadius, borderWidth and borderColor of a UIView, something that most probably every iOS developer has to deal with during the development of an App.

Use Case: PlaceholderTextView

One of the most annoying things about UITextView (for me, at least), was the omission of a placeholder like UITextFields have. In my experience, most of the times a UITextView is used in conjunction with other UITextFields, you have to implement a placeholder for giving them all the same appearance. Now with IBDesignable/IBInspectable, we can easily build a custom UITextView that includes a placeholder string and behaves exactly like a UITextField will when some text is entered. Let’s do it!

First, we’ll define our subclass of UITextView, and we’ll make it Designable. We’ll add some attributes for the placeholder string and color:

XCode nasty bugs!The default values will be honored by the Attribute Inspector as the “Default” configuration for the attribute. If you plan to add some custom functionality, you would probably need to override the initializers init(frame: CGRect) and init(coder aDecoder: NSCoder). If you just override these two initializers, Xcode will complaint about AutoLayout crashing while trying to render your custom view onscreen. In order to avoid this, we will make use of the TARGET_INTERFACE_BUILDER condition to make sure that the initializers only get compiled for the App in runtime, not in IB.

For our PlaceholderTextView, we will have to make sure that if the user enters some text, the placeholder string disappears, and if the user removes all characters, the placeholder appears again. Thus, we will add ourselves as listeners of the UITextViewTextDidChangeNotification and UITextViewTextDidBeginEditingNotification notifications in a method called from our initialization methods:

The listerForTextChangedNotifications() method simply calls NSNotificationCenter.defaultCenter().addObserver(…).

We must assure that once the view disappears, we remove ourselves as observers, but how can we do that? In the (good?) old days before ARC, we could use the method dealloc(), but nowadays we cannot use this. As we don’t have a UIViewController here, we cannot resort to viewDidDisappear(…) or viewWillDisappear(…). What’s the solution? The UIView method willMoveToWindow(newWindow: UIWindow?) gets called whenever the UIView is about to move to a new window. This method gets called when the view disappears, with newWindow set to nil. Thus, we can easily check for this condition to remove ourselves as observers in NSNotificationCenter:

The method textChangedForPlaceholderTextView(…) will just refresh the view by calling setNeedsDisplay().

Also, there are other situations in which we would need to call setNeedsDisplay, concretely when any of the attributes relative to the text and how it’s rendered change, so we will override the following variables: text, attributedText, contentInset, font and textAlignment, and call setNeedsDisplay() thanks to the swift didSet mechanism.

Pick up your pencils

Now that we have defined when we have to refresh our subview, let’s actually define what will be drawn. We need to calculate the bounds where we will draw the placeholder text. This bounds can be easily calculated by referring to the container bounds, while taking into account the possible indentation due to the paragraph style:

Now we can override the drawRect method, that gets called whenever the view needs to be drawn/refreshed. The text will be drawn by calling string.drawInRect, being careful about the possible permutations in the paragraph style:

I’m preparing a tutorial on advanced graphic drawing in Cocoa, but If you want to know more, there is a very nice book called Learning Cocoa with Objective-C, that I can recommend.

Viewing the results

Now that we have all the pieces in place, XCode will compile our subclass and, thanks to the @IBDesignable attribute, render the view using the drawRect. Thus, we will be able to change the placeholder text, its color, and observe the results in realtime in the Interface Builder, isn’t it cool?

placeholderFinal

You can find the full code for the PlaceholderTextView in my github.

Edit: There seems to be some bug in latest versions of Xcode that causes problems when displaying the designable controls in IB. If you find that your designable controls are working in the simulator or in a device, but won’t display in IB, try these steps:

• While in a storyboard, go to “Editor” in the top bar menu
• deselect “Automatically refresh views” if it was still turned on
• Close all your storyboard tabs
• Rebuild the project
• Fix all build errors if any
• Re-open your storyboard.
• Go to “Editor” again and click “Refresh All Views”
Hope it helps.
4 Comments
  • saxahan

    June 10, 2015 at 11:01 pm Reply

    Thank you for this tutorial. It really helps and works.

    • Joseph Allsön

      February 7, 2017 at 11:10 am Reply

      Fantastic tutorial, thanks a lot!

  • Raul Silva

    February 14, 2017 at 5:04 pm Reply

    Do you know if something changed? I cannot get the default value of my @IBInspectables to show up on the attributes inspector when I create an instance of the class.

    For example:

    @IBInspectable var score:Int = 45

    Should present a Score field with a default value of 45, instead it presents the Score field with a nil (–) value.

    • Ignacio Nieto Carvajal

      February 14, 2017 at 7:48 pm Reply

      Hi there Raul!

      Thanks for commenting. Well, certainly there’s a bug with Xcode in the latest versions when displaying designable controls. Try the following, it usually works for me:
      • While in a storyboard, go to “Editor” in the top bar menu
      • deselect “Automatically refresh views” if it was still turned on
      • Close all your storyboard tabs
      • Rebuild the project
      • Fix all build errors if any
      • Re-open your storyboard.
      • Go to “Editor” again and click “Refresh All Views”
      Let me know if that worked.

Post a Comment

Before you continue...

Hi there! I created the Digital Tips List, the newsletter for Swift and iOS developers, to share my knowledge with you.


It features exclusive weekly tutorials on Swift and iOS development, Swift code snippets and the Digital Tip of the week.


Besides, If you join the list you'll receive the eBook: "Your First iOS App", that will teach you how to build your first iOS App in less than an hour with no prior Swift or iOS knowledge.

I hate spam, and I promise I'll keep your email address safe.