2

I am trying to make a simple single view app for my own practice, but I ran into a problem and haven't got around this problem for past couple of days.

Screenshot of the current program

Basically, when I press any of these style buttons or adjust the slider, I want it to redraw the CGContext that's on the bottom of the screen (red dotted line). For example, if I press the STYLE 1 button, it should call the function called drawStyleOne() and apply these two different changes:

context?.setLineDash(phase: 0, lengths: [2,3])
context?.setStrokeColor(UIColor.green.cgColor)

My goal is to re-draw the dotted line (smaller dot) and change the color of the context to green. (context is an instance of CGContext)

I have everything drawn on the override func draw(_ rect: CGRect), but I cannot get it to re-draw the CGContext to the way I want it to.

Here's part of my code to help explain this situation bit better.

import UIKit

class VectorView: UIView {
private var context: CGContext? = nil

override init(frame: CGRect) {
    super.init(frame: frame)
    //contentMode = .redraw //tried this, didn't work.
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
    // lets us make calls
    context = UIGraphicsGetCurrentContext()!
    context?.move(to: CGPoint(x: 0.0, y: 10.0))
    context?.setLineDash(phase: 0, lengths: [2,3])
    context?.setLineJoin(CGLineJoin.bevel)
    context?.setLineCap(CGLineCap.square)

    context?.addLine(to: CGPoint(x: 50.0, y: 70.0))
    context?.addLine(to: CGPoint(x: 100.0, y: 20.0))

    context?.setLineWidth(1.0)
    context?.setStrokeColor(UIColor.red.cgColor)
    context?.drawPath(using: CGPathDrawingMode.stroke)        
}

func drawStyleOne () {

    context = UIGraphicsGetCurrentContext()
    context?.move(to: CGPoint(x: 0.0, y: 10.0))
    context?.setLineDash(phase: 0, lengths: [2,3])
    context?.setLineJoin(CGLineJoin.bevel)
    context?.setLineCap(CGLineCap.square)

    context?.addLine(to: CGPoint(x: 50.0, y: 70.0))
    context?.addLine(to: CGPoint(x: 100.0, y: 20.0))

    context?.setStrokeColor(UIColor.green.cgColor)

    context?.drawPath(using: CGPathDrawingMode.stroke)
    self.setNeedsDisplay()
    //contentMode = .redraw //Tried this and didn't work either..
    NSLog("drawStyleOne got called")
}

and the drawStyleOne function from class above gets called inside the AppDelegate.

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    let screenSize = UIScreen.main.bounds

    private var _vectorView: VectorView? = nil
    private var _buttonStylesView: ButtonStyleView? = nil

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        window = UIWindow()
        window?.rootViewController = ViewController()
        window?.rootViewController?.view.backgroundColor = UIColor.white
        window?.makeKeyAndVisible()

        _vectorView = VectorView()
        _vectorView?.frame = CGRect(x: 0.0, y: 650, width: 414, height: 70)
        _vectorView?.backgroundColor = UIColor.lightGray
        window?.rootViewController?.view.addSubview(_vectorView!)

        _buttonStylesView = ButtonStyleView()
        _buttonStylesView?.frame = CGRect (x: screenSize.width / 10, y: screenSize.height * 6 / 10, width: 414, height: 100)
        _buttonStylesView?.backgroundColor = UIColor.clear

        window?.rootViewController?.view.addSubview(_buttonStylesView!)

        _buttonStylesView?.styleOne?.addTarget(self, action: #selector(styleButtonPressed), for: UIControlEvents.touchDown)     
        return true
    }

    func styleButtonPressed() {
        NSLog("STYLE BUTTON PRESSED")
        _vectorView?.drawStyleOne()
    }
}

I even put NSLog("drawStyleOne got called") at the end just to verify that the function is getting called (and it does), however it will not redraw the line no matter what I try.

Any help/advice would be much appreciated.

whgnsdlr7
  • 77
  • 2
  • 9

2 Answers2

4

You need to do all of your drawing inside override func draw(_ rect: CGRect) (or inside routines called from there). So your draw needs a flag set outside (by your actions) to tell it which of your styles to draw. You trigger the redraw with setNeedsDisplay() - but that should be called from your action, not from the drawing code. Your current func drawStyleOne() tries to draw (but probably to an invalid currentContext) and then calls setNeedsDisplay(), which just schedules your actual draw routine to be run, which always draws the same "style".

The UIView class works behind the scenes to do quite a lot - if makes sure that draw isn't called unnecessarily, and schedules it when it is necessary, it figures out which parts of the view need re-drawn if they have been obscured (that's why it passes you a rect), and it sets up the current graphics context for draw, amongst other things.

See this Ray Wenderlich tutorial, or the Apple docs. As the Apple docs say:

For custom views, you must override the drawRect: method and perform all your drawing inside it.

(my emphasis)

So your code should look something like this:

class VectorView: UIView {
    public var whichStyle: SomeEnumOfYours = // your default value

    override func draw(_ rect: CGRect) {
        switch self.whichStyle {
        case .style1:
            self.drawStyleOne()
        case .style2:
            self.drawStyleTwo()
        //...
        }
    }
}

//...
func styleButtonPressed() {
    NSLog("STYLE BUTTON PRESSED")
    _vectorView?.whichStyle = // whichever style required
    _vectorView?.setNeedsDisplay()
}
Grimxn
  • 22,115
  • 10
  • 72
  • 85
  • Thank you so much for your response. I would so much love to up vote this, but I don't have enough reputation points right now... :( – whgnsdlr7 Feb 03 '17 at 07:52
  • @whgnsdlr7 - you should still be able to accept it as the correct answer (which may in itself give you enough rep to up vote it!) :) – Grimxn Feb 03 '17 at 09:28
0

I know it's not really an answer to your question, but you can look at your problem another way. You don't really need to work with context or to override drawRect to be able to draw lines. You can work directly with layers. Add them to your view and delete them when you want to change the style and add another one. You would use it like this:

import UIKit

class VectorView: UIView {

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

   var myColor = UIColor.red
   var myPhase = 0
   var myLength = [2,3]

   override func layoutSubviews() {
      super.layoutSubviews()
      guard let sublayers = self.layer.sublayers else { return }
      // remove sublayer if any exists
      for layer in sublayers {
          layer.removeFromSuperlayer()
      }
      // draws your lines according to the style you have defined with your variables above
      styledDrawing()
   }

   func styledDrawing() {
       // creates a path and styles it
       let path = UIBezierPath()
       path.move(to: CGPoint(x: 0.0, y: 10.0))
       path.setLineDash(myLength, count: myLength.count, phase: myPhase)
       path.lineJoinStyle = .bevel
       path.lineCapStyle = .square

       // add lines to the path
       path.addLine(to: CGPoint(x: 50.0, y: 70.0))
       path.addLine(to: CGPoint(x: 100.0, y: 20.0))

       // creates a layer,adds your path to it and adds the layer to your view's layer
       let layer = CAShapeLayer()
       layer.path = path.cgPath
       layer.lineWidth = 1.0
       layer.strokeColor = myColor.cgColor
       self.layer.addSublayer(layer) 
   }
}

Then in you appDelegate:

func styleButtonPressed() {
    NSLog("STYLE BUTTON PRESSED")
    _vectorView?.myColor = UIColor.green
    _vectorView?.myLength = [4,1]
    _vectorView?.setNeedsLayout()
}

Whenever you want to change the style, you change your variables myPhase, myLength and myColor and call yourView.setNeedsLayout() in the appDelegate. That way you just need one method to draw all your styles.

Ocunidee
  • 1,769
  • 18
  • 20