how to change format of substring in swift - swift

I get a message from my response like "your bill is: 10.00"
But I need to show in bold the number and only that (everything after the ":"). I know I could use SubString, but don't understand exactly how to split text and correctly format it
my old test:
self.disclaimerLabel.attributedText = String(format: my).htmlAttributedString(withBaseFont: Font.overlineRegular07.uiFont, boldFont: Font.overlineBold02.uiFont, baseColor: Color.blueyGreyTwo.uiColor, boldColor: Color.blueyGreyTwo.uiColor)

How was my built ? If from 2 parts, set attributes to each before joining.
If you get my as a whole, you can access substrings with
let parts = my.split(separator: ":")
parts[1] will be "your bill is"
parts[2] will be "10:00"

The need to add styling to a single word or phrase is so common that it is worth having on hand a method to help you:
extension NSMutableAttributedString {
func apply(attributes: [NSAttributedString.Key: Any], to targetString: String) {
let nsString = self.string as NSString
let range = nsString.range(of: targetString)
guard range.length != 0 else { return }
self.addAttributes(attributes, range: range)
}
}
So then your only problem is discovering the stretch of text that you want to apply the attributes to. If you don't know that it is "10.00" then, as you've been told, you can find out by splitting the string at the colon-plus-space.

You can split your string into char : and then you can change text attributes like :
var str = "your bill is: 10.00"
var splitArray = str.components(separatedBy: ":")
let normalText = NSMutableAttributedString(string: splitArray[0] + ":")
let boldText = splitArray[1]
let boldTextAtr = NSMutableAttributedString(string: boldText, attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 16.0) ])
normalText.append(boldTextAtr)
let labell = UILabel()
labell.attributedText = normalText
labell.attributedText will print what you exactly want

Related

NSAttributedString: turn (multiple) #username[userid] mentions into clickable #username links

