Subclassing UILabel doesn't work as expected

169 views Asked by At

I want to add a UILabel to the view which slides down when an error occurs to send the error message to user and after 3 seconds it will slide up to disappear. The prototype of it is like the one Facebook or Instagram shows. I need errorLabel in many ViewControllers, so I tried to subclass UILabel. Here is my subclass ErrorLabel:

class ErrorLabel: UILabel {
    var errorString: String?

    func sendErrorMessage() {
        self.text = errorString

        showErrorLabel()
        let timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: "hideErrorLabel", userInfo: nil, repeats: false)
    }
    func animateFrameChange() {
        UIView.animateWithDuration(1, animations: { self.layoutIfNeeded() }, completion: nil)
    }
    func showErrorLabel() {
        let oldFrame = self.frame
        let newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.height + 30, oldFrame.width)
        self.frame = newFrame
        self.animateFrameChange()
    }
    func hideErrorLabel() {
        let oldFrame = self.frame
        let newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, oldFrame.height - 30, oldFrame.width)
        self.frame = newFrame
        self.animateFrameChange()
    }
}

Then, I tried to add the errorLabel to one of my ViewController like following:

class ViewController: UIViewController {
    var errorLabel = ErrorLabel()

    override func viewDidLoad() {
        super.viewDidLoad()

        let errorLabelFrame = CGRectMake(0, 20, self.view.frame.width, 0)
        self.errorLabel.frame = errorLabelFrame
        self.errorLabel.backgroundColor = translucentTurquoise
        self.errorLabel.font = UIFont.systemFontOfSize(18)
        self.errorLabel.textColor = UIColor.whiteColor()
        self.errorLabel.textAlignment = NSTextAlignment.Center
        self.view.addSubview(errorLabel)
        self.view.bringSubviewToFront(errorLabel)
    }

    func aFunc(errorString: String) {
        self.errorLabel.errorString = errorString
        self.errorLabel.sendErrorMessage()
    }
}

When I run it in iOS Simulator, it doesn't work as expected:

  1. errorLabel shows on the left horizontally and in the middle vertically with only I... which should be Invalid parameters.
  2. After 1 second, it goes to the position as expected but its width is still not self.view.frame.width.
  3. After that, nothing happens but it should slide up after 3 seconds.

Can you tell me what's wrong and how to fix the error?

2

There are 2 answers

0
Penkey Suresh On

I might have partial solution to your issues. Hope it helps.

  • The I... happens when the string is longer than the view. For this you'll need to increase the size of UILabel.
  • For aligning text inside a UILable refer to this.
  • To animate away use the same code in the completion block of the UIView.animateWithDuration. Refer to this link

I suggest you to consider using Extensions to accomplish what you are trying to do.

0
Sajjon On

Rather than subclassing UILabel I would subclass UIViewController, which maybe you have aldready done? Let's call out subclass - BaseViewController and let all our UIViewControllers subclass this class.

I would then programatically create an UIView which contains a vertically and horizontally centered UILabel inside this BaseViewController class. The important part here is to create NSLayoutConstraints for it. I would then hide and show it by changing the values of the constraints.

I would use the excellent pod named Cartography to create constraints, which makes it super easy and clean!

With this solution you should be able to show or hide an error message in any of your UIViewControllers

This is untested code but hopefully very near a solution to your problem.

import Cartography /* Requires that you have included Cartography in your Podfile */
class BaseViewController: UIViewController {

    private var yPositionForErrorViewWhenVisible: Int { return 0 }
    private var yPositionForErrorViewWhenInvisible: Int { return -50 }
    private let hideDelay: NSTimeInterval = 3
    private var timer: NSTimer!

    var yConstraintForErrorView: NSLayoutConstraint!
    var errorView: UIView!
    var errorLabel: UILabel!

    //MARK: - Initialization
    required init(aDecoder: NSCoder) {
        super.init(aDecoder)
        setup()
    }

    //MARK: - Private Methods
    private func setup() {
        setupErrorView()
    }

    private func setupErrorView() {
        errorView = UIView()
        errorLabel = UILabel()

        errorView.addSubview(errorLabel)
        view.addSubview(errorView)

        /* Set constraints between viewController and errorView and errorLabel */
        layout(view, errorView, errorLabel) {
            parent, errorView, errorLabel in

            errorView.width == parent.width
            errorView.centerX == parent.centerX
            errorView.height == 50

            /* Capture the y constraint, which defaults to be 50 points out of screen, so that it is not visible */
            self.yConstraintForErrorView = (errorView.top == parent.top - self.yPositionForErrorViewWhenInvisible)

            errorLabel.height = 30
            errorLabel.width == errorView.width
            errorLabel.centerX == errorView.centerX
            errorLabel.centerY = errorView.centerY
        }
    }

    private func hideOrShowErrorMessage(hide: Bool, animated: Bool) {
         if hide {
            yConstraintForErrorView.constant = yPositionForErrorViewWhenInvisible
        } else {
            yConstraintForErrorView.constant = yPositionForErrorViewWhenVisible
        }

        let automaticallyHideErrorViewClosure: () -> Void = {
            /* Only scheduling hiding of error message, if we just showed it. */
            if show {
                automaticallyHideErrorMessage()
            }
        }

        if animated {
            view.animateConstraintChange(completion: {
                (finished: Bool) -> Void in
                automaticallyHideErrorViewClosure()
            })
        } else {
            view.layoutIfNeeded()
            automaticallyHideErrorViewClosure()
        }
    }

    private func automaticallyHideErrorMessage() {
        if timer != nil {
            if timer.valid {
                timer.invalidate()
            }
            timer = nil
        }
        timer = NSTimer.scheduledTimerWithTimeInterval(hideDelay, target: self, selector: "hideErrorMessage", userInfo: nil, repeats: false)    
    }

    //MARK: - Internal Methods
    func showErrorMessage(message: String, animated: Bool = true) {
        errorLabel.text = message
        hideOrShowErrorMessage(false, animated: animated)
    }

    //MARK: - Selector Methods
    func hideErrorMessage(animated: Bool = true) {
        hideOrShowErrorMessage(true, animated: animated)
    }
}

extension UIView {

    static var standardDuration: NSTimeInterval { return 0.3 }

    func animateConstraintChange(duration: NSTimeInterval = standardDuration, completion: ((Bool) -> Void)? = nil) {
        UIView.animate(durationUsed: duration, animations: {
            () -> Void in
            self.layoutIfNeeded()
        }, completion: completion)
    }
}