Swift - Array of AVplayers? - swift

I am putting together an app that plays drum beats based on hard-coded arrays as such:
var beatArray = [[String]]()
var beat01 = [["kick"], ["snare"], ["kick"], ["snare"]]
var beat02 = [["kick"], ["kick", "snare"], ["kick"], ["kick","snare"]]
Based on a set of parameters, beat01 or beat02 is chosen and when a button is pressed, an NSTimer is started with this following Selector:
// THIS IS WHAT HAPPENS EVERY BEAT
func increaseCounter() {
beatCounter++
if beatCounter >= beatArray.count {
beatCounter = 0
}
print(beatCounter)
if beatArray[beatCounter].contains("kick") {
kickPlayer.currentTime = 0
kickPlayer.play()
}
if beatArray[beatCounter].contains("snare") {
snarePlayer.currentTime = 0
snarePlayer.play()
}
}
kickPlayer and snarePlayer are AVAudioPlayers.
Since there are only two instruments, separately declaring each IF statement is fine, but as I add more and more instruments, this will soon be chaotic.
Is there a way to streamline this process? I'm looking for something along the lines of:
if beatArray[beatCounter].contains("INSTRUMENT") {
INSTRUMENTPlayer.currentTime = 0
INSTRUMENTPlayer.play()
}

put your players in a map
let players = ["kick": kickPlayer, "snare": snarePlayer, ...]
then
for beat in beatArray[beatCounter] {
if let player = players[beat] {
player.currentTime = 0
player.play()
}
}

Related

Using swift built in partition to manage elements in an array

iOS 14, Swift 5.x
I watched this excellent WWDC from 2018
https://developer.apple.com/videos/play/wwdc2018/223/
And I wrote a shapes editor... and have been trying to use partition as Dave in the video says you should. I got the first three to work, but the last one I had to use a loop- cannot for the life of me figure out how to get it to work with partition.
Can someone see how I might do this?
The first method moves the selected object to the end of the list, works perfectly.
func bringToFrontEA() {
let subset = objects.partition(by: { $0.selected })
let selected = objects[subset...]
let unselected = objects[..<subset]
let reordered = unselected + selected
objects = Array(reordered)
}
The second method moves the selected object to the front of the list. Works prefectly.
func sendToBackEA() {
let subset = objects.partition(by: { !$0.selected })
let selected = objects[subset...]
let unselected = objects[..<subset]
let reordered = unselected + selected
objects = Array(reordered)
}
The third method moves the element just one element back in the list. Works perfectly.
func sendBackEA() {
if let i = objects.firstIndex(where: { $0.selected }) {
if i == 0 { return }
let predecessor = i - 1
let shapes = objects[predecessor...].partition(by: { !$0.selected })
let slice = objects[predecessor...]
let row = objects[..<predecessor]
let selected = Array(slice[..<shapes])
let unselected = Array(slice[shapes...])
objects = row + selected + unselected
}
}
The last method moves the element forward in the list, works perfectly... but unlike the other methods it will not scale as described in the WWDC video.
func bringForwardEA() {
let indexes = objects.enumerated().filter { $0.element.selected == true }.map{$0.offset}
for i in indexes {
if objects[i+1].unused {
return
}
objects.swapAt(i+1, i)
}
}
Objects is an array of shapes with a property indicating if it is selected or not. I want to exchange the loop in the last method by using a partition as I did in the first three. It needs to work for one or more selected shapes.
Looking at the WWDC video, it appears that what you are calling sendBackEA is what WWDC calls bringForward, and what you are calling bringForwardEA is what WWDC calls sendBack.
Just like how you move the first selected element forward one index (index decreases) in sendBackEA, then move all the other selected elements to immediately after that first selected element. bringForwardEA should do the reverse: move the last selected element backward one index (index increases), then move all the other selected elements to immediately before the last selected element. (See circa 19:10 in the video)
You seem to have confused yourself by trying to increase the indices of all the selected index by 1. This obviously cannot be done with a partition in general.
Also note that partition(by:) already modifies the collection, you don't need to get each partition, then recombine.
Your 4 methods can be written like this:
func bringToFrontEA() {
objects.partition(by: { $0.selected })
}
func sendToBackEA() {
objects.partition(by: { !$0.selected })
}
func sendBackEA() {
if let i = objects.indices.first(where: { objects[$0].selected }) {
if i == 0 { return }
let predecessor = i - 1
objects[predecessor...].partition(by: { !$0.selected })
}
}
func bringForwardEA() {
if let i = objects.indices.last(where: { objects[$0].selected }) {
if i == objects.indices.last { return }
let successor = i + 1
objects[...successor].partition(by: { !$0.selected })
}
}
Notice the symmetry between sendBackEA and bringForwardEA.