I am in the process of writing code to display mentions within an NSAttributedString, which need to link out to a user profile. The format of the mentions is #username[userid], which would need to be displayed as simply #username, which is tappable.
I have the code working so far that the username becomes clickable, but I now need to remove the [userid] part, which of course modifies the length of the string so that ranges don't match anymore, etc. Not sure how I can solve this.
import Foundation
import UIKit
let comment = "Hello #kevin[1], #john and #andrew[2]!"
let wholeRange = NSRange(comment.startIndex..<comment.endIndex, in: comment)
let regex = try NSRegularExpression(pattern: #"(#[\w.-#]+)\[(\d+)\]"#)
let attributedString = NSMutableAttributedString(string: comment)
regex.enumerateMatches(in: comment, options: [], range: wholeRange) { match, _, _ in
guard let match = match else {
return
}
let userIdRange = Range(match.range(at: 2), in: comment)!
let userId = comment[userIdRange]
let usernameRange = match.range(at: 1)
attributedString.addAttribute(NSAttributedString.Key.link, value: URL(string: "test://profile/\(userId)")!, range: usernameRange)
}
print(attributedString)
The result right now can be represented like this, when printed:
Hello {
}#kevin{
NSLink = "test://profile/1";
}[1], #john and {
}#andrew{
NSLink = "test://profile/2";
}[2]!{
}
So #kevin and #andrew are links, #john is not (which is expected!), but the user ids are still visible. Surely this is a problem that has been solved before but I can't find any examples, not sure what keywords to search for. There are plenty of questions about detecting usernames/mentions in strings, and even more about making links in NSAttributedString, but that's not the problem I am trying to solve.
How would I turn the #username[userid] mentions into clickable #username links, so that the [userid] part is hidden?
You just need to get all the matching ranges, iterate them in reverse order, add the link to it and then replace the whole range with the name. Something like:
let comment = "Hello #kevin[1], #john and #andrew[2]!"
let attributedString = NSMutableAttributedString(string: comment)
let regex = try NSRegularExpression(pattern: #"(#[\w.-#]+)\[(\d+)\]"#)
var ranges: [(NSRange,NSRange,NSRange)] = []
regex.enumerateMatches(in: comment, range: NSRange(comment.startIndex..., in: comment)) { match, _, _ in
guard let match = match else {
return
}
ranges.append((match.range(at: 0),
match.range(at: 1),
match.range(at: 2)))
}
ranges.reversed().forEach {
let userId = attributedString.attributedSubstring(from: $0.2).string
let username = attributedString.attributedSubstring(from: $0.1).string
attributedString.addAttribute(.link, value: URL(string: "test://profile/\(userId)")!, range: $0.0)
attributedString.replaceCharacters(in: $0.0, with: username)
}
print(attributedString)
This will print
Hello {
}#kevin{
NSLink = "test://profile/1";
}, #john and {
}#andrew{
NSLink = "test://profile/2";
}!{
}
Quickly done:
let comment = "Hello #kevin[1], #john and #andrew[2]!"
let attributedString = NSMutableAttributedString(string: comment)
let wholeRange = NSRange(attributedString.string.startIndex..<attributedString.string.endIndex, in: attributedString.string)
let regex = try NSRegularExpression(pattern: #"(#[\w.-#]+)\[(\d+)\]"#)
let matches = regex.matches(in: attributedString.string, options: [], range: wholeRange)
matches.reversed().forEach { aResult in
let fullMatchRange = Range(aResult.range(at: 0), in: attributedString.string)! //#kevin[1]
let replacementRange = Range(aResult.range(at: 1), in: attributedString.string)! //#kevin
let userIdRange = Range(aResult.range(at: 2), in: attributedString.string)! // 1
let atAuthor = String(attributedString.string[replacementRange])
attributedString.addAttribute(.link,
value: URL(string: "test://profile/\(attributedString.string[userIdRange])")!,
range: NSRange(fullMatchRange, in: attributedString.string))
attributedString.replaceCharacters(in: NSRange(fullMatchRange, in: attributedString.string),
with: atAuthor)
}
print(attributedString)
Output:
Hello {
}#kevin{
NSLink = "test://profile/1";
}, #john and {
}#andrew{
NSLink = "test://profile/2";
}!{
}
What's to see:
I changed the pattern, for easy captures. See the sample in comment in the forEach().
I used matches in reverse order, else the ranges won't be accurate anymore!
I kept playing with attributedString.string instead of comment in case it's "unsync".

Converting numbers to string in a given string in Swift

I am given a string like 4eysg22yl3kk and my output should be like this:
foureysgtweny-twoylthreekk or if I am given 0123 it should be output as one hundred twenty-three. So basically, as I scan the string, I need to convert numbers to string.
I do not know how to implement this in Swift as I iterate through the string? Any idea?
You actually have two basic problems.
The first is convert a "number" to "spelt out" value (ie 1 to one). This is actually easy to solve, as NumberFormatter has a spellOut style property
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
let text = formatter.string(from: NSNumber(value: 1))
which will result in "one", neat.
The other issue though, is how to you separate the numbers from the text?
While I can find any number of solutions for "extract" numbers or characters from a mixed String, I can't find one which return both, split on their boundaries, so, based on your input, we'd end up with ["4", "eysg", "22", "yl", "3", "kk"].
So, time to role our own...
func breakApart(_ text: String, withPattern pattern: String) throws -> [String]? {
do {
let regex = try NSRegularExpression(pattern: "[0-9]+", options: .caseInsensitive)
var previousRange: Range<String.Index>? = nil
var parts: [String] = []
for match in regex.matches(in: text, options: [], range: NSRange(location: 0, length: text.count)) {
guard let range = Range(match.range, in: text) else {
return nil
}
let part = text[range]
if let previousRange = previousRange {
let textRange = Range<String.Index>(uncheckedBounds: (lower: previousRange.upperBound, upper: range.lowerBound))
parts.append(String(text[textRange]))
}
parts.append(String(part))
previousRange = range
}
if let range = previousRange, range.upperBound != text.endIndex {
let textRange = Range<String.Index>(uncheckedBounds: (lower: range.upperBound, upper: text.endIndex))
parts.append(String(text[textRange]))
}
return parts
} catch {
}
return nil
}
Okay, so this is a little "dirty" (IMHO), but I can't seem to think of a better approach, hopefully someone will be kind enough to provide some hints towards one ;)
Basically what it does is uses a regular expression to find all the groups of numbers, it then builds an array, cutting the string apart around the matching boundaries - like I said, it's crude, but it gets the job done.
From there, we just need to map the results, spelling out the numbers as we go...
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
let value = "4eysg22yl3kk"
if let parts = try breakApart(value, withPattern: pattern) {
let result = parts.map { (part) -> String in
if let number = Int(part), let text = formatter.string(from: NSNumber(value: number)) {
return text
}
return part
}.joined(separator: " ")
print(result)
}
This will end up printing four eysg twenty-two yl three kk, if you don't want the spaces, just get rid of separator in the join function
I did this in Playgrounds, so it probably needs some cleaning up
I was able to solve my question without dealing with anything extra than converting my String to an array and check char by char. If I found a digit I was saving it in a temp String and as soon as I found out the next char is not digit, I converted my digit to its text.
let inputString = Array(string.lowercased())

