A library for formatting strings on iOS and macOS

Related tags

Text Sprinter
Overview

Travis Coveralls Swift 3.2 Swift 4.0 License Twitter

Sprinter

Introduction

What?

Sprinter is a library for Mac and iOS for formatting strings at runtime using the printf / NSLog format token conventions.

The aim is to provide a type-safe, Swift-friendly interface for string formatting that is fully compatible with the printf specification, as well as Apple's proprietary extensions for working with Objective-C data types.

The name "Sprinter" is derived from "String-Printer", just like the sprintf function in the C standard library.

Why?

Although Swift already offers string formatting support in the form of the String(format:arguments:) initializer, Swift's support is a fairly crude wrapper around the Objective-C API, and lacks support for some of the standard printf formatting features and data types. For example, there is no way to use the following format string in Swift:

"Hello %s, how are you?"

Because the %s token expects a C string (a pointer to a zero-terminated array of CChar), which the Swift String(format:arguments:) method won't accept. Instead, you must use the platform-specific %@ token instead, which limits reusability of strings between platforms.

Swift also provides no way to validate or inspect format strings. If the format contains a typo, or the format arguments don't match the ones in your code, the string will be displayed incorrectly at runtime, or worse, may crash or cause silent memory corruption.

Sprinter solves these issues by exposing the argument types for each format string, so you can write runtime validation logic and handle errors gracefully.

The Sprinter library could also be used as the basis for unit tests that validate your strings at build time, or even as part of a code generation pipeline to provide strongly-typed string properties and methods.

How?

Sprinter implements a robust string format parser based on the original IEEE printf spec along with Apple's additions for Objective-C. It makes use of Swift's string formatter internally, but performs pre-validation and type conversion of arguments to ensure that invalid types are never passed to the underlying implementation.

Sprinter includes a comprehensive test suite to ensure spec compliance, and output compatibility with Apple's formatter.

Usage

Installation

The entire Sprinter API is encapsulated in a single file, and everything public is prefixed or namespaced, so you can simply drag the Sprinter.swift file into your project to use it. If you prefer, there's a framework for Mac and iOS that you can import, or you can use CocoaPods, Carthage, or Swift Package Manager on Linux.

To install Sprinter using CocoaPods, add the following to your Podfile:

pod 'Sprinter', '~> 0.2.0'

Sprinter works with Swift 3.2 and 4.x and supports iOS 9 or macOS 10.0 and above

Integration

To format a string using Sprinter, you first create a FormatString instance, as follows:

let formatString = try FormatString("I have %i apples and %i bananas")

Note the try keyword - the FormatString initializer performs validation of the string, and will throw an error if the format is invalid. Once you have constructed the formatString object, you can use the print() method to output the formatted string. The print() method is variadic, which is convenient for passing arguments. There is also a second form that accepts a single array of arguments.

You would use the print() method as follows:

let string = try formatString.print(5, 6)
print(string) // I have 5 apples and 6 bananas

You'll notice that the print() function also requires try. This method will throw an error if the arguments you pass do not match the placeholders in the original format string. Errors thrown by either the FormatString initializer or the print() method will all be of type FormatString.Error, for example:

let formatString = try FormatString("I have %y apples") // throws FormatString.error.unexpectedToken("y")

let string = try FormatString("I have %i apples").print("foo") // throws FormatString.error.argumentMismatch(1, String.self, Int.self)

You can determine the required argument types before calling the print() method by using the types property of the FormatString, which returns an array of Swift Type values:

let types = formatString.types
print(types) // Int, Int

This is typically not useful at runtime (incorrect arguments would be a programming error that should be fixed before release), but it could be used in an automated test to verify that a given localized string key has the same argument types in each language.

Localization

The FormatString constructor also takes an optional locale argument, which can be used to localize the output:

let french = try FormatString("I have %i apples", locale: Locale(identifier: "fr-FR"))

