UIButton under UIStackView do not updated properly

Hi.


According to article "Start Developing iOS Apps (Swift) / Implement a Custom Control"


https://developer.apple.com/library/archive/referencelibrary/GettingStarted/DevelopiOSAppsSwift/ImplementingACustomControl.html#//apple_ref/doc/uid/TP40015214-CH19-SW1


i implemented all the steps in latest XCode environment but stubbed to one issue: when i update any button's property in my created RatingControl it is not affected imediatly on user interface. Only after i click on any button i can see new button's property updated. I even try to force layout update procedure

button.superview?.superview?.layoutIfNeeded()

button.superview?.superview?.setNeedsDisplay()

but without succeed.



Could someone advice me what do i need for imediatly apply changes on UI.


Thanks!

Replies

Food tracker is known as being pretty buggy, so that may be just one more…


The article you reference is really long.

Could you tell where (copy the part of code) you try to updatea button's property ?


Maybe you could try update the stackView alone :

button.superview?.setNeedsDisplay()

Hi Claude31

I tryed to update stackView and even stackView.superview but all the same.


Here are cutted part of RatingControl UI




@IBDesignable

class RatingControl: UIStackView {


// MARK: Properties

private var ratingButtons = [UIButton]()

var rating = 0 {didSet { updateButtonSelectionStates() }}


@IBInspectable

var starCount : Int = 5





//MARK: Button action

@objc func ratingButtonTapped(button: UIButton) {

print("buttonPressed")

guard let indexOfButton = ratingButtons.firstIndex(of: button) else {

fatalError("The button \(button) is not in the buttons array \(ratingButtons)")

}

let selectedIndex = indexOfButton + 1

if(selectedIndex == rating) {

rating = 0

} else {

rating = selectedIndex

}

}


func updateButtonSelectionStates() {

for (index, button) in ratingButtons.enumerated() {

print("executing: index: \(index), button: \(button.description)")

button.isSelected = index < rating

button.superview?.superview?.layoutIfNeeded() // this don't work

button.superview?.superview?.setNeedsDisplay() // this don't work

}

}

}


Could you suggest maybe kind of contemporary book for people coming from C# like me )

Where and how did you define that button appearance should change when selected ?


Just try this to see if any effect


    func updateButtonSelectionStates() {
        for (index, button) in ratingButtons.enumerated() {
            print("executing: index: \(index), button: \(button.description)")
            button.isSelected = index < rating
          if button.isSelected  {
            button.backgroundColor = UIColor.red
          } else {
            button.backgroundColor = UIColor.blue
          }
            button.superview?.superview?.layoutIfNeeded() // this don't work
            button.superview?.superview?.setNeedsDisplay() // this don't work
        }
    }

I tryed, but behavior didn't changed ((


When i first time touch the button i don't see any changes. But when i once more touch anything from UI - and magic - RateControl updates. I presume that there is have to be additional calling something like "refreshing" method, or redraw control.


PS I tried to programmably add new button and update it appearance from TimerFireUp Procedure - and all work nice. Maybe problem with the bunch stackView->buttons array but i don't think so.


Anyway, thanks.

On first touch, what do you get from the print ?

print("executing: index: \(index), button: \(button.description)")


Can you show the code where you initialize ratingButtons ?


In fact, could you copy the complete code of the class ?

So, is it solved ?

Hi Claude31! Sorry for delay - was on vacation in suburb.


First thing:

On first touch, what do you get from the print ?

print("executing: index: \(index), button: \(button.description)")


On first touch updateButtonSelectionStates procedure executes properly and i see this:

buttonPressed

executing: index: 0, button: <UIButton: 0x7fcf0cc22ec0; frame = (0 0; 20 20); opaque = NO; layer = <CALayer: 0x600000ed5a40>>

executing: index: 1, button: <UIButton: 0x7fcf0cc231e0; frame = (30 0; 20 20); opaque = NO; layer = <CALayer: 0x600000ed6400>>