Bold words that start with letter

I want to set a label's text with a string that's partly bold. The words I want to make bold all begin with the same letter, say "~".
For example I could have the string, "This ~word is bold, and so is ~this"
Then the label's text would contain the string "This word is bold, and so is this".
Does anybody know if it's possible to make a function like this? I tried the following:
func makeStringBoldForLabel(str: String) {
var finalStr = ""
let words = str.components(separatedBy: " ")
for var word in words {
if word.characters.first == "~" {
var att = [NSFontAttributeName : boldFont]
let realWord = word.substring(from: word.startIndex)
finalStr = finalStr + NSMutableAttributedString(string:realWord, attributes:att)
} else {
finalStr = finalStr + word
}
}
}
but get the error:
Binary operator '+' cannot be applied to operands of type 'String' and 'NSMutableAttributedString'
Easy to solve problem.
use:
func makeStringBoldForLabel(str: String) {
let finalStr = NSMutableAttributedString(string: "")
let words = str.components(separatedBy: " ")
for var word in words {
if word.characters.first == "~" {
var att = [NSFontAttributeName : boldFont]
let realWord = word.substring(from: word.startIndex)
finalStr.append(NSMutableAttributedString(string:realWord, attributes:att))
} else {
finalStr.append(NSMutableAttributedString(string: word))
}
}
}
The error message is clear, you cannot concatenate String and NSAttributedString with the + operator.
You are looking for the API enumerateSubstrings:options. It enumerates strings word by word passing the .byWords option. Unfortunately the tilde (~) is not recognized as a word separator, so we have to check if a word has a preceding tilde. Then change the font attributes at the specific range.
let string = "This ~word is bold, and so is ~this"
let attributedString = NSMutableAttributedString(string: string, attributes:[NSFontAttributeName : NSFont.systemFont(ofSize: 14.0)])
let boldAttribute = NSFont.boldSystemFont(ofSize: 14.0)
string.enumerateSubstrings(in: string.startIndex..<string.endIndex, options: .byWords) { (substring, substringRange, enclosingRange, stop) -> () in
if substring == nil { return }
if substringRange.lowerBound != string.startIndex {
let tildeIndex = string.index(before: substringRange.lowerBound)
if string[tildeIndex..<substringRange.lowerBound] == "~" {
let location = string.distance(from: string.startIndex, to: tildeIndex)
let length = string.distance(from: tildeIndex, to: substringRange.upperBound)
attributedString.addAttribute(NSFontAttributeName, value: boldAttribute, range: NSMakeRange(location, length))
}
}
}

How to get the first characters in a string? (Swift 3)

