5
\$\begingroup\$

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
            if shouldAddSeparator {
                string.appendAttributedString(separator)
            }
            else {
                shouldAddSeparator = true
            }
            string.appendAttributedString(element)
            return string
        })
    }
}
\$\endgroup\$
4
\$\begingroup\$

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 }
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.