executing: index: 2, button: <UIButton: 0x7fcf0cc23500; frame = (60 0; 20 20); opaque = NO; layer = <CALayer: 0x600000ed7fe0>>

executing: index: 3, button: <UIButton: 0x7fcf0cc23820; frame = (90 0; 20 20); opaque = NO; layer = <CALayer: 0x600000ed5160>>

executing: index: 4, button: <UIButton: 0x7fcf0cc23b40; frame = (120 0; 20 20); opaque = NO; layer = <CALayer: 0x600000ed5fa0>>


And nothing happens on user interface... But then i second time click on any button - ui updates regarding previouse button state (not current state).


Here my full code of RatingControl.swift


//
//  RatingControl.swift
//  HelloWorld.Foods
//
//  Created by Romeo on 21/08/2019.
//  Copyright © 2019 Romeo. All rights reserved.
//


import UIKit


@IBDesignable
class RatingControl: UIStackView {
    /*
    // Only override draw() if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    override func draw(_ rect: CGRect) {
        // Drawing code
    }
    */
  
    // MARK: Properties
    private var ratingButtons = [UIButton]()
    var rating = 0 {didSet { updateButtonSelectionStates() }}
  
    @IBInspectable
    var starSize : CGSize = CGSize(width: 44.0, height: 44.0) {
        didSet {
            setupButtons()
        }
    }
  
    @IBInspectable
    var starCount : Int = 5 {
        didSet {
            setupButtons()
        }
    }
  
  
    // MARK: Initialization
  
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupButtons()
    }
  
    required init(coder: NSCoder) {
        super.init(coder: coder)
        setupButtons()
    }
  
    // MARK: Private buttons
  
    private func setupButtons()->Void {
        let bundle = Bundle(for: type(of: self))
        let emptyStar = UIImage(named: "emptyStar", in: bundle, compatibleWith: self.traitCollection)
        let filledStar = UIImage(named: "filledStar", in: bundle, compatibleWith: self.traitCollection)
        let highLightedStar = UIImage(named: "highLightedStar", in: bundle, compatibleWith: self.traitCollection)
      
      
        for button in ratingButtons {
            removeArrangedSubview(button)
            button.removeFromSuperview()
        }
        ratingButtons.removeAll()
      
      
        for _ in 0..<starCount {
            let button = UIButton()
            button.backgroundColor = UIColor.blue
            //button.isUserInteractionEnabled = true
            //button.setImage(emptyStar, for: .normal)
            //button.setImage(filledStar, for: .selected)
            //button.setImage(highLightedStar, for: .highlighted)
            //button.setImage(highLightedStar, for: [.selected, .highlighted])


            button.translatesAutoresizingMaskIntoConstraints = false
            button.heightAnchor.constraint(equalToConstant: starSize.height).isActive = true
            button.widthAnchor.constraint(equalToConstant: starSize.width).isActive = true
            button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(button:)), for: UIControl.Event.touchUpInside)
          
            addArrangedSubview(button)
            ratingButtons.append(button)
        }
    }
  
    //MARK: Button action
    @objc func ratingButtonTapped(button: UIButton) {
        print("buttonPressed")
      
        guard let indexOfButton = ratingButtons.firstIndex(of: button) else {
            fatalError("The button \(button) is not in the buttons array \(ratingButtons)")
        }
      
        let selectedIndex = indexOfButton + 1
      
        if(selectedIndex == rating) {
            rating = 0
        } else {
            rating = selectedIndex
        }
    }
  
    func updateButtonSelectionStates() {
        for (index, button) in ratingButtons.enumerated() {
            print("executing: index: \(index), button: \(button.description)")
            button.isSelected = index < rating
          
            if button.isSelected  {
                button.backgroundColor = UIColor.red
            } else {
                button.backgroundColor = UIColor.blue
            }
          
            //button.backgroundColor = UIColor(red: 0.1, green: 0.80, blue: 0.1, alpha: 1.0)
            button.setTitle("s", for: .selected)
            //button.layoutIfNeeded()
          
            button.superview?.superview?.layoutIfNeeded()
            button.superview?.superview?.setNeedsDisplay()
            button.superview?.setNeedsDisplay()
        }
    }
  
    @objc
    func testBtnUpdate()->Void {
        print("testBtnUpdate()")
        let t0 = ratingButtons.first?.isSelected
        if let t1 = t0 {
            ratingButtons.first?.isSelected = !t1
        }
    }


}

