SwiftUI & Scheduled timers - swiftui

I'm building an app with SwiftUI with a stop watch functionality.
The TimedSession class has a var timer: Timer computed property like this:
Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
guard let time = self.currentTime else {
print("no current time")
return
}
if self.status != .running {
timer.invalidate()
return
}
time.duration += 1
self.objectWillChange.send()
}
this works well until I start to interact with other parts of the UI, like a scrollview. It's not just that the timer UI is locked, but the block: callbck on scheduledTimer isn't being executed.
What's the best way to put the callback block into the background? I've tried with GCD but no good so far.

Scheduled Timer works by default in .default run loop mode, but during user interaction, like scrolling, a run loop works in different tracking mode.
The solution is set up Timer for all standard modes, named .common, like below
// just create non attached timer
let timer = Timer(timeInterval: 0.01, repeats: true) { timer in
guard let time = self.currentTime else {
print("no current time")
return
}
if self.status != .running {
timer.invalidate()
return
}
time.duration += 1
self.objectWillChange.send()
}
...
init() {
// Start timer in run-loop on common modes
RunLoop.main.add(timer, forMode: .common)
}

Related

How to continue performing a task when Apple Watch App is in inactive mode?

This bounty has ended. Answers to this question are eligible for a +100 reputation bounty. Bounty grace period ends in 23 hours.
Bartłomiej Semańczyk is looking for a canonical answer.
This is my code what I do on appear:
import SwiftUI
import CloudKit
#main
struct DemoApp: App {
var isDownloading = false
#Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
// some content
}
.onChange(of: scenePhase) { phase in
if case .active = phase {
Task {
await loadData() // this loads should be done on background thread (I think), that is all.
}
}
}
}
private let container = CKContainer(identifier: "iCloud.pl.myapp.identifier")
private var privateDatabase: CKDatabase {
return container.privateCloudDatabase
}
private func loadData() async {
if !isDownloading {
isDownloading = true
var awaitingChanges = true
var changedRecords = [CKRecord]()
var deletedRecordIDs = [CKRecord.ID]()
let zone = CKRecordZone(zoneName: "fieldservice")
var token: CKServerChangeToken? = nil
do {
while awaitingChanges {
let allChanges = try await privateDatabase.recordZoneChanges(inZoneWith: zone.zoneID, since: token)
let changes = allChanges.modificationResultsByID.compactMapValues { try? $0.get().record }
changes.forEach { _, record in
changedRecords.append(record)
print("Fetching \(changedRecords.count) private records.")
// update ui here with printed info
}
let deletetions = allChanges.deletions.map { $0.recordID }
deletetions.forEach { recordId in
deletedRecordIDs.append(recordId)
print("Fetching \(changedRecords.count) private records.")
// update ui here with printed info
}
token = allChanges.changeToken
awaitingChanges = allChanges.moreComing
}
isDownloading = false
print("in future all records should be saved to core data here")
} catch {
print("error \(error)")
isDownloading = false
}
}
}
}
This is simplified code as much as it can be to better understand the problem.
My Apple Watch when I run the app FIRST TIME it must fetch all cloudkit record changes (but in my opinion it doesn't matter what actually is the task). To make it finished it needs to download ~3k records and it takes ~5-6 minutes on the watch. Downloading is in progress ONLY when an app is in ACTIVE mode. When it changes to INACTIVE (after ~10-11 seconds) it stops downloading and I have to move my wrist to make it ACTIVE again to continue downloading.
I need to change it to not to stop downloading when app is in INACTIVE mode. It should continue downloading until it is finished. While it is being downloaded I update UI with info for example "Fetched 1345 records", "Fetched 1346 records" and so on... When app is inactive it is downloaded and when I make an app ACTIVE I can see the current status of download.
What do I do inside loadData? I simply fetch all CloudKit changes starting with serverChangeToken = nil. It takes ~ 5-6 minutes for ~3k records.

How to jump back "n" seconds from current position in video using the YouTubePlayerKit Package in SwiftUI

I am very new to programming in SwiftUI (Currently running Xcode 14.2). I am using the YouTubePlayerKit 1.3.0 Package (https://swiftpackageindex.com/SvenTiigi/YouTubePlayerKit) to load a YouTube video and play it. One of the abilities I want to build into the app is to jump back "n" seconds in the video when a button is pushed and start playing the video from that point. There is a function called: getCurrentTime() that I believe returns an integer representing the elapsed time in seconds from the beginning of the video. I want to subtract "n" seconds from that value and use the: .seek(to: , allowSeekAhead: true) function to jump to the desired location of the video based on the above calculation. I have the code to a point where I can load the video and when I click the "SEEK" button it will jump to a static value I have hard coded into the script and play from that point. I am struggling with how to retrieve the current time, subtract "n" seconds and use that value in the .seek() function.
This is a link to information on the YouTubePlayerKit Package: https://github.com/SvenTiigi/YouTubePlayerKit
Any help would be greatly appreciated.
Below is the SwiftUI script I have so far. This is the description of the function I believe
will give me the time in seconds up to the current point in the video:
/*
/// Retrieve the elapsed time in seconds since the video started playing
/// - Parameter completion: The completion closure
func getCurrentTime(
completion: #escaping (Result<Double, YouTubePlayerAPIError>) -> Void
)
*/
import SwiftUI
import YouTubePlayerKit
struct ContentView: View {
let youTubePlayer = YouTubePlayer(
source: .url("https://www.youtube.com/watch?v=qL-ry_tz6V0"),
configuration: .init(
autoPlay: true
)
)
#State private var JumpTo: Double = 27
var body: some View {
ZStack {
YouTubePlayerView(self.youTubePlayer) { state in
// Overlay ViewBuilder closure to place an overlay View
// for the current `YouTubePlayer.State`
switch state {
case .idle:
ProgressView()
case .ready:
EmptyView()
case .error(let error):
Text(verbatim: "YouTube player couldn't be loaded")
}
}
HStack {
Button("PLAY") {
//Play video
youTubePlayer.play()
}
Button("PAUSE") {
// Pause video
youTubePlayer.pause()
}
Button("STOP") {
// Stop video
youTubePlayer.stop()
}
Button("SEEK") {
youTubePlayer.seek(to: JumpTo, allowSeekAhead: true)
}
}
.offset(y: 250)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
According to your comments the culprit seems to be:
Button("SEEK") {
JumpTo = youTubePlayer.getCurrentTime() // this line gives the error asnyc call and try without catch
youTubePlayer.seek(to: JumpTo, allowSeekAhead: true)
}
You are in an async context here. Swift async / concurrency is a very broad field that cannot be answered in a single answer. I would recommend you put some effort into reading this stuff up.
The reason for the 2 errors are:
youTubePlayer.getCurrentTime() is a throwing function. You need to catch any possible errors and either do something with them or dismiss them. Either way the Swift compiler demands from you a certain syntax. It is called do/catch clause.
do{
try .... //throwing function
} catch{
// handle error here or just print it
print(error)
}
Your function is asyncronous. It means the code you write will proceed without waiting for the result of the function. To handle this you need to await it. But as your Button("SEEK") { method does not support something like that you have to wrap it into a Task:
Task{
await ... // await async function
}
If you combine both aproaches you get....
Button("SEEK") {
Task{
do{
JumpTo = try await youTubePlayer.getCurrentTime()
// now you can manipulate it and do
Jumpto = .....
youTubePlayer.seek(to: JumpTo, allowSeekAhead: true)
} catch{
// handle error here or just print it
print(error)
}
}
}

How to conditionally execute code on onAppear method

I have a swiftUi view depending on a class data.
Before displaying the data, I have to compute it in .onAppear method.
I would like to make this heavy computation only when my observed object changes.
The problem is that .onAppear is called every time I open the view, but the object value does not change very often.
Is it possible to conditionally execute the compute function, only when observed data has effectively been modified ?
import SwiftUI
struct test2: View {
#StateObject var person = Person()
#State private var computedValue = 0
var body: some View {
List {
Text("age = \(person.age)")
Text("computedValue = \(computedValue)")
}
.onAppear {
computedValue = compute(person.age) /// Executed much too often :(
}
}
func compute(_ age: Int) -> Int {
return age * 2 /// In real life, heavy computing
}
}
class Person: ObservableObject {
var age: Int = 0
}
Thanks for advice :)
It would probably be a little less code in the view model, but to do all of the calculations in the view, you will need a few changes. First, your class Person has no #Published variables, so it will never call for a state change. I have fixed that.
Second, now that your state will update, you can add an .onReceive() to the view to keep track of when age updates.
Third, and extremely important, to keep from blocking the main thread with the "heavy computing", you should implement Async Await. As a result, even though I sleep the thread for 3 seconds, the UI is still fluid.
struct test2: View {
#StateObject var person = Person()
#State private var computedValue = 0
var body: some View {
List {
Text("age = \(person.age)")
Text("computedValue = \(computedValue)")
Button {
person.age = Int.random(in: 1...80)
} label: {
Text("Randomly Change Age")
}
}
// This will do your initial setup
.onAppear {
Task {
computedValue = await compute(person.age) /// Executed much too often :(
}
}
// This will keep it current
.onReceive(person.objectWillChange) { _ in
Task {
computedValue = await compute(person.age) /// Executed much too often :(
}
}
}
func compute(_ age: Int) async -> Int {
//This is just to simulate heavy work.
do {
try await Task.sleep(nanoseconds: UInt64(3.0 * Double(NSEC_PER_SEC)))
} catch {
//handle error
}
return age * 2 /// In real life, heavy computing
}
}
class Person: ObservableObject {
#Published var age: Int = 0
}
A possible solution is that create a EnvironmentObject with a Bool Value, Change The value of that variable when there are Change in your object.
So onappear just check if environmentobject value is true or false and it will execute you function.
Hope You Found This Useful.
task(id:priority:_:) is the solution for that.
"A view that runs the specified action asynchronously when the view appears, or restarts the task with the id value changes."
Set the id param to the data you want to monitor for changes.

How to stop a method while it's running in swiftui

I have a problem and I have to find a solution. I must immediately stop the "Starting" method if the user presses the Cancel button.
I have not fixed the problem yet because I can not block the method while it is running but only at the end or at the beginning of the execution.
ContentView:
Button(action: {
self.Starting()
DispatchQueue.main.asyncAfter(deadline: .init(uptimeNanoseconds: 0), execute: self.workItem)
}) {
Text("START")
}
Button(action: {
self.workItem.cancel()
}) {
Text("CANCEL")
}
Starting method:
func Starting() {
self.workItem = DispatchWorkItem {
DispatchQueue.main.async {
self.impostazioniChek=true
}
DispatchQueue.global().async {
if (self.pm.MarksSwitch)
{
sleep(UInt32(self.MarksT))
self.Marks()
}
if (self.pm.ReadySwitch)
{
sleep(UInt32(self.ReadyT))
self.Ready()
}
self.Start()
sleep(3)
DispatchQueue.main.async {
self.tm.start()
}
}
}
}
You cannot cancel because
1) almost all the time work items runs on main (UI) queue, so block it
2) inside work item there is no check for isCancelled. DispatchWorkItem.cancel() only marks it cancelled, but does not stops execution you have to do this.
So to make this work it needs to redirect everything not-UI related in your work item into .background queue explicitly.
Eg. instead of
DispatchQueue.main.asyncAfter(deadline: .init(uptimeNanoseconds: 0), execute: self.workItem)
use
DispatchQueue.global(qos: .background).async { self.workItem }
Instead of
DispatchQueue.global().async {
if (self.pm.MarksSwitch)
Just use (because it is already in background)
if (self.pm.MarksSwitch)
and I'm not sure what is (probably it also should be run on background queue)
self.tm.start()
And on second add inside block interruption on cancellation by regular check, like below
...
}
if self.workItem.isCancelled { // add as many such as needed
return
}
if (self.pm.ReadySwitch)
...

RxSwift: Repeat a (completed) stream

Assume I have a button which can be used to start and stop (toggle) an action.
let toggleStream: Observable<Bool> = toggleBtn.rx.tap.scan(false) { state, _ in !state }
I have another stream, that emits Integers continuously.
let emitter = Observable<Int>.interval(2.0, scheduler: timerScheduler)
Now I want to use the toggle stream to start and stop the emitting of the second stream. This is my approach:
Observable.combineLatest(toggleStream, emitter) { shouldEmit, evt in
return (shouldEmit, evt)
}.takeWhile{ (shouldEmit, evt:Int) in
return shouldEmit == true
}.map {(_, evt) in
return evt
}
This works great for the first time. I can press the button and the Observable starts emitting its Ints. Also stopping works. Sadly I can't start it for a second time, because the stream is completed. How can I restart/retry/repeat it when the user toggles the button again?
Here's how I did it in the playground. You should be able to extrapolate:
//: Playground - noun: a place where people can play
import RxSwift
let toggleButton = PublishSubject<Void>()
let toggleStream: Observable<Bool> = toggleButton
.scan(false) { state, _ in !state }
.debug()
.shareReplayLatestWhileConnected()
let emit = toggleStream
.filter { $0 }
.flatMapLatest { _ in
Observable<Int>.interval(2.0, scheduler: MainScheduler.instance)
.takeUntil(toggleStream.filter { !$0 })
}
_ = emit.subscribe( {
print($0)
})
toggleButton.onNext()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5.0) {
toggleButton.onNext()
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 11.0) {
toggleButton.onNext()
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 17.0) {
toggleButton.onNext()
}
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true