Pass Computed Values as Parameters in Swift - ios

I am making a custom animation handler class with swift, and GameKit. In summary, I want to initialize an Animator (a custom class), on any SKNode that I want to animate. In principle, this animator has a list of animations (SKActions) that all have various "trigger States", which correspond with the various game states the game can be in. The Animator class then observers the global state variable, and if it changes, and matches the animations' trigger states, then it runs that animation on the target node.
I got this to work, however, I want to add just a bit more functionality to this, in the form of changing timings for the animation. An example of this would be something like a dog running, perhaps over a set period of time (where the state does not change), I want the dog to start running fast, and slow over time. The way I'm thinking of implementing something like this is to have a variable, speed, for example, that represents the speed of the animation, and have it slow over time. Then I would pass in a computed property (return the current value of the speed var)to the animator class, such that every time it calls the function for this specific animation, instead of having a fixed time interval, it would get the speed var and pass it in.
I'm having trouble because when I pass in a computed property, swift just takes its value when it's getting passed and holds onto that, instead of taking the entire getter with it. I'm wondering if there is a way I can pass a computed property with a getter, into a function/class. Here is the animator classes that I'm referring to, if you would like to see some code :)
class Animation {
let waitForCompletion: Bool
let repeating: Bool
let animation: SKAction
var running: Bool = false
let triggerState: State
var currentState: State {
return States.currentState
}
let target: SKNode
init(_ triggerState: State, animates animation: SKAction, for target: SKNode, waitForCompletion: Bool = true, repeating: Bool = true) {
self.triggerState = triggerState
self.target = target
//here I simply just pass in an animation, which allows for grouping animations if I want to do more than jsut SKAction.Animat(with:,timeInterval:)
self.waitForCompletion = waitForCompletion
self.repeating = repeating
self.animation = animation
}
func update(_ first: Bool) {
if (currentState == triggerState || first) && !running {
self.running = true
if !self.waitForCompletion { target.removeAllActions() }
if !target.hasActions() {
target.run(animation) {
self.running = false
self.target.removeAllActions()
if self.repeating { self.update(false) }
}
}
}
}
}
heres the animator class:
class Animator {
var itemObserver: AnyCancellable?
var animations: [Animation] = []
var state: State {
didSet {
update()
}
}
init(_ animations: [Animation]) {
self.animations = animations
self.state = States.currentState
//initialize a temporary value, such that the class has initialized self
itemObserver = States.$currentState.sink() { let _ = ($0) }
itemObserver = States.$currentState
.sink() { self.state = $0 }
}
func update() {
if let animation = animations.first(where: {$0.triggerState == state}) {
animation.update(true)
}
}
}
and a sample initialization in an SKNode class:
//this initializes the animtor with a series of animations. The animation being passed here is a group animation(an important capabality of this system, and in the scaleY, I pass a stretchVar, which is a computed property, that only passes its value in this example)
animator = Animator([
Animation(State.throwing, animates: SKAction.group([
SKAction.animate(with: ballAtlas, timePerFrame: 0.1),
SKAction.scaleY(to: stretchVar, duration: 0)
]), for: self)
])

Related

Swift: A problem with timers within a struct