And nothing happens on user interface... But then i second time click on any button - ui updates regarding previouse button state (not current state).


What should occur ? Color change ? Other ?


Could you check the value of rating in updateButtonSelectionStates.


UIStackView doc says:

The UIStackView is a nonrendering subclass of UIView; that is, it does not provide any user interface of its own. Instead, it just manages the position and size of its arranged views. As a result, some properties (like backgroundColor) have no effect on the stack view.


So, I you may have to ask for redraw buttons themselves:


    func updateButtonSelectionStates() {
        for (index, button) in ratingButtons.enumerated() {
            print("executing: index: \(index), button: \(button.description)")
            button.isSelected = index < rating
            print("rating", rating, "button.isSelected", button.isSelected)          // Just to check
            if button.isSelected  {
                button.backgroundColor = UIColor.red
            } else {
                button.backgroundColor = UIColor.blue
            }
       
            //button.backgroundColor = UIColor(red: 0.1, green: 0.80, blue: 0.1, alpha: 1.0)
            button.setTitle("s", for: .selected)
            //button.layoutIfNeeded()
       
            button.superview?.superview?.layoutIfNeeded()
            button.superview?.superview?.setNeedsDisplay()
            button.superview?.setNeedsDisplay()
            button.setNeedsDisplay()          // ADD this
       }
    }


In your code :

        if (selectedIndex == rating) {
            rating = 0
        } else {
            rating = selectedIndex
        }


Do you reset rating to zero if you type the present rating ?

The thing is that rating in updateButtonSelectionStates changes properly and it reset to zero if i type to present rating. Only issue that i can't see present changes in user interface.


See my action in screenshots


At first i click 3-rd star - i see nothing

https://i.postimg.cc/ydvh2Bsg/scr00.png

Then i click last star - i see updated control with previouse state - first 3 stars active

https://i.postimg.cc/X7ZK1Thq/scr01.png


Now i click 2nd star - i see updated control with all stars active

https://i.postimg.cc/PJ7Wr4Ld/scr02.png


and so on.....

https://i.postimg.cc/L4QBTHxG/scr03.png

And to clarify situation, i've updated RatingControl code as this:


//
//  RatingControl.swift
//  HelloWorld.Foods
//
//  Created by Romeo on 21/08/2019.
//  Copyright © 2019 Romeo. All rights reserved.
//


import UIKit


@IBDesignable
class RatingControl: UIStackView {
    /*
    // Only override draw() if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    override func draw(_ rect: CGRect) {
        // Drawing code
    }
    */
    
    // MARK: Properties
    private var ratingButtons = [UIButton]()
    var rating = 0 {didSet { updateButtonSelectionStates() }}
    
    @IBInspectable
    var starSize : CGSize = CGSize(width: 44.0, height: 44.0) {
        didSet {
            setupButtons()
        }
    }
    
