Icon for File with UIKit

There’s a general consensus among many Mac and (mostly) iOS developers, that AppKit is “old and busted” and UIKit is “new and refined.” I am still fairly limited in my experience with UIKit, but in many ways I agree that in developing the framework, they left some of the more annoying baggage of AppKit behind. However, they also left off many conveniences that AppKit developers like myself have come to rely upon.

One of the easiest things in the world in AppKit is asking the framework for an image to represent a file’s icon. Say you’ve got a Swift source file handy and you want to show it in your app with an appropriate icon. It’s an easy one-liner on the Mac:

let swiftIcon = NSWorkspace.shared.icon(forFile: "/tmp/Test.swift")

Screenshot of Xcode showing code to request an icon from NSWorkspace.

On UIKit, as far as I can tell, it takes a bit more work. There is no UIWorkspace, which is probably fine, but there is also no UIImage.iconForFile, or similar method to make this quite as straightforward as it is on the Mac.

I came across “UIDocumentInteractionController” which seems to be the key to easily obtaining icons for arbitrary image types on iOS. After initializing it with a file URL, you can ask it for an array of icons, which it will reveal in order from smallest to largest. Tying this all together in an extension on UIImage, you could add a pretty handy helper method to your collection of code (sorry, one of these days I’ll get syntax highlighting working on this blog):

import UIKit

extension UIImage {
	public enum FileIconSize {
		case smallest
		case largest
	}

	public class func icon(forFileURL fileURL: URL, preferredSize: FileIconSize = .smallest) -> UIImage {
		let myInteractionController = UIDocumentInteractionController(url: fileURL)
		let allIcons = myInteractionController.icons

		// allIcons is guaranteed to have at least one image
		switch preferredSize {
		case .smallest: return allIcons.first!
		case .largest: return allIcons.last!
		}
	}
}

Now I’ve got basically the same functionality I had on AppKit:

Screenshot of invoking the helper method from code snippet above.

This is cool enough, but what’s even cooler is the UIKit method works on a URL irrespective of whether the underlying file actually exists! This, combined with the fact that a URL can be initialized with just about any String, means we can expand our icon utilities with a method for obtaining icons by file name alone:

extension UIImage {
	public class func icon(forFileNamed fileName: String, preferredSize: FileIconSize = .smallest) -> UIImage {
		return icon(forFileURL: URL(fileURLWithPath: fileName), preferredSize: preferredSize)
	}
}

Here we use it to get the icon for an arbitrary Pages file which may or may not exist:

Screenshot of calling the code snippet above to obtain a file icon for a Pages document.

Ah, now this is starting to feel even cooler than AppKit. But wait, NSWorkspace also supports another handy method:

let zipIcon = NSWorkspace.shared.icon(forFileType: "zip")

Well, we can achieve similar by taking advantage of the icon(forFileNamed:) method:

extension UIImage {
	public class func icon(forPathExtension pathExtension: String, preferredSize: FileIconSize = .smallest) -> UIImage {
		let baseName = "Generic"
		let fileName = (baseName as NSString).appendingPathExtension(pathExtension) ?? baseName
		return icon(forFileNamed: fileName, preferredSize: preferredSize)
	}
}

Now we’re really catching up:

Screenshot of code calling the iconForPathExtension example above.

But AppKit’s equivalent actually works on UTI type strings, as well. Wouldn’t that be cool to support on iOS? Seeing as MobileCoreServices gives us access to low level functions for converting UTI and file extensions, we can take a UTI, convert it, and let our existing helper methods take it from there:

import MobileCoreServices

extension FileManager {
	public func fileExtension(forUTI utiString: String) -> String? {
		guard
			let cfFileExtension = UTTypeCopyPreferredTagWithClass(utiString as CFString, kUTTagClassFilenameExtension)?.takeRetainedValue() else
		{
			return nil
		}

		return cfFileExtension as String
	}
}

extension UIImage {
	public class func icon(forUTI utiString: String, preferredSize: FileIconSize = .smallest) -> UIImage? {
		guard let fileExtension = FileManager.default.fileExtension(forUTI: utiString) else {
			return nil
		}
		return icon(forPathExtension: fileExtension, preferredSize: preferredSize)
	}
}

And voila!

Screenshot of code calling the iconForUTI code above.

It would be fairly easy to further extend this to support creating icon images from MIME types. Hope this helps some of you folks, especially coming from the Mac, who expected getting icons for file types to be slightly easier than it is.