AnimatableData for Line Chart in SwiftUI - animation

I'd like to create animation of the line chart like drawing from start point to end point.
I've created InsettableShape (code below) in order to use with strokeBorder(), but can't figure out how to define animatableData.
Thank you!
(I know I can do it with Shape and trim(), but I need inset.)
import SwiftUI
struct LineChartInsettableShape: InsettableShape {
let series: [CGFloat]
var insetAmount: CGFloat = 0
func inset(by amount: CGFloat) -> some InsettableShape {
var chart = self
chart.insetAmount += amount
return chart
}
func path(in rect: CGRect) -> Path {
guard !series.isEmpty else { return Path() }
let minY = series.min()!
let maxY = series.max()!
let xStep = (rect.width - insetAmount * 2) / CGFloat(series.count - 1)
func point(index: Int) -> CGPoint {
let yValue: CGFloat = CGFloat(series[index])
return CGPoint(
x: xStep * CGFloat(index) + insetAmount,
y: (rect.height - insetAmount * 2) * (1 - (yValue - minY) / (maxY - minY)) + insetAmount
)
}
return Path { path in
path.move(to: point(index: 0))
for index in 1..<series.count {
path.addLine(to: point(index: index))
}
}
}
}

It's not hard once you figure it out, but initially how this works can be a bit confusing. You need to have a property named animatableData which tells SwiftUI the name of the variable you'll be using for animation, and how to get it and set its value. For example, I have a small piece of animation code that uses a variable named inset, which is very similar to your insetAmount. Here's all the code my app needs to use this variable for animation:
var inset: CGFloat
var animatableData: CGFloat {
get { inset }
set { self.inset = newValue}
}
Not all data types can be used for animation. CGFloat, used here, certainly can be. Paul Hudson has an excellent short video on this topic: https://www.hackingwithswift.com/books/ios-swiftui/animating-simple-shapes-with-animatabledata.

There is a bar chart library, you can check it out as well https://github.com/dawigr/BarChart

Related

Cropping NSImage with onscreen coordinates incorrect on different screen sizes

