Sometimes it is useful for an app to launch at login. While a user can always accomplish this in the system preferences, having the possibility to turn on auto-launching inside the app is better.This tutorial will show how. This tutorial is based on this one but is more detailed and Xamarin-oriented.
First of all, we will need to create a solution with two projects. First one will be our primary application and the second one will be a helper app. Why do we need a helper? Cause Apple told us to do so.
Applications can contain a helper application as a full application bundle, stored inside the main application bundle in the
Contents/Library/LoginItems
directory. Set either theLSUIElement
orLSBackgroundOnly
key in theInfo.plist
file of the helper application’s bundle.Use the
SMLoginItemSetEnabled
function (available in OS X v10.6.6 and later) to enable a helper application. It takes two arguments, aCFStringRef
containing the bundle identifier of the helper application, and aBoolean
specifying the desired state. Passtrue
to start the helper application immediately and indicate that it should be started every time the user logs in. Passfalse
to terminate the helper application and indicate that it should no longer be launched when the user logs in. This function returnstrue
if the requested change has taken effect; otherwise, it returnsfalse
. This function can be used to manage any number of helper applications.If multiple applications (for example, several applications from the same company) contain a helper application with the same bundle identifier, only the one with the greatest bundle version number is launched. Any of the applications that contain a copy of the helper application can enable and disable it.
Then we will enable sandboxing for both our apps.
Main app
For some reason, Xamarin decided not to add SMLoginItemSetEnabled function to the framework but we can easily do that!
public class StartAtLoginOption { [DllImport("/System/Library/Frameworks/ServiceManagement.framework/ServiceManagement")] static extern bool SMLoginItemSetEnabled(IntPtr aId, bool aEnabled); public static bool StartAtLogin(bool value) { CoreFoundation.CFString id = new CoreFoundation.CFString("my.helper.app.bundle.id"); return SMLoginItemSetEnabled(id.Handle, value); } }
In your main app’s Main.storyboard file, add a Check Box Button to the Window, connect the ViewController’s action with OnLaunchAtStartupChanged method. Your main app’s window could now look like this:
Add action implementation to your ViewController class:
partial void OnLaunchAtStartupChanged(NSButton sender) { StartAtLoginOption.StartAtLogin(sender.State == NSCellStateValue.On); }
Helper app
Helper app should be marked as either Background only app (LSBackgroundOnly) or as Agent (LSUIElement).As we don’t want our helper app to show any UI double click on the Main.storyboard and in Xcode uncheck the “Is Initial Controller” box on Window Controller.
In the helper app, add these lines of code. We need to check if the main app is already running because the helper app will be launched every time we set up the login item via the SMLoginItemSetEnabled function in the main app.
public override void DidFinishLaunching(NSNotification notification) { if (!NSWorkspace.SharedWorkspace.RunningApplications.Any(a => a.BundleIdentifier == "my.main.app.bundle.id")) { var path = new NSString(NSBundle.MainBundle.BundlePath) .DeleteLastPathComponent() .DeleteLastPathComponent() .DeleteLastPathComponent() .DeleteLastPathComponent(); var pathToExecutable = path + @"Contents/MacOS/LoginItemTestMain"; if (NSWorkspace.SharedWorkspace.LaunchApplication(pathToExecutable)) { } else NSWorkspace.SharedWorkspace.LaunchApplication(path); } NSApplication.SharedApplication.Terminate(this); }
Building the app
Now we need to assemble the app. Not having all those nice things Xcode gives to native developers we will have to apply some third-party tools. I prefer to use Cake build which is always a good choice when you develop a Xamarin application.
The code is very simple here. After building the solution we need to place our helper app in Contents/Library/LoginItems folder of the main application package in the Artefacts task.
var target = Argument("target", "Default"); var configuration = Argument("configuration", "Debug"); Task("Restore-NuGet-Packages") .Does(() => { NuGetRestore("./LoginItemTest.sln"); }); Task("Build") .IsDependentOn("Restore-NuGet-Packages") .Does (() => { DotNetBuild("./LoginItemTest.sln", c => c.Configuration = configuration); }); Task("Artefacts") .IsDependentOn("Build") .Does (() => { CleanDirectories("./Artefacts"); CopyDirectory("./LoginItemTestMain/bin/" + configuration + "/LoginItemTestMain.app", "./Artefacts/LoginItemTestMain.app"); CopyDirectory("./LoginItemTestHelper/bin/" + configuration + "/LoginItemTestHelper.app", "./Artefacts/LoginItemTestMain.app/Contents/Library/LoginItems/LoginItemTestHelper.app"); }); Task("Default") .IsDependentOn("Artefacts"); RunTarget (target);
Now build the app and copy it to your /Applications folder. Start the app, turn on launch at login and log out. Then, log in again and your main app will be launched. Keep in mind that this approach will only work if your app is located either in /Applications or ~/Applications, so for testing, you’ll have to copy your app to this location.
Sample project can be found here.
“For some reason, Xamarin decided not to add SMLoginItemSetEnabled function to the framework but we can easily do that!”
The short answer is that we know we don’t have bindings for everything yet on XM. 🙂
The entirety of the Cocoa (and related) API is huge.
I’ve filed a bug for the specific API here:
https://bugzilla.xamarin.com/show_bug.cgi?id=45114
I’ve never come across Cake before either, pretty cool.
LikeLiked by 1 person
Thanks, Chris!
I’m surprised you haven’t used Cake before as all Xamarin devs (at least all mobile Xamarin devs) I know use it as for CI.
LikeLike
Thanks for your post. This was helpful. By the way, have you solved how to check if SMLoginItemSetEnabled is set for the app? SMJobCopyDictionary does not work in a Sandboxed app also marked as deprecated by Apple. My solution was to save a bool LaunchAtLogin with NSUserDefaults. On subsequent launch if LaunchAtLogin is true, I reset SMLoginItemSetEnabled just to confirm that its still true.
Cake seems helpful. How are you creating your installer package, can you add that to the cake build script? I had some trouble with getting cake to take the configuration, I entered Debug and I continued to build Release. For the moment I wrote a shell script to cp the helper app into the main app and added it to Xamarin as a Before Build command.
LikeLike
Your way of detecting if the app starts at login seems to be the best one. I use the same approach.
The only other way I know is quite complicated:
1. do not terminate helper app in DidFinishLaunching
2. wait till the main app is launched
3. check if helper app is running in the main app
4. send a notification from the main app
5. handle notification in the helper app and then terminate it.
No, I use this app http://s.sudre.free.fr/Software/Packages/about.html to create a package project. Then build a package with a bash script and console utility from the app.
This line of code should change building configuration: DotNetBuild(“./LoginItemTest.sln”, c => c.Configuration = configuration); and you can pass configuration to the build script but executing this command “./build.sd –configuration=Release”
LikeLike
Pingback: Auto launching Xamarin Mac apps at login | Damian Mehers' Blog