Линейка рисования UIScrollView с использованием drawRect

Я пытаюсь нарисовать линейку поверх UIScrollView. Я делаю это путем добавления настраиваемого представления под названием RulerView. Я добавляю этот rulerView в супервизор scrollView, устанавливая его фрейм таким же, как фрейм scrollView. Затем я делаю собственный рисунок, чтобы рисовать линии как прокрутки scrollView. Но рисунок не гладкий, он заикается, когда я прокручиваю, и конечная или начальная линия внезапно появляется / исчезает. Что не так в моем drawRect?

class RulerView: UIView {

   public var contentOffset = CGFloat(0) {
      didSet {
         self.setNeedsDisplay()
      }
   }

   public var contentSize = CGFloat(0)

   let smallLineHeight = CGFloat(4)
   let bigLineHeight = CGFloat(10)


  override open func layoutSubviews() {   
    super.layoutSubviews()
    self.backgroundColor = UIColor.clear     
  }

  override func draw(_ rect: CGRect) {
 
     UIColor.white.set()
     let contentWidth = max(rect.width, contentSize)
     let lineGap:CGFloat = 5
    
     let totalNumberOfLines = Int(contentWidth/lineGap)
    
     let startIndex = Int(contentOffset/lineGap)
     let endIndex = Int((contentOffset + rect.width)/lineGap)
     let beginOffset = contentOffset - CGFloat(startIndex)*lineGap
    
     if let context = UIGraphicsGetCurrentContext() {
        for i in startIndex...endIndex {
            let path = UIBezierPath()
            path.move(to: CGPoint(x: beginOffset + CGFloat(i - startIndex)*lineGap , y:0))
            path.addLine(to: CGPoint(x: beginOffset + CGFloat(i - startIndex)*lineGap, y: i % 5 == 0 ? bigLineHeight : smallLineHeight))
            path.lineWidth = 0.5
            path.stroke()

            
        }
    }
    
}

И в делегате scrollview я установил это:

  //MARK:- UIScrollViewDelegate

public func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let offset = scrollView.contentOffset.x
    
    rulerView.contentSize = scrollView.contentSize.width
    rulerView.contentOffset = offset
}

person Deepak Sharma    schedule 26.10.2020    source источник
comment
Вид вашей линейки находится внутри или за пределами прокрутки?   -  person DonMag    schedule 26.10.2020
comment
Он находится за пределами UIScrollView вместе с текстами на больших линиях.   -  person Deepak Sharma    schedule 27.10.2020


Ответы (1)


Ваш override func draw(_ rect: CGRect) очень тяжелый. Я думаю, вы получите гораздо лучшую производительность, если будете использовать слой-фигуру для отметок и позволить UIKit обрабатывать рисунок.


Изменить - согласно комментариям

Добавлена ​​нумерация делений с использованием CATextLayer в качестве подслоев.

Вот образец RulerView (с использованием размеров и интервалов ваших отметок):

class RulerView: UIView {

    public var contentOffset: CGFloat = 0 {
        didSet {
            layer.bounds.origin.x = contentOffset
        }
    }
    public var contentSize = CGFloat(0) {
        didSet {
            updateRuler()
        }
    }
    
    let smallLineHeight: CGFloat = 4
    let bigLineHeight: CGFloat = 10
    let lineGap:CGFloat = 5
    
    // numbers under the tick marks
    //  with 12-pt system font .light
    //  40-pt width will fit up to 5 digits
    let numbersWidth: CGFloat = 40
    let numbersFontSize: CGFloat = 12

    var shapeLayer: CAShapeLayer!
    
