0

I built an iOS app with the SwiftUI framework. I want to run a task for a long period of time depending on whether the user is running or not. So I used the Combine framework to start and stop some processes related to running activity using a background thread with quality of service as .background. However, I noticed that sometimes the process of starting and stop gets delayed or killed. I'm okay with delaying the process but I'm not okay with it being killed.

I wonder how can I solve this? Because if you use google maps and you hit start navigation for your location. The process doesn't get killed. So there must be a way to start the process on the background thread for a longer period of time. I just don't know-how. Any Ideas?

        self.activaity.$running
        .subscribe(on: self.BackgroundQueue)
        .receive(on: self.BackgroundQueue)
        .removeDuplicates(by: {$0 == $1})
        .sink(receiveValue: { [self] value in
                if value {
                        self.start()
                }
                else{
                        self.stop()
                }
        })
        .store(in: &self.cancellables)   


func start() {
    self.persistMetaData()
    self.persist()
    self.sensorManager.startUpdates()
   }
        

Thanks

Abdulmajed
  • 19
  • 5
  • There is no way to answer this without you showing some of your code. Also, Google Maps navigation (a background process) is a totally different thing than a Combine call on a different thread getting killed. – jnpdx Mar 17 '21 at 19:34
  • How you get information that your process got killed? can you show code for that? – ios coder Mar 17 '21 at 19:35
  • I edited and added some code. – Abdulmajed Mar 17 '21 at 19:40
  • I'm not actually sure whether the thread got killed or not. But from a data perspective, it seems like it. – Abdulmajed Mar 17 '21 at 19:43
  • Threads don't just mysteriously get killed on iOS, so there's something else going on. Since you haven't shown what `self.activaity.$running` is, it's pretty hard to say what's going on. Also, there's no reason to wrap all of that in `self.BackgroundQueue.async` -- you're already doing `.receive(on: self.BackgroundQueue)`. Try editing the question to show a minimal, reproducible example – jnpdx Mar 17 '21 at 19:45
  • @jnpdx Oh! What about the self.start() and self.end() anything withing the methods block will run on the background thread? – Abdulmajed Mar 17 '21 at 19:47
  • Yes. See https://developer.apple.com/documentation/combine/publishers/zip/receive(on:options:) and https://trycombine.com/posts/subscribe-on-receive-on/ – jnpdx Mar 17 '21 at 19:50
  • @jnpdx I have a class called a sensor, which basically starts and stops updates from sensors provided by CoreMotion and CoreLocation. Motion activity provided whether a user is running or walking or whatever. So I'm getting this information and starting and stopping the process based on that. – Abdulmajed Mar 17 '21 at 19:50
  • @jnpdx, so from my understanding whatever thread that started self.start() the method will live in that thread? – Abdulmajed Mar 17 '21 at 20:03
  • Yes, unless it dispatches to something else – jnpdx Mar 17 '21 at 20:04
  • Oky, so you think this might casuses an issue? regarding the behaviour of the program?. I'll fix everything. – Abdulmajed Mar 17 '21 at 20:05
  • Also, I have a question if in start updates method I have another publisher say timer for example, how can I make it run on the same thread being called? Because when I used the "on: .current" timer didn't fire. – Abdulmajed Mar 17 '21 at 20:06
  • It's too hard to guess about what's going on without showing code. – jnpdx Mar 17 '21 at 20:12
  • I'm also a little concerned that you may be thinking that being on a background thread will mean that it'll run when your app is in the background -- those two things are not the same thing. – jnpdx Mar 17 '21 at 20:17
  • "I'm okay with delaying the process but I'm not okay with it being killed." It's not quite clear what you mean here. The `background` QoS means that the work may be delayed indefinitely (which is identical to never running it). Do you really mean that? As a rule, you shouldn't expect it to run unless the device is plugged in (and maybe not then). And as jnpdx indicates, this has nothing to do with running when the app is not in the foreground. You probably actually want BGTaskScheduler – Rob Napier Mar 17 '21 at 20:19
  • @jnpdx, if the app is not killed. it's on the background is not the same as background threading? I don't know how to make run then -..- – Abdulmajed Mar 17 '21 at 20:38
  • @RobNapier How do i run start and stop, through combine using BFTtask scheduler? – Abdulmajed Mar 17 '21 at 20:40
  • None of these concepts are related. If you need to do some processing when the application itself is in the background, that has nothing to do with Combine. If you are trying to update some UI based on a process that might take a while to run, and you want to throttle it a bit, use QoS `.utility` ("long-running tasks whose progress the user does not follow actively"). If the user *does* care about this, then just use `default`. But when the app itself is in the background, none of this matters. Your app doesn't just keep running forever. – Rob Napier Mar 17 '21 at 20:44
  • There are many different application backgrounding systems in iOS. It matters exactly what you're trying to do to decide which one is appropriate. None of them look like "the app just keeps running normally, but in the background." Nothing like that exists in iOS. Location tracking is one thing. Bluetooth connections is another. Long-running processes is another. VoIP is another. Large file downloads is another. The precise problem will determine which tool is what you want. (But none of them are Combine.) – Rob Napier Mar 17 '21 at 20:47
  • How is your BackgroundQueue defined? – LuLuGaGa Mar 17 '21 at 20:54
  • @LuLuGaGa DispatchQueue(label:"main", qos: .background) – Abdulmajed Mar 17 '21 at 21:02
  • @Rob Napier My purpose is basically to track user running behaviour. Then give statistics to the user. I only want the user to enter the app then I'll start collecting behaviour when the user starts running. That's it. – Abdulmajed Mar 17 '21 at 21:03
  • For that, see HealthKit Workouts and Activity Rings. https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings In particular, you may want Creating a Workout Route https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/creating_a_workout_route – Rob Napier Mar 17 '21 at 21:17
  • Apple has a sample project to get you started called SpeedySloth: https://developer.apple.com/documentation/healthkit/workouts_and_activity_rings/speedysloth_creating_a_workout You can also read HealthKit records that the system or other apps have recorded, if you just want to create reports and don't want to create your own records. See also HKObserverQuery, which will let you know when new records are created (along with some other more specialized queries that might be useful for this): https://developer.apple.com/documentation/healthkit/reading_data_from_healthkit – Rob Napier Mar 17 '21 at 21:17
  • @RobNapier thank you for the proposals. However, I think the issue here is not about whether someone else did it or not. The thing i'm asking for is why this approach not working. Because I think this approach should be generic. Say for example instead of tracking user running behaviour, I want to make this process run when the user is driving and stops when the user stops driving (using Core motion auotmative property). – Abdulmajed Mar 17 '21 at 21:53
  • You can't use Core Motion that way, you'd have to use Core Location, which would be a completely different from the HealthKit solution, and give you completely different features and limitations, tailored for tracking navigation and location. There is no generic solution to this problem. Apple forbids you from doing what you're suggesting. You cannot just keep running and track arbitrary things when you're not in the foreground. The reason your approach does not work is that iOS kills your app. That's on purpose, and you can't stop it. Apple has specific solutions for some limited use cases. – Rob Napier Mar 17 '21 at 23:25
  • See my answer. Background threads and keeping your app running while it is in the background are two very different things. – Duncan C Mar 18 '21 at 01:44