Swift for iOS - 2 for loops run at the same time?

I have two objects where I need to update their UI at the same time. I have a for loop for one, and after that another for loop. Each iteration in the for loop I have a short delay so that for elements in the object I am making a UI change... one after the other - not seemingly all at once.
func update(value: Int){
var delay: Double = 0.05
// first loop
for i in 0...value {
delayWithSeconds(delay) {
//do something with object 1
}
delay = delay + 0.05
}
var delay2: Double = 0.05
// second loop
for i in 0...value {
delayWithSeconds(delay2) {
//do something with object 2
}
delay2 = delay2 + 0.05
}
}
// Utility
func delayWithSeconds(_ seconds: Double, completion: #escaping () -> ()) {
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
completion()
}
}
I have tried wrapping each for loop with DispatchQueue.main.async and it didn't make a difference. In short - I would like to run both for loops at the same time (or perceived as such). These are on the UI thread.
I tried this and it seemed to work out quite well. It does exactly what I want it to do (at least visually they seem to run at the same time).
let concurrentQueue = DispatchQueue(label: "net.ericd.hello", attributes: .concurrent)
concurrentQueue.async {
//my loop with delay here for object 1.
}
concurrentQueue.async {
//my separate loop with delay here for object 2.
}
We can use it when we want execute different arrays at the same time:
using this Generic Function
zip(_:_:)
Here i took 2 array:
var arrOfInt = ["1","2","3"]
var arrOfIntString = ["one","two","three"]
for (intNum, intString) in zip(arrOfInt, arrOfIntString) {
print("Int:\(intNum), String:\(intString)")
}

"Attemped to add a SKNode which already has a parent:" in Repeat Loop. Any simple work around?

I am pretty Newbie to programming. And I am trying to pile up the random blocks dynamically till it hits the upper frame. But it seems that Swift doesn't let me to do so. Did I miss anything please? Any input are appreciated.
let blocks =[block1,block2,block3,block4,block5,block6,block7,block8,block9,block10,block11,block12]
var block:SKSpriteNode!
let blockX:Double = 0.0
var blockY:Double = -(self.size.height/2)
repeat{
block = blocks.randomBlock()
block.zPosition = 2
block.position = CGPoint(x:blockX, y:blockY)
block.size.height = 50
block.size.width = 50
self.addChild(block)
blockY += 50
} while( block.position.y < self.size.height)
extension Array {
func randomBlock()-> Element {
let randint = Int(arc4random_uniform(UInt32(self.count)))
return self[randint]
}
}
you need to have someway of tracking which blocks have been selected and ensure that they don't get selected again. The method below uses an array to store the indexes of selected blocks and then uses recursion to find a cycle through until an unused match is found.
private var usedBlocks = [Int]()
func randomBlock() -> Int {
guard usedBlocks.count != blocks.count else { return -1 }
let random = Int(arc4random_uniform(UInt32(blocks.count)))
if usedBlocks.contains(random) {
return randomBlock()
}
usedBlocks.append(random)
return random
}
in your loop change your initializer to
let index = randomBlock()
if index > -1 {
block = blocks[index]
block.zPosition = 2
block.position = CGPoint(x:blockX, y:blockY)
}
remember that if you restart the game or start a new level, etc. you must clear all of the objects from usedBlocks
usedBlocks.removeAll()

one score at a time, update method