I am building a SwiftUI App and have all my UI related stuff within a global observable class UILogic. That class itself has a #Published variable called bp which is of type BoxParameters (struct).
My SwiftUI View observes this published variable, which has a lot of components: aspectRatio, frameWidth, xOffset, yOffset, etc. If I want my View to be wider for example, I just call the setWidth() function like this:
struct BoxParameters {
private(set) var frameWidth: CGFloat = 175
mutating func setWidth(newWidth: Double) {
self.frameWidth = newWidth
}
}
class UILogic: ObservableObject {
#Published var bp = BoxParameters
func doubleWidth() {
bp.setWidth(bp.frameWidth * 2)
}
}
This works fine: because it’s mutating, it creates a new struct instance, which triggers #Published to send an update and the view changes with the new width.
What I'm struggling to do is to change the frameWidth (or any other struct variable) with a timer.
So let’s say I don’t want to change the value instantly, but want to change it by incrementing the value 10 times every second.
My first guess was to use timer directly:
mutating func setWidth(newWidth: Double, slow: Bool = false) {
if !slow {
self.frameWidth = newWidth
} else {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
self.frameWidth += 1
if self.frameWidth >= newWidth {
self.frameWidth = newWidth
timer.invalidate()
}
}
}
}
This code doesn't compile and throws an error: Escaping closure captures mutating 'self' parameter
This has already made me scratch my head a bit, so I started digging around for solutions:
https://stackoverflow.com/a/47173607/12596719\
https://developer.apple.com/forums/thread/652094\
Those two threads sparked a hope that my problem might be finally solved didn’t change anything: the compiler was still complaining.
What seemed to solve my problem was this thread, so I tried to adapt it in my code (just to test out it is a void function just increases the frameWidth by 50):
struct BoxParameters {
...
var timerLogic: TimerLogic!
class TimerLogic {
var structRef: BoxParameters!
var timer: Timer!
init(_ structRef: BoxParameters){
self.structRef = structRef;
self.timer = Timer.scheduledTimer(
timeInterval: 0.1,
target: self,
selector: #selector(timerTicked),
userInfo: nil,
repeats: true)
}
func stopTimer(){
self.timer?.invalidate()
self.structRef = nil
}
#objc private func timerTicked(){
self.structRef.timerTicked()
}
}
mutating func startTimer(){
print("Start Timer")
self.timerLogic = TimerLogic(self)
}
mutating func stopTimer() {
print("Stop Timer")
self.timerLogic.stopTimer()
self.timerLogic = nil
}
mutating func timerTicked(){
self.frameWidth += 50
print("Timer: new frame width: \(self.frameWidth)")
}
}
Expected behavior: it increased the frameWidth by 50
What happens: it prints that the frame width has been increased by 50 (printing value is correct), but nothing changes.
BUT: if I call the function timerTicked manually, the frameWidth changes by 50 as expected! ugh!
What I think is happening is that the timer is changing the frameWidth of a copy of the struct without changing the real struct, but then again, the timerTicked function should change the parent struct itself. (because of self.)
Anyone knows a way to solve this issue? Changing the struct to an observed class would've been an option but due to Swift's design, a change of a #Published variable inside a #Published class doesn’t notify SwiftUI of a change...
Why are you putting classes and any code logic in a struct? I think you need to work the logic into classes and just use the struct for simple variable usage.
A struct is better used to call variables around the app .
struct AllstructFittings {
static var collectedWorks: Bool = false
static var collected: String = "notcollected"
static var failclicked: Bool = false
}
https://www.appypie.com/struct-vs-class-swift-how-to

Creating a simple timer in WatchOS with Swift 4

Seemingly simple but I'm struggling...the code below crashes on the line setting the date of the workoutTimer. Also my WKInterfaceTimer isn't hooked up to an IBOutlet, does it need to be? I wanted to use it just for purposes of the time.
class InterfaceController {
var workoutTimer: WKInterfaceTimer!
var workoutStartTime: NSDate? = nil
func startWorkOutTimer() {
self.startWorkout()
if let test = self.workoutSecondsElapsed() {
print("timer seconds = \(test)")
}
}
func startWorkout() {
// To count up use 0.0 or less, otherwise the timer counts down.
workoutTimer.setDate(NSDate(timeIntervalSinceNow: 0.0) as Date)
workoutTimer.start()
self.workoutStartTime = NSDate()
}
func stopWorkout() {
workoutTimer.stop()
}
func workoutSecondsElapsed() -> TimeInterval? {
// If the timer hasn't been started then return nil
guard let startTime = self.workoutStartTime else {
return nil
}
// Time intervals from past dates are negative, so
// multiply by -1 to get the elapsed time.
return -1.0 * (self.workoutStartTime?.timeIntervalSinceNow)!
}
}
From Apple doc:
Do not subclass or create instances of this class yourself. Instead, define outlets in your interface controller class and connect them to the corresponding objects in your storyboard file.
Your app probably is crashing because your timer is nil, but for what you need you can use Timer class instead of WKInterfaceTimer.

