Home > Enterprise >  Tried making a custom ScrollView, but instead of scrolling it's spamming up and down
Tried making a custom ScrollView, but instead of scrolling it's spamming up and down

Time:09-21

I tried to create some kind of timeline (with the Vector Illustrator mentality), using UIBezier and UI Label (kind of like in the calendar app) and then use UIPanGestureRecognizer to scroll it up and down. But whenever I scroll it in the simulator, it multiplies itself instead of moving like the images below (I use setNeedsDisplay as the scrollValue changes to redraw the whole mechanism). This is probably a small mistake a I did or maybe my code doesn't work.

I know I could use a UIScrollView or UITableView instead, but I tried making this as a small challenge as a custom made table because using pre-made objects feels limiting for someone like me who is used to CAD drawing or Vector Illustrator.

This image explains what happens in the Simulator:

Image - Project Results

The code I used is below:

import UIKit

class ViewController: UIViewController {
    
    
    var tlobject = TimelineView()
    let gesto = UIPanGestureRecognizer()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ===== Add TimelineView Object to view
        let TLObjectFrame = CGRect(x: 0, y: 40, width: 100, height: 100)
        tlobject = TimelineView(frame: TLObjectFrame)
        view.addSubview(tlobject)
        
        // ===== ADD TOUCH GESTURE =====
        gesto.addTarget(self, action: #selector(touchinput))
        view.addGestureRecognizer(gesto)
    }
    
    var touchStartLocation: Int = 0
    var scrollDistance: Int = 0
    var lastScrollDistance: Int = 0
    
    //The following func calculates the distance scrolled/travelled by Touch gesture on the YAxis and sends the result value (scrollDistance) to the Timeline mechanism where it defines the Yposition of every UIBezier. Thanks to Mitchell Hudson on Youtube for helping me figure out how to do it on his Tutorial "06 11 touches value"
    @objc func touchinput (sender: UIPanGestureRecognizer) {
        
        if sender.state == UIGestureRecognizer.State.began {
            touchStartLocation = Int(sender.location(in: view).y)
            lastScrollDistance = scrollDistance
        }
        
        if sender.state == UIGestureRecognizer.State.changed {
            let touchEndLocation = Int(sender.location(in: view).y)
            let currentScrollDistance = touchEndLocation - touchStartLocation
            print("deltaY", currentScrollDistance)
            var newScrollDistance = lastScrollDistance   currentScrollDistance
            scrollDistance = newScrollDistance
            
            tlobject.totalScrollDistance = scrollDistance //send scrollValue to TimelineView
        }
        
        if sender.state == UIGestureRecognizer.State.ended {
            print("lastScrollDistance", lastScrollDistance)
            print("scroll Distance", scrollDistance)
        }
    }
}


//Created a new View with the TimeLine mechanism
class TimelineView: UIView {
    
    var totalScrollDistance: Int = 0 {
        didSet{
            setNeedsDisplay() //this gets called everytime UIgesture position changes
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    
        self.backgroundColor = UIColor.clear
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func draw(_ rect: CGRect) {
        timelinemechanism()
    }
    
    func timelinemechanism() {
        
        let lineElements: Array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        let spacing: Int = 30
        let scrollDistance: Int = totalScrollDistance
        let totalElements: Int = lineElements.count
        
        for n in 1...totalElements {
            
            //Get UILabel/UILine Yposition on screen = Array index number * the spacing   scroll distance by touch pan gesture
            let yPosition = lineElements[n - 1] * spacing   scrollDistance
            
            let linepath = UIBezierPath()
            linepath.move(to: CGPoint(x: 60, y: yPosition))
            linepath.addLine(to: CGPoint(x: 300, y: yPosition))
            
            let lineshape = CAShapeLayer()
            lineshape.path = linepath.cgPath
            lineshape.strokeColor = UIColor.blue.cgColor
            //lineshape.fillColor = UIColor.white.cgColor
            //lineshape.lineWidth = 1
            self.layer.addSublayer(lineshape)
            
            let hourlabel = UILabel()
            hourlabel.frame = CGRect(x: 5, y: yPosition - 20, width: 45, height: 40)
            hourlabel.text = "\(n):00"
            //hourlabel.font = UIFont(name: "Avenir-Claro", size: 12)
            hourlabel.textColor = UIColor.blue
            hourlabel.textAlignment = NSTextAlignment.right
            self.addSubview(hourlabel)
        }
    }
}

CodePudding user response:

Inside draw you only have to draw something. You add new subviews/sublayers and do not remove old ones.

Creating a new view every time you change a frame is very resource-intensive. And you don't need that, because you have the same views, you only need to change the position.

Instead, you can create your views at start and use layoutSubviews to update your views positions:

class TimelineView: UIView {

    var totalScrollDistance: Int = 0 {
        didSet{
            setNeedsLayout() //this gets called everytime UIgesture position changes
        }
    }

    private var lastLayoutTotalScrollDistance: Int = 0

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

        self.backgroundColor = UIColor.clear
        createTimelinemechanism()
    }

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

    var lineShapes = [CAShapeLayer]()
    var hourLabels = [UILabel]()

    override func layoutSubviews() {
        super.layoutSubviews()
        let offset = totalScrollDistance - lastLayoutTotalScrollDistance
        lastLayoutTotalScrollDistance = totalScrollDistance
        lineShapes.forEach { lineShape in
            lineShape.frame.origin.y  = CGFloat(offset)
        }
        hourLabels.forEach { hourLabel in
            hourLabel.frame.origin.y  = CGFloat(offset)
        }
    }

    func createTimelinemechanism() {
        let lineElements: Array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
        let spacing: Int = 30
        let totalElements: Int = lineElements.count

        for n in 1...totalElements {

            //Get UILabel/UILine Yposition on screen = Array index number * the spacing   scroll distance by touch pan gesture
            let yPosition = lineElements[n - 1] * spacing

            let linepath = UIBezierPath()
            linepath.move(to: CGPoint(x: 60, y: yPosition))
            linepath.addLine(to: CGPoint(x: 300, y: yPosition))

            let lineshape = CAShapeLayer()
            lineshape.path = linepath.cgPath
            lineshape.strokeColor = UIColor.blue.cgColor
            //lineshape.fillColor = UIColor.white.cgColor
            //lineshape.lineWidth = 1

            // disable default layer position animation
            lineshape.actions = [
                "position": NSNull(),
            ]
            self.layer.addSublayer(lineshape)
            lineShapes.append(lineshape)
            let hourlabel = UILabel()
            hourlabel.frame = CGRect(x: 5, y: yPosition - 20, width: 45, height: 40)
            hourlabel.text = "\(n):00"
            //hourlabel.font = UIFont(name: "Avenir-Claro", size: 12)
            hourlabel.textColor = UIColor.blue
            hourlabel.textAlignment = NSTextAlignment.right
            self.addSubview(hourlabel)
            hourLabels.append(hourlabel)
        }
    }
}

More generally, you can just list all the subviews/sublayers and not keep them in separate containers.

CodePudding user response:

You need to explicitly clear the area of the view's layer that you want to draw into so that older graphics are removed before you draw new graphics.

I your draw method:

override func draw(_ rect: CGRect) {
    if let cgContext = UIGraphicsGetCurrentContext() {
      cgContext.clear(rect)
    }

    timelinemechanism()
}
  • Related