Sam Rowlands has been developing Xojo apps since 1997. He and his wife, Joy Sha, make up Ohanaware and they have been building award winning apps since 2008, notably Funtastic Photos, HDRtist and Shine. Sam is an active Xojo developer and he also offers App Wrapper, which simplifies the process of preparing applications for submission to the Mac App Store and deploying on web sites, in the Xojo Third Party Store.
What is the App Sandbox and why is it a chore?
The App Sandbox is a great end user security concept. The Sandbox governs what an application can and cannot do. This means that a virally infected word processor, cannot go on to infect other applications nor rampantly damage files on the users disk. It can however affect files which the user has created or opened within the word processor.
The developers’ pain comes from the App Sandbox’s greatest strength. The app developer must perform additional work, just to reclaim functionality that the App Sandbox took away. If you want to sell an app in the Mac App Store, the app must adopt the App Sandbox security.
Note: There are some functions which simply cannot be done within a Sandboxed application.
Trying to figure out solutions to these new issues often results in much head scratching and consulting with other developers via the Xojo Forums. I’ve seen some really creative solutions to reclaim features when using this security mechanism. In this post, I’m going to cover a few Sandboxing issues and how to resolve them with the tools / code that I’ve built for the apps that we sell.
Taking my latest, greatest app, which just so happens to be a really cool Text Editor built with Xojo.
1. If I simply Sandbox the application (using App Wrapper 3, with just the minimum), I can create new documents, but I can’t save or even open documents.
2. The Recent Items menu no longer works.
3. I personally like apps that re-open the last documents I was working on. The same issue persists, code that used to work, no longer does.
4. Even more scary, is that users can’t even print- what kind of text editor can’t print?
Problems 1 and 4
We’ll start with the simplest, problems #1 & #4 are easily resolved with the options (“Open & Save Panels” & “Printer”) on the “Capabilities” panel within App Wrapper.
Note: There appears to be a new issue with Yosemite, I’m not 100% certain of the cause, but it seemed that changing the case of the application’s bundle identifier solved it. So “com.ohanaware.mytexteditor” doesn’t work, where as “com.ohanaware.myTextEditor” does.
Problem 2: Recent Items list
In order to solve problem 2, we’ll use the “OWRecentItems” class from the Sandbox Kit. It’s a little more complicated than just selecting checkboxes, but not much more complicated.
Add the Sandbox Kit to your Xojo project.
In the “Open” event of the “App” object, add the following line of code
OWRecentItems.installRecentMenu( fileMenu, 1 )
This single line of code will tell the OWRecentItems class to install the recent menu, you pass in the menu on the to add to, and where in the menubar you’d like the “Open Recent” submenu to appear.
For my Text Editor, I added it to the file menu and in position 2.
3. Now all that’s required is when a document is saved or opened, another line of code needs to be called.
OWRecentItems.addToRecentMenu( file )
Simply call the OWRecentItems class again and use the addToRecentMenu method, passing in the Xojo folderitem where the document is being saved. It will be added to the Recent Menu (it’s okay to call it multiple times as it will only list it once).
That’s it. Now the application has a Sandbox safe recent items menu.
Note: Using this code will also add the Recent Item menu to the App’s dock item. It’s also pretty cool in the fact that the Apple Recent Item system work across Sandboxed and un-sandboxed versions.
Problem 3: Re-opening last open documents
There’s a bit more work involved to implement this solution, the great news is once you’ve mastered it for one window, it’s trivial to add to other windows.
1. In your Xojo application, you need to select your document window and click on the “Choose” button next to “Interfaces” in the Xojo inspector.
2. Some new methods will be added to the Window class.
- classIdentifer as string
- restoreData( data as JSONItem )
- storeData as JSONItem
“classIdentifer” is where we return a identifier, to which our restoration service uses to determine what window instance to create when the data is restored. I tend to return the window class name.
Function classIdentifier() As string // Part of the OWRestorableWindow interface. return "documentWindow" End Function
“restoreData” is used to populate the controls when restoring the window. For our word processor, we’ll use the code below.
Sub restoreData(data as JSONItem) // Part of the OWRestorableWindow interface. if data <> nil then if data.hasName( "text" ) then TextArea1.text = data.value( "text" ) end if End Sub
It’s important to check that the data object isn’t NIL and also to check for each setting (this will help to future proof your application).
“storeData” is the last method to populate in the Window class. For our purposes we’ll use the following code. It will simply store the TextArea into the property “text”.
Note: As we’re using JSONItems to save and restore the data, it’s not designed to contain vast amounts of data and some data types will need to be converted to text before storing.
Function storeData() As JSONItem // Part of the OWRestorableWindow interface. if me.folderitem <> nil then call saveFile Dim r as new JSONItem if textArea1.text <> "" then r.value( "text" ) = textArea1.text Return r End Function
It’s important to know that if you’re writing Auto Save files, this is the place to do so. Once Window Restoration is properly configured, the window will never receive the “CancelClose” or “Close” events. Consider this the last place to save the user data.
Modify the Save routine of the window, to store the file into the Window, this is done by using the window.folderitem property.
me.folderitem = f
Note: This is one of the key elements, without this the Window Restoration will not work correctly. The folderitem must be attached to the Window using this method.
Modify your file open routine to also use this property.
3. Now we need to alter the App object.
In the “CancelClose” event of the “App” Object, add the code to store the open windows.
Function CancelClose() As Boolean OWWindowRestoration.storeOpenWindows End Function
Note: Once this line is executed, no more code will be executed after it, nor will the application’s “Close”, window “CancelClose” and “Close” events will not fire.
4. The next step is to create a subclass of the “OWWindowRestoration” class, name this class something appropriate (for “My Text Editor”, I used the name “MTERestoration” ).
Add the event “windowByClassIdentifier” and in here is where we create a new instance of our window when requested. Earlier on, I returned “documentWindow” from the Window’s classIdentifer method.
Function windowByClassIdentifier(classIdentifier as string) As window select case classIdentifier case "documentWindow" return new documentWindow end select End Function
5. Back to the “Open” event of the “App” and we ask our subclass to restore windows. For example, this app uses the following code.
Sub Open() // --- Initiate the Recent Items Menu. OWRecentItems.installRecentMenu( fileMenu, 2 ) // --- Initate Window Restoration. Dim nwrc as new MTERestoration End Sub
In order to test this, make sure that “Close windows when quitting an app” is unchecked, in the “General” section of “System Preferences”.
Now run the application, leave some windows open, quit the application and re-launch it…
6. Cleaning up
In the final step, we need to restore window positions for files that were auto re-loaded (not just data that is stored). This is done by adding the following line to your file open routine.
OWWindowRestoration.restoreWindowBounds( myNewWindow, fileToOpen )
Replace “MyNewWindow” with the new instance of your window and “fileToOpen” with the file that was opened by the routine. Below is how it’s used in “My Text Editor”.
Sub OpenDocument(item As FolderItem) Dim theText as string try Dim tis as textInputStream = textInputStream.open( item ) if tis <> nil then theText = tis.readAll tis.close Dim ndw as new documentWindow ndw.TextArea1.text = theText ndw.contentsChanged = false ndw.folderitem = item OWRecentItems.addToRecentMenu( item ) OWWindowRestoration.restoreWindowBounds( ndw, item ) ndw.show end if catch err as IOException MsgBox "Unable to open the file """ + item.name + """." + EndOfLine + endOfLine + err.message End try End Sub
You may have noticed that your Xojo application is also creating new blank windows, which may be undesirable. This is can be solved by adding a property to the “App” class, name it “blockFirstNewWindow” and make it a boolean. Update the “NewDocument” event to use this new property.
Sub NewDocument() if blockFirstNewWindow = false then Dim ndw as new documentWindow ndw.show else blockFirstNewWindow = false end if End Sub
Switch back to the “Open” event and add a line of code to set that property to the result of the window restoration code.
Sub Open() // --- Initiate the Recent Items Menu. OWRecentItems.installRecentMenu( fileMenu, 2 ) // --- Initate Window Restoration. Dim nwrc as new MTERestoration blockFirstNewWindow = nwrc.restored End Sub
Now, if you run the project, create some documents, open some, save others, then quit and re-launch, your app should restore the open documents as they were when the application was quit.
Benefits of using this code
Apart from solving some App Sandbox issues, you’ll also get some additional functionality for free.
- Using the Apple Recent Item system, recent documents are also available from the dock menu.
- The Apple Recent Item system is compatible with both the Sandboxed application and unsandboxed (while everything else is not). Users of either version can swap between the two and still be able to use the Recent Items.
- Using the Window Restoration system will make an application follow and behave more consistently with other applications.
- The Window Restoration system will also add the file icon to the Window titlebar, and if the user control clicks on the file icon, they can see where on the disk the file is located.
What’s included in the Sandbox Kit
- OWRecentItems; Used to implement a Sandbox Safe Recent Items menu
- OWWindowRestoration; The Window Restoration handler
- OWRestorableWindow; A Class Interface adding the functions for Window Restoration
- OW_Sandbox_Kit; A shared module that the Sandbox Kit object require
- OWAppleScript; The preferred method of executing Apple Scripts from within a Sandboxed application
- OWBookmark; A last resort function for storing references to files, use this only if OWRecentItems & OWWindowRestoration cannot solve the issue
- SSBToken; Used by the OWBookmark class to ensure access to a referenced file is closed properly
- OWShell; A Shell subclass that can also use NSTask, the only way to launch embedded “Helper” applications within a Sandboxed application
How to get the Sandbox Kit
The Sandbox Kit is sold via the Ohanaware web site for $99.99.