HiveBrain v1.2.0
Get Started
← Back to all entries
patternswiftMinor

Concatenating an array of optional NSAttributedStrings

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
arraynsattributedstringsconcatenatingoptional

Problem

Imagine you had to join together attributed strings with a separator in between them. Which of the following methods would you use?

An extension on SequenceType with a function that takes in a separator. Can't use any optionals in the array.

extension SequenceType where Generator.Element: NSAttributedString {
    func join(withSeparator separator: NSAttributedString) -> NSAttributedString {
        var shouldAddSeparator = true
        return self.reduce(NSMutableAttributedString()) {(element, sequence) in
            if shouldAddSeparator {
            shouldAddSeparator = false
            }
            else {
                element.appendAttributedString(separator)
            }
        element.appendAttributedString(sequence)
        return element
        }
    }
}


A private function that takes in an array of optional NSAttributedString and a separator.

private func join(attributedStrings strings: [NSAttributedString?], withSeparator separator: NSAttributedString) -> NSAttributedString? {
    let unwrappedStrings = strings.flatMap{$0} as [NSAttributedString]
    guard unwrappedStrings.count == strings.count else { return nil }
    let finalString = NSMutableAttributedString()
    for (index, string) in unwrappedStrings.enumerate() {
        if index == 0 {
            finalString.appendAttributedString(string)
        }
        else {
            finalString.appendAttributedString(separator)
            finalString.appendAttributedString(string)
        }
    }

    return finalString
}


Extension on _ArrayType that can take in an array of options NSAttributedString

```
extension _ArrayType where Generator.Element == NSAttributedString? {
private func join(withSeparator separator: NSAttributedString) -> NSAttributedString? {
let unwrappedStrings = flatMap{$0} as [NSAttributedString]

var shouldAddSeparator = false
return unwrappedStrings.reduce(NSMutableAttributedString(), combine: { (string, element) in

Solution

The second and third approach combine two tasks into one:

  • Extract the non-nil elements from the given array of optional


attributed strings, and

  • concatenate these with a given separator.



My suggestion is to separate these concerns. This rules out #2 and #3
and leaves us with your #1 approach, the extension method on SequenceType.

Another disadvantage of the extension _ArrayType is that it uses an internal type,
so that might break in a future version of Swift.

The flatMap() method from the Swift standard library already provides a method for the first task:

let attributedStrings = optionalAttributedStrings.flatMap { $0 }


and this can be combined with your extension method by the caller:

let joined = optionalAttributedStrings.flatMap { $0 }.join(withSeparator: separator)


A user might also want to concatenate non-optional strings, which
works with the extension method:

let joined = attributedStrings.join(withSeparator: separator)


but not with the other two approaches.

Another argument for approach #1 is that it resembles
the existing method to join strings:

extension SequenceType where Generator.Element == String {
    public func joinWithSeparator(separator: String) -> String
}


The method itself can be improved. The reduce() method creates a new
NSMutableAttributedString in each iteration. Better create only one
and append all elements (similar as on your join() function):

extension SequenceType where Generator.Element: NSAttributedString {

    func join(withSeparator separator: NSAttributedString) -> NSAttributedString {
        let finalString = NSMutableAttributedString()
        for (index, string) in enumerate() {
            if index > 0 {
                finalString.appendAttributedString(separator)
            }
            finalString.appendAttributedString(string)
        }
        return finalString
    }
}


Update: The Swift language changes constantly. For the convenience
of future readers, here is an update to Swift 4 of the above code:

extension Sequence where Element: NSAttributedString {

    func join(withSeparator separator: NSAttributedString) -> NSAttributedString {
        let finalString = NSMutableAttributedString()
        for (index, string) in enumerated() {
            if index > 0 {
                finalString.append(separator)
            }
            finalString.append(string)
        }
        return finalString
    }
}


Also flatMap has been renamed to compactMap:

let attributedStrings = optionalAttributedStrings.compactMap { $0 }

Code Snippets

let attributedStrings = optionalAttributedStrings.flatMap { $0 }
let joined = optionalAttributedStrings.flatMap { $0 }.join(withSeparator: separator)
let joined = attributedStrings.join(withSeparator: separator)
extension SequenceType where Generator.Element == String {
    public func joinWithSeparator(separator: String) -> String
}
extension SequenceType where Generator.Element: NSAttributedString {

    func join(withSeparator separator: NSAttributedString) -> NSAttributedString {
        let finalString = NSMutableAttributedString()
        for (index, string) in enumerate() {
            if index > 0 {
                finalString.appendAttributedString(separator)
            }
            finalString.appendAttributedString(string)
        }
        return finalString
    }
}

Context

StackExchange Code Review Q#135814, answer score: 4

Revisions (0)

No revisions yet.