JohnSF
JohnSF

Reputation: 4320

Can't set Variable Interval for SwiftUI Timer

I'm trying to automatically display a sequence of images in a SwiftUI view. I can create a fixed timer with a predetermined "every" value and trigger the view update in an .onReceive closure but I have not been able to construct a timer with a property for the interval that can be adjusted by the user with a slider. The code below works for the fixed timer. The subscription fires appropriately but I cannot seem to find an equivalent to .onReceive for a subscription to update the image. I also tried creating my own TimerPublisher but I had the same issue - I could not initialize it without specifying a fixed value.

My view:

struct ImageStream2: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    @FetchRequest(fetchRequest: DermPhoto.getAllDermPhotos()) var dermPhotos: FetchedResults<DermPhoto>
    @State private var activeImageIndex = 0
    @State private var animateSpeed: Double = 1.0
    @State private var startTimer = false

    let timer = Timer.publish(every: 1.0, on: .main, in: .default).autoconnect()
    @State private var mst: MyTimerSubscription!

    var body: some View {
        GeometryReader { geo in
            VStack {
                Text("Time Sequence")
                    .foregroundColor(.blue)
                    .font(.system(size: 25))
                Image(uiImage: UIImage(data: self.dermPhotos[self.activeImageIndex].myImage!)!)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: geo.size.width - 20, height: geo.size.width - 20, alignment: .center)
                    .cornerRadius(20)
                    .shadow(radius: 10, x: 10, y: 10)

                    .onReceive(self.timer) { t in
                        print("in fixed timer")
                        if self.startTimer {
                            self.activeImageIndex = (self.activeImageIndex + 1) % self.dermPhotos.count
                        }
                    }
                Group {
                    HStack {
                        Text("0.5").foregroundColor(.blue)
                        Slider(value: self.$animateSpeed, in: 0.5...5.0, step: 0.5)
                        Text("5.0").foregroundColor(.blue)
                    }
                    .padding()
                    Text("Replay at: " + "\(self.animateSpeed) " + (self.animateSpeed == 1.0 ? "second" : "seconds") + " per frame")
                        .foregroundColor(.blue)
                    HStack {
                        Button(action: {
                            self.startTimer.toggle()
                            if self.startTimer {
                                self.mst = MyTimerSubscription(interval: 5.0)
                            } else {
                                self.mst.subscription.cancel()
                            }
                        }) {
                            ZStack {
                                RoundedRectangle(cornerRadius: 20)
                                    .fill(Color.yellow)
                                    .frame(width: 200, height: 40)
                                Text(self.startTimer ? "Stop" : "Start").font(.headline)
                            }
                        }
                    }
                    .padding()
                }//group
            }//top VStack
        }.onDisappear{ self.startTimer = false }
    }
}

And the subscription file:

class MyTimerSubscription {
    let subscription: AnyCancellable
    let interval: Double
    init(interval: Double) {
        subscription =
            Timer.publish(every:interval, on:.main, in:.default)
                .autoconnect()
                .sink { _ in
                    print("in MyTimerSubscription print")
        }
        self.interval = interval
    }

    deinit {
        subscription.cancel()
    }
}

Any guidance would be appreciated. Xcode 11.3 (11C29)

Upvotes: 0

Views: 1312

Answers (1)

JohnSF
JohnSF

Reputation: 4320

Create an Observable Timer:

class TimerWrapper : ObservableObject {
    let willChange = PassthroughSubject<TimerWrapper, Never>()
    @Published var timer : Timer!

    func start(withTimeInterval interval: Double) {
        self.timer?.invalidate()
        self.timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
            guard let self = self else { return }
            self.willChange.send(self)
        }
    }
}

Then with the view, add:

@State var timer = Timer.publish(every: 1.0, on: .main, in: .default).autoconnect()
@State private var animateSpeed: Double = 1.0

Then with the Image:

.onReceive(self.timer) { t in
    if self.startTimer {
        if self.timerToggle {
            self.activeImageIndex = (self.activeImageIndex + 1) % self.dermPhotos.count
        }        
        self.goTimerAnimation.toggle() //changes the shape of the frame
        self.timerToggle.toggle()
        self.pct.toggle() //changes the size of the frame
    }
}//onReceive

And in the button action:

self.timer = Timer.publish(every: self.animateSpeed, on: .main, in: .default).autoconnect()

Upvotes: 1

Related Questions