I want to get a substring out of a string which starts with either "<ONLINE>" or "<OFFLINE>" (which should become my substring). When I try to create a Range object, I can easily access the the first character by using startIndex but how do I get the index of the closing bracket of my substring which will be either the 8th or 9th character of the full string?
UPDATE:
A simple example:
let onlineString:String = "<ONLINE> Message with online tag!"
let substring:String = // Get the "<ONLINE> " part from my string?
let onlineStringWithoutTag:String = onlineString.replaceOccurances(of: substring, with: "")
// What I should get as the result: "Message with online tag!"
So basically, the question is: what do I do for substring?
let name = "Ajay"
// Use following line to extract first chracter(In String format)
print(name.characters.first?.description ?? "");
// Output : "A"
If you did not want to use range
let onlineString:String = "<ONLINE> Message with online tag!"
let substring:String = onlineString.components(separatedBy: " ")[0]
print(substring) // <ONLINE>
The correct way would be to use indexes as following:
let string = "123 456"
let firstCharIndex = string.index(string.startIndex, offsetBy: 1)
let firstChar = string.substring(to: firstCharIndex)
print(firstChar)
This Code provides you the first character of the string.
Swift provides this method which returns character? you have to wrap it before use
let str = "FirstCharacter"
print(str.first!)
Similar to OOPer's:
let string = "<ONLINE>"
let closingTag = CharacterSet(charactersIn: ">")
if let closingTagIndex = string.rangeOfCharacter(from: closingTag) {
let mySubstring = string.substring(with: string.startIndex..<closingTagIndex.upperBound)
}
Or with regex:
let string = "<ONLINE>jhkjhkh>"
if let range = string.range(of: "<[A-Z]+>", options: .regularExpression) {
let mySubstring = string.substring(with: range)
}
This code be some help for your purpose:
let myString = "<ONLINE>abc"
if let rangeOfClosingAngleBracket = myString.range(of: ">") {
let substring = myString.substring(to: rangeOfClosingAngleBracket.upperBound)
print(substring) //-><ONLINE>
}
Swift 4
let firstCharIndex = oneGivenName.index(oneGivenName.startIndex, offsetBy: 1)
let firstChar = String(oneGivenName[..<firstCharIndex])
let character = MyString.first
it's an simple way to get first character from string in swift.
In swift 5
let someString = "Stackoverflow"
let firstChar = someString.first?.description ?? ""
print(firstChar)
Swift 5 extension
extension String {
var firstCharactor: String? {
guard self.count > 0 else {
return nil
}
return String(self.prefix(1))
}
}

Calculate range of string from word to end of string in swift

I have an NSMutatableString:
var string: String = "Due in %# (%#) $%#.\nOverdue! Please pay now %#"
attributedText = NSMutableAttributedString(string: string, attributes: attributes)
How to calculate both the length and starting index from the word Overdue in swift?
so far I have tried:
let startIndex = attributedText.string.rangeOfString("Overdue")
let range = startIndex..<attributedText.string.finishIndex
// Access the substring.
let substring = value[range]
print(substring)
But it doesn't work.
You should generate the resulting string first:
let string = String(format: "Due in %# (%#) $%#.\nOverdue! Please pay now %#", "some date", "something", "15", "some date")
Then use .disTanceTo to get the distance between indices;
if let range = string.rangeOfString("Overdue") {
let start = string.startIndex.distanceTo(range.startIndex)
let length = range.startIndex.distanceTo(string.endIndex)
let wordToEndRange = NSRange(location: start, length: length)
// This is the range you need
attributedText.addAttribute(NSForegroundColorAttributeName,
value: UIColor.blueColor(), range: wordToEndRange)
}
Please do note that NSRange does not work correctly if the string contains Emojis or other Unicode characters so that the above solution may not work properly for that case.
Please look at the following SO answers for a better solution which cover that case as well:
https://stackoverflow.com/a/27041376/793428
https://stackoverflow.com/a/27880748/793428
Look at this,
let nsString = element.text as NSString
let range = nsString.range(of: word, options: .widthInsensitive)
let att: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.systemBlue]
attText.addAttributes(att, range: NSRange(location: range.location, length: range.length))