Oleg Gryb
Oleg Gryb

Reputation: 5249

New lines are not included to String count in iOS Swift 5

Here is a weird problem in iOS 14.1/Swift 5 that caused many griefs and consumed hours, until I understood what's going on. I still want to know if this is a bug or "by design". In the latter case, please provide a link describing the behavior and provide a list of other characters that are not counted.

Let us assume that I have a string like below:

let data = "l1\r\nl2\r\nl3"

I need to create an HTTP response manually and replace the Content-Length with the data length. I use a template for that:

static let RESPONSE = 
"""
HTTP/1.1 200 OK
Content-Length: LENGTH
Content-Type: text/plain

DATA
""".trimmingCharacters(in: .whitespaces)

Finally, I create a response from a template:

let response = RESPONSE.replacingOccurrences(of: ": LENGTH", with: ": \(data.count)")
        .replacingOccurrences(of:"DATA", with:data)

As a result, Content-Length was set to 8, not to 10, and a client didn't receive "l3".

Please note that the string with carriage returns has been generated by Apple's own BASE64 API, so there is nothing "special" that I did here myself:

Data(digest).base64EncodedString(options: [.lineLength64Characters])

Very strange behavior that I didn't see in any other languages.

Upvotes: 1

Views: 1347

Answers (2)

Paul Patterson
Paul Patterson

Reputation: 6918

Swift treats the combination of \r\n as a single newline character (abbreviated in the docs to CR-LF).

let combo = Character('\r\n')
print(combo.isNewline) // true

So when you convert this Character to a String and count it you get the answer one.

print(String(combo).count) // 1

Character has no count because by definition it represents a single user-perceived character even if it is constructed from a number of components.

I guess Swift's developers decided that the count property of String should output the number of user perceived characters, and since \r\n to all intents and purposes has the same effect has a single newline character it is counted as a single character.

Note however that String does not throw away the data from which it was constructed; you can still get the 'raw' count property that is most relevant to your case via the unicodeScalars property.

let data = "l1\r\nl2\r\nl3"
print(data.count) // 8
print(data.unicodeScalars.count)  // 10

By the way, it's not just CR-LF that gets this special treatment; national flag emojis are a single user perceived character that are actually composed of two scalars.

let unionJack = Character("🇬🇧")
        
for scalar in unionJack.unicodeScalars {
   print(String(scalar.value, radix: 16).uppercased()  )
}

// 1F1EC
// 1F1E7


Upvotes: 2

matt
matt

Reputation: 535306

Change data.count to data.utf16.count in order to get the "outside world" view of how "long" the string is.

(Alternatively you could say (data as NSString).length.)

Upvotes: 0

Related Questions