58

I have a string that I got from a text file.

Text file:

Line 1
Line 2
Line 3
...

I want to convert it to an array, one array element per line.

[ "Line 1", "Line 2", "Line 3", ... ]

Depending on how the file was saved, the string could take one of the following forms:

  • string = "Line 1\nLine 2\nLine 3\n..." where \n is the new line (line feed) character

  • string = "Line 1\r\nLine 2\r\nLine 3\r\n..." where \r is the carriage return character.

As I understand it, \n is commonly used in Apple/Linux today, while \r\n is used in Windows.

How do I split a string at any line break to get a String array without any empty elements?

Update

There are several solutions that work below. At this point I don't have any compelling reason to choose one as more correct than the others. Some factors that may influence choice could be (1) how "Swift" it is and (2) how fast it is for very long strings. You can provide feedback by upvoting one or more of them and/or leaving a comment.

See my summarized answer here

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
  • probably as simple as `split(stringFromFile, { newLineChars.characterIsMember($0) }, maxSplit: Int.max, allowEmptySlices: false)` – njzk2 Aug 15 '15 at 05:23
  • @njzk2, I like the look of this, but I got an error "Cannot invoke `split` with an argument list of type `String, (unichar) -> Bool, maxSplit Int, allowEmptySlices: Bool)`" Could it be that the syntax has changed in Swift 2? – Suragch Aug 15 '15 at 06:10
  • let test1 = "Line1\n\rLine2\n\rLine3\n\rLine4\nLine5\r\r\n\rLine6" let t1 =test1.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()) let t2 = t1.filter{ $0 != ""} works for me (Xcode 7, beta5) – user3441734 Aug 15 '15 at 07:26

9 Answers9

132

Swift 5.2 or later

You can split your String using the new Character property isNewline:

let sentence = "Line 1\nLine 2\nLine 3\n"
let lines = sentence.split(whereSeparator: \.isNewline)
print(lines)   // "["Line 1", "Line 2", "Line 3"]\n"

You can also extend StringProtocol and create a lines instance property to break up the string lines into subsequences:

extension StringProtocol {
    var lines: [SubSequence] { split(whereSeparator: \.isNewline) }
}

let sentence = "Line 1\nLine 2\r\nLine 3\n"
for line in sentence.lines {
    print(line)
}
let lines = sentence.lines  // ["Line 1", "Line 2", "Line 3"]


Original Answer

You can use String method enumerateLines:

Enumerates all the lines in a string.

Swift 3 or later

let sentence = "Line 1\nLine 2\nLine 3\n"
var lines: [String] = []
sentence.enumerateLines { line, _ in
    lines.append(line)
}
print(lines)   // "["Line 1", "Line 2", "Line 3"]\n"

extension String {
    var lines: [String] {
        var result: [String] = []
        enumerateLines { line, _ in result.append(line) }
        return result
    }
}

let sentence2 = "Line 4\nLine 5\nLine 6\n"
let sentence2Lines = sentence2.lines
print(sentence2Lines)    // "["Line 4", "Line 5", "Line 6"]\n"
let sentence3 = "Line 7\r\nLine 8\r\nLine 9\r\n"
let sentence3Lines = sentence3.lines
print(sentence3Lines)  // "["Line 7", "Line 8", "Line 9"]\n"
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • https://developer.apple.com/documentation/swift/stringprotocol/2923404-enumeratelines or https://developer.apple.com/documentation/foundation/nsstring/1408459-enumeratelines doesn't say anything – Leo Dabus Jan 15 '18 at 02:04
  • Hmm you're right it doesn't say anything in the docs. Though, in Xcode 9 it shows it crossed out in red when used on a String. https://i.imgur.com/g6vOXoO.png Does the strike-through mean something else? – Ryan R Jan 15 '18 at 19:20
  • @RyanR What is the type of your `contents`? option click it. Try cleaning your project. – Leo Dabus Jan 15 '18 at 19:22
  • `var contents: String` – Ryan R Jan 15 '18 at 19:25
  • Try writing it manually. You probably have an issue somewhere else which it is causing the strange behavior. – Leo Dabus Jan 15 '18 at 19:26
  • 1
    Clean/build seems to have resolved the red strike-through on `enumerateLines`. Odd. Thanks! – Ryan R Jan 15 '18 at 19:29
  • `split(whereSeparator: \.isNewline)` discards empty lines, good we still have `enumerateLines` – Phil Dukhov Oct 23 '21 at 15:22
  • That’s the default of omittingEmptySubsequences (true). If you don’t want to omit just pass false – Leo Dabus Oct 23 '21 at 16:51
  • value of type 'Character' has no member 'isNewLine' - swift 5.2.2 on Linux, so I assume it's only for Apple-based Swift. – Sebastian Oct 31 '21 at 19:07
  • @Sebastian Swift is case sensitive. The correct is `isNewline` – Leo Dabus Nov 02 '21 at 00:37
  • 1
    @Sebastian Whaat is not achievable? – Leo Dabus Nov 03 '21 at 11:00
