Сhange radius for UibezierPath swift - ios

I drew an arc and added action to it. How can I use this action to change the radius of the arc ?? (Example: there was a radius of 30, clicked on the arc and +10. Clicked again and -10).I tried to change the radius but there was no result.While this does not move and I do not quite understand why this is so.
Here is the full class code.
#IBDesignable class circleView: UIView {
typealias ArcAction = () -> Void
struct ArcInfo {
var outlinePath: UIBezierPath
var action: ArcAction
}
private var arcInfos: [ArcInfo]!
let bgShapeLayer = CAShapeLayer()
var redRadius: Float?
required init?(coder: NSCoder) {
super.init(coder: coder)
let recognizer = UITapGestureRecognizer(target: self, action: #selector(tap(_ :)))
addGestureRecognizer(recognizer)
}
override func draw(_ rect: CGRect) {
let fullCircle = CGFloat.pi * 2
let arcAngle = fullCircle * 1.5 / 6
var lastArcAngle = CGFloat.pi / 4.0 + CGFloat.pi //-CGFloat.pi
arcInfos = []
// Red Arc
func redArc( action: #escaping ArcAction) {
let path = UIBezierPath(arcCenter: CGPoint(x: rect.width/2, y: rect.height/2), radius: CGFloat(redRadius ?? 46), startAngle: lastArcAngle, endAngle: lastArcAngle + arcAngle, clockwise: true)
#colorLiteral(red: 0.7450980392, green: 0.007843137255, blue: 0.262745098, alpha: 1).setStroke()
path.lineWidth = 10
path.stroke()
lastArcAngle += arcAngle
// separators
#colorLiteral(red: 0.927436769, green: 0.9490308166, blue: 0.967099607, alpha: 1).setStroke()
let outlinePath = hitTestPath(for: path)
outlinePath.lineWidth = 3
outlinePath.stroke()
arcInfos.append(ArcInfo(outlinePath: outlinePath, action: action))
}
//Add Arc
redArc {
self.redRadius = 56
}
}
#objc func tap(_ recognizer: UITapGestureRecognizer) {
let location = recognizer.location(in: self)
if let hitPath = (arcInfos.first { $0.outlinePath.contains(location) }) {
hitPath.action()
print(hitPath)
}
}
func hitTestPath(for path: UIBezierPath) -> UIBezierPath {
let pathCopy = path.cgPath.copy(strokingWithWidth: 15, lineCap: .butt, lineJoin: .miter, miterLimit: 0)
return UIBezierPath(cgPath: pathCopy)
}
}

You need to trigger a UI update...
if let hitPath = (arcInfos.first { $0.outlinePath.contains(location) }) {
hitPath.action()
// add this line
setNeedsDisplay()
print(hitPath)
}
Note: your code as-is starts with a radius of 46... on tap, with that line added, it redraws itself with a radius of 56.

Related

CircleProgressBar around an image

