Developers / Open In

Open In

UIDocumentInteractionControllerforScore 10.4

On iOS and iPadOS, apps can allow users to share content in a variety of ways. To copy a single file to another app, UIDocumentInteractionController is commonly used. This class makes a user’s file available to any other apps on their device that support the same file type, and as such it’s often helpful to use popular formats whenever possible for maximum compatibility.

If a developer wants to share a file while also providing additional information about that file, they must generally do one of two things: embed that information into the file and rewrite it on disk before sharing it (if the file type can contain the kind of information they’re trying to send), or they can wrap the original file’s data into a custom format. The first option is resource-intensive and not always possible, while the second option significantly reduces compatibility with other apps.

iOS and iPadOS’ sharing protocols allow for a third way of transmitting this information, however, by using UIDocumentInteractionController’s annotation property to store a dictionary of keys and values. In forScore we adopt the following kinds of information if found:

Keyword forScore Value Format
title Title NSString
composers Composers NSString (one or more comma-separated values)
genres Genres
tags Tags
labels Labels
rating Rating NSNumber or NSString representing a whole number between 0 and 5
difficulty Difficulty NSNumber or NSString representing a whole number between 0 and 3
duration Duration NSNumber or NSString representing a non-negative whole number (in seconds)
keysf Key NSNumber or NSString representing a whole number between -7 and 7*
keymi NSNumber or NSString representing 0 (major) or 1 (minor)*

*Key is stored as two MIDI-style values, learn more about these values here.

This method is perfect for sending sheet music to forScore. It allows apps to provide standard formats while also including helpful information—like a score’s duration—without permanently embedding that information into PDF metadata fields that weren’t designed for that purpose. The receiving app can take advantage of this information if desired, otherwise the user’s experience remains completely unchanged and compatibility is not diminished in any way.

Examples

Supplying Annotations

var annotation = [AnyHashable: Any]()

// Title may be provided, otherwise the filename (minus the .pdf extension) will be used
annotation["title"] = "Nocturne Op. 90 № 2"

// These four keys can contain any number of comma-separated values as strings
// Extra leading/trailing whitespace is trimmed automatically
annotation["composers"] = "Frédéric Chopin"
annotation["genres"] = "My Genre"
annotation["tags"] = "My Tag"
annotation["labels"] = "Label One, Label Two, Label Three"

// These values can be provided as NSNumbers or as digit-only strings
annotation["difficulty"] = NSNumber(2) // between 0 and 3
annotation["rating"] = NSNumber(5) // between 0 and 5

// Provide duration as a whole number of seconds: n = (minutes*60)+seconds
// stored as either NSNumber or string
annotation["duration"] = NSNumber(72)

annotation["keysf"] = NSNumber(0) // follows MIDI formatting; -7 (flats) through +7 (sharps)
annotation["keymi"] = NSNumber(0) // follows MIDI formatting; 0=major, 1=minor

// This must be stored as an instance variable or it will be deallocated prematurely and not appear
documentInteractionController = UIDocumentInteractionController(url: shareURL)
documentInteractionController.annotation = annotation

// Present open in menu; sender here is a UIBarButtonItem tapped to call this method
documentInteractionController.presentOpenInMenu(from: sender, animated: true)

Reading Annotations

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
	// [...] scene configuration code
	connectionOptions.urlContexts.forEach { handleOpenURLContext($0) }
}

func scene(_ scene: UIScene, openURLContexts URLContexts: Set) {
	URLContexts.forEach { handleOpenURLContext($0) }
}

private func handleOpenURLContext(_ context: UIOpenURLContext) {
	// [...] file access & import, as needed
	if let dictionary = context.options.annotation as? [AnyHashable: AnyObject] {
		// parse and save values accordingly
		// e.g. let title = dictionary["title"] as? String
	}
}

Processing Comma-Separated Values (Composers, Genres, Tags, and Labels)

if let composers = dictionary["composers"] as? String, !composers.isEmpty {
	let components = composers.components(separatedBy: ",")
	let values = components.compactMap { value in
		var result = value.trimmingCharacters(in: .whitespacesAndNewlines)
		// perform any other necessary validation and sanitization functions as needed
		if !result.isEmpty { return result }
		return nil
	}
	values.forEach { value in
		// save value data as needed
	}
}

Processing Number Values (Rating, Difficulty, Duration, and Key)

// certain values may be either string or NSNumber objects
var seconds: Int? = nil
if let s = dictionary["duration"] as? NSNumber { seconds = s.intValue }
else if let s = dictionary["duration"] as? String { seconds = Int(s) }
if let seconds, seconds > 0 {
	// save data as needed
}