So far in this course, we have only covered the Swift Standard Library, but when it comes to strings we must also include the Foundation framework, as it contains a lot of both basic and advanced text functionality that is missing from the Swift Standard Library.

Foundation is available on all Apple platforms and has been around for a long time (there is also a version for other platforms, re-implemented in Swift; see: https://github.com/apple/swift-corelibs-foundation). It is written in and for Objective-C, but a lot of its API has been updated to be easier to work with from Swift. Not all of it has been though, and as we'll see, you might run into some problems when converting Foundation types to Swift types.

Foundation's string type is NSString, and it works directly with UTF-16 encoded text. It does not know what the Character type is, and does not necessarily handle Unicode text correctly like Swift does. NSString can be used as Swift String and vice versa as they can share the same underlying storage.

It also has CharacterSet, which, despite the name, is a set of UnicodeScalar. It has several useful predefined sets, like CharacterSet.alphanumerics, .whitespaces, .decimalDigits, and more. You can only use them if you're lucky enough to have characters consisting of only one UnicodeScalar:

CharacterSet.alphanumerics.contains(character.unicodeScalars.first!)

Foundation's range type is NSRange, and it uses integers to refer to positions in an NSString. It can do this efficiently because each element of NSString takes up the same amount of space. We can always convert a Swift Range to NSRange with NSRange(range, in: string), but we can't necessarily go the other way, as we will see later on.

Let's look at creating strings by following these steps:

  1. There are many ways of creating strings. You've already seen the string literal:
    let literal = "string from literal"
  2. There are also multi-line literals:
    let multilineLiteral = """
      line 1
      line 2
        line 3 indented
    
      """

    The result is "line 1\nline 2\n\tline 3 indented\n". The closing three quotes must be at the beginning of the line (excluding indentation) and any indentation that precedes it will be removed from the beginning of every line in the string.

  3. Use backslash to insert special characters like \\ (backslash), \t (horizontal tab), \n (line feed), \r (carriage return), \" (double quotation mark), and \' (single quotation mark).
  4. We can create characters directly from their hexadecimal Unicode code points, like this:
    let blackDiamond = "\u{2666}" // ♦
    let brokenHeart = "\u{1F494}" // 💔
  5. To include variables in the text, we use string interpolation, like this:
    let array = [1,2,3]
    let stringInterpolation = "The array \(array) has \(array.count) items."
    // "The array [1, 2, 3] has 3 items."
  6. Strings can describe absolutely any type, as shown here:
    struct CustomType {
      let value: Int
      let otherValue: Bool
    }
    
    let customType = CustomType(value: 5, otherValue: false)
    String(describing: customType) // "CustomType(value: 5, otherValue: false)"
  7. We can customize the description, like this:
    extension CustomType: CustomStringConvertible {
      var description: String {
        return "\(value) and \(otherValue)"
      }
    }
    
    String(describing: customType) // "5 and false"
  8. Text can be repeated, as shown here:
    String(repeating: "la", count: 5)
  9. We can read text files, like this:
    import Foundation
    
    do {
      let fileContents = try String(contentsOfFile: "file.txt")
    } catch { /* ... */ }

Follow these steps to look at how to implement common operations on a string:

  1. Many of the common sequences and collection methods are useful on strings too, as shown here:
    let string = """
               Line 1
               line 2
               """
    let range1 = ..<string.index(of: "1")!
    
    // return the substring over range 1
    string[range1]
    
    // return true if the string begins with "Line"
    string.hasPrefix("Line")
    // return true if the string ends with "2"
    string.hasSuffix("2")
  2. These mutate the string:
    var mutablestring = string
    
    // remove the characters in range1, and insert "line up" there.
    mutablestring.replaceSubrange(range1, with: "line up")
    // remove the characters in range1.
    mutablestring.removeSubrange(range1)
    // remove the first character.
    mutablestring.removeFirst()
    // remove the first 2 characters.
    mutablestring.removeFirst(2)
    // remove the last character.
    mutablestring.removeLast()
    // remove the last 2 characters.
    mutablestring.removeLast(2)
  3. There aren't many operations specifically made for strings:
    // return a new string in uppercase.
    string.uppercased()
    // return a new string in lowercase.
    string.lowercased()
  4. We get a lot more if we import Foundation, like this simple test for the existence of a substring:
    string.contains(" 1")
  5. All of the following methods return a new string with the changes; the original string is left intact:
    // new string with all the words capitalised (ignoring language)
    string.capitalized
    // new string with all the words capitalised, using the rules of the language from the provided locale
    string.capitalized(with: Locale.current)
    // new string with all occurrences of one substring replaced with another
    string.replacingOccurrences(of: "Line", with: "line")
    // new string with all occurrences of a substring removed
    string.replacingOccurrences(of: "Line", with: "")
    // new string with all occurrences of a substring in the provided range removed, using the provided options
    string.replacingOccurrences(of: "line", with: "triangle", options: .caseInsensitive, range: string.startIndex..<string.index(of: "\n")!)
    
    // the range of the first character that belongs to the provided CharacterSet
    string.rangeOfCharacter(from: .decimalDigits)
    // the range of the first occurrence of the substring
    let range = string.range(of: "Line")!
    // the substring over this range
    string[range]
    // the range of the line or lines containing the provided range
    string.lineRange(for: range)
    // new string with the characters in the provided CharacterSet removed from the beginning and the end
    " \t  trim  \n ".trimmingCharacters(in: .whitespacesAndNewlines)
    // a new string of the given length, by either removing characters from the end or adding 'withPad' to the end
    "Padded".padding(toLength: 10, withPad: " ", startingAt: 0)
    "Pad".padding(toLength: 10, withPad: "_ ", startingAt: 1)
  6. The following methods return an array of strings:
    // an array of strings, from splitting the original string over the provided substring
    string.components(separatedBy: ". ")
    // an array of strings, from splitting the original string over characters in the provided CharacterSet
    string.components(separatedBy: .newlines)

There is already a method on String for finding the first range of a substring. This method will find all of the ranges of a substring.

To use an Xcode playground to create a method on string which finds all ranges of a substring.

Perhaps the most straightforward way of counting the number of words in a string is to count the number of spaces and add one. But, even if you only have text using the Latin alphabet, this will often be wrong (there could be two spaces in a row, and doesn't is technically two words). Foundation has NSLinguisticTagger, which handles these things and other alphabets. Not all of its APIs have been updated for Swift yet, so it can be a bit cumbersome to use, but the method that we will use here is fairly straightforward.

To use an Xcode playground to create a method on string which can count words, sentences, and paragraphs.

  1. Open the StringsExtra Xcode project, and go to the StringsExtra.swift file.
  2. Enter the following code:
    extension String {
  3. You can call it like this:
    string.countLinguisticTokens(ofType: .paragraph)
    string.countLinguisticTokens(ofType: .sentence)
    string.countLinguisticTokens(ofType: .word)
  4. Go to the unit tests in StringsExtraTests.swift.
  5. Uncomment the next comment block, so these become active:
    let english = """
    func testCountLinguisticTokens_English() {
    let internationalText = """
    func testCountLinguisticTokens_International() {
  6. Run all unit tests and verify that they pass.