Is there joinWithSeparator for attributed strings - swift

Array of strings can be joined together with specific separator using joinWithSeparator method.
let st = [ "apple", "pie", "potato" ]
st.joinWithSeparator(", ")
As a result we will have "apple, pie, potato".
What if I have attributed strings inside my array? Is there easy way to combine them into one big attributed string?

Swift 5:
import Foundation
extension Sequence where Iterator.Element == NSAttributedString {
func joined(with separator: NSAttributedString) -> NSAttributedString {
return self.reduce(NSMutableAttributedString()) {
(r, e) in
if r.length > 0 {
r.append(separator)
}
r.append(e)
return r
}
}
func joined(with separator: String = "") -> NSAttributedString {
return self.joined(with: NSAttributedString(string: separator))
}
}
Swift 4:
import Foundation
extension SequenceType where Generator.Element: NSAttributedString {
func joinWithSeparator(separator: NSAttributedString) -> NSAttributedString {
var isFirst = true
return self.reduce(NSMutableAttributedString()) {
(r, e) in
if isFirst {
isFirst = false
} else {
r.appendAttributedString(separator)
}
r.appendAttributedString(e)
return r
}
}
func joinWithSeparator(separator: String) -> NSAttributedString {
return joinWithSeparator(NSAttributedString(string: separator))
}
}

Updated answer for Swift 4, together with some basic documentation, using Sequence:
extension Sequence where Iterator.Element: NSAttributedString {
/// Returns a new attributed string by concatenating the elements of the sequence, adding the given separator between each element.
/// - parameters:
/// - separator: A string to insert between each of the elements in this sequence. The default separator is an empty string.
func joined(separator: NSAttributedString = NSAttributedString(string: "")) -> NSAttributedString {
var isFirst = true
return self.reduce(NSMutableAttributedString()) {
(r, e) in
if isFirst {
isFirst = false
} else {
r.append(separator)
}
r.append(e)
return r
}
}
/// Returns a new attributed string by concatenating the elements of the sequence, adding the given separator between each element.
/// - parameters:
/// - separator: A string to insert between each of the elements in this sequence. The default separator is an empty string.
func joined(separator: String = "") -> NSAttributedString {
return joined(separator: NSAttributedString(string: separator))
}
}

For Swift 3.0, Sequence type wasn't supported the I switch to Array. I also change the method name to use the swift 3.0 style
extension Array where Element: NSAttributedString {
func joined(separator: NSAttributedString) -> NSAttributedString {
var isFirst = true
return self.reduce(NSMutableAttributedString()) {
(r, e) in
if isFirst {
isFirst = false
} else {
r.append(separator)
}
r.append(e)
return r
}
}
func joined(separator: String) -> NSAttributedString {
return joined(separator: NSAttributedString(string: separator))
}
}

Took this solution from SwifterSwift, it's similar to the other ones:
public extension Array where Element: NSAttributedString {
func joined(separator: NSAttributedString) -> NSAttributedString {
guard let firstElement = first else { return NSMutableAttributedString(string: "") }
return dropFirst().reduce(into: NSMutableAttributedString(attributedString: firstElement)) { result, element in
result.append(separator)
result.append(element)
}
}
func joined(separator: String) -> NSAttributedString {
guard let firstElement = first else { return NSMutableAttributedString(string: "") }
let attributedStringSeparator = NSAttributedString(string: separator)
return dropFirst().reduce(into: NSMutableAttributedString(attributedString: firstElement)) { result, element in
result.append(attributedStringSeparator)
result.append(element)
}
}
}

Related

Swift4, successor()