Alternative to animation(forKey:) (now deprecated)?

I am working with iOS 11 (for ARKit) and while many point to a sample app for SceneKit from Apple with a Fox, I am having problem with the extension it uses in that sample project (file) to add animations:
extension CAAnimation {
class func animationWithSceneNamed(_ name: String) -> CAAnimation? {
var animation: CAAnimation?
if let scene = SCNScene(named: name) {
scene.rootNode.enumerateChildNodes({ (child, stop) in
if child.animationKeys.count > 0 {
animation = child.animation(forKey: child.animationKeys.first!)
stop.initialize(to: true)
}
})
}
return animation
}
}
It seems that this extension is very handy but I am not sure how to migrate this now that it is deprecated? Is it built into SceneKit by default now?
The documentation didn't really show much info on why it was deprecated or where to go from here.
Thanks
TL;DR: examples of how to use new APIs can be found in Apple's sample game (search for SCNAnimationPlayer)
Even though animation(forKey:) and its sister methods that work with CAAnimation have been deprecated in iOS11, you can continue using them – everything will work.
But if you want to use new APIs and don't care about backwards compatibility (which you wouldn't need in the case of ARKit anyway, because it's only available since iOS11), read on.
The newly introduced SCNAnimationPlayer provides a more convenient API compared to its predecessors. It is now easier to work with animations in real time.
This video from WWDC2017 would be a good starting point to learn about it.
As a quick summary: SCNAnimationPlayer allows you to change animation's speed on the fly. It provides a more intuitive interface for animation playback using methods such as play() and stop() compared to adding and removing CAAnimations.
You also can blend two animations together which, for example, can be used to make smooth transitions between them.
You can find examples of how to use all of this in the Fox 2 game by Apple.
Here's the extension you've posted adapted to use SCNAnimationPlayer (which you can find in the Character class in the Fox 2 sample project):
extension SCNAnimationPlayer {
class func loadAnimation(fromSceneNamed sceneName: String) -> SCNAnimationPlayer {
let scene = SCNScene( named: sceneName )!
// find top level animation
var animationPlayer: SCNAnimationPlayer! = nil
scene.rootNode.enumerateChildNodes { (child, stop) in
if !child.animationKeys.isEmpty {
animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
stop.pointee = true
}
}
return animationPlayer
}
}
You can use it as follows:
Load the animation and add it to the corresponding node
let jumpAnimation = SCNAnimationPlayer.loadAnimation(fromSceneNamed: "jump.scn")
jumpAnimation.stop() // stop it for now so that we can use it later when it's appropriate
model.addAnimationPlayer(jumpAnimation, forKey: "jump")
Use it!
model.animationPlayer(forKey: "jump")?.play()
Lësha Turkowski's answer without force unwraps.
extension SCNAnimationPlayer {
class func loadAnimationPlayer(from sceneName: String) -> SCNAnimationPlayer? {
var animationPlayer: SCNAnimationPlayer?
if let scene = SCNScene(named: sceneName) {
scene.rootNode.enumerateChildNodes { (child, stop) in
if !child.animationKeys.isEmpty {
animationPlayer = child.animationPlayer(forKey: child.animationKeys[0])
stop.pointee = true
}
}
}
return animationPlayer
}
}
Here's an example of SwiftUI and SceneKit
import SwiftUI
import SceneKit
struct ScenekitView : UIViewRepresentable {
#Binding var isPlayingAnimation: Bool
let scene = SCNScene(named: "art.scnassets/TestScene.scn")!
func makeUIView(context: Context) -> SCNView {
// create and add a camera to the scene
let cameraNode = SCNNode()
cameraNode.camera = SCNCamera()
scene.rootNode.addChildNode(cameraNode)
let scnView = SCNView()
return scnView
}
func updateUIView(_ scnView: SCNView, context: Context) {
scnView.scene = scene
// allows the user to manipulate the camera
scnView.allowsCameraControl = true
controlAnimation(isAnimating: isPlayingAnimation, nodeName: "TestNode", animationName: "TestAnimationName")
}
func controlAnimation(isAnimating: Bool, nodeName: String, animationName: String) {
guard let node = scene.rootNode.childNode(withName: nodeName, recursively: true) else { return }
guard let animationPlayer: SCNAnimationPlayer = node.animationPlayer(forKey: animationName) else { return }
if isAnimating {
print("Play Animation")
animationPlayer.play()
} else {
print("Stop Animation")
animationPlayer.stop()
}
}
}
struct DogAnimation_Previews: PreviewProvider {
static var previews: some View {
ScenekitView(isPlayingAnimation: .constant(true))
}
}
A 2023 example.
I load typical animations like this:
func simpleLoadAnim(filename: String) -> SCNAnimationPlayer {
let s = SCNScene(named: filename)!
let n = s.rootNode.childNodes.filter({!$0.animationKeys.isEmpty}).first!
return n.animationPlayer(forKey: n.animationKeys.first!)!
}
So,
laugh = simpleLoadAnim(filename: "animeLaugh") // animeLaugh.dae
giggle = simpleLoadAnim(filename: "animeGiggle")
You then, step one, have to add them to the character:
sally.addAnimationPlayer(laugh, forKey: "laugh")
sally.addAnimationPlayer(giggle, forKey: "giggle")
very typically you would have only one going at a time. So set the weights, step two.
laugh.blendFactor = 1
giggle.blendFactor = 0
to play or stop an SCNAnimationPlayer it's just step three
laugh.play()
giggle.stop()
Almost certainly, you will have (100s of lines) of your own code to blend between animations (which might take only a short time, 0.1 secs, or may take a second or so). To do so, you would use SCNAction.customAction.
If you prefer you can access the animation, on, the character (sally) using the keys. But really the "whole point" is you can just start, stop, etc etc the SCNAnimationPlayer.
You will also have lots of code to set up the SCNAnimationPlayer how you like (speed, looping, mirrored, etc etc etc etc etc etc etc etc)
You will need THIS very critical answer to get collada files (must be separate character / anim files) working properly https://stackoverflow.com/a/75093081/294884
Once you have the various SCNAnimationPlayer animations working properly, it is quite easy to use, run, blend etc animes.
The essential sequence is
each anime must be in its own .dae file
load each anime files in to a SCNAnimationPlayer
"add" all the animes to the character in question
program the blends
then simply play() or stop() the actual SCNAnimationPlayer items (don't bother using the keys on the character, it's a bit pointless)