I'm creating a simple game with SpriteKit (mostly for learning), and I got a question about score adding. some background: I'm checking if a sprite (SKShapeNode) contains another one, if true, I'm checking their color, if it is the same color, the player should get 1 score. I wrote this function:
func onMatch(){
for ring in mColorRings {
if(mPlayer.contains(ring.position)){
if mPlayer.fillColor.isEqual(ring.fillColor) {
score += 1
mScoreLbl.text = "\(score)"
}
}
}
}
which works, the problem is, I'm calling this function inside the update method. as the update method runs a lot, it calls my function a lot of time and as long as mPlayer contains ring it is adding 1 score to the player. How can I avoid that ?
This depends on your game mechanics. If the ring is supposed to give you a score one time then disappear you can safely remove it within that if test. If you want the ring to stay put and maybe be reused later you can add a boolean to the ring class called something like "scoreGiven" and redo your if test to something like this:
func onMatch(){
for ring in mColorRings {
if !ring.scoreGiven{
if(mPlayer.contains(ring.position)){
if mPlayer.fillColor.isEqual(ring.fillColor) {
score += 1
mScoreLbl.text = "\(score)"
ring.scoreGiven = true
}
}
}else if(!mPlayer.contains(ring.position)){
ring.scoreGiven = false
}
}
This is just an example, but note the "not"s in the updated if statements
Ok, so this is what i suggest:
var playerPassed = false
func onMatch(){
for ring in mColorRings {
if(mPlayer.contains(ring.position)) {
if ((mPlayer.fillColor.isEqual(ring.fillColor)) && playerPassed == false) {
score += 1
mScoreLbl.text = "\(score)"
playerPassed = true
}
}
}
}
You're creating a bool and you check if that is false(default) and if so, the block executes and when it does, the bool is set to true and the condition will be false and no longer return true.

Int in swift closure not incrementing

I have a closure in the code below that executes over an array of custom SKShapeNodes (blockNode). The problem I'm having is that completedActionsCount is always 1, no matter how many how many times the closure is executed. Is completedActionsCount just copied at the start of the closure?? Is there something I'm missing here?
for action in blockActionSets[index]
{
var blockActionSet = blockActionSets[index]
var completedActionsCount = 0
let blockNode = getBlockNodeForBlock(action.block)
if blockNode
{
blockNode!.updateWithAction(action, completion:
{ blockAction in
completedActionsCount++
println(completedActionsCount)
println(blockActionSet.count)
if (completedActionsCount == blockActionSet.count)
{
index = index + 1
self.executeBlockActionSetAtIndexRecursive(index, blockActionSets: blockActionSets, completion: completion)
}
})
}
else
{
completedActionsCount++
}
}
The following code is how the block is executed:
func updateWithAction(blockAction : BlockAction, completion : (BlockAction) -> Void)
{
if blockAction.blockActionType == BlockActionType.ChangeColor
{
var startColor = CIColor(CGColor: self.fillColor.CGColor)
var endColor = CIColor(CGColor: UI.getUIColorForBlockType(blockAction.endBlockType).CGColor)
var startRed = startColor.red()
var startGreen = startColor.green()
var startBlue = startColor.blue()
var endRed = endColor.red()
var endGreen = endColor.green()
var endBlue = endColor.blue()
var action = SKAction.customActionWithDuration(CHANGE_COLOR_ANIMATION_DURATION, actionBlock: {(node : SKNode!, elapsedTime : CGFloat)->Void in
var blockNode = node as? BlockNode
if blockNode
{
var ratio : CGFloat = min(1.0, elapsedTime / CGFloat(self.CHANGE_COLOR_ANIMATION_DURATION))
blockNode!.fillColor = UIColor(red: startRed + ratio * (endRed - startRed),
green: startGreen + ratio * (endGreen - startGreen),
blue: startBlue + ratio * (endBlue - startBlue),
alpha: 1.0)
}
if elapsedTime >= CGFloat(self.CHANGE_COLOR_ANIMATION_DURATION)
{
completion(blockAction)
}
})
self.runAction(action)
}
}
I believe the issue is that you are defining the completedActionsCount variable inside the loop, so each time through the loop a new Int value is defined and the value is reset to 0
does this work for you?
var completedActionsCount = 0
for action in blockActionSets[index] {
var blockActionSet = blockActionSets[index]
…
}
re-reading the question… I'm not sure if you intended for each blockActionSet to have it's own counter. But, to answer one of your sub-questions, I'm pretty sure that even though Int is a value type, it is not being copied into the closure, it's available via the outer scope, and you should be able to modify it.
Is updateWithAction:completion: run in the current thread? if it's asynchronous, that may be a factor.
response to update: Generally any API that has "withDuration" in the name will be async to avoid blocking the main thread.
I'm not sure exactly what is happening, but if the counter is being copied because it's a value type I don't know if switching to the main thread will actually fix your issue, but it's worth a try:
blockNode!.updateWithAction(action, completion:
{ blockAction in
dispatch_async(dispatch_get_main_queue()) {
completedActionsCount++
…
}
}
})
I have somewhat low hopes for this approach though since I suspect the value is being copied and copying it again is certainly not going to give you back the original reference. You may be able to wrap the reference to the counter in a reference type (like a variable array or dictionary):
var completedActionsCounts : [Int:Int] = [:]
for action in blockActionSets[index] {
var blockActionSet = blockActionSets[index]
completedActionsCounts[index] = 0
…
blockNode!.updateWithAction(action, completion:
{ blockAction in
dispatch_async(dispatch_get_main_queue()) {
completedActionsCounts[index] += 1
…
}
}
})
}