0

How do I create an IBDesignable UITextView such that I can adjust the insets of the text in interface builder? I've added inspectable properties topInset, bottomInset, etc. but now I'm having trouble figure out how to actually update the insets of the UITextView such that the changes are reflected in IB

import UIKit

private let kPlaceholderTextViewInsetSpan: CGFloat = 8

@IBDesignable class UIDesignableTextView: UITextView {
    // variables

    @IBInspectable var topInset: CGFloat = 0.0
    @IBInspectable var leftInset: CGFloat = 0.0
    @IBInspectable var bottomInset: CGFloat = 0.0
    @IBInspectable var rightInset: CGFloat = 0.0

    var insets: UIEdgeInsets {
        get {
            return UIEdgeInsetsMake(topInset, leftInset, bottomInset, rightInset)
        }
        set {
            topInset = newValue.top
            leftInset = newValue.left
            bottomInset = newValue.bottom
            rightInset = newValue.right
        }
    }

    @IBInspectable var placeholder: NSString? { didSet { setNeedsDisplay() } }
    @IBInspectable var placeholderColor: UIColor = UIColor.lightGray

    override var text: String! { didSet { setNeedsDisplay() } }

    override var attributedText: NSAttributedString! { didSet { setNeedsDisplay() } }

    override var contentInset: UIEdgeInsets { didSet { setNeedsDisplay() } }

    override var font: UIFont? { didSet { setNeedsDisplay() } }

    override var textAlignment: NSTextAlignment { didSet { setNeedsDisplay() } }

    // MARK: - Lifecycle

    /** Override coder init, for IB/XIB compatibility */
    #if !TARGET_INTERFACE_BUILDER
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        listenForTextChangedNotifications()
    }

    /** Override common init, for manual allocation */
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        listenForTextChangedNotifications()
    }
    #endif

    /** Initializes the placeholder text view, waiting for a notification of text changed */
    func listenForTextChangedNotifications() {
        NotificationCenter.default.addObserver(self, selector: #selector(UIDesignableTextView.textChangedForPlaceholderTextView(_:)), name:NSNotification.Name.UITextViewTextDidChange , object: self)
        NotificationCenter.default.addObserver(self, selector: #selector(UIDesignableTextView.textChangedForPlaceholderTextView(_:)), name:NSNotification.Name.UITextViewTextDidBeginEditing , object: self)
    }

    /** willMoveToWindow will get called with a nil argument when the window is about to dissapear */
    override func willMove(toWindow newWindow: UIWindow?) {
        super.willMove(toWindow: newWindow)
        if newWindow == nil { NotificationCenter.default.removeObserver(self) }
        else { listenForTextChangedNotifications() }
    }

            func textChangedForPlaceholderTextView(_ notification: Notification) {
        setNeedsDisplay()
        setNeedsLayout()
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)

        if text.characters.count == 0 && self.placeholder != nil {
            let baseRect = placeholderBoundsContainedIn(self.bounds)
            let font = self.font ?? self.typingAttributes[NSFontAttributeName] as? UIFont ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)

            self.placeholderColor.set()

            var customParagraphStyle: NSMutableParagraphStyle!
            if let defaultParagraphStyle =  typingAttributes[NSParagraphStyleAttributeName] as? NSParagraphStyle {
                customParagraphStyle = defaultParagraphStyle.mutableCopy() as! NSMutableParagraphStyle
            } else { customParagraphStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle }
            // set attributes
            customParagraphStyle.lineBreakMode = NSLineBreakMode.byTruncatingTail
            customParagraphStyle.alignment = self.textAlignment
            let attributes = [NSFontAttributeName: font, NSParagraphStyleAttributeName: customParagraphStyle.copy() as! NSParagraphStyle, NSForegroundColorAttributeName: self.placeholderColor]
            // draw in rect.
            self.placeholder?.draw(in: baseRect, withAttributes: attributes)
        }
    }

    func placeholderBoundsContainedIn(_ containerBounds: CGRect) -> CGRect {
        // get the base rect with content insets.
        let baseRect = UIEdgeInsetsInsetRect(containerBounds, UIEdgeInsetsMake(kPlaceholderTextViewInsetSpan, kPlaceholderTextViewInsetSpan/2.0, 0, 0))

        // adjust typing and selection attributes
        if let paragraphStyle = typingAttributes[NSParagraphStyleAttributeName] as? NSParagraphStyle {
            baseRect.offsetBy(dx: paragraphStyle.headIndent, dy: paragraphStyle.firstLineHeadIndent)
        }

        return baseRect
    }
Apollo
  • 8,874
  • 32
  • 104
  • 192

2 Answers2

2

This is all you need to do:

import UIKit

@IBDesignable class TextViewWithInsets: UITextView {

    @IBInspectable var topInset: CGFloat = 0 {
        didSet {
            self.contentInset = UIEdgeInsetsMake(topInset, self.contentInset.left, self.contentInset.bottom, self.contentInset.right)
        }
    }

    @IBInspectable var bottmInset: CGFloat = 0 {
        didSet {
            self.contentInset = UIEdgeInsetsMake(self.contentInset.top, self.contentInset.left, bottmInset, self.contentInset.right)
        }
    }

    @IBInspectable var leftInset: CGFloat = 0 {
        didSet {
            self.contentInset = UIEdgeInsetsMake(self.contentInset.top, leftInset, self.contentInset.bottom, self.contentInset.right)
        }
    }

    @IBInspectable var rightInset: CGFloat = 0 {
        didSet {
            self.contentInset = UIEdgeInsetsMake(self.contentInset.top, self.contentInset.left, self.contentInset.bottom, rightInset)
        }
    }
}

As you can see, these are properties of the TextViewWithInsets subclass of UITextView I have created. You need to override the didSet portion of the property method. Then, in Interface Builder these four properties (Top Inset, Bottom Inset, Left Inset, and Right Inset) will appear in the Attributes Inspector: attributes inspector in IB for new class

Just make sure that in the Identity Inspector you set the TextView object in the storyboard to be TextViewWithInsets or whatever you choose to name it like this: Set class of Text View object in storyboard to custom class

eschos24
  • 303
  • 2
  • 10
0

Swift 5

If you want to use it for all UITextViews in the project use the following:

import UIKit
@IBDesignable extension UITextView {

    @IBInspectable var topPadding: CGFloat {
        get {
            return contentInset.top
        }
        set {
            self.contentInset = UIEdgeInsets(top: newValue,
                                             left: self.contentInset.left,
                                             bottom: self.contentInset.bottom,
                                             right: self.contentInset.right)
        }
    }

    @IBInspectable var bottomPadding: CGFloat {
        get {
            return contentInset.bottom
        }
        set {
            self.contentInset = UIEdgeInsets(top: self.contentInset.top,
                                             left: self.contentInset.left,
                                             bottom: newValue,
                                             right: self.contentInset.right)
        }
    }

    @IBInspectable var leftPadding: CGFloat {
        get {
            return contentInset.left
        }
        set {
            self.contentInset = UIEdgeInsets(top: self.contentInset.top,
                                             left: newValue,
                                             bottom: self.contentInset.bottom,
                                             right: self.contentInset.right)
        }
    }

    @IBInspectable var rightPadding: CGFloat {
        get {
            return contentInset.right
        }
        set {
            self.contentInset = UIEdgeInsets(top: self.contentInset.top,
                                             left: self.contentInset.left,
                                             bottom: self.contentInset.bottom,
                                             right: newValue)
        }
    }
}
R. A.
  • 192
  • 12