I have a circular ProgressBar and I try to change to .clear the colour of a mask (CAShapeLayer) over a ImageView (under the ProgressBar) but for some reason when the progress is loading then the whole mask disappear instantly instead to vanish during the progress.
Can anyone help me to identify the bug ?
Here is my demo project: https://github.com/tygruletz/CircularProgressBar
Here is the code for my ProgressBar:
class CircularProgressBar: UIView {
var currentProgress = 0
//MARK: awakeFromNib
override func awakeFromNib() {
super.awakeFromNib()
setupView()
label.text = "\(currentProgress)"
}
//MARK: Public
public var lineWidth:CGFloat = 120 {
didSet{
foregroundLayer.lineWidth = lineWidth
backgroundLayer.lineWidth = lineWidth - (0.20 * lineWidth)
}
}
public var labelSize: CGFloat = 20 {
didSet {
label.font = UIFont.systemFont(ofSize: labelSize)
label.sizeToFit()
configLabel()
}
}
public var safePercent: Int = 100 {
didSet{
setForegroundLayerColorForSafePercent()
}
}
public func setProgress(to progressConstant: Double, withAnimation: Bool) {
var progress: Double {
get {
if progressConstant > 1 { return 1 }
else if progressConstant < 0 { return 0 }
else { return progressConstant }
}
}
foregroundLayer.strokeEnd = CGFloat(progress)
if withAnimation {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = progress
animation.duration = 2
foregroundLayer.add(animation, forKey: "foregroundAnimation")
}
var currentTime:Double = 0
let timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { (timer) in
if currentTime >= 2{
timer.invalidate()
} else {
currentTime += 0.05
let percent = currentTime/2 * 100
self.currentProgress = Int(progress * percent)
self.label.text = "\(self.currentProgress)%"
self.setForegroundLayerColorForSafePercent()
self.configLabel()
}
}
timer.fire()
}
//MARK: Private
private var label = UILabel()
private let foregroundLayer = CAShapeLayer()
private let backgroundLayer = CAShapeLayer()
private var radius: CGFloat {
get{
if self.frame.width < self.frame.height { return (self.frame.width - lineWidth)/2 }
else { return (self.frame.height - lineWidth)/2 }
}
}
private var pathCenter: CGPoint{ get{ return self.convert(self.center, from:self.superview) } }
private func makeBar(){
self.layer.sublayers = nil
drawBackgroundLayer()
drawForegroundLayer()
}
private func drawBackgroundLayer(){
let path = UIBezierPath(arcCenter: pathCenter, radius: self.radius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
self.backgroundLayer.path = path.cgPath
self.backgroundLayer.strokeColor = UIColor(red: CGFloat(105/255.0), green: CGFloat(105/255.0), blue: CGFloat(105/255.0), alpha: 0.85).cgColor
self.backgroundLayer.lineWidth = lineWidth - (lineWidth * 20/100)
self.backgroundLayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(backgroundLayer)
}
private func drawForegroundLayer(){
let startAngle = (-CGFloat.pi/2)
let endAngle = 2 * CGFloat.pi + startAngle
let path = UIBezierPath(arcCenter: pathCenter, radius: self.radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
foregroundLayer.lineCap = CAShapeLayerLineCap.round
foregroundLayer.path = path.cgPath
foregroundLayer.lineWidth = lineWidth
foregroundLayer.fillColor = UIColor.clear.cgColor
foregroundLayer.strokeColor = UIColor.clear.cgColor
foregroundLayer.strokeEnd = 0
self.layer.addSublayer(foregroundLayer)
}
private func makeLabel(withText text: String) -> UILabel {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
label.text = text
label.font = UIFont.systemFont(ofSize: labelSize)
label.sizeToFit()
label.center = pathCenter
return label
}
private func configLabel(){
label.sizeToFit()
label.center = pathCenter
}
private func setForegroundLayerColorForSafePercent(){
let percent = currentProgress
if percent > 0 && percent < 100 {
self.backgroundLayer.strokeColor = UIColor.clear.cgColor
self.label.textColor = .orange
}
else {
self.backgroundLayer.strokeColor = UIColor(red: CGFloat(105/255.0), green: CGFloat(105/255.0), blue: CGFloat(105/255.0), alpha: 0.85).cgColor
}
}
private func setupView() {
makeBar()
self.addSubview(label)
}
//Layout Sublayers
private var layoutDone = false
override func layoutSublayers(of layer: CALayer) {
if !layoutDone {
let tempText = label.text
setupView()
label.text = tempText
layoutDone = true
}
}
}
Here I call the class in ViewController:
class ViewController: UIViewController {
//MARK: IBOutlets
#IBOutlet weak var progressBar: CircularProgressBar!
override func viewDidLoad() {
super.viewDidLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
}
#objc func handleTap() {
progressBar.labelSize = 60
progressBar.safePercent = 100
progressBar.setProgress(to: 1, withAnimation: true)
}
}
Here is a record with the bug:
Thanks for reading this.

Recall drawRect after view is recalled

I have a view that is using drawRect to draw a round timer.
In its superview there's a button that pushes another VC to the screen.
The problem is when the user clicks the back button, the timer (which is drawn using drawRect) is automatically starting with the toValue value instad of the fromValue. I want it to be redrawed exactly as it is drawn when I first launch the view.
Does anybody know how can I achieve that?
Thank you!
Code:
class CountDownTimer: UIView {
public var backgroundStrokeColor: CGColor = UIColor.white.cgColor
public var backgroundFillColor: CGColor = UIColor.clear.cgColor
public var backgroundLineWidth: CGFloat = 15
public var timeLeftSrtokeColor: CGColor = UIColor.red.cgColor
public var timeLeftFillColor: CGColor = UIColor.clear.cgColor
public var timeLeftLineWidth: CGFloat = 10
public var textColor: UIColor = UIColor.white
public var textFont: UIFont = UIFont.balooRegular(10.0)
fileprivate var timeLeft: TimeInterval = 0
fileprivate var endDate: Date?
fileprivate var timeLeftShapeLayer: CAShapeLayer?
fileprivate var bgShapeLayer: CAShapeLayer?
fileprivate var timeLabel: UILabel?
fileprivate var timer = Timer()
fileprivate let strokeIt = CABasicAnimation(keyPath: "strokeEnd")
//MARK: - UIView
override func draw(_ rect: CGRect) {
drawBgShape()
drawTimeLeftShape()
addTimeLabel()
strokeIt.toValue = 1 //"fromValue" is set in "startTimer(duration, timerProgress)
strokeIt.duration = self.timeLeft
// add the animation to your timeLeftShapeLayer
timeLeftShapeLayer?.add(strokeIt, forKey: nil)
// define the future end time by adding the timeLeft to now Date()
}
//MARK: - Public
public func startTimer(duration: TimeInterval, timerProgress: Double) {
self.timeLeft = duration
endDate = Date().addingTimeInterval(timeLeft)
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateTime), userInfo: nil, repeats: true)
strokeIt.fromValue = timerProgress
}
//MARK: - Private
fileprivate func drawBgShape() {
//we initialize and add the layer only if there is not initialized
if(bgShapeLayer == nil){
bgShapeLayer = CAShapeLayer()
self.layer.addSublayer(bgShapeLayer!)
}
bgShapeLayer?.path = UIBezierPath(arcCenter: CGPoint(x: self.frame.midX , y: self.frame.midY), radius:
min((frame.width - self.timeLeftLineWidth)/2, (frame.height - self.timeLeftLineWidth)/2), startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
bgShapeLayer?.strokeColor = self.backgroundStrokeColor
bgShapeLayer?.fillColor = self.backgroundFillColor
bgShapeLayer?.lineWidth = self.backgroundLineWidth
}
fileprivate func drawTimeLeftShape() {
//we initialize and add the layer only if there is not initialized
if(timeLeftShapeLayer == nil){
timeLeftShapeLayer = CAShapeLayer()
self.layer.addSublayer(timeLeftShapeLayer!)
}
timeLeftShapeLayer?.path = UIBezierPath(arcCenter: CGPoint(x: self.frame.midX , y: self.frame.midY), radius:
min((frame.width - self.timeLeftLineWidth)/2, (frame.height - self.timeLeftLineWidth)/2), startAngle: -90.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: true).cgPath
timeLeftShapeLayer?.strokeColor = self.timeLeftSrtokeColor
timeLeftShapeLayer?.fillColor = self.timeLeftFillColor
timeLeftShapeLayer?.lineWidth = self.timeLeftLineWidth
}
fileprivate func addTimeLabel() {
//we initialize and add the UILabel only if there is not initialized
if(timeLabel == nil){
timeLabel = UILabel()
self.addSubview(timeLabel!)
}
timeLabel?.frame = CGRect(x: self.frame.midX-50 ,y: self.frame.midY-25, width: 100, height: 50)
timeLabel?.adjustsFontSizeToFitWidth = true
timeLabel?.textAlignment = .center
timeLabel?.text = self.timeLeft.stringTime
timeLabel?.textColor = self.textColor
timeLabel?.font = self.textFont
}
//MARK: - Actions
#objc fileprivate func updateTime() {
if timeLeft > 0 {
timeLeft = endDate?.timeIntervalSinceNow ?? 0
timeLabel?.text = self.timeLeft.stringTime
} else {
timeLabel?.text = self.timeLeft.stringTime
timer.invalidate()
}
}
}

