I'm trying to make a button move to 5 different locations depending on a random number (1-6), I also want to display they random number in a label.
I have written the following code but the button doesn't seem to move to the location I specified in the IF statement: -
import UIKit
class game: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
//Decalare display label
#IBOutlet var d1: UILabel!
//One Button declaration
#IBOutlet var oneBTN: UIButton!
#IBAction func rollBTNPress(sender: AnyObject) {
//Generate random number
var r = arc4random_uniform(5)
//Display dice1 number in d1 Label
d1.text = "\(dice1)"
if (r == 1) {
oneBTN.center = CGPointMake(10, 70);
} else if (r == 2) {
oneBTN.center = CGPointMake(30, 70);
} else if (r == 3) {
oneBTN.center = CGPointMake(50, 70);
} else if (r == 4) {
oneBTN.center = CGPointMake(70, 70);
} else if (r == 5) {
oneBTN.center = CGPointMake(90, 70);
} else {
oneBTN.center = CGPointMake(0, 70);
}
}
}
The code runs and compiles without any issues. However the button position seems to be random and it's actually ignoring the coordinates specified in the IF statement.
What's even stranger is that if I comment out the d1.text = "\(dice1)" the button begins to move in the correct positions depending on the random number.
I also tried to change the CGPointMake and use CGPoint instead but I get exactly the same behaviour.
Thank you to vacawama for answering this question in the comments, indeed disabling Autolayout solved the issue.
More detailed answer here: -
Swift NSTimer and IBOutlet Issue
#IBAction func moveButton(button: UIButton) {
// Find the button's width and height
let buttonWidth = button.frame.width
let buttonHeight = button.frame.height
// Find the width and height of the enclosing view
let viewWidth = button.superview!.bounds.width
let viewHeight = button.superview!.bounds.height
// Compute width and height of the area to contain the button's center
let xwidth = viewWidth - buttonWidth
let yheight = viewHeight - buttonHeight
// Generate a random x and y offset
let xoffset = CGFloat(arc4random_uniform(UInt32(xwidth)))
let yoffset = CGFloat(arc4random_uniform(UInt32(yheight)))
// Offset the button's center by the random offsets.
button.center.x = xoffset + buttonWidth / 2
button.center.y = yoffset + buttonHeight / 2
}
Related
I'm making a game with two methods of moving. When tapping the screen the player moves up. The game world also moves towards the player looking like the bird is flying forward. When the user holds down the on the screen the player will start flying forward in its position. When the player doesn't touch the screen at all, the player falls (almost like flappy bird).
The game worked fine and smooth until I added the long press method. Because one second is too short, it gets detected as a tap. So when I tap the screen fast 2 times the long press method gets called as well. So I'm thinking there must be another way instead of using long press gesture?
Is there any way to programmatically detect touch up inside and touch and hold in spritekit? I know you could do this in UIKit singleview applications on buttons.
But I need to make it work anywhere on the screen and not just from tapping a button.
I'm using Spritekit as game technology and Xcode as Platform.
Any ideas?
Here is a full working solution for you. Tap to jump the block, hold the screen to move forward. when you touch a wall you will reset position.
Swift3:
class Bird: SKSpriteNode {
private let flyUpImpulse = CGVector(dx: 0, dy: 20)
private let flyForwardForce = CGVector(dx: 100, dy: 0)
// var pb: SKPhysicsBody { return self.physicsBody! }
func initialize() {
color = .blue
size = CGSize(width: 50, height: 50)
physicsBody = SKPhysicsBody(rectangleOf: size)
physicsBody!.categoryBitMask = UInt32(1)
}
func flyUp() {
physicsBody!.applyImpulse(flyUpImpulse)
}
func flyForward() {
physicsBody!.applyForce(flyForwardForce)
}
func resetPosition() {
guard let scene = self.scene else {
print("reset position failed: bird has not been added to scene yet!")
return
}
run(.move(to: CGPoint(x: scene.frame.minX + 50, y: scene.frame.midY), duration: 0))
physicsBody!.velocity = CGVector.zero
}
}
class GameScene: SKScene, SKPhysicsContactDelegate {
enum ControlToDo { case tap, longPress, none }
var controlToDo = ControlToDo.none
// How many frames we have to release screen in order to recognize a "tap".
// Less frames will give a faster response time, but may also give incorrect input:
let tapThreshold = 10
var isTouchingScreen = false
var frameCountSinceFirstTouch = 0
let bird = Bird()
// Scene setup:
override func didMove(to view: SKView) {
removeAllChildren() // Remove this from your actual project.
anchorPoint = CGPoint(x: 0.5, y: 0.5)
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsBody!.categoryBitMask = UInt32(2)
physicsBody!.contactTestBitMask = UInt32(1)
physicsWorld.gravity = CGVector(dx: 0, dy: -2)
physicsWorld.contactDelegate = self
bird.initialize()
addChild(bird)
}
// Touch handling stuff:
func tap() {
bird.flyUp()
controlToDo = .none
}
func longPress() {
bird.flyForward()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
isTouchingScreen = true
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
isTouchingScreen = false
if frameCountSinceFirstTouch > tapThreshold {
controlToDo = .none
frameCountSinceFirstTouch = 0
}
else if frameCountSinceFirstTouch < tapThreshold {
controlToDo = .tap
frameCountSinceFirstTouch = 0
}
}
override func update(_ currentTime: TimeInterval) {
// Increase counter if touching the screen:
if isTouchingScreen {
frameCountSinceFirstTouch += 1
}
// If we have held the screen for long enough, do a longPress:
if frameCountSinceFirstTouch > tapThreshold {
controlToDo = .longPress
}
switch controlToDo {
case .tap: tap()
case .longPress: longPress()
case .none: break
}
}
// Reset positon on touch of wall:
func didBegin(_ contact: SKPhysicsContact) {
bird.resetPosition()
}
}
Objective C:
// MAKE SURE YOU HAVE <SKPhysicsContactDelegate> in your .h file!
#import "GameScene.h"
#implementation GameScene {
SKSpriteNode *_bird;
/// 1 is tap, 2 is longpress (i couldn't figure out how to compare strings, or do an enum)
int _controlToDo;
CGFloat _tapThreshold;
CGFloat _frameCountSinceFirstTouch;
Boolean _isTouchingScreen;
}
- (void) setupProperties {
_controlToDo = 0;
_tapThreshold = 10;
_isTouchingScreen = false;
_frameCountSinceFirstTouch = 0;
}
- (void) setupScene {
self.anchorPoint = CGPointMake(0.5, 0.5);
[self removeAllChildren];
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
self.physicsBody.categoryBitMask = 2;
self.physicsBody.contactTestBitMask = 1;
self.physicsWorld.gravity = CGVectorMake(0, -2);
self.physicsWorld.contactDelegate = self;
_bird = [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:CGSizeMake(50, 50)];
_bird.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:_bird.size];
_bird.physicsBody.categoryBitMask = 1;
[self addChild:_bird];
}
- (void) birdResetPosition {
CGFloat min = 0 - (self.size.width / 2) + (_bird.size.width / 2) + 5;
CGPoint center = CGPointMake(min, 0);
SKAction *movement = [SKAction moveTo:center duration:0];
[_bird runAction:movement];
}
- (void)didMoveToView:(SKView *)view {
[self setupProperties];
[self setupScene];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
_isTouchingScreen = true;
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
_isTouchingScreen = false;
if (_frameCountSinceFirstTouch > _tapThreshold) {
_controlToDo = 0;
}
else if (_frameCountSinceFirstTouch < _tapThreshold) {
_controlToDo = 1;
}
_frameCountSinceFirstTouch = 0;
}
-(void)update:(CFTimeInterval)currentTime {
// Increase counter if touching the screen:
if (_isTouchingScreen == true) {
_frameCountSinceFirstTouch += 1;
}
// If we have held the screen for long enough, do a longPress:
if (_frameCountSinceFirstTouch > _tapThreshold) {
_controlToDo = 2;
}
// Bird fly up:
if (_controlToDo == 1) {
[_bird.physicsBody applyImpulse:CGVectorMake(0, 20)];
_controlToDo = 0;
}
// Bird fly forward:
else if (_controlToDo == 2) {
[_bird.physicsBody applyForce:CGVectorMake(100, 0)];
}
}
- (void) didBeginContact:(SKPhysicsContact *)contact {
[self birdResetPosition];
_bird.physicsBody.velocity = CGVectorMake(0, 0);
}
#end
All SKNodes have access to touch events, you just need to enable userInteractionEnabled
By default, this is set to false, but if you are using an SKS file, then the SKS default is set to true
As long as you do not have any other node enabled, when you touch the scene, it will fire for the scene.
To get a hold event going, I would recommend using SKAction's on your scene.
Basically, we want to wait for a specific time period, then fire the event
If at any point the finger is removed, then we remove the action, not firing your event.
Swift:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
let wait = SKAction.wait(forDuration:1)
let beginHoldEvent = SKAction(run:{//function to start hold event})
run(SKAction.sequence([wait,beginHoldEvent],withKey:"holding")
}
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
guard let _ = action(forKey:"holding") else {return}
removeAction(forKey:"holding")
//do single click event
}
Objective C:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
void (^holdBlock)(void) = ^{
//do holding code here
};
SKAction* wait = [SKAction waitForDuration:1];
SKAction* beginHoldEvent = [SKAction runBlock:holdBlock];
SKAction* seq = [SKAction sequence:#[wait,beghinHoldEvent]];
[self runAction:seq withKey:#"holding"];
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if([self actionForKey:#"holding"] == nil){
//we are on a long touch do nothing
return;
}
[self removeActionForKey:#"holding")];
//do tap event
}
Now this code does not handle multiple touches, you need to handle that.
I am able to reorder my UICollectionViewCells on iOS 9 by dragging it using a gesture recognizer and simplementing newly iOS 9 support for reordering.
public func beginInteractiveMovementForItemAtIndexPath(indexPath: NSIndexPath) -> Bool // returns NO if reordering was prevented from beginning - otherwise YES
public func updateInteractiveMovementTargetPosition(targetPosition: CGPoint)
public func endInteractiveMovement()
public func cancelInteractiveMovement()
I noticed that when I start dragging a cell, its center changes to be the touch location, I don't like that.
I would like to be able to drag my cell by it's corner if I want.
Do you know how to do this?
Thanks a lot.
(Written in Swift 3.1)
For the targetPosition parameter of the updateInteractiveMovementTargetPosition function, instead of using the gesture recognizer's location directly like this...
var location = recognizer.location(in: collectionView)
collectionView.updateInteractiveMovementTargetPosition(location)
... I created a function that takes the center of the cell to be dragged (The location that the collectionView updateInteractiveMovementTargetPosition would use, and then takes the location of the gesture recognizer's touch in the cell, and subtracts that from the center of the cell.
func offsetOfTouchFrom(recognizer: UIGestureRecognizer, inCell cell: UICollectionViewCell) -> CGPoint {
let locationOfTouchInCell = recognizer.location(in: cell)
let cellCenterX = cell.frame.width / 2
let cellCenterY = cell.frame.height / 2
let cellCenter = CGPoint(x: cellCenterX, y: cellCenterY)
var offSetPoint = CGPoint.zero
offSetPoint.y = cellCenter.y - locationOfTouchInCell.y
offSetPoint.x = cellCenter.x - locationOfTouchInCell.x
return offSetPoint
}
I have a simple var offsetForCollectionViewCellBeingMoved: CGPoint = .zero in my view controller that will store that offset so function above doesn't need to be called every time the gesture recognizer's location changes.
So the target of my gesture recognizer would look like this:
func collectionViewLongPressGestureRecognizerWasTriggered(recognizer: UILongPressGestureRecognizer) {
guard let indexPath = collectionView.indexPathForItem(at: recognizer.location(in: self.collectionView)),
let cell = collectionView.cellForItem(at: indexPath), indexPath.item != 0 else { return }
switch recognizer.state {
case .began:
collectionView.beginInteractiveMovementForItem(at: indexPath)
// This is the class variable I mentioned above
offsetForCollectionViewCellBeingMoved = offsetOfTouchFrom(recognizer: recognizer, inCell: cell)
// This is the vanilla location of the touch that alone would make the cell's center snap to your touch location
var location = recognizer.location(in: collectionView)
/* These two lines add the offset calculated a couple lines up to
the normal location to make it so you can drag from any part of the
cell and have it stay where your finger is. */
location.x += offsetForCollectionViewCellBeingMoved.x
location.y += offsetForCollectionViewCellBeingMoved.y
collectionView.updateInteractiveMovementTargetPosition(location)
case .changed:
var location = recognizer.location(in: collectionView)
location.x += offsetForCollectionViewCellBeingMoved.x
location.y += offsetForCollectionViewCellBeingMoved.y
collectionView.updateInteractiveMovementTargetPosition(location)
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
If your collection view is only scrolling in once direction then the easiest way to achieve this is to simple lock the axis which isnt scrolling to something hardcoded, this means your cell will only move in the axis that you can scroll. Here is the code, see the changed case...
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
guard let selectedIndexPath = self.collectionView
.indexPathForItem(at: gesture
.location(in: self.collectionView)) else { break }
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
var gesturePosition = gesture.location(in: gesture.view!)
gesturePosition.x = (self.collectionView.frame.width / 2) - 20
collectionView.updateInteractiveMovementTargetPosition(gesturePosition)
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
I am trying to take a screenshot of my UIView and Crop it, save it to my Photo library. As i am trying to do this there are 3 conflicts.
(1) - I want to take Screenshot with Blur in it, As blur filter never gets saved in the screenshot.
(2) - The image quality is very low.
(3) - I am not able to crop the image.
This is my code -
#IBAction func Screenshot(_ sender: UIButton) {
// Declare the snapshot boundaries
let top: CGFloat = 70
let bottom: CGFloat = 400
// The size of the cropped image
let size = CGSize(width: view.frame.size.width, height: view.frame.size.height - top - bottom)
// Start the context
UIGraphicsBeginImageContext(size)
// we are going to use context in a couple of places
let context = UIGraphicsGetCurrentContext()!
// Transform the context so that anything drawn into it is displaced "top" pixels up
// Something drawn at coordinate (0, 0) will now be drawn at (0, -top)
// This will result in the "top" pixels being cut off
// The bottom pixels are cut off because the size of the of the context
context.translateBy(x: 0, y: -top)
// Draw the view into the context (this is the snapshot)
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
let snapshot = UIGraphicsGetImageFromCurrentImageContext()
// End the context (this is required to not leak resources)
UIGraphicsEndImageContext()
// Save to photos
UIImageWriteToSavedPhotosAlbum(snapshot!, nil, nil, nil)
}
You said:
I want to take Screenshot with Blur in it, As blur filter never gets saved in the screenshot.
I wonder if the view being snapshotted might not be the one with the UIVisualEffectView as a subview. Because when I use the code at the end of the answer, the blur effect (and the impact of changing the fractionCompleted) is captured.
The image quality is very low.
If you use UIGraphicsBeginImageContextWithOptions with a scale of zero, it should capture the image at the resolution of the device:
UIGraphicsBeginImageContextWithOptions(size, isOpaque, 0)
I am not able to crop the image.
I personally capture the whole view, and then crop as needed. See UIView extension below.
In Swift 3:
class ViewController: UIViewController {
var animator: UIViewPropertyAnimator?
#IBOutlet weak var imageView: UIImageView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let blur = UIBlurEffect(style: .light)
let effectView = UIVisualEffectView(effect: blur)
view.addSubview(effectView)
effectView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
effectView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor),
effectView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor),
effectView.topAnchor.constraint(equalTo: imageView.topAnchor),
effectView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor)
])
animator = UIViewPropertyAnimator(duration: 0, curve: .linear) { effectView.effect = nil }
}
#IBAction func didChangeValueForSlider(_ sender: UISlider) {
animator?.fractionComplete = CGFloat(sender.value)
}
#IBAction func didTapSnapshotButton(_ sender: AnyObject) {
if let snapshot = view.snapshot(of: imageView.frame) {
UIImageWriteToSavedPhotosAlbum(snapshot, nil, nil, nil)
}
}
}
extension UIView {
/// Create snapshot
///
/// - parameter rect: The `CGRect` of the portion of the view to return. If `nil` (or omitted),
/// return snapshot of the whole view.
///
/// - returns: Returns `UIImage` of the specified portion of the view.
func snapshot(of rect: CGRect? = nil) -> UIImage? {
// snapshot entire view
UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, 0)
drawHierarchy(in: bounds, afterScreenUpdates: true)
let wholeImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// if no `rect` provided, return image of whole view
guard let image = wholeImage, let rect = rect else { return wholeImage }
// otherwise, grab specified `rect` of image
let scale = image.scale
let scaledRect = CGRect(x: rect.origin.x * scale, y: rect.origin.y * scale, width: rect.size.width * scale, height: rect.size.height * scale)
guard let cgImage = image.cgImage?.cropping(to: scaledRect) else { return nil }
return UIImage(cgImage: cgImage, scale: scale, orientation: .up)
}
}
}
Or in Swift 2:
class ViewController: UIViewController {
var animator: UIViewPropertyAnimator?
#IBOutlet weak var imageView: UIImageView!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let blur = UIBlurEffect(style: .Light)
let effectView = UIVisualEffectView(effect: blur)
view.addSubview(effectView)
effectView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activateConstraints([
effectView.leadingAnchor.constraintEqualToAnchor(imageView.leadingAnchor),
effectView.trailingAnchor.constraintEqualToAnchor(imageView.trailingAnchor),
effectView.topAnchor.constraintEqualToAnchor(imageView.topAnchor),
effectView.bottomAnchor.constraintEqualToAnchor(imageView.bottomAnchor)
])
animator = UIViewPropertyAnimator(duration: 0, curve: .Linear) { effectView.effect = nil }
}
#IBAction func didChangeValueForSlider(sender: UISlider) {
animator?.fractionComplete = CGFloat(sender.value)
}
#IBAction func didTapSnapshotButton(sender: AnyObject) {
if let snapshot = view.snapshot(of: imageView.frame) {
UIImageWriteToSavedPhotosAlbum(snapshot, nil, nil, nil)
}
}
}
extension UIView {
/// Create snapshot
///
/// - parameter rect: The `CGRect` of the portion of the view to return. If `nil` (or omitted),
/// return snapshot of the whole view.
///
/// - returns: Returns `UIImage` of the specified portion of the view.
func snapshot(of rect: CGRect? = nil) -> UIImage? {
// snapshot entire view
UIGraphicsBeginImageContextWithOptions(bounds.size, opaque, 0)
drawViewHierarchyInRect(bounds, afterScreenUpdates: true)
let wholeImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// if no `rect` provided, return image of whole view
guard let rect = rect, let image = wholeImage else { return wholeImage }
// otherwise, grab specified `rect` of image
let scale = image.scale
let scaledRect = CGRect(x: rect.origin.x * scale, y: rect.origin.y * scale, width: rect.size.width * scale, height: rect.size.height * scale)
guard let cgImage = CGImageCreateWithImageInRect(image.CGImage!, scaledRect) else { return nil }
return UIImage(CGImage: cgImage, scale: scale, orientation: .Up)
}
}
So, when I capture four images at four different slider positions, that yields:
I am not able to crop the image in the right way, As there is navigation bar and status bar showing with blank (White) background. (Rest of the image crops well).
here is the code -
let top: CGFloat = 70
let bottom: CGFloat = 280
// The size of the cropped image
let size = CGSize(width: view.frame.size.width, height: view.frame.size.height - top - bottom)
// Start the context
UIGraphicsBeginImageContext(size)
// we are going to use context in a couple of places
let context = UIGraphicsGetCurrentContext()!
// Transform the context so that anything drawn into it is displaced "top" pixels up
// Something drawn at coordinate (0, 0) will now be drawn at (0, -top)
// This will result in the "top" pixels being cut off
// The bottom pixels are cut off because the size of the of the context
context.translateBy(x: 0, y: 0)
// Draw the view into the context (this is the snapshot)
UIGraphicsBeginImageContextWithOptions(size,view.isOpaque, 0)
self.view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
let snapshot = UIGraphicsGetImageFromCurrentImageContext()
I'm trying to make a simple drawing app (OSX Mac app) and I'm trying to figure out how the user can draw a line by two mouse clicks, for example, the first mouse click (mouseDown then mouseUP) would mark the origin point of the line, and the second mouse click (mouseDown then mouseUP) would mark the end point of the line. Before the user makes the second click of the end point, I'd like the line (before anchoring the end point) to be shown live, kind of like in photoshop. Both Objective-C and Swift are fine.
So far I've got...
var newLinear = NSBezierPath()
override func mouseDown(theEvent: NSEvent) {
super.mouseDown(theEvent)
var lastPoint = theEvent.locationInWindow
lastPoint.x -= frame.origin.x
lastPoint.y -= frame.origin.y
newLinear.moveToPoint(lastPoint)
}
override func mouseUp(theEvent: NSEvent) {
var newPoint = theEvent.locationInWindow
newPoint.x -= frame.origin.x
newPoint.y -= frame.origin.y
newLinear.lineToPoint(newPoint)
needsDisplay = true
}
Cheers!
enums with associated values are great for this as your application scales up and possibly adds other tools and states.
enum State {
case normal
case drawingLine(from: CGPoint, to: CGPoint)
}
var state = State.normal
override func mouseDown(theEvent: NSEvent) {
super.mouseDown(theEvent)
var lastPoint = theEvent.locationInWindow
lastPoint.x -= frame.origin.x
lastPoint.y -= frame.origin.y
state = .drawingLine(from: lastPoint, to: lastPoint)
}
override func mouseUp(theEvent: NSEvent) {
if case .drawingLine(let firstPoint, _) = state {
var newPoint = theEvent.locationInWindow
newPoint.x -= frame.origin.x
newPoint.y -= frame.origin.y
//finalize line from `firstPoint` to `newPoint`
}
}
override func mouseMoved(theEvent: NSEvent) {
if case .drawingLine(let firstPoint, _) = state {
needsDisplay = true
var newPoint = theEvent.locationInWindow
newPoint.x -= frame.origin.x
newPoint.y -= frame.origin.y
state = .drawingLine(from: firstPoint, to: newPoint)
}
}
override func draw(_ dirtyRect: NSRect) {
if case .drawingLine(let firstPoint, let secondPoint) = state {
//draw your line from `firstPoint` to `secondPoint`
}
}
I understand what you are attempting to achieve here! So I have added lines of code to the andyvn22's solution that should assist your endeavours, please note: for simplicity purposes start a new 'Xcode Project' and we can make sure everything is covered together.
In your new project Right Click 'ViewController.swift' and add New File... Select 'Cocoa Class' and Click Next, name this file 'DrawingView' assure Subclass of: is 'NSView' and select Next.
Now your finished that lets setup your interface, enter 'Main.storyboard' and drag/drop a 'Custom View' make sure to add constrains according to your preferences. Now enter your 'Identity Inspector' while 'Custom View' is selected and add 'DrawingView' to Class at the top.
I hope this is making sense soo far! Ok open 'Assistant Editor' so you can view 'ViewController.swift' and 'Main.storyboard' simultaneously, Right Click and drag from 'Custom View' to 'ViewController.swift', name this 'draw' and select connect.
You will notice Xcode has automatically updated #IBOutlet to your Subclass, and your 'ViewController.swift' looks like the example below.
import Cocoa
class ViewController: NSViewController {
#IBOutlet var draw: DrawingView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
Now select the 'DrawingView.swift' file and clear all content on page, highlight the code below, copy and paste into your project.
import Cocoa
class DrawingView: NSView {
// >>>CODE ADDED BY LC<<<
private var path: NSBezierPath = {
let path = NSBezierPath()
path.lineWidth = 50.0
path.lineJoinStyle = .roundLineJoinStyle
path.lineCapStyle = .roundLineCapStyle
return path
}()
enum State {
case normal
case drawingLine(from: CGPoint, to: CGPoint)
}
var state = State.normal
override func mouseDown(with event: NSEvent) {
super.mouseDown(with: event)
var lastPoint = event.locationInWindow
lastPoint.x -= frame.origin.x
lastPoint.y -= frame.origin.y
state = .drawingLine(from: lastPoint, to: lastPoint)
}
override func mouseUp(with event: NSEvent) {
if case .drawingLine(let firstPoint, _) = state {
var newPoint = event.locationInWindow
newPoint.x -= frame.origin.x
newPoint.y -= frame.origin.y
// >>>CODE ADDED BY LC<<<
path.move(to: convert(event.locationInWindow, from: nil))
path.line(to: firstPoint)
needsDisplay = true
}
}
override func draw(_ dirtyRect: NSRect) {
if case .drawingLine(let firstPoint, let secondPoint) = state {
// >>>CODE ADDED BY LC<<<
NSColor.orange.set()
path.lineWidth = 5.0
path.stroke()
path.line(to: firstPoint)
path.line(to: secondPoint)
}
}
}
Everything should be running as intended, you will be incapable of drawing a crocked line. I hope this has helped and feel free to reply for additional information.
On UIView you can change the backgroundColour animated. And on a UISlideView you can change the value animated.
Can you add a custom property to your own UIView subclass so that it can be animated?
If I have a CGPath within my UIView then I can animate the drawing of it by changing the percentage drawn of the path.
Can I encapsulate that animation into the subclass.
i.e. I have a UIView with a CGPath that creates a circle.
If the circle is not there it represents 0%. If the circle is full it represents 100%. I can draw any value by changing the percentage drawn of the path. I can also animate the change (within the UIView subclass) by animating the percentage of the CGPath and redrawing the path.
Can I set some property (i.e. percentage) on the UIView so that I can stick the change into a UIView animateWithDuration block and it animate the change of the percentage of the path?
I hope I have explained what I would like to do well.
Essentially, all I want to do is something like...
[UIView animateWithDuration:1.0
animations:^{
myCircleView.percentage = 0.7;
}
completion:nil];
and the circle path animate to the given percentage.
If you extend CALayer and implement your custom
- (void) drawInContext:(CGContextRef) context
You can make an animatable property by overriding needsDisplayForKey (in your custom CALayer class) like this:
+ (BOOL) needsDisplayForKey:(NSString *) key {
if ([key isEqualToString:#"percentage"]) {
return YES;
}
return [super needsDisplayForKey:key];
}
Of course, you also need to have a #property called percentage. From now on you can animate the percentage property using core animation. I did not check whether it works using the [UIView animateWithDuration...] call as well. It might work. But this worked for me:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"percentage"];
animation.duration = 1.0;
animation.fromValue = [NSNumber numberWithDouble:0];
animation.toValue = [NSNumber numberWithDouble:100];
[myCustomLayer addAnimation:animation forKey:#"animatePercentage"];
Oh and to use yourCustomLayer with myCircleView, do this:
[myCircleView.layer addSublayer:myCustomLayer];
Complete Swift 3 example:
public class CircularProgressView: UIView {
public dynamic var progress: CGFloat = 0 {
didSet {
progressLayer.progress = progress
}
}
fileprivate var progressLayer: CircularProgressLayer {
return layer as! CircularProgressLayer
}
override public class var layerClass: AnyClass {
return CircularProgressLayer.self
}
override public func action(for layer: CALayer, forKey event: String) -> CAAction? {
if event == #keyPath(CircularProgressLayer.progress),
let action = action(for: layer, forKey: #keyPath(backgroundColor)) as? CAAnimation,
let animation: CABasicAnimation = (action.copy() as? CABasicAnimation) {
animation.keyPath = #keyPath(CircularProgressLayer.progress)
animation.fromValue = progressLayer.progress
animation.toValue = progress
self.layer.add(animation, forKey: #keyPath(CircularProgressLayer.progress))
return animation
}
return super.action(for: layer, forKey: event)
}
}
/*
* Concepts taken from:
* https://stackoverflow.com/a/37470079
*/
fileprivate class CircularProgressLayer: CALayer {
#NSManaged var progress: CGFloat
let startAngle: CGFloat = 1.5 * .pi
let twoPi: CGFloat = 2 * .pi
let halfPi: CGFloat = .pi / 2
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(progress) {
return true
}
return super.needsDisplay(forKey: key)
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
UIGraphicsPushContext(ctx)
//Light Grey
UIColor.lightGray.setStroke()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let strokeWidth: CGFloat = 4
let radius = (bounds.size.width / 2) - strokeWidth
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true)
path.lineWidth = strokeWidth
path.stroke()
//Red
UIColor.red.setStroke()
let endAngle = (twoPi * progress) - halfPi
let pathProgress = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle , clockwise: true)
pathProgress.lineWidth = strokeWidth
pathProgress.lineCapStyle = .round
pathProgress.stroke()
UIGraphicsPopContext()
}
}
let circularProgress = CircularProgressView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut, animations: {
circularProgress.progress = 0.76
}, completion: nil)
There is a great objc article here, which goes into details about how this works
As well as a objc project that uses the same concepts here:
Essentially action(for layer:) will be called when an object is being animated from an animation block, we can start our own animations with the same properties (stolen from the backgroundColor property) and animate the changes.
For the ones who needs more details on that like I did:
there is a cool example from Apple covering this question.
E.g. thanks to it I found that you don't actually need to add your custom layer as sublayer (as #Tom van Zummeren suggests). Instead it's enough to add a class method to your View class:
+ (Class)layerClass
{
return [CustomLayer class];
}
Hope it helps somebody.
you will have to implement the percentage part yourself. you can override layer drawing code to draw your cgpath accroding to the set percentage value. checkout the core animation programming guide and animation types and timing guide
#David Rees answer get me on the right track, but there is one issue. In my case
completion of animation always returns false, right after animation has began.
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut, animations: {
circularProgress.progress = 0.76
}, completion: { finished in
// finished - always false
})
This is the way it've worked for me - action of animation is handled inside of CALayer.
I have also included small example how to make layer with additional properties like "color".
In this case, without initializer that copies the values, changing the color would take affect only on non-animating view. During animation it would be visble with "default setting".
public class CircularProgressView: UIView {
#objc public dynamic var progress: CGFloat {
get {
return progressLayer.progress
}
set {
progressLayer.progress = newValue
}
}
fileprivate var progressLayer: CircularProgressLayer {
return layer as! CircularProgressLayer
}
override public class var layerClass: AnyClass {
return CircularProgressLayer.self
}
}
/*
* Concepts taken from:
* https://stackoverflow.com/a/37470079
*/
fileprivate class CircularProgressLayer: CALayer {
#NSManaged var progress: CGFloat
let startAngle: CGFloat = 1.5 * .pi
let twoPi: CGFloat = 2 * .pi
let halfPi: CGFloat = .pi / 2
var color: UIColor = .red
// preserve layer properties
// without this specyfic init, if color was changed to sth else
// animation would still use .red
override init(layer: Any) {
super.init(layer: layer)
if let layer = layer as? CircularProgressLayer {
self.color = layer.color
self.progress = layer.progress
}
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(progress) {
return true
}
return super.needsDisplay(forKey: key)
}
override func action(forKey event: String) -> CAAction? {
if event == #keyPath(CircularProgressLayer.progress) {
guard let animation = action(forKey: #keyPath(backgroundColor)) as? CABasicAnimation else {
setNeedsDisplay()
return nil
}
if let presentation = presentation() {
animation.keyPath = event
animation.fromValue = presentation.value(forKeyPath: event)
animation.toValue = nil
} else {
return nil
}
return animation
}
return super.action(forKey: event)
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
UIGraphicsPushContext(ctx)
//Light Gray
UIColor.lightGray.setStroke()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let strokeWidth: CGFloat = 4
let radius = (bounds.size.width / 2) - strokeWidth
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true)
path.lineWidth = strokeWidth
path.stroke()
// Red - default
self.color.setStroke()
let endAngle = (twoPi * progress) - halfPi
let pathProgress = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle , clockwise: true)
pathProgress.lineWidth = strokeWidth
pathProgress.lineCapStyle = .round
pathProgress.stroke()
UIGraphicsPopContext()
}
}
The way to handle animations differently and copy layer properties I have found in this article:
https://medium.com/better-programming/make-apis-like-apple-animatable-view-properties-in-swift-4349b2244cea