26

in Xcode 8.2, Swift 3.0.1:

Use NSString method components(separatedBy:)

let text = "line1\nline2"
let array = text.components(separatedBy: CharacterSet.newlines)

Or use String method enumerateLines, like Leo Dabus's answer

zagger
  • 261
  • 3
  • 4
  • 2
    `let array = text.components(separatedBy: .newlines)` – Leo Dabus Jan 31 '17 at 21:12
  • 16
    While `let array = text.components(separatedBy: .newlines)` looks clean and swifty it actually splits CRLF line terminators (`\r\n`) **TWICE**, leading to empty lines. – Frederick Squid Jan 20 '18 at 15:14
  • Any way to use components(separetdBy:) with CRLF line terminators? – Raisen Jul 21 '20 at 18:10
  • @Raisen you just need to filter the empty strings after separating the new lines. You can also use the split method as I suggested above and map the resulting Substrings into Strings if needed. – Leo Dabus Dec 28 '20 at 18:10
11

In Swift 2, the top-level split function is now a method on CollectionType (which each of Strings "character views" conforms to). There are two versions of the method, you want the one that takes a closure as a predicate to indicate whether a given element should be treated as a separator.

You can get the character collection from the string as a collection of UTF16 characters using string.utf16, making them compatible with the NSCharacterSet APIs. This way, we can easily check inside the closure whether a given character in the string is a member of the newline character set.

It's worth noting that split(_:) will return a SubSequence of characters (basically a Slice), so it needs transforming back into an array of Strings which is generally more useful. I've done this below using flatMap(String.init) - the UTF16View initialiser on String is failable, so using flatMap will ignore any nil values that might be returned, ensuring you get an array of non-optional strings back.

So for a nice Swift-like way of doing this:

let str = "Line 1\nLine 2\r\nLine 3\n"
let newlineChars = NSCharacterSet.newlineCharacterSet()
let lines = str.utf16.split { newlineChars.characterIsMember($0) }.flatMap(String.init)
// lines = ["Line 1", "Line 2", "Line 3"]

What makes this nice is that the split method has a parameter allowEmptySubsequences, which ensures you don't receive any empty character sequences in the result. This is false by default, so you don't actually need to specify it at all.

Edit

If you want to avoid NSCharacterSet altogether, you can just as easily split the collection of unicode compliant Characters.

let lines = str.characters.split { $0 == "\n" || $0 == "\r\n" }.map(String.init)

Swift is able to treat "\r\n" as a single extended grapheme cluster, using it as a single Character for the comparison instead of creating a String. Also note that the initialiser for creating a string from a Character is non failable, so we can just use map.

Community
  • 1
  • 1
Stuart
  • 36,683
  • 19
  • 101
  • 139
  • I think you can use `map(String.init)` instead of `map { String($0)! }`. – hennes Aug 15 '15 at 11:23
  • 1
    @hennes You could, although since the `UTF16View` initialiser on `String` is failable, it will return and array of optionals strings (`[String?]`), and I don't think you can just force unwrap it directly. Better would be to use `flatMap(String.init)`, which will automatically ignore `nil`s and return an array of non-optionals. But thanks for the tip, I've updated the answer. – Stuart Aug 15 '15 at 11:26
  • What will it mean for text that contains emoji or other characters that require surrogate encoding in utf16? Do I have to worry about big and little ending encoding now, too? Is there a "pure Swift" way of breaking at the newline without using `NSCharacterSet`? – Suragch Aug 16 '15 at 04:28
  • As far as I'm aware (and as far as I have experimented), this shouldn't matter. `utf16` does decompose the string into those code units – splitting up the extended grapheme clusters – but after removing the newline characters the slices are reassembled into unicode strings anyway. It's perfectly fine to avoid `NSCharacterSet` and use the unicode compliant `Character` collection anyway – I've edited the answer with an example of this. – Stuart Aug 16 '15 at 08:29
  • I had an incredibly hard time finding a good answer to this splitting problem but the code in your edit finally did the trick for me. All the solutions using NSCharacterSet don't work in the IBM Sandbox. – JJP Mar 21 '16 at 14:01
  • @hnh Have you tried it, or read the explanation beneath the code in the edit? `"\r\n"` is expressible as a single `Character` value, and `str.characters` is a collection of `Character`s. – Stuart Jun 27 '16 at 09:50
  • @Stuart Ups, you are right, srz. This is pretty strange :-) – hnh Jun 27 '16 at 15:26
9

