Solution to the Platinum Challenge the Hardest Way


#1

It is not clear from the Platinum Challenge requirements how the color panel should be presented and have a look. This matter of a guess led me to three options:

  1. An instance of UIMenuController;
  2. A subview with buttons added to the ‘DrawView’;
  3. A separate UIView with buttons and with its own UIViewController.

My choice was the third option because:

  1. UIMenuController is a context menu aimed to copy past etc. the selected item. Using this menu for control is not appropriate to its purpose. And this is a too easy solution to be named ‘Platinum’.
  2. The advantage of adding and removing a subview is that just one UIViewController does all the job. The drawbacks are over complicated storyboard and breaking the MVC rule that each main view is managed by a separate controller.
  3. Well, here is all clear UISwipeGestureRecognizer trigger UISegue to the UIViewController managing color buttons logic. Nice.

Implementation begins with changing the name of the canvas view controller from ‘ViewController’ to ‘DrawViewController’ both in the source code and in the storyboard plus creating new ‘ColorPanelViewController’ here and there too. In the storyboard, I added 8 UIButtons to this new controller’s view: ‘Blue’, ‘Yellow’, ‘Orange’, ‘Purple’, Green’, ‘Red’, ‘Cyan’ and ‘Default’. Their tag property values were changed in the attribute inspector from 1 to 8 correspondingly. All these buttons were connected to an action method buttonPressed(: ) where the value of the tag property sets the color property of the instance of the ‘DrawView’.

Therefore full implementation of the new ColorPanelViewController is this:

class ColorPanelViewController: UIViewController {

var theView: DrawView!

@IBAction func buttonPressed(_ sender: UIButton) {
    let tag = sender.tag
    switch tag {
    case 1:
        theView.chosenColor = UIColor.blue
    case 2:
        theView.chosenColor = UIColor.yellow
    case 3:
        theView.chosenColor = UIColor.orange
    case 4:
        theView.chosenColor = UIColor.purple
    case 5:
        theView.chosenColor = UIColor.green
    case 6:
        theView.chosenColor = UIColor.red
    case 7:
        theView.chosenColor = UIColor.cyan
    default:
        theView.chosenColor = nil
    }
    
    dismiss(animated: true, completion: nil)
}

}

As you may have noticed, the instance of ‘DrawView’ has a new color property ‘chosenColor’. This is an optional UIColor? storing the user’s color preference. The default is nil letting the color to be set the old way. Here you are the changes in the ‘DrawView’ implementation, post here only the changes relevant to this topic:

class DrawView: UIView, UIGestureRecognizerDelegate {

...

var chosenColor: UIColor?
var swipeRecognizer: UISwipeGestureRecognizer!
...

required init?(coder aDecoder: NSCoder) {
    ...
    addGestureRecognizer(longPressRecognizer)

    swipeRecognizer = UISwipeGestureRecognizer(target: nil, action: nil)
    swipeRecognizer.numberOfTouchesRequired = 3
    swipeRecognizer.direction = .up
    swipeRecognizer.delaysTouchesBegan = true
    addGestureRecognizer(swipeRecognizer)

    ...
    moveRecognizer.require(toFail: swipeRecognizer)
    ...
}
...

override func draw(_ rect: CGRect) {
   ...
    for (_, line) in currentLines {
        if chosenColor == nil {
            currentLineColor = UIColor(hue: line.angleSin, saturation: 1, brightness: 1, alpha: 1) //Solution to the Chapter 18 Silver Challenge
        } else {
        currentLineColor = chosenColor
        } 
        currentLineColor.setStroke()
        stroke(line)
    }
    ...
}
...

}

The second change in the ‘DrawView’ is creating a UISwipeGestureRecognizer and adding it to this view. The target and the action parameters of the recognizer’s initializer are ‘nil’ because they will be provided in the view controller. It is very important to put UISwipeGestureRecognizer before the ‘moveRecognizer’ so that the latter can check whether a swipe was detected. The instance of the UISwipeGestureRecognizer is placed in a property ‘swipeRecognizer’ reference to which will be in the ‘DrawViewController’, post here only the changes relevant to this topic:

