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
Stringby default; it is copy-on-write and fast for typical app work. - Bridging is automatic: most Foundation APIs that expect
NSStringacceptStringseamlessly. - 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
Stringunless you need anNSStringAPI.
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
countfor 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
StringoverNSString, bridge only when you need Foundation-only behaviors, and convertSubstringtoStringif you keep it. - Use formatters for user-facing numbers and dates; keep low-level scalar work for specialized cases.