Can't get good performance with Draw(_ rect: CGRect)

I have a Timer() that asks the view to refresh every x seconds:
func updateTimer(_ stopwatch: Stopwatch) {
stopwatch.counter = stopwatch.counter + 0.035
Stopwatch.circlePercentage += 0.035
stopwatchViewOutlet.setNeedsDisplay()
}
--
class StopwatchView: UIView, UIGestureRecognizerDelegate {
let buttonClick = UITapGestureRecognizer()
override func draw(_ rect: CGRect) {
Stopwatch.drawStopwatchFor(view: self, gestureRecognizers: buttonClick)
}
}
--
extension Stopwatch {
static func drawStopwatchFor(view: UIView, gestureRecognizers: UITapGestureRecognizer) {
let scale: CGFloat = 0.8
let joltButtonView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 75, height: 75))
let imageView: UIImageView!
// -- Need to hook this to the actual timer --
let temporaryVariableForTimeIncrement = CGFloat(circlePercentage)
// Exterior of sphere:
let timerRadius = min(view.bounds.size.width, view.bounds.size.height) / 2 * scale
let timerCenter = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
let path = UIBezierPath(arcCenter: timerCenter, radius: timerRadius, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
path.lineWidth = 2.0
UIColor.blue.setStroke()
path.stroke()
// Interior of sphere
let startAngle = -CGFloat.pi / 2
let arc = CGFloat.pi * 2 * temporaryVariableForTimeIncrement / 100
let cPath = UIBezierPath()
cPath.move(to: timerCenter)
cPath.addLine(to: CGPoint(x: timerCenter.x + timerRadius * cos(startAngle), y: timerCenter.y))
cPath.addArc(withCenter: timerCenter, radius: timerRadius * CGFloat(0.99), startAngle: startAngle, endAngle: arc + startAngle, clockwise: true)
cPath.addLine(to: CGPoint(x: timerCenter.x, y: timerCenter.y))
let circleShape = CAShapeLayer()
circleShape.path = cPath.cgPath
circleShape.fillColor = UIColor.cyan.cgColor
view.layer.addSublayer(circleShape)
// Jolt button
joltButtonView.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
joltButtonView.layer.cornerRadius = 38
joltButtonView.backgroundColor = UIColor.white
imageView = UIImageView(frame: joltButtonView.frame)
imageView.contentMode = .scaleAspectFit
imageView.image = image
joltButtonView.addGestureRecognizer(gestureRecognizers)
view.addSubview(joltButtonView)
view.addSubview(imageView)
}
}
My first problem is that the subviews are getting redrawn each time. The second one is that after a couple seconds, the performance starts to deteriorate really fast.
Too many instances of the subview
Trying to drive the blue graphic with a Timer()
Should I try to animate the circle percentage graphic instead of redrawing it each time the timer function is called?
The major observation is that you should not be adding subviews in draw. That is intended for rendering a single frame and you shouldn't be adding/removing things from the view hierarchy inside draw. Because you're adding subviews roughly ever 0.035 seconds, your view hierarchy is going to explode, with adverse memory and performance impact.
You should either have a draw method that merely calls stroke on your updated UIBezierPath for the seconds hand. Or, alternatively, if using CAShapeLayer, just update its path (and no draw method is needed at all).
So, a couple of peripheral observations:
You are incrementing your "percentage" for every tick of your timer. But you are not guaranteed the frequency with which your timer will be called. So, instead of incrementing some "percentage", you should save the start time when you start the timer, and then upon every tick of the timer, you should recalculate the amount of time elapsed between then and now when figuring out how much time has elapsed.
You are using a Timer which is good for most purposes, but for the sake of optimal drawing, you really should use a CADisplayLink, which is optimally timed to allow you to do whatever you need before the next refresh of the screen.
So, here is an example of a simple clock face with a sweeping second hand:
open class StopWatchView: UIView {
let faceLineWidth: CGFloat = 5 // LineWidth of the face
private var startTime: Date? { didSet { self.updateHand() } } // When was the stopwatch resumed
private var oldElapsed: TimeInterval? { didSet { self.updateHand() } } // How much time when stopwatch paused
public var elapsed: TimeInterval { // How much total time on stopwatch
guard let startTime = startTime else { return oldElapsed ?? 0 }
return Date().timeIntervalSince(startTime) + (oldElapsed ?? 0)
}
private weak var displayLink: CADisplayLink? // Display link animating second hand
public var isRunning: Bool { return displayLink != nil } // Is the timer running?
private var clockCenter: CGPoint { // Center of the clock face
return CGPoint(x: bounds.midX, y: bounds.midY)
}
private var radius: CGFloat { // Radius of the clock face
return (min(bounds.width, bounds.height) - faceLineWidth) / 2
}
private lazy var face: CAShapeLayer = { // Shape layer for clock face
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = self.faceLineWidth
shapeLayer.strokeColor = #colorLiteral(red: 0.1215686277, green: 0.01176470611, blue: 0.4235294163, alpha: 1).cgColor
shapeLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor
return shapeLayer
}()
private lazy var hand: CAShapeLayer = { // Shape layer for second hand
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 3
shapeLayer.strokeColor = #colorLiteral(red: 0.2196078449, green: 0.007843137719, blue: 0.8549019694, alpha: 1).cgColor
shapeLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0).cgColor
return shapeLayer
}()
override public init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
// add necessary shape layers to layer hierarchy
private func configure() {
layer.addSublayer(face)
layer.addSublayer(hand)
}
// if laying out subviews, make sure to resize face and update hand
override open func layoutSubviews() {
super.layoutSubviews()
updateFace()
updateHand()
}
// stop display link when view disappears
//
// this prevents display link from keeping strong reference to view after view is removed
override open func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
pause()
}
}
// MARK: - DisplayLink routines
/// Start display link
open func resume() {
// cancel any existing display link, if any
pause()
// start new display link
startTime = Date()
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.add(to: .main, forMode: .commonModes)
// save reference to it
self.displayLink = displayLink
}
/// Stop display link
open func pause() {
displayLink?.invalidate()
// calculate floating point number of seconds
oldElapsed = elapsed
startTime = nil
}
open func reset() {
pause()
oldElapsed = nil
}
/// Display link handler
///
/// Will update path of second hand.
#objc func handleDisplayLink(_ displayLink: CADisplayLink) {
updateHand()
}
/// Update path of clock face
private func updateFace() {
face.path = UIBezierPath(arcCenter: clockCenter, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
}
/// Update path of second hand
private func updateHand() {
// convert seconds to an angle (in radians) and figure out end point of seconds hand on the basis of that
let angle = CGFloat(elapsed / 60 * 2 * .pi - .pi / 2)
let endPoint = CGPoint(x: clockCenter.x + cos(angle) * radius * 0.9, y: clockCenter.y + sin(angle) * radius * 0.9)
// update path of hand
let path = UIBezierPath()
path.move(to: clockCenter)
path.addLine(to: endPoint)
hand.path = path.cgPath
}
}
And
class ViewController: UIViewController {
#IBOutlet weak var stopWatchView: StopWatchView!
#IBAction func didTapStartStopButton () {
if stopWatchView.isRunning {
stopWatchView.pause()
} else {
stopWatchView.resume()
}
}
#IBAction func didTapResetButton () {
stopWatchView.reset()
}
}
Now, in the above example, I'm just updating the CAShapeLayer of the seconds hand in my CADisplayLink handler. Like I said at the start, you can alternatively have a draw method that simply strokes the paths you need for single frame of animation and then call setNeedsDisplay in your display link handler. But if you do that, don't change the view hierarchy within draw, but rather do any configuration you need in init and draw should just stroke whatever the path should be at that moment.

