Search code examples
swiftscrolluilabelsubclassmarquee

How to make a simple UILabel subclass for marquee/scrolling text effect in swift?


As you can see above i trying to code an simple(!) subclass of UILabel to make an marquee or scrolling text effect if the text of the label is too long. I know that there are already good classes out there (e.g https://cocoapods.org/pods/MarqueeLabel), but i want to make my own :)

Down below you can see my current class. I can't also fix an issue where the new label(s) are scrolling right, but there is also a third label which shouldn't be there. I think it's the label itself. But when i try the replace the first additional label with that label i won't work. I hope it's not too confusing :/

It's important to me that i only have to assign the class in the storyboard to the label. So that there is no need go and add code e.g in an view controller (beside the outlets). I hope it's clear what i want :D

So again:

  • Simple subclass of UILabel
  • scrolling label
  • should work without any additional code in other classes (except of outlets to change the labels text for example,...)

(it's my first own subclass, so feel free to teach me how to do it right :) )

Thank you very much !

It's by far not perfect, but this is my current class:

import UIKit

class LoopLabel: UILabel {

    var labelText : String?
    var rect0: CGRect!
    var rect1: CGRect!
    var labelArray = [UILabel]()
    var isStop = false
    var timeInterval: TimeInterval!
    let leadingBuffer = CGFloat(25.0)
    let loopStartDelay = 2.0

   required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.lineBreakMode = .byClipping
   }

    override var text: String? {
        didSet {
            labelText = text
            setup()
        }
    }

    func setup() {
        let label = UILabel()
        label.frame = CGRect.zero
        label.text = labelText

        timeInterval = TimeInterval((labelText?.characters.count)! / 5)
        let sizeOfText = label.sizeThatFits(CGSize.zero)
        let textIsTooLong = sizeOfText.width > frame.size.width ? true : false

        rect0 = CGRect(x: leadingBuffer, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
        rect1 = CGRect(x: rect0.origin.x + rect0.size.width, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
        label.frame = rect0

        super.clipsToBounds = true
        labelArray.append(label)
        self.addSubview(label)

        self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: 0, height: 0))

        if textIsTooLong {
            let additionalLabel = UILabel(frame: rect1)
            additionalLabel.text = labelText
            self.addSubview(additionalLabel)

            labelArray.append(additionalLabel)

            animateLabelText()
        }
    }

    func animateLabelText() {
        if(!isStop) {
            let labelAtIndex0 = labelArray[0]
            let labelAtIndex1 = labelArray[1]

            UIView.animate(withDuration: timeInterval, delay: loopStartDelay, options: [.curveLinear], animations: {
                labelAtIndex0.frame = CGRect(x: -self.rect0.size.width,y: 0,width: self.rect0.size.width,height: self.rect0.size.height)
                labelAtIndex1.frame = CGRect(x: labelAtIndex0.frame.origin.x + labelAtIndex0.frame.size.width,y: 0,width: labelAtIndex1.frame.size.width,height: labelAtIndex1.frame.size.height)
            }, completion: { finishied in
                labelAtIndex0.frame = self.rect1
                labelAtIndex1.frame = self.rect0

                self.labelArray[0] = labelAtIndex1
                self.labelArray[1] = labelAtIndex0
                self.animateLabelText()
            })
        } else {
            self.layer.removeAllAnimations()
        }
    }
}

Solution

  • First of, I would keep the variables private if you don't need them to be accessed externally, especially labelText (since you're using the computed property text to be set).

    Second, since you're adding labels as subviews, I'd rather use a UIView as container instead of UILabel. The only difference in the storyboard would be to add a View instead of a Label.

    Third, if you use this approach, you should not set the frame (of the view) to zero.

    Something like that would do:

    import UIKit
    
    class LoopLabelView: UIView {
    
        private var labelText : String?
        private var rect0: CGRect!
        private var rect1: CGRect!
        private var labelArray = [UILabel]()
        private var isStop = false
        private var timeInterval: TimeInterval!
        private let leadingBuffer = CGFloat(25.0)
        private let loopStartDelay = 2.0
    
        required public init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
        }
    
        var text: String? {
            didSet {
                labelText = text
                setup()
            }
        }
    
        func setup() {
            self.backgroundColor = UIColor.yellow
            let label = UILabel()
            label.text = labelText
            label.frame = CGRect.zero
    
            timeInterval = TimeInterval((labelText?.characters.count)! / 5)
            let sizeOfText = label.sizeThatFits(CGSize.zero)
            let textIsTooLong = sizeOfText.width > frame.size.width ? true : false
    
            rect0 = CGRect(x: leadingBuffer, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
            rect1 = CGRect(x: rect0.origin.x + rect0.size.width, y: 0, width: sizeOfText.width, height: self.bounds.size.height)
            label.frame = rect0
    
            super.clipsToBounds = true
            labelArray.append(label)
            self.addSubview(label)
    
            //self.frame = CGRect(origin: self.frame.origin, size: CGSize(width: 0, height: 0))
    
            if textIsTooLong {
                let additionalLabel = UILabel(frame: rect1)
                additionalLabel.text = labelText
                self.addSubview(additionalLabel)
    
                labelArray.append(additionalLabel)
    
                animateLabelText()
            }
        }
    
        func animateLabelText() {
            if(!isStop) {
                let labelAtIndex0 = labelArray[0]
                let labelAtIndex1 = labelArray[1]
    
                UIView.animate(withDuration: timeInterval, delay: loopStartDelay, options: [.curveLinear], animations: {
                    labelAtIndex0.frame = CGRect(x: -self.rect0.size.width,y: 0,width: self.rect0.size.width,height: self.rect0.size.height)
                    labelAtIndex1.frame = CGRect(x: labelAtIndex0.frame.origin.x + labelAtIndex0.frame.size.width,y: 0,width: labelAtIndex1.frame.size.width,height: labelAtIndex1.frame.size.height)
                }, completion: { finishied in
                    labelAtIndex0.frame = self.rect1
                    labelAtIndex1.frame = self.rect0
    
                    self.labelArray[0] = labelAtIndex1
                    self.labelArray[1] = labelAtIndex0
                    self.animateLabelText()
                })
            } else {
                self.layer.removeAllAnimations()
            }
        }
    }