This will affect how locale-specific formatting and punctuation is displayed, for example:

let english = try FormatString("%'g", locale: Locale(identifier: "en-US"))
try print(english.print(1234.56)) // 1,234.56

let french = try FormatString("%'g", locale: Locale(identifier: "fr-FR"))
try print(french.print(1234.56)) // 1 234,56

let german = try FormatString("%'g", locale: Locale(identifier: "de-DE"))
try print(german.print(1234.56)) // 1.234,56

Thread Safety

It is safe to create FormatString instances on a background thread.

Once created, a given FormatString instance is stateless, so the same instance can safely be used to print strings on multiple threads concurrently.

Advanced Usage

It may seem cumbersome to have to create a StringFormat object before printing, but it serves two purposes:

  1. It allows validation and type inspection of the string before the point of use. This means you can be confident that there will be no surprise errors when it is called.

  2. The expensive string parsing and NumberFormatter initialization steps can be performed once and then stored, not repeated each time the string is displayed.

For these reasons, it's recommended that you store and re-use your FormatString objects. You can either do this up-front for all strings, or lazily the first time each string is displayed - whichever makes more sense for your app.

A good approach would be to create a wrapper function that encapsulates your app-specific string requirements. For example, you might want to ignore string format errors in production (since it's too late to fix by that point), and just display a blank string instead. Here is an example wrapper that you might use in your app:

private var cache = [String: FormatString]()
private let queue = DispatchQueue(label: "com.Sprinter")

func localizedString(_ key: String, _ args: Any...) -> String {
    do {
        var formatString: FormatString?
        queue.sync { formatString = cache[key] }
        if formatString == nil {
            formatString = try FormatString(NSLocalizedString(key, comment: ""), locale: Locale.current)
            queue.async { cache[key] = formatString }
        }
        return try formatString?.print(arguments: args) ?? ""
    } catch {
        // Crash in development, but not in production
        assertionFailure("\(error)")
        return ""
    }
}

This function provides:

  • A convenient API for displaying keys from your Localizable.strings file
  • Encapsulated error handling, which will crash in development but fail gracefully in production
  • Thread-safe caching of FormatString instances for better performance

This is just an example approach, but it should work for most use cases.

You might also like...
CodeMirror-Swift is a lightweight wrapper of CodeMirror for macOS and iOS
CodeMirror-Swift is a lightweight wrapper of CodeMirror for macOS and iOS

CodeMirror-Swift is a lightweight wrapper of CodeMirror for macOS and iOS. Features 🍭 Lightweight CodeMirror wrapper (build 5.52.2) ✅ 100% Native Swi

Simple, keyboard-first, markdown note-taking for MacOS

QuickDown Simple, keyboard-first, markdown note-taking for MacOS Main Features Global Hotkey: ⌘-⌥-N Save Note: ⌘-S Launch on Login (Optional) Addition

A Cross-Platform String and Regular Expression Library written in Swift.

Guitar 🎸 A Cross-Platform String and Regular Expression Library written in Swift. About This library seeks to add common string manipulation function

SZMentionsSwift is a lightweight mentions library for iOS.
SZMentionsSwift is a lightweight mentions library for iOS.

SZMentionsSwift is a lightweight mentions library for iOS. This library was built to assist with the adding, removing and editing of a mention within a textview.

iOS port from libphonenumber (Google's phone number handling library)

libPhoneNumber for iOS NBPhoneNumberUtil NBAsYouTypeFormatter ARC only Update Log https://github.com/iziz/libPhoneNumber-iOS/wiki/Update-Log Issue You

BonMot is a Swift attributed string library
BonMot is a Swift attributed string library

BonMot (pronounced Bon Mo, French for good word) is a Swift attributed string library. It abstracts away the complexities of the iOS, macOS, tvOS, and

Croc is a swift emoji string parsing library
Croc is a swift emoji string parsing library

Croc is a library for parsing emojis on iOS. It provides a simple and lightweight interface for detecting, generating, categorizing and managing emoji characters, making emoji-powered features an easy task for developers.

SwiftVerbalExpressions is a Swift library that helps to construct difficult regular expressions

SwiftVerbalExpressions Swift Regular Expressions made easy SwiftVerbalExpressions is a Swift library that helps to construct difficult regular express

Swift markdown library
Swift markdown library

Markdown ![Swift version](https://img.shields.io/badge/Swift-2.1 | 2.2-blue.svg) ![GitHub license](https://img.shields.io/badge/license-LGPL v3-green.

Comments
  • Strings with '%' symbols which aren't token placeholders don't format correctly

    Strings with '%' symbols which aren't token placeholders don't format correctly

    For instance the following: "%s 0% fat Greek style yogurt" when formatted with: "60ml" should ideally result in: "60ml 0% fat Greek style yogurt" Instead the outcome is nil

    I've taken a look over the code and this could be fixed by tweaking line 587 in Sprinter.swift: if first == "%" { break } to become:

    if first == "%" {
      if string.last == nil || string.last == " " {
        break
      }
    }
    

    I've tested this locally and all unit tests pass with the change (as well as this new case). I'm happy to create a PR? Let me know, cheers.

    opened by lawmaestro 3
Owner
Nick Lockwood
Nick Lockwood
🌭 Mustard is a Swift library for tokenizing strings when splitting by whitespace doesn't cut it.

Mustard ?? Mustard is a Swift library for tokenizing strings when splitting by whitespace doesn't cut it. Quick start using character sets Foundation

Mathew Sanders 695 Nov 11, 2022
A simple library for building attributed strings, for a more civilized age.

Veneer A simple library for building attributed strings, for a more civilized age. Veneer was created to make creating attributed strings easier to re

Wess Cope 26 Dec 27, 2022
Converts Markdown files and strings into NSAttributedStrings with lots of customisation options.

SwiftyMarkdown 1.0 SwiftyMarkdown converts Markdown files and strings into NSAttributedStrings using sensible defaults and a Swift-style syntax. It us

Simon Fairbairn 1.5k Dec 22, 2022
Texstyle allows you to format iOS attributed strings easily.

Texstyle allows you to format attributed strings easily. Features Applying attributes with strong typing and autocompletion Cache for attributes Subst

Rosberry 79 Sep 9, 2022
µframework for Attributed strings.

Attributed µframework for Attributed strings. What is Attributed? Attributed aims to be a drop in replacement to the current version of the NSAttribut

Nicholas Maccharoli 754 Jan 9, 2023
A Swifty API for attributed strings

SwiftyAttributes A Swifty API for attributed strings. With SwiftyAttributes, you can create attributed strings like so: let fancyString = "Hello World

Eddie Kaiger 1.5k Jan 5, 2023
An easier way to compose attributed strings

TextAttributes makes it easy to compose attributed strings. let attrs = TextAttributes() .font(name: "HelveticaNeue", size: 16) .foregroundCol

Damien 2.2k Dec 31, 2022
A Swift framework for using custom emoji in strings.

Emojica – a Swift framework for using custom emoji in strings. What does it do? Emojica allows you to replace the standard emoji in your iOS apps with

Dan 101 Nov 7, 2022
Generate SwiftUI Text or AttributedString from markdown strings with custom style names.

iOS 15.0 / macOS 12.0 / tvOS 15.0 / watchOS 8.0 StyledMarkdown is a mini library that lets you define custom styles in code and use them in your local

null 19 Dec 7, 2022
🌍⏩📄 Convert ISO8859 1-16 Encoded Text to String in Swift. Supports iOS, tvOS, watchOS and macOS.

ISO8859 Convert ISO8859 1-16 Encoded Text to String in Swift. Usage let encoding = ISO8859.part1 let string = String([...], iso8859Encoding: encoding)

Devran Cosmo Uenal 18 Jan 2, 2023