    override class var layerClass: AnyClass {
        return CAShapeLayer.self
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        shapeLayer = self.layer as? CAShapeLayer
        // these properties don't change
        backgroundColor = .clear
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 0.5
        shapeLayer.masksToBounds = true
    }
    func updateRuler() -> Void {
        // size is set by .fontSize, so ofSize here is ignored
        let numbersFont = UIFont.systemFont(ofSize: 1, weight: .light)
        let pth = UIBezierPath()
        var x: CGFloat = 0
        var i = 0
        while x < contentSize {
            pth.move(to: CGPoint(x: x, y: 0))
            pth.addLine(to: CGPoint(x: x, y: i % 5 == 0 ? bigLineHeight : smallLineHeight))
            
            // number every 10 ticks - change as desired
            if i % 10 == 0 {
                let layer = CATextLayer()
                
                layer.contentsScale = UIScreen.main.scale
                layer.font = numbersFont
                layer.fontSize = numbersFontSize
                layer.alignmentMode = .center
                layer.foregroundColor = UIColor.white.cgColor

                // if we want to number by tick count
                layer.string = "\(i)"
                
                // if we want to number by point count
                //layer.string = "\(i * Int(lineGap))"
                
                layer.frame = CGRect(x: x - (numbersWidth * 0.5), y: bigLineHeight, width: numbersWidth, height: numbersFontSize)
                
                shapeLayer.addSublayer(layer)
            }
            
            x += lineGap
            i += 1
        }
        shapeLayer.path = pth.cgPath

    }
}

и вот пример класса контроллера для демонстрации:

class RulerViewController: UIViewController, UIScrollViewDelegate {
    var rulerView: RulerView = RulerView()
    var scrollView: UIScrollView = UIScrollView()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .blue
        
        [scrollView, rulerView].forEach {
            view.addSubview($0)
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
        
        // sample scroll content will be a horizontal stack view
        //  with 30 labels
        //  spaced 20-pts apart
        let stack = UIStackView()
        stack.translatesAutoresizingMaskIntoConstraints = false
        stack.spacing = 20
        
        for i in 1...30 {
            let v = UILabel()
            v.textAlignment = .center
            v.backgroundColor = .yellow
            v.text = "Label \(i)"
            stack.addArrangedSubview(v)
        }
        
        scrollView.addSubview(stack)
        
        let g = view.safeAreaLayoutGuide
        let contentG = scrollView.contentLayoutGuide
        
        NSLayoutConstraint.activate([
            
            // scroll view 20-pts Top / Leading / Trailing
            scrollView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            // scroll view Height: 60-pts
            scrollView.heightAnchor.constraint(equalToConstant: 60.0),
            
            // stack view 20-pts Top, 0-pts Leading / Trailing / Bottom (to scroll view's content layout guide)
            stack.topAnchor.constraint(equalTo: contentG.topAnchor, constant: 20.0),
            stack.leadingAnchor.constraint(equalTo: contentG.leadingAnchor, constant: 0.0),
            stack.trailingAnchor.constraint(equalTo: contentG.trailingAnchor, constant: 0.0),
            stack.bottomAnchor.constraint(equalTo: contentG.bottomAnchor, constant: 0.0),
            
            // ruler view 4-pts from scroll view Bottom
            rulerView.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: 4.0),
            rulerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
            // ruler view 0-pts from scroll view Leading / Trailing (equal width and horizontal position of scroll view)
            rulerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
            // ruler view Height: 24-pts (make sure it's enough to accomodate ruler view's bigLineHeight plus numbering height)
            rulerView.heightAnchor.constraint(equalToConstant: 24.0),

        ])
        
        scrollView.delegate = self
        
        // so we can see the sroll view frame
        scrollView.backgroundColor = .red
        
        // if we want to see the rulerView's frame
        //rulerView.backgroundColor = .brown
        
    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // this is when we know the scroll view's content size
        rulerView.contentSize = scrollView.contentSize.width
    }
    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // update rulerView's x-offset
        rulerView.contentOffset = scrollView.contentOffset.x
    }
    
}

Выход:

введите описание изображения здесь

отметки (и числа), конечно, будут прокручиваться влево-вправо синхронно с просмотром прокрутки.