    @IBInspectable
    var starCount : Int = 5 {
        didSet {
            setupButtons()
        }
    }
    
    
    // MARK: Initialization
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupButtons()
    }
    
    required init(coder: NSCoder) {
        super.init(coder: coder)
        setupButtons()
    }
    
    // MARK: Private buttons
    
    private func setupButtons()->Void {
        let bundle = Bundle(for: type(of: self))
        let emptyStar = UIImage(named: "emptyStar", in: bundle, compatibleWith: self.traitCollection)
        let filledStar = UIImage(named: "filledStar", in: bundle, compatibleWith: self.traitCollection)
        let highLightedStar = UIImage(named: "highLightedStar", in: bundle, compatibleWith: self.traitCollection)
        
        
        for button in ratingButtons {
            removeArrangedSubview(button)
            button.removeFromSuperview()
        }
        ratingButtons.removeAll()
        
        
        for _ in 0..<starCount {
            let button = UIButton()
            //button.backgroundColor = UIColor.blue
            button.isUserInteractionEnabled = true
            button.setImage(emptyStar, for: .normal)
            button.setImage(filledStar, for: .selected)
            button.setImage(highLightedStar, for: .highlighted)
            button.setImage(highLightedStar, for: [.selected, .highlighted])


            button.translatesAutoresizingMaskIntoConstraints = false
            button.heightAnchor.constraint(equalToConstant: starSize.height).isActive = true
            button.widthAnchor.constraint(equalToConstant: starSize.width).isActive = true
            button.addTarget(self, action: #selector(RatingControl.ratingButtonTapped(button:)), for: UIControl.Event.touchUpInside)
            
            addArrangedSubview(button)
            ratingButtons.append(button)
        }
    }
    
    //MARK: Button action
    @objc func ratingButtonTapped(button: UIButton) {
        print("buttonPressed")
        
        guard let indexOfButton = ratingButtons.firstIndex(of: button) else {
            fatalError("The button \(button) is not in the buttons array \(ratingButtons)")
        }
        
        let selectedIndex = indexOfButton + 1
        
        if(selectedIndex == rating) {
            rating = 0
        } else {
            rating = selectedIndex
        }
    }
    
    func updateButtonSelectionStates() {
        for (index, button) in ratingButtons.enumerated() {
            print("executing: index: \(index), button: \(button.description)")
            
            button.isSelected = index < rating


            //button.superview?.superview?.layoutIfNeeded()
            //button.superview?.superview?.setNeedsDisplay()
            //button.superview?.setNeedsDisplay()
        }
    }
    
    @objc
    func testBtnUpdate()->Void {
        print("testBtnUpdate()")
        let t0 = ratingButtons.first?.isSelected
        if let t1 = t0 {
            ratingButtons.first?.isSelected = !t1
        }
    }


}

Thanks.


But you didn't answer my questions:

And nothing happens on user interface... But then i second time click on any button - ui updates regarding previouse button state (not current state).

- What should occur when you tap ? Color change ? Other ?


- Could you check the value of rating in updateButtonSelectionStates.

Claude31

thanks.


So then i tap the button, color of the buttons to the left must change.



The thing is that rating in updateButtonSelectionStates changes properly and it reset to zero if i type to present rating. Only issue that i can't see present changes in user interface.

I notice in your code:


for _ in 0..<starCount {

let button = UIButton()

button.isUserInteractionEnabled = true

button.setImage(emptyStar, for: .normal)

button.setImage(filledStar, for: .selected)

button.setImage(highLightedStar, for: .highlighted)

button.setImage(highLightedStar, for: [.selected, .highlighted])


So, image is specified twice for .selected and .highlighted



Doc states also:

var isSelected: Bool { get set }

Discussion

Set the value of this property to true to select it or false to deselect it. Most controls do not modify their appearance or behavior when selected, but some do.


So, you could try

    func updateButtonSelectionStates() {
        for (index, button) in ratingButtons.enumerated() {
            print("executing: index: \(index), button: \(button.description)")
           
            button.isSelected = index < rating
            button.setImage(filledStar, for: .selected)     // Call again
            button.setNeedsDisplay()

            //button.superview?.superview?.layoutIfNeeded()
            //button.superview?.superview?.setNeedsDisplay()
            //button.superview?.setNeedsDisplay()
        }
    }

I tryed your code, but unfortunately all the same... Moreother, i tryed to load compleate demo project (https://developer.apple.com/sample-code/swift/downloads/04_ImplementACustomControl.zip), updated it to XCode lastest version, compiled and run it..... and the same issue occurs! So, as i understand this is global issue

You should file a bug report against the demo.