1 Answers1

2

In iOS the term background can mean 2 different things: Running on a background thread, or when your app is no longer the front-most app but is still getting processing time.

The two are only loosely related. Starting a task running on a background thread is not the same thing as setting up your app to keep getting CPU time after the user switches it to the background or locks the device.

A background thread is a separate execution path through your code that is usually (but not always) run on a different processor and usually at the same time as your primary thread. It's like you have two cooks in a kitchen, both cooking different dishes. They have to share the same equipment and supplies, and if cook one leaves a mess, or steals ingredients cook two one has prepared, it can screw up cook two. Similarly, multiple threads have to be careful not to access the same memory at the same time without a way to synchronize that access, or talk to the hardware systems at the same time.

In order to understand the other meaning for background, we need to talk about how the OS manages apps. If your app is the frontmost app, it runs in the foreground. Normally, if the user swaps apps, you get a notification that you are being suspended. You are expected to stop running timers an save your app's data. You can ask for extra time to keep running in the background, but that is typically only about 3 minutes max. (In this sense of the word background, it means your app keeps doing things like downloading files or recording GPS readings while another app is on the screen and the user is interacting with it.)

After possibly running for a short time in the background, your app is switched to the "suspended" state. In that state it does not get any processor time. (None of your app's threads get any processor time.) Once in the suspended state, it can be terminated (removed from memory and killed) at any time without further notice. If it's killed, all of it's threads are killed, and all of its state data in memory is lost. That's why you should save your app state when you are told you are about to be suspended.

A very limited number of app types (turn-by-turn navigation apps, streaming music apps, and one or two others) are allowed to run indefinitely in the background. (I believe HealthKit apps are another of those app types that are allowed to run continuously in the background, although I've never looked at HealthKit in any detail.) Apple limits the apps that can run in the background because the CPU takes a lot more power when it is running at full speed, and more power to get any CPU time at all. Apple only allows very specific apps to run in the background, and only when the user authorizes them to do so. That lets Apple (and the user) limit power consumption and conserve battery life. Apps running in the background can also slow down the performance of the foreground app, although the hardware can throttle the background app to prevent that. Your example, Google Maps, is a turn-by-turn navigation app. It declares itself as such, and before iOS lets it keep running in the background the user has to grant it permission to do so.

Ignoring those types of apps and getting back to your app:

If the user switches back to your app while it is suspended, it picks up right where it left off, with all its state variables sill intact and the foreground thread and any background threads that were running start getting CPU time again.

When your app is terminated, any background threads are killed along with it. When it is re-launched, you will have to start any background threads that you want to run over again.

Starting tasks on a background thread will not keep your app running when the user switches to another app. Again, the two uses of the term background mean quite different things.

Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • Oh, Okay so I get what you are saying related to states. Okay, this is exactly what's happening when the user enters the app and running is true and the user goes to background the app still runs. However, when the user enters the app and running is not true it doesn't trigger the process, and when running is true and process starts. Now my question is How can I solve it? do I have to approach the problem differently? – Abdulmajed Mar 18 '21 at 11:16
  • I haven't looked into HealthKit in any detail. I strongly suspect that there is a mechanism to request background access to health data even when your app is in the background or the app is sleeping. I suggest you research that. – Duncan C Mar 18 '21 at 12:32