person DonMag    schedule 26.10.2020
comment
Спасибо за ответ. Мне также нужно нанести текст на маркировку линейки, такую ​​как 1.0, 2.0, 3.0, ... Я думал, что drawRect позволит мне это сделать. Ваш подход не использует drawRect. Как мне расширить его, чтобы нанести текст на метки? Я вижу, вы помещаете метки в стек. Однако я бы хотел, чтобы они были ниже отметки. - person Deepak Sharma; 27.10.2020
comment
@DeepakSharma - при обращении за помощью рекомендуется размещать свой полный вопрос, а не мелкие кусочки и кусочки. Я отредактировал свой ответ с помощью одного способа сделать это. Конечно, существует несколько подходов, но, не зная, что вы на самом деле пытаетесь сделать, я не могу понять, подходит ли это для вашей конечной цели. - person DonMag; 27.10.2020
comment
@DeepakSharma - Если прокрутка сейчас очень медленная, значит, вы делаете что-то еще, чего не раскрыли здесь. - person DonMag; 27.10.2020
comment
@DeepakSharma - вот ссылка на неотредактированную запись экрана: диск .google.com / file / d / 1bwhIsO3g_Ta37Qf__zxK2iB7bA__B89- / - person DonMag; 27.10.2020
comment
Теперь он работает, я устанавливал и contentSize, и contentOffset в scrollViewDidScroll. Повторная установка contentSize была проблемой. - person Deepak Sharma; 27.10.2020
comment
В этом ответе используется CAShapeLayer, фрейм которого по сути является шириной содержимого UIScrollView. По мере прокрутки вы просто обновляете начало границ слоя формы (что, по сути, то, что UIScrollView делает внутренне, я полагаю). Мой вопрос в том, рисует ли UIKit только ту часть, которая видна на экране во время выполнения, которую мы прокручиваем, или он сохраняет полное представление, которое поддерживает слой в памяти? Просто пытаюсь понять основы здесь. - person Deepak Sharma; 27.10.2020
comment
@DeepakSharma - насколько я понимаю, UIKit решает, какие части слоя (-ов) нужно отрисовывать. Для получения более подробной информации я бы предложил провести профилирование или проконсультироваться с инженером Apple UIKit. - person DonMag; 28.10.2020
comment
Я пытался расширить ваше решение, увеличивая CAShapeLayer при увеличении scrollView. Потребуется увеличить или уменьшить отметки, а также промежуток между отметками при увеличении масштаба прокрутки. Похоже, это не очень гибко, когда у вас есть CAShapeLayer. Необходимо реализовать собственный чертеж, который имеет легкий вес. - person Deepak Sharma; 01.11.2020
comment
@DeepakSharma - похоже, что его следует опубликовать как новый вопрос, так как вам нужно уточнить, что вы хотите, чтобы произошло. Вы хотите, чтобы изменился только промежуток между отметками? Вы хотите, чтобы частота числовых меток изменилась? Вы хотите, чтобы значения галочки изменились? И так далее ... Однозначно выходит за рамки этого (вашего исходного) вопроса. - person DonMag; 02.11.2020
comment
Вы занимаетесь частным консультированием, готовы платить и получаете полное руководство по пользовательскому интерфейсу проекта. - person Deepak Sharma; 02.11.2020
comment
@DeepakSharma - да, я (иногда) занимаюсь частным консультированием. Я бы предложил создать частное репо на GitHub и пригласить меня в качестве соавтора (мой идентификатор GitHub также DonMag). Используйте ReadMe для подробного описания того, в чем вам нужна помощь. - person DonMag; 02.11.2020
comment
Хорошо, я сделаю частный репозиторий на github только для пользовательской части проекта и приглашаю вас - person Deepak Sharma; 03.11.2020
comment
Хорошо, я отправил вам приглашение на Github. Пожалуйста, проверьте. - person Deepak Sharma; 03.11.2020
comment
@DeepakSharma - я буду использовать раздел проблем этого репозитория GitHub для дальнейшего общения. - person DonMag; 05.11.2020