Dominic Chong Dominic Chong - 5 months ago 19
Swift Question

Setting/Changing variables of a nested custom UIView in another custom UIView

I'm currently running into problems with custom UIViews nested in other custom UIViews, with the variables of the nested custom UIView not being able to be changed and hence reflected in the view itself.

Currently, I have 3 custom UIViews:

TokenView, which only has drawRect overridden to make a view with an image in a circle.

@IBDesignable class TokenView: UIView {

@IBInspectable var tokenOutlineColor: UIColor = UIColor.blackColor()
@IBInspectable var tokenBackgroundColor: UIColor = UIColor.lightGrayColor()
@IBInspectable var tokenImage: UIImage!

override init(frame: CGRect){
super.init(frame:frame)
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

override func drawRect(rect: CGRect) {

var context = UIGraphicsGetCurrentContext()

var thickness: CGFloat = 3
var path = UIBezierPath(ovalInRect: CGRect(origin: CGPointMake(thickness/2, thickness/2), size: CGSize(width: rect.width - thickness, height: rect.height - thickness)))

CGContextSaveGState(context)
path.addClip()

tokenOutlineColor.setStroke()
tokenBackgroundColor.setFill()
path.fill()
if let image = tokenImage {
tokenImage.drawInRect(rect)
}
path.stroke()

}
}


PersonView, which has a TokenView() as a subview, and works as intended as well, when I change my variables of said TokenView.

class PersonView: UIView {

var name: String?
var group: PersonGroup?

required init?(coder aDecoder: NSCoder){
super.init(coder: aDecoder)
makePersonView()
}

override init(frame: CGRect) {
super.init(frame: frame)
makePersonView()
}

func makePersonView(){

if name == nil {
self.name = "Bob"
}
if group == nil {
self.group = PersonGroup.Good
}

let token = TokenView()
token.translatesAutoresizingMaskIntoConstraints = false

switch group! {
case .Good:
token.tokenOutlineColor = UIColor.greenColor()
case .Bad:
token.tokenOutlineColor = UIColor.redColor()
default:
token.tokenOutlineColor = UIColor.blueColor()
}
token.tokenImage = UIImage(named: name!)
token.tokenBackgroundColor = UIColor.clearColor()
token.backgroundColor = UIColor.clearColor()
addSubview(token)

let label = UILabel()
label.text = name
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = NSTextAlignment.Center
addSubview(label)

NSLayoutConstraint.activateConstraints([
token.topAnchor.constraintEqualToAnchor(topAnchor),
token.leadingAnchor.constraintEqualToAnchor(leadingAnchor),
token.trailingAnchor.constraintEqualToAnchor(trailingAnchor),
token.heightAnchor.constraintEqualToConstant(frame.size.height*0.70),
token.bottomAnchor.constraintEqualToAnchor(label.topAnchor),
label.leadingAnchor.constraintEqualToAnchor(leadingAnchor),
label.trailingAnchor.constraintEqualToAnchor(trailingAnchor),
label.bottomAnchor.constraintEqualToAnchor(bottomAnchor)
])
}


and DetailedPersonView class, whereby a PersonView instance is created and its variables being modified from there, and that is where the problem comes about. The other added "details" work fine, just not the ones in the nested PersonView when modified in a function in the class.

class DetailedPersonView: UIView {

var name: String = "Marley"
var group: PersonGroup = PersonGroup.Bad

var quantity: Float = 0.00
var stat: String? = "Strength"

required init?(coder aDecoder: NSCoder){
super.init(coder: aDecoder)
makeDetailedPersonView()

}

override init(frame: CGRect) {
super.init(frame: frame)
makeDetailedPersonView()
}

func makeDetailedPersonView(){
let personView = PersonView()
personView.translatesAutoresizingMaskIntoConstraints = false
personView.name = name
personView.group = group
personView.type = type
addSubview(personView)

let label = UILabel()
let quantityString = String(format: quantity == floor(quantity) ? "%.0f":"%.1f", quantity)
if unit != nil {
label.text = quantityString + " " + stat!
} else {
label.text = quantityString
}
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = NSTextAlignment.Center

addSubview(label)

NSLayoutConstraint.activateConstraints([
personView.topAnchor.constraintEqualToAnchor(topAnchor),
personView.leadingAnchor.constraintEqualToAnchor(leadingAnchor),
personView.trailingAnchor.constraintEqualToAnchor(trailingAnchor),
personView.bottomAnchor.constraintEqualToAnchor(label.topAnchor),
label.leadingAnchor.constraintEqualToAnchor(leadingAnchor),
label.trailingAnchor.constraintEqualToAnchor(trailingAnchor),
label.heightAnchor.constraintEqualToConstant(frame.size.height*0.25),
label.bottomAnchor.constraintEqualToAnchor(bottomAnchor)
])

}


}


So currently my DetailedPersonView has its TokenView (nested within PersonView) showing PersonView's default tokenName and tokenImage, being "Bob", same goes for the circle's outline reflecting PersonGroup.Bad .

I have tried using custom init methods for PersonView, to try to initialize a PersonView in a DetailedPersonView with the variables of DetailedPersonView, but to limited success.

Ideally, this custom subview should work similar to how UILabel does, where UILabel().text changes its text in the view as its variables changes, if that makes sense.

So far haven't found a clear solution to this problem yet :\

Thank you for reading this question! :D

EDIT:
One workaround that I thought might work is to implement a class method to the custom UIView to reset each subview's contents and call it every time a variable has been changed. Or is there already a function in a delegate that can achieve the same results, and is called whenever the view is changed?

Answer

You are changing properties on the subview but not propagating those changes to the subviews that depend on those properties. In swift this is typically done with property observing. See the property observers section here for more info. But you can use the didSetobserver to propagate your changes. For example, in your PersonView class you should first keep a reference to it's TokenView subview var tokenView:TokenView? and it's UILabel subviewvar nameLabel:UILabel?and set them equal to theTokenViewandUILabelyou create inmakePersonView`. Then add property observing on name:

    var name: String?{
        didSet{
            if let image = UIImage(named: name!)
            {
                self.tokenView?.tokenImage = image
                self.nameLabel?.text = name
                self.tokenView?.setNeedsDisplay()
            }
        }
    }

This way any changes to name by DetailPersonView will automatically change TokenView's image.

Similarly with tokenImage and tokenOutlineColor you need to redraw your view each time they are set.

   @IBInspectable var tokenOutlineColor: UIColor = UIColor.blackColor(){
        didSet{
            self.setNeedsDisplay()
        }
    }
    @IBInspectable var tokenImage: UIImage!{
        didSet{
            self.setNeedsDisplay()
        }
    }
Comments