How to implement successor() to Swift4, Swift5?
func withMask(mask: String) -> String {
var resultString = String()
let chars = self
let maskChars = mask
var stringIndex = chars.startIndex
var maskIndex = mask.startIndex
while stringIndex < chars.endIndex && maskIndex < maskChars.endIndex {
if (maskChars[maskIndex] == "#") {
resultString.append(chars[stringIndex])
stringIndex = stringIndex.successor()
} else {
resultString.append(maskChars[maskIndex])
}
maskIndex = maskIndex.successor()
}
return resultString
}
Value of type 'String.Index' has no member 'successor'
The Swift 3+ equivalent of successor() is index(after
stringIndex = chars.index(after: stringIndex)
This is all incredibly convoluted. Just user zip and map:
extension String {
func masked(using mask: String) -> String {
let newChars = zip(self, mask).map { sourceChar, maskChar in
return (maskChar == "#") ? "#" : sourceChar
}
return String(newChars)
}
}
Although using a String of characters, whose characters encode booleans (true if # otherwise false) probably isn't a great idea. Better to just use an IndexSet.

How to extract Hashtags from text using SwiftUI?

Is there any way to find Hashtags from a text with SwiftUI?
This is how my try looks like:
calling the function like this : Text(convert(msg.findMentionText().joined(separator: " "), string: msg)).padding(.top, 8)
.
But it does not work at all.
My goal something like this:
extension String {
func findMentionText() -> [String] {
var arr_hasStrings:[String] = []
let regex = try? NSRegularExpression(pattern: "(#[a-zA-Z0-9_\\p{Arabic}\\p{N}]*)", options: [])
if let matches = regex?.matches(in: self, options:[], range:NSMakeRange(0, self.count)) {
for match in matches {
arr_hasStrings.append(NSString(string: self).substring(with: NSRange(location:match.range.location, length: match.range.length )))
}
}
return arr_hasStrings
}
}
func convert(_ hashElements:[String], string: String) -> NSAttributedString {
let hasAttribute = [NSAttributedString.Key.foregroundColor: UIColor.orange]
let normalAttribute = [NSAttributedString.Key.foregroundColor: UIColor.black]
let mainAttributedString = NSMutableAttributedString(string: string, attributes: normalAttribute)
let txtViewReviewText = string as NSString
hashElements.forEach { if string.contains($0) {
mainAttributedString.addAttributes(hasAttribute, range: txtViewReviewText.range(of: $0))
}
}
return mainAttributedString
}
You need to initailize Text() with a String, but instead you are attempting to initialize it with an Array of String.
You could either just display the first one if the array is not empty:
msg.findMentionText().first.map { Text($0) }
Or you could join the elements array into a single String:
Text(msg.findMentionText().joined(separator: " "))

Generate random String without repeating in swift

I want the function to generate random String without repeating.
For example this function maybe will print: ABCC
func randomString(length:Int) -> String {
let charSet = "ABCDEF"
var c = charSet.characters.map { String($0) }
var s:String = ""
for _ in (1...length) {
s.append(c[Int(arc4random()) % c.count])
}
return s
} print(randomString(length: 4))
and i want print random unique string only, E.g : ABCD
import GameplayKit
func randomString(length : Int) -> String {
let charSet = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZ".characters)
let shuffled = GKRandomSource.sharedRandom().arrayByShufflingObjects(in: charSet) as! [Character]
let array = shuffled.prefix(length)
return String(array)
}
print(randomString(length: 4))
func randomString(length: Int) -> String {
let charSet = "ABCDEF"
var charSetArray = charSet.characters.map { String($0) }
var randArray: [String] = []
while charSetArray.count > 0 {
let i = Int(arc4random_uniform(UInt32(charSetArray.count)))
randArray.append(charSetArray[i])
charSetArray.remove(at: i)
}
var output: String = ""
for i in 0..<length {
output.append(randArray[i])
}
return output
}
How to use:
let randomString = "ABCDEF".random(length: 3)!
The return value is optional because the length might exceed the length of provided string.
Check out the full implementation:
import UIKit
import PlaygroundSupport
extension MutableCollection where Indices.Iterator.Element == Index {
mutating func shuffle() {
let c = count
guard c > 1 else { return }
for (firstUnshuffled , unshuffledCount) in zip(indices, stride(from: c, to: 1, by: -1)) {
let d: IndexDistance = numericCast(arc4random_uniform(numericCast(unshuffledCount)))
guard d != 0 else { continue }
let i = index(firstUnshuffled, offsetBy: d)
swap(&self[firstUnshuffled], &self[i])
}
}
}
extension Sequence {
func shuffled() -> [Iterator.Element] {
var result = Array(self)
result.shuffle()
return result
}
}
extension String {
func random(length: Int) -> String? {
let uniqueCharacters = Array(Set(characters.map({ String($0) })))
guard length <= uniqueCharacters.count else { return nil }
guard length > 0 else { return nil }
return uniqueCharacters[0..<length].shuffled().joined()
}
}

resolving hashtags in textview

I'm setting the text on a textview and then calling this method of the UITextView extension in order to create links out of the words that are hashtags and mentions (Swift 3)
extension UITextView {
func resolveHashTags(font: UIFont = UIFont.systemFont(ofSize: 17.0)){
if let text = self.text {
let words:[String] = text.components(separatedBy: " ")
let attrs = [ NSFontAttributeName : font ]
let attrString = NSMutableAttributedString(string: text, attributes:attrs)
for word in words {
if (word.characters.count > 1 && ((word.hasPrefix("#") && word[1] != "#") || (word.hasPrefix("#") && word[1] != "#"))) {
let matchRange = text.range(of: word)
let newWord = String(word.characters.dropFirst())
if let matchRange = matchRange {
attrString.addAttribute(NSLinkAttributeName, value: "\(word.hasPrefix("#") ? "hashtag:" : "mention:")\(newWord)", range: text.NSRangeFromRange(range: matchRange))
}
}
}
self.attributedText = attrString
}
}
}
My issue is very simple. I have no way to create a link for something like this "helloworld#hello" simply because my word does not have a prefix of "#"
Another scenario I can't figure out is when the user puts multi hashtags together for example "hello world, how are you? #success#moments#connect" as this would all be considered 1 hashtag with the current logic when it should be 3 different links.
How do i correct? thank you
func resolveHashTags(text : String) -> NSAttributedString{
var length : Int = 0
let text:String = text
let words:[String] = text.separate(withChar: " ")
let hashtagWords = words.flatMap({$0.separate(withChar: "#")})
let attrs = [NSFontAttributeName : UIFont.systemFont(ofSize: 17.0)]
let attrString = NSMutableAttributedString(string: text, attributes:attrs)
for word in hashtagWords {
if word.hasPrefix("#") {
let matchRange:NSRange = NSMakeRange(length, word.characters.count)
let stringifiedWord:String = word
attrString.addAttribute(NSLinkAttributeName, value: "hash:\(stringifiedWord)", range: matchRange)
}
length += word.characters.count
}
return attrString
}
To separate words I used a string Extension
extension String {
public func separate(withChar char : String) -> [String]{
var word : String = ""
var words : [String] = [String]()
for chararacter in self.characters {
if String(chararacter) == char && word != "" {
words.append(word)
word = char
}else {
word += String(chararacter)
}
}
words.append(word)
return words
}
}
func textViewDidChange(_ textView: UITextView) {
textView.attributedText = resolveHashTags(text: textView.text)
textView.linkTextAttributes = [NSForegroundColorAttributeName : UIColor.red]
}
I hope this is what you are looking for. Tell me if it worked out for you.
If you're willing to use a 3rd party library, you could try using Mustard (disclaimer: I'm the author).
You could match hashtag tokens using this tokenizer:
struct HashtagTokenizer: TokenizerType, DefaultTokenizerType {
// start of token is identified by '#'
func tokenCanStart(with scalar: UnicodeScalar) -> Bool {
return scalar == UnicodeScalar(35) // ('35' is scalar value for the # character)
}
// all remaining characters must be letters
public func tokenCanTake(_ scalar: UnicodeScalar) -> Bool {
return CharacterSet.letters.contains(scalar)
}
}
Which can then be used to match the hashtags in the text:
let hashtags = "hello world, how are you? #success#moments#connect".tokens(matchedWith: HashtagTokenizer())
// hashtags.count -> 3
// hashtags[0].text -> "#success"
// hashtags[0].range -> 26..<34
// hashtags[1].text -> "#moments"
// hashtags[1].range -> 34..<42
// hashtags[2].text -> "#connect"
// hashtags[2].range -> 42..<50
The array returned is an array of tokens, where each one contains a text property of the matched text, and a range property of the range of that matched text in the original string that you can use to create a link on the text view.

NSTextField Mask (Swift 2.2)

Does anyone know how to implement a NSTextField mask in Swift? Need to do a MAC address mask for it.
This should get you going.
class MacAddressFormatter : NSFormatter {
override func stringForObjectValue(obj: AnyObject?) -> String? {
if let string = obj as? String {
return string
}
return nil
}
override func getObjectValue(obj: AutoreleasingUnsafeMutablePointer<AnyObject?>, forString string: String, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>) -> Bool {
if obj != nil {
obj.memory = string
}
return true
}
override func isPartialStringValid(partialString: String, newEditingString newString: AutoreleasingUnsafeMutablePointer<NSString?>, errorDescription error: AutoreleasingUnsafeMutablePointer<NSString?>) -> Bool {
if partialString.isEmpty { return true } //allow empty field
if partialString.characters.count > 17 { return false } //don't allow too many chars
let disallowedChars = NSCharacterSet(charactersInString: "0123456789ABCDEFabcdef:").invertedSet
if let _ = partialString.rangeOfCharacterFromSet(disallowedChars, options: .CaseInsensitiveSearch) {
error.memory = "Invalid entry. MAC Address can only contain 0-9 & A-F"
return false }
var string = ""
for char in partialString.characters {
if char != ":" {
string = string + String(char)
if string.characters.count % 3 == 0 {
string.insert(":", atIndex: string.endIndex.advancedBy(-1))
}
}
}
newString.memory = string.uppercaseString
return false
}
}
Just assign this formatter to you NSTextField and give it a try!