swiftswiftuigrand-central-dispatchnsoperationqueuensoperation

Alternatives to GCD to run code with a delay under SwiftUI


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...


Solution

  • 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")
            }
        }
    }