3
votes

I am currently messing around with IBDesignable Views, and I am curious if anyone has been able to solve this. I would like to have views added through the interface builder be automatically arranged using a custom layout algorithm within my subview. The view works great when I run the app, but in the interface builder, the views do not rearrange in real time.

I have tried debugging my UIView class, but it seems at all times when the interface builder is initializing the element, it thinks it has zero subviews. It seems the interface builder does not give you a chance to arrange these views after the fact. However, I'm wondering if maybe there is just something I'm missing. Is it possible to rearrange subviews added from the interface builder within an IBDesignable class, and have the views show up rearranged in the interface builder?

2

2 Answers

0
votes

Try using the provided method for a custom view and IBDesignable if you are not already. You also might need to refresh your views in Xcode or have it automatically refresh views. Below is the function you may be missing. This is never called in a live app. It is only called in Xcode IB.

override func prepareForInterfaceBuilder() {
    super.prepareForInterfaceBuilder()
    setUpView() 
}

In this instance setUpView is laying out my subviews.
Here is an example that I made. https://github.com/agibson73/ICONButton

import UIKit

@IBDesignable class AGIconButton: UIControl {

private var iconImageView : UIImageView!
private var iconLabel : UILabel!
private var mainSpacer : UIView!
private var highlightView:UIView!
private var widthContraint : NSLayoutConstraint!
var padding : CGFloat = 5


override init(frame: CGRect) {
    super.init(frame: frame)
    setUpView()
    addTarget(self, action: #selector(AGIconButton.userDidTouchDown), for: .touchDown)
    addTarget(self, action: #selector(AGIconButton.userDidTouchUp), for: .touchUpInside)
    addTarget(self, action: #selector(AGIconButton.userDidTouchUpOutside), for: .touchUpOutside)

}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    setUpView()
    addTarget(self, action: #selector(AGIconButton.userDidTouchDown), for: .touchDown)
    addTarget(self, action: #selector(AGIconButton.userDidTouchUp), for: .touchUpInside)
    addTarget(self, action: #selector(AGIconButton.userDidTouchUpOutside), for: .touchUpOutside)


}

//only called at design time
override func prepareForInterfaceBuilder() {
    super.prepareForInterfaceBuilder()
    setUpView()
    addTarget(self, action: #selector(AGIconButton.userDidTouchDown), for: .touchDown)
    addTarget(self, action: #selector(AGIconButton.userDidTouchUp), for: .touchUpInside)
    addTarget(self, action: #selector(AGIconButton.userDidTouchUpOutside), for: .touchUpOutside)

}




@IBInspectable var iconImage: UIImage = UIImage() {
    didSet {
        iconImageView.image = iconImage
    }
}


@IBInspectable var imageSize: CGFloat = 40 {
    didSet {
         setUpView()
    }
}


@IBInspectable var imagePadding: CGFloat = 10 {
    didSet {
           setUpView()
    }
}

@IBInspectable var iconText: String = "Icon Button Time" {
    didSet {
          setUpView()
    }
}

@IBInspectable var iconTextSize: CGFloat = 15 {
    didSet {
          setUpView()
    }
}

@IBInspectable var iconTextColor: UIColor = UIColor.black {
    didSet {
        setUpView()
    }
}

@IBInspectable var alignment: Int = 1 {
    didSet {
        setUpView()
    }
}


override var intrinsicContentSize: CGSize {
    let label = UILabel()
    label.font = UIFont.systemFont(ofSize: iconTextSize)
    label.text = iconText
    label.sizeToFit()
    return CGSize(width: imageSize + label.frame.width + imagePadding + (padding * 2), height: CGFloat(max(label.frame.height, imageSize) + padding * 2))
}


@IBInspectable var highLightColor: UIColor = UIColor.lightGray {
    didSet {
         setUpView()
    }
}

@IBInspectable var shouldBounce: Bool = true

@IBInspectable var borderColor: UIColor = UIColor.clear {
    didSet {
        layer.borderColor = borderColor.cgColor
    }
}

@IBInspectable var borderWidth: CGFloat = 0 {
    didSet {
        layer.borderWidth = borderWidth
    }
}

@IBInspectable var cornerRadius: CGFloat = 0 {
    didSet {
        layer.cornerRadius = cornerRadius
    }
}

private func setUpView(){

    if iconImageView == nil{
        iconImageView = UIImageView(image: iconImage)
        iconImageView.contentMode = .scaleAspectFit
        iconImageView.isUserInteractionEnabled = false
        self.addSubview(iconImageView)

    }

    if mainSpacer == nil{
        mainSpacer = UIView(frame: CGRect(x: 0, y: 0, width: imagePadding, height: self.bounds.height))
        mainSpacer.isUserInteractionEnabled = false
        self.addSubview(mainSpacer)
    }



    if iconLabel == nil{
        iconLabel = UILabel()
        iconLabel.isUserInteractionEnabled = false
        self.addSubview(iconLabel)

    }

    if highlightView == nil{
        highlightView = UIView(frame: self.bounds)
        highlightView.autoresizingMask = [.flexibleWidth,.flexibleHeight]
        highlightView.alpha = 0
        highlightView.isUserInteractionEnabled = false
        self.addSubview(highlightView)
        self.bringSubview(toFront: highlightView)
    }
    highlightView.backgroundColor = highLightColor

    iconLabel.font = UIFont.systemFont(ofSize: iconTextSize)
    iconLabel.text = iconText
    iconLabel.textColor = iconTextColor
    iconLabel.sizeToFit()


    var usedWidth : CGFloat = self.intrinsicContentSize.width

    if bounds.width < usedWidth{
        usedWidth = bounds.width
    }

    let maxImageHeight = min(self.bounds.height - padding, imageSize)

    //resize iconlabel if we have to
    if maxImageHeight + imagePadding + iconLabel.bounds.width + padding * 2 > usedWidth{
        iconLabel.frame = CGRect(x: 0, y: 0, width: self.bounds.width - iconImageView.bounds.width - imagePadding - padding * 2, height: iconLabel.bounds.height)
        iconLabel.fitFontForSize(minFontSize: 1, maxFontSize: iconTextSize, accuracy: 1.0)

    }

    let maxWidth = (self.bounds.width - iconLabel.bounds.width - maxImageHeight - imagePadding) / 2

    switch alignment {
    case 0:
        //intrinsic left
        iconImageView.frame = CGRect(x:padding, y: self.bounds.midY - maxImageHeight/2,width:maxImageHeight, height: maxImageHeight)
        mainSpacer.frame = CGRect(x: maxImageHeight + padding, y: 0, width: imagePadding, height: self.bounds.height)
        iconLabel.frame = CGRect(x: maxImageHeight + imagePadding + padding, y: 0, width: iconLabel.frame.width, height: bounds.height)

        break
    case 1:
        //intrinsic center
        iconImageView.frame = CGRect(x: maxWidth, y: self.bounds.midY - maxImageHeight/2,width:maxImageHeight, height: maxImageHeight)
        mainSpacer.frame = CGRect(x: maxWidth + maxImageHeight, y: 0, width: imagePadding, height: self.bounds.height)
        iconLabel.frame = CGRect(x: maxWidth + maxImageHeight + imagePadding, y: 0, width: iconLabel.frame.width, height: self.bounds.height)
        break
    case 2:
        //intrinsic icon right text aligned right
        iconLabel.frame = CGRect(x: maxWidth, y: 0, width: iconLabel.frame.width, height: self.bounds.height)
        iconLabel.textAlignment = .right
        mainSpacer.frame = CGRect(x: iconLabel.frame.width + maxWidth, y: 0, width: imagePadding, height: self.bounds.height)
        iconImageView.frame = CGRect(x: iconLabel.frame.width + imagePadding + maxWidth, y: self.bounds.midY - maxImageHeight/2,width:maxImageHeight, height: maxImageHeight)
        break
    case 3:
        //intrinsic center invert icon
        iconLabel.frame = CGRect(x:maxWidth, y: 0, width: iconLabel.frame.width, height: self.bounds.height)
        mainSpacer.frame = CGRect(x: maxWidth + iconLabel.bounds.width, y: 0, width: imagePadding, height: self.bounds.height)
        iconImageView.frame = CGRect(x: maxWidth + iconLabel.bounds.width + imagePadding, y: self.bounds.midY - maxImageHeight/2,width:maxImageHeight, height: maxImageHeight)

        break

    default:
        //intrinsic center
        iconImageView.frame = CGRect(x: maxWidth, y: self.bounds.midY - maxImageHeight/2,width:maxImageHeight, height: maxImageHeight)
        mainSpacer.frame = CGRect(x: maxWidth + maxImageHeight, y: 0, width: imagePadding, height: self.bounds.height)
        iconLabel.frame = CGRect(x: maxWidth + maxImageHeight + imagePadding, y: 0, width: iconLabel.frame.width, height: self.bounds.height)
    }


}


//layout subviews
override func layoutSubviews() {
    super.layoutSubviews()
    setUpView()
}


//MARK: Touch Events
//TODO: run on timer to simulate a real press
func userDidTouchDown(){
    if shouldBounce == true{
        animateBouncyDown()
    }else{
        self.animateHighlightTo(alpha: 0.3)
    }

}


func userDidTouchUp(){
    if shouldBounce == true{
        animateBouncyUp()
    }else{
        self.animateHighlightTo(alpha: 0)
    }
}

func userDidTouchUpOutside(){
    if shouldBounce == true{
        animateBouncyUp()
    }else{
        self.animateHighlightTo(alpha: 0)
    }
}

func animateHighlightTo(alpha:CGFloat){
    UIView.animate(withDuration: 0.2, animations: {  [weak self] in
        self?.highlightView.alpha = alpha
    })

}

func animateBouncyDown(){
    self.transform = CGAffineTransform.identity
    UIView.animate(withDuration: 0.15, animations: { [weak self] in
        self?.transform = CGAffineTransform(scaleX: 0.85, y: 0.85)
    })
}

func animateBouncyUp(){
    UIView.animate(withDuration: 0.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.8, options: .curveEaseInOut, animations: {[weak self] in
        if self != nil{
            self?.transform = CGAffineTransform.identity
        }
    }, completion: nil)
}
}

extension UILabel {

func fitFontForSize( minFontSize : CGFloat = 1.0, maxFontSize : CGFloat = 300.0, accuracy : CGFloat = 1.0) {
    var maxFontSize = maxFontSize
    var minFontSize = minFontSize
    assert(maxFontSize > minFontSize)
    layoutIfNeeded() // Can be removed at your own discretion
    let constrainedSize = bounds.size
    while maxFontSize - minFontSize > accuracy {
        let midFontSize : CGFloat = ((minFontSize + maxFontSize) / 2)
        font = font.withSize(midFontSize)
        sizeToFit()
        let checkSize : CGSize = bounds.size
        if  checkSize.height < constrainedSize.height && checkSize.width < constrainedSize.width {
            minFontSize = midFontSize
        } else {
            maxFontSize = midFontSize
        }
    }
    font = font.withSize(minFontSize)
    sizeToFit()
    layoutIfNeeded()
}
  }
0
votes

There doesn't seem to be any way to do this. If you drop a custom control into another xib and add subviews to the custom control in Interface Builder, those subviews appear at the same level as the custom control in the view hierarchy when you load the xib. It looks like custom controls can not act as containers in other xibs.