This answer is a summary of the other solutions already given. It comes from my fuller answer, but it would be useful to have the actual method choices available here.

New lines are usually made with the \n character, but can also be made with \r\n (from files saved in Windows).

Solutions

1. componentsSeparatedByCharactersInSet

let multiLineString = "Line 1\nLine 2\r\nLine 3\n"
let newlineChars = NSCharacterSet.newlineCharacterSet()
let lineArray = multiLineString.componentsSeparatedByCharactersInSet(newlineChars).filter{!$0.isEmpty}
// "[Line 1, Line 2, Line 3]"

If filter were not used, then \r\n would produce an empty array element because it gets counted as two characters and so separates the string twice at the same location.

2. split

let multiLineString = "Line 1\nLine 2\r\nLine 3\n"
let newlineChars = NSCharacterSet.newlineCharacterSet()
let lineArray = multiLineString.utf16.split { newlineChars.characterIsMember($0) }.flatMap(String.init)
// "[Line 1, Line 2, Line 3]"

or

let multiLineString = "Line 1\nLine 2\r\nLine 3\n"
let lineArray = multiLineString.characters.split { $0 == "\n" || $0 == "\r\n" }.map(String.init)
// "[Line 1, Line 2, Line 3]"

Here \r\n gets counted as a single Swift character (an extended grapheme cluster)

3. enumerateLines

let multiLineString = "Line 1\nLine 2\r\nLine 3\n"
var lineArray = [String]()
multiLineString.enumerateLines { (line, stop) -> () in
    lineArray.append(line)
}
// "[Line 1, Line 2, Line 3]"

For more about the enumerateLine syntax, see this answer also.

Notes:

  • a multi line string would not usually mix both \r\n and \n but I am doing this here to show that these methods can handle both formats.
  • NSCharacterSet.newlineCharacterSet() are newline characters defined as (U+000A–U+000D, U+0085), which include \r and \n.
  • This answer is a summary of the answers to my previous question. Read those answers for more detail.
Community
  • 1
  • 1
Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
7
let test1 = "Line1\n\rLine2\nLine3\rLine4"
let t1 = test1.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())
let t2 = t1.filter{ $0 != "" }
let t3 = t1.filter{ !$0.isEmpty }
user3441734
  • 16,722
  • 2
  • 40
  • 59
3

For the record, Swift's Foundation CharacterSet can be used within split:

alternative 1

extension String {
    var lines: [String] {
        return split { String($0).rangeOfCharacter(from: .newlines) != nil }.map(String.init)
    }
}

alternative 2

extension String {
    var lines: [String] {
        return split { CharacterSet.newlines.contains($0.unicodeScalars.first!) }.map(String.init)
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
1

How do I split a string at any line break to get a String array without any empty elements?

You were almost there - it's just the trailing closure which is different here:

let array = stringFromFile.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet()).filter{!$0.isEmpty}

Which is the same as:

let newLineChars = NSCharacterSet.newlineCharacterSet() // newline characters defined as (U+000A–U+000D, U+0085)
let array = stringFromFile.componentsSeparatedByCharactersInSet(newLineChars).filter{!$0.isEmpty}

ETA: removed unnecessary extra brackets at trailing closure

simons
  • 2,374
  • 2
  • 18
  • 20
1

Swift 4:

I would recommend to first save your CSV into string if you haven't already done it, then "clean" the string by removing unnecessary carriage returns

        let dataString = String(data: yourData!, encoding: .utf8)!

        var cleanFile = dataString.replacingOccurrences(of: "\r", with: "\n")
        cleanFile = cleanFile.replacingOccurrences(of: "\n\n", with: "\n")

Above will give you a string with a most desirable format, then you can separate the string using \n as your separator:

        let csvStrings = cleanFile.components(separatedBy: ["\n"])

Now you have an array of 3 items like:

["Line1","Line2","Line3"]

I am using a CSV file and after doing this, I am splitting items into components, so if your items were something like:

["Line1,Line2,Line3","LineA,LineB,LineC"]

        let component0 = csvStrings[0].components(separatedBy: [","]) // ["Line1","Line2","Line3"]
        let component1 = csvStrings[1].components(separatedBy: [","]) // ["LineA","LineB","LineC"]
Alan Gonzalez
  • 43
  • 1
  • 6
1

Option 1:

let getName = "Davender+Verma"
let cleanFile = getName.replacingOccurrences(of: "+", with: "+\n")
self.upcomingViewPetName.text = cleanFile

// Output
Davender+
verma

Option 2:

let getName = "Davender+Verma"
let cleanFile = getName.replacingOccurrences(of: "+", with: "\n")
self.upcomingViewPetName.text = cleanFile

//Output:
Davender
verma
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
Davender Verma
  • 503
  • 2
  • 12