Drop annotation on virtual map swift3

please help me out.
First time working on a project, with iBeacon involved, which creating Virtual map of inside of a store.
I know how to drop pins, with a MapKit, but how can i do it, if i only have CGPoints on View ?
I managed how to drip UIImage(with pin image), on a view. But when i rotate or pinch, it's not stay on that coordinates that i dropped.
Here's a code :
import UIKit
class MapViewController: UIViewController{
private var scaleView: CGFloat = 1
private var rotateView: CGFloat = 0
private var anchorPoint = CGPoint(x: 0.5, y: 0.5)
private let gestureRecognizerDelegate = GestureRecognizerDelegate()
#IBOutlet weak var mapView: MapView!
#IBOutlet var pinchGestureRecognizer: UIPinchGestureRecognizer!
#IBOutlet var panGestureRecognizer: UIPanGestureRecognizer!
#IBOutlet var rotateGestureRecognizer: UIRotationGestureRecognizer!
#IBOutlet weak var pin: UIImageView!
override func viewDidAppear(_ animated: Bool) {
if !cartIsEmpty {
cartBtn.setImage(UIImage(named: "haveitem"), for: .normal)
} else {
cartBtn.setImage(UIImage(named: "2772"), for: .normal)
}
cartBtn.addTarget(self, action: #selector(openCart), for: .touchUpInside)
let item1 = UIBarButtonItem(customView: cartBtn)
self.navigationItem.rightBarButtonItem = item1
}
override func viewDidDisappear(_ animated: Bool) {
cartBtn.removeTarget(self, action: #selector(openCart), for: .touchUpInside)
}
override func viewDidLoad() {
super.viewDidLoad()
ApplicationManager.sharedInstance.onApplicationStart()
NotificationCenter.default.addObserver(self, selector: #selector(self.onOrientationChanged), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
self.panGestureRecognizer.delegate = gestureRecognizerDelegate
self.pinchGestureRecognizer.delegate = gestureRecognizerDelegate
self.rotateGestureRecognizer.delegate = gestureRecognizerDelegate
ApplicationManager.sharedInstance.gotFloorData = drawFloor
ApplicationManager.sharedInstance.currentUserPoint = drawCurrentUserPoint
}
func drawFloor (floor: Floor) {
mapView.setFloor(currentFloor: floor)
mapView.setNeedsDisplay()
}
func drawCurrentUserPoint(currentUserPoint: CurrentUserLocation, beaconRangingData: [BeaconRangingPoint]) {
mapView.setUserPoint(currentUserPoint: currentUserPoint, beaconRangingData: beaconRangingData)
mapView.setNeedsDisplay()
}
#IBAction func handlePan(recognizer:UIPanGestureRecognizer) {
let translation = recognizer.translation(in: mapView)
if recognizer.view != nil {
let offsetX = translation.x * CGFloat(cosf(Float(rotateView))) - translation.y * CGFloat(sinf(Float(rotateView)))
let offsetY = translation.x * CGFloat(sinf(Float(rotateView))) + translation.y * CGFloat(cosf(Float(rotateView)))
mapView.center = CGPoint(x:mapView.center.x + offsetX * scaleView,
y:mapView.center.y + offsetY * scaleView)
}
recognizer.setTranslation(CGPoint(x: 0, y: 0), in: mapView)
}
#IBAction func handlePinch(recognizer : UIPinchGestureRecognizer) {
if recognizer.view != nil {
setAnchor(point: recognizer.location(in: mapView))
mapView.transform = mapView.transform.scaledBy(x: recognizer.scale, y: recognizer.scale)
scaleView = recognizer.scale * scaleView
recognizer.scale = 1
}
}
#IBAction func handleRotate(recognizer : UIRotationGestureRecognizer) {
if recognizer.view != nil {
setAnchor(point: recognizer.location(in: mapView))
mapView.transform = mapView.transform.rotated(by: recognizer.rotation)
rotateView = rotateView + recognizer.rotation
recognizer.rotation = 0
}
}
private func setAnchor(point : CGPoint) {
let anchor = CGPoint(x: point.x / mapView.bounds.width, y: point.y / mapView.bounds.height)
mapView.layer.anchorPoint = CGPoint(x: anchor.x, y: anchor.y)
let translationX = (mapView.bounds.width * (anchor.x - anchorPoint.x)) * scaleView
let translationY = (mapView.bounds.height * (anchor.y - anchorPoint.y)) * scaleView
let offsetX = translationX * CGFloat(cosf(Float(rotateView))) - translationY * CGFloat(sinf(Float(rotateView)))
let offsetY = translationX * CGFloat(sinf(Float(rotateView))) + translationY * CGFloat(cosf(Float(rotateView)))
mapView.layer.position = CGPoint(x: mapView.layer.position.x + offsetX,
y: mapView.layer.position.y + offsetY)
anchorPoint = anchor
}
private func showAlert(title: String, message: String?, style: UIAlertControllerStyle = .alert) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: style)
let tryAgainAction = UIAlertAction(title: "Try again", style: .default) {
alertAction in
ApplicationManager.sharedInstance.onApplicationStart()
}
let cancelAction = UIAlertAction(title: "Cancel", style: .default, handler: nil)
alertController.addAction(tryAgainAction)
alertController.addAction(cancelAction)
present(alertController, animated: true, completion: nil)
}
func onOrientationChanged() {
self.mapView.setNeedsDisplay()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
also code of mapView:
import UIKit
class MapView: UIView {
private var floor = Floor(walls: [], doors: [], beacons: [])
private let wallColor = UIColor.black
private let doorColor = UIColor.red
private let triangleColor = UIColor.red
private let perpendicularColor = UIColor.darkGray
private let doorLength = 3.0
private let beaconColor = UIColor.green
private let beaconNoActiveColor = UIColor(red: 20/255.0, green: 154.0/255.0, blue: 53.0/255.0, alpha: 1.0)
private let beaconFrameColor = UIColor.brown
private let lineWidthBeaconFrame:CGFloat = 0.25
private let beaconRadius: CGFloat = 5.0
private let userColor = UIColor.blue
private let userFrameColor = UIColor.brown
private let lineWidthUserFrame:CGFloat = 0.25
private let userRadius: CGFloat = 5.0
private let distanceBeaconColor = UIColor.clear
private let distanceBeaconFrameColor = UIColor.green
private let lineWidthOfDistanceFrame: CGFloat = 0.5
private let userRawRadius: CGFloat = 3.0
private let userRawColor = UIColor.darkGray
private let userRawFrameColor = UIColor.lightGray
private let lineWidthOfuserRawFrame: CGFloat = 0.8
private var beaconText: NSString = ""
private let textColor: UIColor = UIColor.red
private let textFont: UIFont = UIFont(name: "Helvetica Neue", size: 5)!
private var currentUserLocation = CurrentUserLocation()
private var beaconRangingData: [BeaconRangingPoint] = []
func setFloor (currentFloor: Floor) {
floor = currentFloor
}
func setUserPoint(currentUserPoint: CurrentUserLocation, beaconRangingData: [BeaconRangingPoint]) {
self.currentUserLocation = currentUserPoint
self.beaconRangingData = beaconRangingData
}
func dropPin(location: CGPoint) {
}
override func draw(_ rect: CGRect) {
let frameToDraw = CoordinatesConverter(boundsWidth: bounds.width, boundsHeight: bounds.height, paddingX: 5, paddingY: 5)
let mapWithScaleCoordinaates = frameToDraw.getSuitableCoordinates(floor: floor, currentUserLocation: currentUserLocation, beaconRangingData: beaconRangingData)
let lines = mapWithScaleCoordinaates.lines
let circles = mapWithScaleCoordinaates.circles
drawLines(lines: lines)
drawCircles(circles: circles)
}
private func drawLines(lines :[Line]) {
let wallPath = UIBezierPath()
let doorPath = UIBezierPath()
let trianglePath = UIBezierPath()
let perpendicularPath = UIBezierPath()
for line in lines {
if line.type == .wall {
wallPath.move(to: CGPoint(x: line.x1, y: line.y1))
wallPath.addLine(to: CGPoint(x: line.x2, y: line.y2))
wallColor.setStroke()
wallPath.stroke()
}
if line.type == .door {
doorPath.move(to: CGPoint(x: line.x1, y: line.y1))
doorPath.addLine(to: CGPoint(x: line.x2, y: line.y2))
doorPath.lineWidth = CGFloat(doorLength)
doorColor.setStroke()
doorPath.stroke()
}
if line.type == .triangle {
trianglePath.move(to: CGPoint(x: line.x1, y: line.y1))
trianglePath.addLine(to: CGPoint(x: line.x2, y: line.y2))
triangleColor.setStroke()
trianglePath.stroke()
}
if line.type == .perpendicular {
perpendicularPath.move(to: CGPoint(x: line.x1, y: line.y1))
perpendicularPath.addLine(to: CGPoint(x: line.x2, y: line.y2))
perpendicularColor.setStroke()
perpendicularPath.stroke()
}
}
}
private func drawCircles(circles: [Circle]) {
let layerViews = layer.sublayers
if layerViews != nil {
for view in layerViews! {
if type(of: view) === CAShapeLayer.self {
view.removeFromSuperlayer()
}
}
}
for circle in circles {
if type(of: circle) === BeaconCircle.self {
if (circle as! BeaconCircle).correctedDistance == 0 {
infoToDrawCircle(circle: circle, radius: beaconRadius, color: beaconNoActiveColor.cgColor, frameColor: beaconFrameColor.cgColor, frameWidth: lineWidthBeaconFrame)
drawBeaconText(circle: circle as! BeaconCircle)
} else {
infoToDrawCircle(circle: circle, radius: beaconRadius, color: beaconColor.cgColor, frameColor: beaconFrameColor.cgColor, frameWidth: lineWidthBeaconFrame)
drawBeaconText(circle: circle as! BeaconCircle)
}
if type(of: circle) === BeaconCircle.self && (circle as! BeaconCircle).correctedDistance != 0 {
infoToDrawCircle(circle: circle, radius: CGFloat((circle as! BeaconCircle).correctedDistance), color: distanceBeaconColor.cgColor, frameColor: distanceBeaconFrameColor.cgColor, frameWidth: lineWidthOfDistanceFrame)
// infoToDrawCircle(circle: circle, radius: CGFloat((circle as! BeaconCircle).notCorrectedDistance), color: UIColor.gray.cgColor, frameColor: distanceBeaconFrameColor.cgColor, frameWidth: lineWidthOfDistanceFrame)
}
} else if type(of: circle) === UserCircle.self {
infoToDrawCircle(circle: circle, radius: userRadius, color: userColor.cgColor, frameColor: userFrameColor.cgColor, frameWidth: lineWidthUserFrame)
} else if type (of: circle) === UserRawCircle.self {
infoToDrawCircle(circle: circle, radius: userRawRadius, color: userRawColor.cgColor, frameColor: userRawFrameColor.cgColor, frameWidth: lineWidthOfuserRawFrame)
} else {
Logger.logMessage(message: "incorrect circle type", level: .error)
break
}
}
}
private func drawBeaconText (circle: BeaconCircle) {
let attributes: NSDictionary = [
NSForegroundColorAttributeName: textColor,
NSFontAttributeName: textFont
]
let minor = circle.minor
let correctDistance = Int(round(100 * circle.correctDistanceForText) / 100)
let notCorrectDistance = Int(round(100 * circle.notCorrectDistanceForText) / 100)
beaconText = "m: \(minor), cd: \(correctDistance), nd: \(notCorrectDistance)" as NSString
if circle.x > Int((bounds.width - 50)) {
beaconText.draw(at: CGPoint(x: circle.x - 70, y: circle.y + 5), withAttributes: attributes as? [String : Any])
} else if circle.x < 20 {
beaconText.draw(at: CGPoint(x: circle.x + 5, y: circle.y + 5), withAttributes: attributes as? [String : Any])
} else {
beaconText.draw(at: CGPoint(x: circle.x + 6, y: circle.y + 5), withAttributes: attributes as? [String : Any])
}
}
private func infoToDrawCircle (circle: Circle, radius: CGFloat, color: CGColor, frameColor: CGColor, frameWidth: CGFloat) {
let center = CGPoint(x: circle.x, y: circle.y)
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 360, clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = color
shapeLayer.lineWidth = frameWidth
shapeLayer.strokeColor = frameColor
layer.addSublayer(shapeLayer)
}
}
I suggest you conform your MapViewController to the MKMapViewDelegate. In this way you can add annotations (also animated) to the map and you won't have to worry about them anymore since it's all handled by the map delegate.
You can use:
self.map.showAnnotations(pins, animated: true)
where pins is an array of MKAnnotation. Here is the link to the documentation for the method.
Cheers

UIBezierPath not updated as expected

This is my code for my custom view:
class CircleView3: UIView {
let endPoint = 270.0
var index = 0.0
var startPoint: Double {
get{
index++
let div = 360.0/60.0
let vaule = div * index
return 290.0 + vaule
}
}
func degreesToRadians (number: Double) -> CGFloat {
return CGFloat(number) * CGFloat(M_PI) / 180.0
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
let startAngleRadiant: CGFloat = degreesToRadians(startPoint)
let endAngleRadiant: CGFloat = degreesToRadians(endPoint)
print("start = \(startAngleRadiant)")
print("end = \(endAngleRadiant)")
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let path = UIBezierPath()
path.moveToPoint(center)
path.addArcWithCenter(center, radius: self.frame.height/2.2, startAngle: startAngleRadiant, endAngle: endAngleRadiant, clockwise: true)
path.addLineToPoint(center)
let strokeColor: UIColor = UIColor(red: 155.0/255.0, green: 137.0/255.0, blue: 22.0/255.0, alpha: 1.0)
strokeColor.setFill() //strokeColor.setStroke()
path.fill()
path.stroke()
}
}
I am drawing a circle. I call the setNeedsDisplay function from a view controller each second. I am supposed to draw a circle that is reducing its start and end point each second, but the circle keep its last line (position) even after the update. Plus the circle is not filled with my color, just the line is filled.
http://www.mediafire.com/watch/xnkew5eu8da5wub/IMG_0066.MOV
Update
I print the start values in drawRect, and the values are correct:
start = 293.6
start = 297.2
start = 300.8
start = 304.4
start = 308.0
start = 311.6
start = 315.2
start = 318.8
start = 322.4
start = 326.0
start = 329.6
start = 333.2
start = 336.8
start = 340.4
of course these are the values before changing them to radians.
Update 2
After I call the strokeColor.setFill() the situation is better, but I still see the problem. Please see this new video:
http://www.mediafire.com/watch/nb1b3v2zgqletwb/IMG_0069.MOV
For what its worth, I put your code in a project and I see a shrinking circle as setNeedsDisplay is called on the customView. It does not draw like your video.
Here is my additional code:
class ViewController: UIViewController {
#IBOutlet weak var circleView: CircleView3!
override func viewDidLoad() {
super.viewDidLoad()
NSTimer.scheduledTimerWithTimeInterval(0.25, target: self, selector: "updateCircle", userInfo: nil, repeats: true)
}
func updateCircle() {
circleView.setNeedsDisplay()
}
}

Resources