Long-running Operations on macOS and iOS with NSProcessInfo

One issue that sometimes comes up in app development is keeping your app alive and running during long running processes. In my case with My Stream Timer, when the user starts a timer, it needs to keep running no matter what. My first solution was to simply turn on Screen Saver mode.

var appDelegate = ((AppDelegate)NSApplication.SharedApplication.Delegate);

appDelegate.MainWindow.Level = NSWindowLevel.ScreenSaver;

This solution "works" but has the side effect that the app will always be on top all the time, no matter what.  As you could imagine this is not an ideal solution. Then out of nowhere Frank told me about starting and stopping Activities with NSProcessInfo. We then did a full podcast on it:

I of course ignored his advice until this morning when I wrote 40 lines of code that solved all of my problems.

What are Activities?

According to Apple:

"The system has heuristics to improve battery life, performance, and responsiveness of applications for the benefit of the user. You can use the following methods to manage activities that give hints to the system that your application has special requirements."

What this means when you Begin or Perform and Activity is that:

"the system disables some or all of the heuristics so your application can finish quickly while still providing responsive behavior if the user needs it."

This is pretty awesome! There are also two distinct categories of activities based on what you app is looking to do.

  • User-initiated activities are explicitly started by the user. Examples include exporting or downloading a user-specified file.
  • Background activities perform the normal operations of your application and aren’t explicitly started by the user. Examples include autosaving, indexing, and automatic downloading of files.

In my case I have a "User-initiated" activity where I want to keep the screen on, app alive, and the processor going. There are several different types of NSActivityOptions that you have fine grain control over to specify this:

In my case this is a NSActivityUserIntitated, which includes NSActivityIdleSystemSleepDisabled, but I also want NSActivityIdleDisplaySleepDisabled. I also have multiple activities running at the same time, so I created a small manager to handle this.

Start Activity

Dictionary<string, NSObject> Activities { get; } = new Dictionary<string, NSObject>();

public void StartActivity(string id)
{
    if (Activities.ContainsKey(id))
    return;

	try
	{
		var options = NSActivityOptions.UserInitiated | NSActivityOptions.IdleDisplaySleepDisabled;

		var activity = NSProcessInfo.ProcessInfo.BeginActivity(options, "User has inititiated a timer that is a long running process.");

	Activities.Add(id, activity);
	}
	catch (Exception ex)
	{
		Debug.WriteLine($"Unable to start activity: {ex}");
    }
}

Now, when the user starts a timer, I kick off an activity and I am good to go.

End Activity

Now, I need to stop it when the timer ends or the users stops or pauses the timer.

public void StopActivity(string id)
{
    if (!Activities.ContainsKey(id))
        return;
    try
    {
        var activity = Activities[id];
        NSProcessInfo.ProcessInfo.EndActivity(activity);
        Activities.Remove(id);
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"Unable to start activity: {ex}");
    }
}


That is it! Easy peasy, and now the system will keep My Stream Timer will continue running without having to stay on top.