I'm trying to replicate macOS's screenshot functionality, dragging a selection onscreen to provide coordinates for cropping an image. I have it working fine on my desktop Mac (2560x1600), but testing on my laptop (2016 rMBP 15", 2880x1800), the cropped image is completely wrong. I don't understand why I'd get the right results on my desktop, but not on my laptop. I think it has something to do with the Quarts coordinates being different from Cocoa coordinates, seeing as how on the laptop, the resulting image seems like the coordinates are flipped on the Y-axis.
Here is the code I am using to generate the cropping CGRect:
# Segment used to draw the CAShapeLayer:
private func handleDragging(_ event: NSEvent) {
let mouseLoc = event.locationInWindow
if let point = self.startPoint,
let layer = self.shapeLayer {
let path = CGMutablePath()
path.move(to: point)
path.addLine(to: NSPoint(x: self.startPoint.x, y: mouseLoc.y))
path.addLine(to: mouseLoc)
path.addLine(to: NSPoint(x: mouseLoc.x, y: self.startPoint.y))
path.closeSubpath()
layer.path = path
self.selectionRect = path.boundingBox
}
}
private func startDragging(_ event: NSEvent) {
if let window = self.window,
let contentView = window.contentView,
let layer = contentView.layer,
!self.isDragging {
self.isDragging = true
self.startPoint = window.mouseLocationOutsideOfEventStream
shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 1.0
shapeLayer.fillColor = NSColor.white.withAlphaComponent(0.5).cgColor
shapeLayer.strokeColor = NSColor.systemGray.cgColor
layer.addSublayer(shapeLayer)
}
}
Then this is the code where I actually generate the screenshot and crop using the CGRect:
public func processResults(_ rect: CGRect) {
if let windowID = self.globalWindow?.windowNumber,
let screen = self.getScreenWithMouse(), rect.width > 5 && rect.height > 5 {
self.delegate?.processingResults()
let cgScreenshot = CGWindowListCreateImage(screen.frame, .optionOnScreenBelowWindow, CGWindowID(windowID), .bestResolution)
var rect2 = rect
rect2.origin.y = NSMaxY(self.getScreenWithMouse()!.frame) - NSMaxY(rect);
if let croppedCGScreenshot = cgScreenshot?.cropping(to: rect2) {
let rep = NSBitmapImageRep(cgImage: croppedCGScreenshot)
let image = NSImage()
image.addRepresentation(rep)
self.showPreviewWindow(image: image)
let requests = [self.getTextRecognitionRequest()]
let imageRequestHandler = VNImageRequestHandler(cgImage: croppedCGScreenshot, orientation: .up, options: [:])
DispatchQueue.global(qos: .userInitiated).async {
do {
try imageRequestHandler.perform(requests)
} catch let error {
print("Error: \(error)")
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.hidePreviewWindow()
}
}
}
self.globalWindow = nil
}
Not 15 minutes after I asked this question, I tried one more thing and it works!
Relevant snippet:
var correctedRect = rect
// Set the Y origin properly (counteracting the flipped Y-axis)
correctedRect.origin.y = screen.frame.height - rect.origin.y - rect.height;
// Checks if we're on another screen
if (screen.frame.origin.y < 0) {
correctedRect.origin.y = correctedRect.origin.y - screen.frame.origin.y
}
// Finally, correct the x origin (if we're on another screen, the origin will be larger than zero)
correctedRect.origin.x = correctedRect.origin.x + screen.frame.origin.x
// Generate the screenshot inside the requested rect
let cgScreenshot = CGWindowListCreateImage(correctedRect, .optionOnScreenBelowWindow, CGWindowID(windowID), .bestResolution)

SwiftUI: How to create multiple views in various positions with timer?

It is pretty easy to get ready the following task in the old-school swift: every three seconds a new view (subview) appears in a new position. Here is a code:
import UIKit
class ViewController: UIViewController {
var someView = UIView()
var posX : CGFloat = 10
var posY : CGFloat = 10
var timer:Timer!
var loopCount = 1
override func viewDidLoad() {
super.viewDidLoad()
startTimer()
view.backgroundColor = .purple
}
func setView() {
someView = UIView(frame: CGRect(x: posX, y: posY, width: 10, height: 10))
someView.backgroundColor = UIColor.orange
view.addSubview(someView)
}
func startTimer() {
if timer == nil {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(loop), userInfo: nil, repeats: true)
}
}
#objc func loop(){
if (loopCount % 3 == 0) {
posX += 15
posY += 15
setView()
}
loopCount += 1
}
}
SwiftUI makes many things much easier, but not this one, I’m afraid. At least I could not find an easy way to solve it until now. Has somebody any idea?
Here is screen with the result (after several seconds):
Here is possible approach (tested with Xcode 11.2 / iOS 13.2). SwiftUI is reactive concept, so instead of add view itself, it is added a new position into view model and SwiftUI view in response to this change in view model refresh itself addition new view (in this case Rectangle) at new added position.
Demo (moment of start recording is not accurate, but rects added regularly):
Code: (see also some comments inline)
// needed to use as ID in ForEach
extension CGPoint: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(self.x)
hasher.combine(self.y)
}
}
// View model holding and generating new positions
class DemoViewModel: ObservableObject {
#Published var positions = [CGPoint]() // all points for view
private var loopCount = 0
func loop() {
if (loopCount % 3 == 0) {
if let last = positions.last { // generate new point
positions.append(CGPoint(x: last.x + 15, y: last.y + 15))
} else {
positions.append(CGPoint(x: 10, y: 10))
}
}
loopCount += 1
}
}
struct DemoAddingView: View {
#ObservedObject var vm = DemoViewModel()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
ForEach(vm.positions, id: \.self) { position in
Rectangle().fill(Color.orange) // just generate a rect view for all points
.frame(width: 10, height: 10)
.position(position) // location of rect in global coordinates
}
.onReceive(timer) { _ in
self.vm.loop() // add next point
}
}
}
}

How can I make SKSpriteNode positions the same for any simulator/device?

