Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom navigation bar items improperly placed

I have a custom navigation bar subclass:

class ProfileNavigationBar: UINavigationBar {

  var titleLabel: UILabel
  var backButton: UIBarButtonItem
  var friendsButton: FriendsButton?

  required init?(coder: NSCoder) {
    titleLabel = UILabel(frame: CGRect(x: 0, y: 40, width: 320, height: 40))
    backButton = UIBarButtonItem.backButton(nil, action: nil)
    friendsButton = FriendsButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
    super.init(coder: coder)
  }

  override func awakeFromNib() {
    super.awakeFromNib()

    let item = UINavigationItem()
    item.titleView = titleLabel

    item.leftBarButtonItem = backButton
    item.hidesBackButton = true

    let friendsItem = UIBarButtonItem(customView: friendsButton!)
    item.rightBarButtonItems = [friendsItem]

    pushItem(item, animated: false)
  }

}

where the FriendsButton resizes itself when it's state property is changed.

Problem is that when the view is first loaded, it appears like this, with the back button and the FriendsButton right at the edge of the nav bar: (.loading state)

enter image description here

However, when I change the FriendsButton state to .add, it appears normally like this:

enter image description here

How can I fix this?

Here is the implementation of FriendsButton:

class FriendsButton: UIView {

  var state: FriendsButtonState {
    didSet {
      style(selected: state)
    }
  }

  var title: String = "" {
    didSet {
      set(title: title)
    }
  }

  var font = UIFont.boldSystemFont(ofSize: 11)

  private var imageView: UIImageView!
  private var button: UIButton!
  var loading: UIActivityIndicatorView!

  init(frame: CGRect, state: FriendsButtonState = .loading) {
    self.state = state
    super.init(frame: frame)
    backgroundColor = .yellow

    let plusSize = frame.size.height/2
    let plusYValue = (frame.size.height-plusSize)/2
    imageView = UIImageView(frame: CGRect(x: plusYValue*2, y: plusYValue, width: plusSize, height: plusSize))
    imageView.contentMode = .scaleAspectFit
    addSubview(imageView)

    let titleSize = (title as NSString).size(attributes: [NSFontAttributeName : font])
    button = UIButton(frame: CGRect(x: plusYValue*3.5 + plusSize, y: 0, width: titleSize.width, height: frame.size.height))
    addSubview(button)

    loading = UIActivityIndicatorView(activityIndicatorStyle: .gray)
    loading.center = center
    addSubview(loading)

    style(selected: state)

    updateSize()
  }

  func addTarget(object: Any, selector: Selector) {
    button.addTarget(object, action: selector, for: .touchUpInside)
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  private func style(selected: FriendsButtonState) {
    configureBorder(state: selected)
    loading.startAnimating()
    loading.isHidden = state != .loading
    isHidden = false

    switch state {
    case .friends:
      backgroundColor = .black
      button.setTitleColor(.white, for: .normal)
      imageView.image = #imageLiteral(resourceName: "friends-tick")
      title = "Friends"
    // ... + all other cases
    }
    self.updateSize()
  }

  private func configureBorder(state: FriendsButtonState) {
    layer.borderColor = UIColor.black.cgColor
    layer.borderWidth = state == .loading ? 0 : 1
    layer.cornerRadius = 5
  }

  private func set(title: String) {
    let plusSize = frame.size.height/2
    let plusYValue = (frame.size.height-plusSize)/2
    let titleSize = (title as NSString).size(attributes: [NSFontAttributeName : font])
    button.titleLabel?.font = font
    button.setTitle(title, for: .normal)
    button.frame = CGRect(x: plusYValue*3.5 + plusSize, y: 0, width: titleSize.width, height: frame.size.height)
    self.updateSize()
  }

  private func updateSize() {
    if state == .loading {
      frame.size.width = frame.size.width
      loading.center = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
      loading.startAnimating()
      return
    }

    let plusSize = frame.size.height/2
    let plusYValue = (frame.size.height-plusSize)/2
    let titleSize = (title as NSString).size(attributes: [NSFontAttributeName : font])
    let totalWidth = plusYValue*5.5 + plusSize + titleSize.width
    frame.size.width = totalWidth
  }

EDIT: I have tried setting the button to the .add state initially but it would still appear at the very right of the nav bar until it was changed to the other state. It seems that the first state of the button always make the nav bar to shift all its children to the edge of the frame until it is updated.

EDIT: I wasn't able to reproduce the problem on another project by copying the relevant code, but this is the specific problem I am having (shown in the image below). The gap between the edge of the navigation bar and the back button is not maintained when first navigating to the view (I managed to get a screenshot midway through the navigation push animation). My question is now, what could be causing this?

enter image description here

like image 604
jacobsieradzki Avatar asked Sep 13 '26 06:09

jacobsieradzki


2 Answers

You'll have to stop the activity indicator, otherwise it won't hide itself:

private func style(selected: FriendsButtonState) {
    configureBorder(state: selected)
    if (state == .loading) {
        loading.startAnimating()
    } else {
        loading.stopAnimating()
    }
    loading.isHidden = state != .loading

Btw: You could also skip loading.isHidden = state != .loading if you configure the UIActivityIndicatorView as to hide itself when its stopped:

init(frame: CGRect, state: FriendsButtonState = .loading) {
     // ...

    loading = UIActivityIndicatorView(activityIndicatorStyle: .gray)
    loading.center = center
    loading.hidesWhenStopped = true
    addSubview(loading)
    // ...
}

To lay out the right navigation bar button correctly, you have to modify FriendsButton.updateSize: When in state .loading - at least for the first time - you also have to update the frame.

private func updateSize() {
    if state == .loading {
        frame.size.width = frame.size.width
        loading.center = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
        loading.startAnimating()
        // do not return here
    }

    let plusSize = frame.size.height/2
    let plusYValue = (frame.size.height-plusSize)/2
    let titleSize = (title as NSString).size(attributes: [NSFontAttributeName : font])
    let totalWidth = plusYValue*5.5 + plusSize + titleSize.width
    frame.size.width = totalWidth
}

I assume the same for the left button, but unfortunalty the your code does not compile here (get an error in the init? method when calling backButton = UIBarButtonItem.backButton(nil, action: nil), because there is no static member backButton on UIBarButtonItem


Now, it works on my machine:

Just after start of the app: Just after start of the app

After state change to .friends After state change to .friends

State changed back again to .loading State changed back again to .loading

Everything is well aligned, no changes etc. If this does not hold true for your project, than there might be other aspects that you didn't publish with your code.

like image 168
Andreas Oetjen Avatar answered Sep 14 '26 20:09

Andreas Oetjen


You should update the frames of the subviews in layoutSubviews(), so you should override this method. The layoutSubviews() method is called whenever the frame changes and when the layout is flagged as being 'dirty' (by calling setNeedsLayout() you can achieve this).

like image 38
Wolfgang Schreurs Avatar answered Sep 14 '26 19:09

Wolfgang Schreurs