How to switch on score to create an SKEmitterNode using SpriteKit?

I want to be able to toggle my SKEmitterNode (rain particles) off and on based on the score. But my update function gets called constantly, i.e. I end up with millions of particles on the screen with my current code below...how can I structure my code so that the rain particles will only get called once when a score is achieved?
class GameScene: SKScene, SKPhysicsContactDelegate {
func setUpRain() {
if let rainParticle = SKEmitterNode(fileNamed: "Rain") {
rainParticle.position = CGPointMake(frame.size.width, frame.size.height)
rainParticle.name = "rainParticle"
rainParticle.zPosition = Layer.Flash.rawValue
worldNode.addChild(rainParticle)
}
}
func makeItRain() {
let startRaining = SKAction.runBlock {
self.setUpRain()
}
runAction(startRaining, withKey: "Rain")
}
func stopRaining() {
removeActionForKey("Rain")
worldNode.enumerateChildNodesWithName("rainParticle", usingBlock: { node, stop in
node.removeFromParent()
})
}
}
class PlayingState: GKState {
unowned let scene: GameScene //used to gain access to our scene
override func updateWithDeltaTime(seconds: NSTimeInterval) {
scene.updateForegroundAndBackground()
scene.updateScore()
if scene.score > 2 {
scene.makeItRain()
}
if scene.score > 4 {
scene.stopRaining()
}
}
There's a few ways you can do this, but the simplest of these is to only call makeItRain() or stopRaining() once per toggle. What I mean by this is once makeItRain is called, it cannot be called again until stopRaining is called. This can be done with a boolean like so:
var rainToggle: Bool = false; //True = Raining
override func updateWithDeltaTime(seconds: NSTimeInterval) {
scene.updateForegroundAndBackground()
scene.updateScore()
if (scene.score > 4){
scene.stopRaining()
rainToggle = false;
}
else if (scene.score > 2 && !rainToggle) {
scene.makeItRain()
rainToggle = true;
}
}
This is only slightly inefficient since you are calling stopRaining() every frame for no reason, however it gets the job done and is easy to understand. Note also that I had to flip the order in which your if statements came (otherwise it wouldn't work).

Execute a method when a variable value changes in Swift

I need to execute a function when a variable value changes.
I have a singleton class containing a shared variable called labelChange. Values of this variable are taken from another class called Model. I have two VC classes, one of them has a button and a label and the second only a button.
When the button in the first VC class is pressed I am updating the label with this func:
func updateLabel(){
self.label.text = SharingManager.sharedInstance.labelChange
}
But I want to call the same method whenever the value of the labelChange is changed. So in button click I will only update the labelChange value and when this thing happen I want to update the label with the new value of the labelChange. Also in the second VC I am able to update the labelChange value but I am not able to update the label when this value is changed.
Maybe properties are the solution but can anyone show me how to do so.
Edited second time:
Singleton Class:
class SharingManager {
func updateLabel() {
println(labelChange)
ViewController().label.text = SharingManager.sharedInstance.labelChange
}
var labelChange: String = Model().callElements() {
willSet {
updateLabel()
}
}
static let sharedInstance = SharingManager()
}
First VC:
class ViewController: UIViewController {
#IBOutlet weak var label: UILabel!
#IBAction func Button(sender: UIButton) {
SViewController().updateMessageAndDismiss()
}
}
Second VC:
func updateMessageAndDismiss() {
SharingManager.sharedInstance.labelChange = modelFromS.callElements()
self.dismissViewControllerAnimated(true, completion: nil)
}
#IBAction func b2(sender: UIButton) {
updateMessageAndDismiss()
}
I made some improvements but I need to reference a label from the first VC class in singleton. Therefore I will update that label of VC in singleton.
When I print the value of labelChange the value is being updated and everything is fine. But when I try to update that value on label from singleton I receive an error:
unexpectedly found nil while unwrapping an Optional value
and the error is pointing in 4th line of singleton class.
You can simply use a property observer for the variable, labelChange, and call the function that you want to call inside didSet (or willSet if you want to call it before it has been set):
class SharingManager {
var labelChange: String = Model().callElements() {
didSet {
updateLabel()
}
}
static let sharedInstance = SharingManager()
}
This is explained in Property Observers.
I'm not sure why this didn't work when you tried it, but if you are having trouble because the function you are trying to call (updateLabel) is in a different class, you could add a variable in the SharingManager class to store the function to call when didSet has been called, which you would set to updateLabel in this case.
Edited:
So if you want to edit a label from the ViewController, you would want to have that updateLabel() function in the ViewController class to update the label, but store that function in the singleton class so it can know which function to call:
class SharingManager {
static let sharedInstance = SharingManager()
var updateLabel: (() -> Void)?
var labelChange: String = Model().callElements() {
didSet {
updateLabel?()
}
}
}
and then set it in whichever class that you have the function that you want to be called, like (assuming updateLabel is the function that you want to call):
SharingManager.sharedInstance.updateLabel = updateLabel
Of course, you will want to make sure that the view controller that is responsible for that function still exists, so the singleton class can call the function.
If you need to call different functions depending on which view controller is visible, you might want to consider Key-Value Observing to get notifications whenever the value for certain variables change.
Also, you never want to initialize a view controller like that and then immediately set the IBOutlets of the view controller, since IBOutlets don't get initialized until the its view actually get loaded. You need to use an existing view controller object in some way.
Hope this helps.
In Swift 4 you can use Key-Value Observation.
label.observe(\.text, changeHandler: { (label, change) in
// text has changed
})
This is basically it, but there is a little catch. "observe" returns an NSKeyValueObservation object that you need to hold! - when it is deallocated, you’ll receive no more notifications. To avoid that we can assign it to a property which will be retained.
var observer:NSKeyValueObservation?
// then assign the return value of "observe" to it
observer = label.observe(\.text, changeHandler: { (label, change) in
// text has changed,
})
You can also observe if the the value has changed or has been set for the first time
observer = label.observe(\.text, changeHandler: { (label, change) in
// just check for the old value in "change" is not Nil
if let oldValue = change.oldValue {
print("\(label.text) has changed from \(oldValue) to \(label.text)")
} else {
print("\(label.text) is now set")
}
})
For More Information please consult Apples documentation here
Apple provide these property declaration type :-
1. Computed Properties:-
In addition to stored properties, classes, structures, and enumerations can define computed properties, which do not actually store a value. Instead, they provide a getter and an optional setter to retrieve and set other properties and values indirectly.
var otherBool:Bool = false
public var enable:Bool {
get{
print("i can do editional work when setter set value ")
return self.enable
}
set(newValue){
print("i can do editional work when setter set value ")
self.otherBool = newValue
}
}
2. Read-Only Computed Properties:-
A computed property with a getter but no setter is known as a read-only computed property. A read-only computed property always returns a value, and can be accessed through dot syntax, but cannot be set to a different value.
var volume: Double {
return volume
}
3. Property Observers:-
You have the option to define either or both of these observers on a property:
willSet is called just before the value is stored.
didSet is called immediately after the new value is stored.
public var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
NOTE:- For More Information go on professional link
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html
There is another way of doing so, by using RxSwift:
Add RxSwift and RxCocoa pods into your project
Modify your SharingManager:
import RxSwift
class SharingManager {
static let sharedInstance = SharingManager()
private let _labelUpdate = PublishSubject<String>()
let onUpdateLabel: Observable<String>? // any object can subscribe to text change using this observable
// call this method whenever you need to change text
func triggerLabelUpdate(newValue: String) {
_labelUpdate.onNext(newValue)
}
init() {
onUpdateLabel = _labelUpdate.shareReplay(1)
}
}
In your ViewController you can subscribe to value update in two ways:
a. subscribe to updates, and change label text manually
// add this ivar somewhere in ViewController
let disposeBag = DisposeBag()
// put this somewhere in viewDidLoad
SharingManager.sharedInstance.onUpdateLabel?
.observeOn(MainScheduler.instance) // make sure we're on main thread
.subscribeNext { [weak self] newValue in
// do whatever you need with this string here, like:
// self?.myLabel.text = newValue
}
.addDisposableTo(disposeBag) // for resource management
b. bind updates directly to UILabel
// add this ivar somewhere in ViewController
let disposeBag = DisposeBag()
// put this somewhere in viewDidLoad
SharingManager.sharedInstance.onUpdateLabel?
.distinctUntilChanged() // only if value has been changed since previous value
.observeOn(MainScheduler.instance) // do in main thread
.bindTo(myLabel.rx_text) // will setText: for that label when value changed
.addDisposableTo(disposeBag) // for resource management
And don't forget to import RxCocoa in ViewController.
For triggering event just call
SharingManager.sharedInstance.triggerLabelUpdate("whatever string here")
HERE you can find example project. Just do pod update and run workspace file.
var item = "initial value" {
didSet { //called when item changes
print("changed")
}
willSet {
print("about to change")
}
}
item = "p"
override var isHighlighted: Bool {
get { super.isHighlighted }
set {
super.isHighlighted = newValue
if newValue {
label.textColor = highlightedTextColor
contentView.backgroundColor = highlightedBackgroundColor
} else {
label.textColor = normalTextColor
contentView.backgroundColor = normalBackgroundColor
}
}
}

Resources