In my game, the position of my SKNodes slightly change when I run the App on a virtual simulator vs on a real device(my iPad).
Here are pictures of what I am talking about.
This is the virtual simulator
This is my Ipad
It is hard to see, but the two red boxes are slightly higher on my iPad than in the simulator
Here is how i declare the size and position of the red boxes and green net:
The following code is located in my GameScene.swift file
func loadAppearance_Rim1() {
Rim1 = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake((frame.size.width) / 40, (frame.size.width) / 40))
Rim1.position = CGPointMake(((frame.size.width) / 2.23), ((frame.size.height) / 1.33))
Rim1.zPosition = 1
addChild(Rim1)
}
func loadAppearance_Rim2(){
Rim2 = SKSpriteNode(color: UIColor.redColor(), size: CGSizeMake((frame.size.width) / 40, (frame.size.width) / 40))
Rim2.position = CGPoint(x: ((frame.size.width) / 1.8), y: ((frame.size.height) / 1.33))
Rim2.zPosition = 1
addChild(Rim2)
}
func loadAppearance_RimNet(){
RimNet = SKSpriteNode(color: UIColor.greenColor(), size: CGSizeMake((frame.size.width) / 7.5, (frame.size.width) / 150))
RimNet.position = CGPointMake(frame.size.width / 1.99, frame.size.height / 1.33)
RimNet.zPosition = 1
addChild(RimNet)
}
func addBackground(){
//background
background = SKSpriteNode(imageNamed: "Background")
background.zPosition = 0
background.size = self.frame.size
background.position = CGPoint(x: self.size.width / 2, y: self.size.height / 2)
self.addChild(background)
}
Additionally my GameViewController.swift looks like this
import UIKit
import SpriteKit
class GameViewController: UIViewController {
var scene: GameScene!
override func viewDidLoad() {
super.viewDidLoad()
//Configure the view
let skView = view as! SKView
//If finger is on iphone, you cant tap again
skView.multipleTouchEnabled = false
//Create and configure the scene
//create scene within size of skview
scene = GameScene(size: skView.bounds.size)
scene.scaleMode = .AspectFill
scene.size = skView.bounds.size
//scene.anchorPoint = CGPointZero
//present the scene
skView.presentScene(scene)
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .Landscape
} else {
return .All
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
How can I make the positions of my nodes be the same for each simulator/physical device?
You should round those floating point values to integers via a call to (int)round(float) so that the values snap to whole pixels. Any place where you use CGPoint or CGSize should use whole pixels as opposed to floating point values.
If you are making a Universal application you need to declare the size of the scene using integer values. Here is an example:
scene = GameScene(size:CGSize(width: 2048, height: 1536))
Then when you initialize the positions and sizes of your nodes using CGPoint and CGSize, make them dependant on SKScene size. Here is an example:
node.position = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2)
If you declare the size of the scene for a Universal App like this:
scene.size = skView.bounds.size
then your SKSpriteNode positions will be all messed up. You may also need to change the scaleMode to .ResizeFill. This worked for me.

Changing OSX app window size based on user input that changes views

I'm trying my hand at developing an OSX app. Xcode and Swift are all new to me.
The defaults are all very good at modifying objects when the user changes the window size, but not so good at changing the window size when objects in the view change.
I've seen a couple of examples of recalculating the origin and frame size - I think the math part of it will be straight forward. However, I cannot get a working reference to the NSWindow object. click-drag will not deposit an IBOutlet for the window in any of the .swift files (AppDelegate, ViewController, custom). And typing it in doesn't bind it.
A simplified example of what I am trying to accomplish:
Based on user input, change the content of the display, and adjust the window size to encompass the newly modified display.
On my main storyboard
- Window Controller, segued to
- View Controller, containing a Horizontal Slider and a Container view. The container view is segued to
- Horizontal Split View Controller, segued to
- three repetitive instances of a View Controller.
When the user changes the slider bar, one or more of the three bottom most view controllers will be hidden/unhidden.
The attached pictures show the behavior I am looking for.
Imagine where the text "small group" is, there is a collection of drop down boxes, text boxes, radio buttons, etc.
I am answering my own question here - still unable to bind IBOutlet with NSWindow. Anyone with better solutions, please let me know.
Below is a capture of my Main.storyboard. All of the coding is done in the class junkViewController2 that is associated with the 1st view controller.
Here is the code for junkViewController2. I know, it could be more concise ...
//
// JunkViewController2.swift
// Scratch1
//
import Cocoa
class JunkViewController2: NSViewController {
var junkSplitViewController: JunkSplitViewController!
var myWindow: NSWindow!
var dy: CGFloat!
#IBOutlet weak var mySlider: NSSlider!
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
dy = 0.0
}
#IBAction func mySlider(sender: NSSlider) {
let junkSplitViewController = self.childViewControllers[0]
dy = 0.0
for controller in junkSplitViewController.childViewControllers {
if controller.title == "smallGroup1" {
if mySlider.intValue > 0 {
if controller.view.hidden {
dy = dy + 30.0
}
controller.view.hidden = false
} else {
if !controller.view.hidden {
dy = dy - 30.0
}
controller.view.hidden = true
}
}
if controller.title == "smallGroup2" {
if mySlider.intValue > 1 {
if controller.view.hidden {
dy = dy + 30.0
}
controller.view.hidden = false
} else {
if !controller.view.hidden {
dy = dy - 30.0
}
controller.view.hidden = true
}
}
if controller.title == "smallGroup3" {
if mySlider.intValue > 2 {
if controller.view.hidden {
dy = dy + 30.0
}
controller.view.hidden = false
} else {
if !controller.view.hidden {
dy = dy - 30.0
}
controller.view.hidden = true
}
}
}
resize()
}
func resize() {
var windowFrame = NSApp.mainWindow!.frame
let oldWidth = windowFrame.size.width
let oldHeight = windowFrame.size.height
let old_x = windowFrame.origin.x
let old_y = windowFrame.origin.y
let toAdd = CGFloat(dy)
let newHeight = oldHeight + toAdd
let new_y = old_y - toAdd
windowFrame.size = NSMakeSize(oldWidth, newHeight)
windowFrame.origin = NSMakePoint(old_x, new_y)
NSApp.mainWindow!.setFrame(windowFrame, display: true)
}
}