class DrawViewController: UIViewController {

override func viewDidLoad() {
    super.viewDidLoad()
    
    let view = self.view as! DrawView
    view.swipeRecognizer.addTarget(self, action: #selector(swipe))
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "ShowColorPanel" {
        let controller = segue.destination as! ColorPanelViewController
        controller.theView = self.view as! DrawView
    }
}

@objc func swipe() {
    print("swipe")
    performSegue(withIdentifier: "ShowColorPanel", sender: self.view)
}

}

In the ‘viewDidLoad()’ method we add target-action pair to the ‘swipeRecognizer’. The action ‘swipe()’ calls performSegue(withIdentifier:sender:) to show the color panel view. This nesting is done to follow OO encapsulation rule that if in the future requirements change what this swipe must do, we will change logic just in one place. In the ‘prepare(for:sender:)’ method we inject dependency of the ‘DrawView’ instance into ‘ColorPanelViewController’ instance to keep the user’s choice of the color.


#2

Hi!
I went the second option you have described. I chose that to go through animation technique presented in QUIZ again. So here is my solution. Forum rules didn’t let me use more then to “@” so I used (at) instead

First I made changes to Line type

Blockquote
struct Line {
var begin = CGPoint.zero
var end = CGPoint.zero
var lineThickness: CGFloat = 5
var lineColor = UIColor.black
init(begin: CGPoint, end: CGPoint, lineColor: UIColor) {
self.begin = begin
self.end = end
self.lineColor = lineColor
}
}

DrawView draw and touch methods would undergo fallowing changes

Blockquote
override func draw(_ rect: CGRect) {
for line in finishedLines {
line.lineColor.setStroke() // here we use self color property of the Line instance in finished lines
stroke(line)
}
for (_ , line) in currentLines {
line.lineColor.setStroke() // here we use self color property of the Line instance in current lines
stroke(line)
}
if let index = selectedLineIndex {
UIColor.green.setStroke()
let selectedLine = finishedLines[index]
stroke(selectedLine)
}
}

In story board I have also created vertical UIStackView filled with horizontal stacks which in their turn were filled with UIButtons. Each button back ground color was set to represent colors to chose. I added fallowing constraint: leading, trailing, aspect(1:1) and bottom stackView to safeArea bottom constraint.
15

I assigned the whole job of performing color pane to ViewController. And it looks now like this

Blockquote
class DrawViewController: UIViewController {
// this is an outlet for color pane (which is stackView) bottom to safe area bottom constraint. It further would be
// modified as the color pane goes on and off the screen
(at)IBOutlet var bottomColorPaneConstraint: NSLayoutConstraint!
// outlet for color pane
(at)IBOutlet var colorPaneView: UIStackView!
// outlet for drawView. And here I have added .up and .down swipe gesture recognizer. So when drawView is created it would receive ones.
(at)IBOutlet var drawView: DrawView! {
didSet {
let swipeUpGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(DrawViewController.showColorPane(:)))
swipeUpGestureRecognizer.numberOfTouchesRequired = 3
swipeUpGestureRecognizer.direction = .up
swipeUpGestureRecognizer.delaysTouchesBegan = true
drawView.addGestureRecognizer(swipeUpGestureRecognizer)
let swipeDownGestureRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(DrawViewController.hideColorPane(
:)))
swipeDownGestureRecognizer.numberOfTouchesRequired = 3
swipeDownGestureRecognizer.direction = .down
swipeDownGestureRecognizer.delaysTouchesBegan = true
drawView.addGestureRecognizer(swipeDownGestureRecognizer)
}
}
// this action is called when button with color is pressed. When button is pressed its alpha drops so to show it was selected. And this for in business inside does setting buttons alpha to 1. So when you select new color previously tapped button has normal, not “selected” alpha.
(at)IBAction func setColorOfStroke(_ sender: UIButton) {
print(“Called set Color of stroke”)
for stacks in colorPaneView.subviews {
for button in stacks.subviews {
button.alpha = 1
}
}
// set color currently used in DrawView
drawView.currentLineColor = sender.backgroundColor!
// change alpha of pressed color button to indicate selection
sender.alpha = 0.3
}
// method below basically updates the constraint constant of the stack view to make it move on screen in animated way with 2 helping methods
(at)objc func showColorPane(_ gestureRecognizer: UIGestureRecognizer) {
updateBottomColorPaneConstraintToOnScreenPosition()
animateColorPaneTransactions()
}
(at)objc func hideColorPane(_ gestureRecognizer: UIGestureRecognizer) {
updateBottomColorPaneConstraintToOffScreenPosition()
animateColorPaneTransactions()
}
// seting initial position of color pane
override func viewDidLoad() {
super.viewDidLoad()
updateBottomColorPaneConstraintToOffScreenPosition()
}
func updateBottomColorPaneConstraintToOffScreenPosition() {
let screenHeight = view.frame.height
bottomColorPaneConstraint.constant = -screenHeight
self.colorPaneView.alpha = 0
}
func updateBottomColorPaneConstraintToOnScreenPosition() {
bottomColorPaneConstraint.constant = 40
}
func animateColorPaneTransactions() {
UIView.animate(withDuration: 0.5 , delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: [.curveLinear], animations: {
//animate alpha
self.colorPaneView.alpha = 1
//animate constraints
self.view.layoutIfNeeded()
}, completion: nil )
}
}


#3

It is very important to put UISwipeGestureRecognizer before the ‘moveRecognizer’ so that the latter can check whether a swipe was detected. https://vidmate.onl/vmate/
https://9apps.ooo/download/
https://luckypatcher.pro/apk/