Skip to content
How to use string literals to easily load bundle assets
Swift
Branch: master
Clone or download

Latest commit

Fetching latest commit…
Cannot retrieve the latest commit at this time.

Files

Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
MarkdownLiteralExample.xcodeproj
MarkdownLiteralExample
.gitignore
LICENSE
README.md

README.md

Markdown files from string literals

Original post: https://frisco-uplink.com/2020/using-swift-string-literals-to-load-bundle-resources/

My new iOS fiction project relies heavily on text.

That means I want to make it easy to create that content anywhere, and I want it to be frictionless to drop it into the project as needed.

My solution: Markdown files I can load from the bundle using string literals. Look how easy:

let markdown: MarkdownFile = "markdown.md"

Here’s how to do it.

String literals

In Swift, you use string literals all the time. Usually to initialize strings.

let string: String = "Hello, I am a string."

But Swift includes a protocol called ExpressibleByStringLiteral. Which means if your Swift type adopts it, that type can be initialized with nothing more than a string. While this is immediately convenient, it has real power for assets that need tedious boilerplate. Say, anything that needs to be loaded from a bundle.

Basic example

struct MarkdownFile: ExpressibleByStringLiteral {
    
    let bundleName: String
    let rawMarkdown: String?
    
    init(stringLiteral: String) {
        
        bundleName = stringLiteral
        
        var loadedMarkdown: String? = nil
                
        if let filepath = Bundle.main.path(forResource: bundleName, ofType: nil) {
        //By skipping the ofType argument above, we'll match to the first file whose name
        //exactly matches bundleName
            do {
                let loadedString = try String(contentsOfFile: filepath)
                loadedMarkdown = loadedString
            } catch {
                print("Could not load string: \(error)")
            }
        } else {
            print("Could not find file: \(bundleName)")
        }
        
        rawMarkdown = loadedMarkdown
    }
}

Here’s a basic example of a MarkdownFile struct. It knows two things about itself: the name of the file used to initialize it, and any string it was able to load from a file in the bundle with that name.

On init it goes looking for a bundle resource matching the name it was provided through the string literal. If it finds one, and it can load its contents as a string, those contents are stored to rawMarkdown. If not, rawMarkdown returns nil.

This is already pretty convenient. Again, to initialize, all you need is:

let markdown: MarkdownFile = "markdown.md"

But we can take it further.

Adding convenience

The MarkdownFile struct can be responsible for converting its contents into a display representation, as well. Let’s add a computed property to parse the Markdown into HTML. I’ll be using Ink for this, but you could use any project—or convert it into something else, like NSAttributedString.

var htmlRepresentation: String? {
    if let raw = rawMarkdown {
        return MarkdownParser().html(from: raw)
    } else {
        return nil
    }
}

Putting it all together

With our output property all set up, we have a small, convenient API for handling Markdown files in any way we want. Here’s how we use it:

let markdown: MarkdownFile = "markdown.md"

if let html = markdown.htmlRepresentation {
    webview.loadHTMLString(html, baseURL: nil)
}

self.title = markdown.bundleName

Screen Shot 2020-03-05 at 10 39 58 AM

Behind the scenes, lots of stuff is happening to load and parse the file. But when you need Markdown across your project, you need only concern yourself with a filename. If you want to change any part of how this works later on, you have a single struct that’s responsible for all the Markdown behavior in your code.

You can’t perform that action at this time.