Alternatives to GCD to run code with a delay under SwiftUI

1.2k Views Asked by At

Swift 5, iOS 13

I am running this code, it works.

var body: some View {
...
Button(action: {
  self.animateTLeft() 
  quest = quest + "1"
}) { Wedge(startAngle: .init(degrees: 180), endAngle: .init(degrees: 270)) 
         .fill(Color.red) 
         .frame(width: 200, height: 200)
         .offset(x: 95, y: 95)
         .scaleEffect(self.tLeft ? 1.1 : 1.0)
}.onReceive(rPublisher) { _ in
  self.animateTLeft() 
}
...
}

private func animateTLeft() {
withAnimation(.linear(duration: 0.25)){
  self.tLeft.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
  withAnimation(.linear(duration: 0.25)){
    self.tLeft.toggle()
  }
})
}

And I want to try some alternative approaches not using GCD if possible. So I tried using a Timer, which didn't compile. And I tried using perform, which also didn't compile. So I tried operations which compiled! :). But sadly then only works once. Is there no alternative to GCD.

private func animateTLeft() {

//perform(#selector(animate), with: nil, afterDelay: 0.25)
//Timer.scheduledTimer(timeInterval: 0.15, target: self, selector: #selector(animateRed), userInfo: nil, repeats: false)

let queue = OperationQueue()
let operation1 = BlockOperation(block: {
  withAnimation(.linear(duration: 1)){
    self.tLeft.toggle()
  }
})
let operation2 = BlockOperation(block: {
  withAnimation(.linear(duration: 1)){
    self.tLeft.toggle()
  }
})
operation2.addDependency(operation1)
queue.addOperations([operation1,operation2], waitUntilFinished: true)

}

Why do I want to do this, cause I have four slices that I want to animate, but and I want less code. I have red, green, yellow and blue slice. I wrote a generic routine to animate them, supplying the color as a parameter. This is my code.

private func animateSlice(slice: inout Bool) {
withAnimation(.linear(duration: 0.25)){
  slice.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
  withAnimation(.linear(duration: 0.25)){
    slice.toggle()
  }
})
}

But this won't compile with the inout parameter giving an a red error message "Escaping closure captures 'inout' parameter 'slice'", which isn't good. I can make it a copy, not in inout parameter. That compiles, but of course doesn't work, cause it isn't changing the stupid value.

Also tried this, but it won't compile. Maybe someone out there can make it work.

private func animateSliceX(slice: String) {
var ptr = UnsafeMutablePointer<Bool>.allocate(capacity: 1)
switch slice {
case "red": ptr = &tLeft
case "green": ptr = &tRight
case "yellow": ptr = &bLeft
default: ptr = &bRight
}
withAnimation(.linear(duration: 0.25)){
  ptr.toggle()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
  withAnimation(.linear(duration: 0.25)){
    ptr.toggle()
  }
})
}

Thanks...

3

There are 3 best solutions below

0
Rob On BEST ANSWER

If I were to use GCD, I might use Combine methods, e.g.:

struct ContentView: View {
    var body: some View {
        Button(action: {
            DispatchQueue.main.schedule(after: .init(.now() + 1)) {
                print("bar")
            }
        }) {
            Text("foo")
        }
    }
}

If you don’t want to use GCD, you can use a Timer:

struct ContentView: View {
    var body: some View {
        Button(action: {
            Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { _ in
                print("bar")
            }
        }) {
            Text("foo")
        }
    }
}

Or as user3441734 said, you can use schedule on RunLoop, too:

struct ContentView: View {
    var body: some View {
        Button(action: {
            RunLoop.main.schedule(after: .init(Date() + 1)) {
                print("bar")
            }
        }) {
            Text("foo")
        }
    }
}
1
Filip Sakel On

I don't think there's another way (at least easier to use that GCD), but you could use the following reusable function.

Supposing that slice is a State or a Binding with a value type of Bool you could do this:

private func animateSlice(slice: Binding<Bool>) {
    DispatchQueue.main.asyncAfter(wallDeadline: .now() + 0.25) {
        withAnimation(Animation.linear(duration: 0.25)) {
            slice.wrappedValue.toggle()
        }
    }
}

With the function above compiles because Swift doesn't consider changing a Binding wrapped value a mutation. As such, you can pass it to your asyncAfter block.

To use the function you need a binding:

self.animateSlice(slice: $self.tLeft)
0
user3069232 On

OK, so I accepted the other answer, cause I thought it was a better one than this one, but thought I would post this anyway, cause it another solution to the same problem.

private func animate(slice: String) {
    animateAction(slice: slice)
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
      self.animateAction(slice: slice)
    })
  }

 private func animateAction(slice: String) {
    withAnimation(.linear(duration: 0.25)){
      switch slice {
        case "red": self.tLeft.toggle()
        case "green": self.tRight.toggle()
        case "yellow": self.bLeft.toggle()
    // blue is the only other slice
        default: self.bRight.toggle()
      }
    }
  }