SKSpriteNode not able to have multiple children

I'm trying to have multiple sprite nodes of the same type/parent, but when I try to spawn another node I get an error. 'NSInvalidArgumentException', reason: 'Attemped to add a SKNode which already has a parent'
Here's my code:
import SpriteKit
class GameScene: SKScene {
//global declarations
let player = SKSpriteNode(imageNamed: "mage")
let fireball = SKSpriteNode(imageNamed: "fireball")
override func didMoveToView(view: SKView) {
/* Setup your scene here */
createScene()
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch in (touches as! Set<UITouch>) {
let location = touch.locationInNode(self)
spawnFireball(location)
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
self.enumerateChildNodesWithName("fireball", usingBlock: ({
(node,error) in
if (self.fireball.position.x < -self.fireball.size.width/2.0 || self.fireball.position.x > self.size.width+self.fireball.size.width/2.0
|| self.fireball.position.y < -self.fireball.size.height/2.0 || self.fireball.position.y > self.size.height+self.fireball.size.height/2.0) {
self.fireball.removeFromParent()
self.fireball.removeAllChildren()
self.fireball.removeAllActions()
}
}))
}
func createScene() {
//player
player.size = CGSizeMake(100, 100)
player.position = CGPointMake(self.frame.size.width/2, self.frame.size.height/2 + 50)
player.zPosition = 2.0
self.addChild(player)
}
func spawnFireball(point: CGPoint) {
//setup
fireball.name = "fireball"
fireball.size = CGSizeMake(100, 50)
let fireballCenter = CGPointMake(fireball.size.width / 4 * 3, fireball.size.height / 2)
fireball.position = player.position
fireball.physicsBody = SKPhysicsBody(circleOfRadius: fireball.size.height/2, center: fireballCenter)
fireball.physicsBody?.affectedByGravity = false
//action
var dx = CGFloat(point.x - player.position.x)
var dy = CGFloat(point.y - player.position.y)
let magnitude = sqrt(dx * dx + dy * dy)
dx /= magnitude
dy /= magnitude
let vector = CGVector(dx: 32.0 * dx, dy: 32.0 * dy)
var rad = atan2(dy,dx)
fireball.runAction(SKAction.rotateToAngle(rad, duration: 0.0))
self.addChild(fireball)
fireball.physicsBody?.applyImpulse(vector)
}
}
You need to instantiate another SKSpriteNode. In your existing code, you create a single fireball and add it to the scene; your program crashes when you try to add the same fireball again.
First, remove your let fireball = SKSpriteNode... line. Move it to inside the spawnFireball() method, like so:
func spawnFireball(point: CGPoint) {
let fireball = SKSpriteNode(imageNamed: "fireball")
//Insert all customization here (your existing code should mostly work)
self.addChild(fireball)
}
Because the fireball variable is a local variable, you can now instantiate a new one every time you call the function. Now, just change your update() method to properly use enumerateChildrenWithName() by getting changing every self.fireball to just node.
This way, the code will loop through every existing fireball that is currently on the scene, rather than your current code, which only allows you to create one fireball.

Resources