Swift strings are Unicode-correct, safe, and come with a few sharp edges if you assume they behave like simple arrays of bytes. Here’s how to handle them without surprises.

Creating and combining

let greeting = "Hello"
let name = "Deya"
let full = greeting + ", " + name  // concatenation
let templated = "Hello, \(name)"   // interpolation

Prefer interpolation for readability, and avoid building strings in loops—use joined() or reduce.

Length: use count, not byte size

let emoji = "👍🏼"
emoji.count        // 1 (one extended grapheme cluster)
emoji.utf8.count   // 6 (bytes)

count reflects user-visible characters; utf8.count is byte length. Choose the right one for your task.

Iterating safely

for ch in "café" {
    print(ch) // c, a, f, é
}

Iteration is by grapheme cluster (what humans see), so composed characters stay together.

If you need low-level processing, iterate over unicodeScalars:

for scalar in "ABC".unicodeScalars {
    print(scalar.value) // 65, 66, 67
}

Slicing strings

Indices are not integers. Always derive indices from the string:

let text = "Swift"
if let idx = text.firstIndex(of: "i") {
let slice = text[text.startIndex...idx] // "Swi"
}

Never do text[2]; use index(_:offsetBy:) to navigate.

Keep slices short-lived. Substring reuses the original storage; convert to String if you keep it:

let shortLived = text[text.startIndex...idx] // Substring
let owned = String(shortLived)               // owns its storage

Searching and replacing

let words = "hello swift world"
let hasSwift = words.contains("swift")
let swapped = words.replacingOccurrences(of: "swift", with: "Swift")

For complex searches, use range(of:) or replacingOccurrences with options like .caseInsensitive.

For localized comparisons, prefer localizedCaseInsensitiveContains:

words.localizedCaseInsensitiveContains("Swift") // true

Trimming and splitting

let raw = "  spaced out  "
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) // "spaced out"

let csv = "apple,banana,pear"
let parts = csv.split(separator: ",") // ["apple", "banana", "pear"]

split returns Substring; convert to String if you need to hold it long-term (String(parts[0])).

Building strings efficiently

For many appends, use joined or reduce. If you must mutate, use append on String:

var log = ""
["one", "two", "three"].forEach { log.append("\($0)\n") }

For very large data, consider TextOutputStream or Data buffers, not naive string concatenation.

Multiline strings are handy for formatting, but trim indentation with care:

let message = """
    Line one
    Line two
    """

Use raw strings (#"..."#) when you need fewer escapes, especially in regex literals.

String vs. NSString (and bridging)

String is Swift-native, Unicode-correct, and value-typed. NSString is the Objective-C class you get when bridging into Cocoa APIs.

  • Use String by default; it is copy-on-write and fast for typical app work.
  • Bridging is automatic: most Foundation APIs that expect NSString accept String seamlessly.
  • When you need Objective-C specific behaviors (like localizedCaseInsensitiveCompare), bridge explicitly:
let swift: String = "Résumé"
let objc: NSString = swift as NSString
let ordered = objc.localizedCaseInsensitiveCompare("resume")
  • Avoid unnecessary bridging in tight loops—keep values as String unless you need an NSString API.

If you must share mutable text with Objective-C, use NSMutableString carefully; in Swift, prefer immutable String and create new copies when needed.

Characters vs. UnicodeScalars

Use Character for user-facing text; UnicodeScalar for low-level processing (e.g., ASCII checks):

for scalar in "ABC".unicodeScalars {
    print(scalar.value) // 65, 66, 67
}

Validating and normalizing

If you compare user input, consider case-insensitive and diacritic-insensitive comparisons:

let a = "café"
let b = "CAFE"
let same = a.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current) ==
           b.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current)

When comparing paths or identifiers, normalize both sides the same way to avoid surprises.

Formatting and interpolation tips

Prefer interpolation over concatenation for readability:

let count = 3
let summary = "You have \(count) messages."

For numbers and dates shown to users, use NumberFormatter and DateFormatter to respect locale:

let formatter = NumberFormatter()
formatter.numberStyle = .decimal
let text = formatter.string(from: 12345.67)!

Takeaways

  • Use interpolation, avoid heavy concatenation in loops.
  • Always use count for user-visible length; bytes and scalars are for low-level needs.
  • Slice with string indices, not integers.
  • Normalize comparisons when dealing with user input or search.
  • Prefer String over NSString, bridge only when you need Foundation-only behaviors, and convert Substring to String if you keep it.
  • Use formatters for user-facing numbers and dates; keep low-level scalar work for specialized cases.