How can I capitalize one word in a UILabel in swift? - swift

I am trying to make a dynamic UILabel like this:
"I Love Watching The BBC"
As the label is dynamic, I will have no idea of its contents.
I can leave the text alone and let the user define what they want. I can capitalize the whole string, the first letter of each word, or all words in a string.
The problem is that when capitalizing, words that are entered as uppercase become lower case.
So, in the example above
BBC
becomes
Bbc
I've searched all over the web, and don't think there is a way to do this.
As requested, code so far:
cell.projectName?.text = projectNameArray[indexPath.row].localizedCapitalized

I suggest you to add this extension to your code.
extension String {
func capitalizingFirstLetter() -> String {
return prefix(1).capitalized + dropFirst()
}
mutating func capitalizeFirstLetter() {
self = self.capitalizingFirstLetter()
}
}
(Form Hacking With Swift)
Also for the UILabel.text property apply a custom function that split each word in string, and apply the capital letter.
An example could be that:
extension String {
func capitalizeWords() -> String {
self.split(separator: " ")
.map({ String($0).capitalizingFirstLetter() })
.joined(separator: " ")
}
}
The complexity could be decreased I think, but it's just a working hint ;)

Related

Limit text to a certain number of words in Swift

In a mobile App I use an API that can only handle about 300 words. How can I trimm a string in Swift so that it doesn't contain more words?
The native .trimmingCharacters(in: CharacterSet) does not seem to be able to do this as it is intended to trimm certain characters.
There is no off-the shelf way to limit the number of words in a string.
If you look at this post, it documents using the method enumerateSubstrings(in: Range) and setting an option of .byWords. It looks like it returns an array of Range values.
You could use that to create an extension on String that would return the first X words of that string:
extension String {
func firstXWords(_ wordCount: Int) -> Substring {
var ranges: [Range<String.Index>] = []
self.enumerateSubstrings(in: self.startIndex..., options: .byWords) { _, range, _, _ in
ranges.append(range)
}
if ranges.count > wordCount - 1 {
return self[self.startIndex..<ranges[wordCount - 1].upperBound]
} else {
return self[self.startIndex..<self.endIndex]
}
}
}
If we then run the code:
let sentence = "I want to an algorithm that could help find out how many words are there in a string separated by space or comma or some character. And then append each word separated by a character to an array which could be added up later I'm making an average calculator so I want the total count of data and then add up all the words. By words I mean the numbers separated by a character, preferably space Thanks in advance"
print(sentence.firstXWords(10))
The output is:
I want to an algorithm that could help find out
Using enumerateSubstrings(in: Range) is going to give much better results than splitting your string using spaces, since there are a lot more separators than just spaces in normal text (newlines, commas, colons, em spaces, etc.) It will also work for languages like Japanese and Chinese that often don't have spaces between words.
You might be able to rewrite the function to terminate the enumeration of the string as soon as it reaches the desired number of words. If you want a small percentage of the words in a very long string that would make it significantly faster (the code above should have O(n) performance, although I haven't dug deeply enough to be sure of that. I also couldn't figure out how to terminate the enumerateSubstrings() function early, although I didn't try that hard.)
Leo Dabus provided an improved version of my function. It extends StringProtocol rather than String, which means it can work on substrings. Plus, it stops once it hits your desired word count, so it will be much faster for finding the first few words of very long strings:
extension StringProtocol {
func firstXWords(_ n: Int) -> SubSequence {
var endIndex = self.endIndex
var words = 0
enumerateSubstrings(in: startIndex..., options: .byWords) { _, range, _, stop in
words += 1
if words == n {
stop = true
endIndex = range.upperBound
}
}
return self[..<endIndex] }
}

Build Recursive Text View in SwiftUI

My goal is to create a SwiftUI view that takes a String and automatically formats that text into Text views. The portion of the string that needs formatting is found using regex and then returned as a Range<String.Index>. This can be used to reconstruct the String once the formatting has been applied to the appropriate Text views. Since there could be multiple instances of text that needs to be formatted, running the formatting function should be done recursively.
struct AttributedText: View {
#State var text: String
var body: some View {
AttributedTextView(text: text)
}
#ViewBuilder
private func AttributedTextView(text: String) -> some View {
if let range = text.range(of: "[0-9]+d[0-9]+", options: .regularExpression) {
//The unattributed text
Text(text[text.startIndex..<range.lowerBound]) +
//Append the attributed text
Text(text[range]).bold() +
//Search for additional instances of text that needs attribution
AttributedTextView(text: String(text[range.upperBound..<text.endIndex]))
} else {
//If the searched text is not found, add the rest of the string to the end
Text(text)
}
}
I get an error Cannot convert value of type 'some View' to expected argument type 'Text', with the recommended fix being to update the recursive line to AttributedTextView(text: String(text[range.upperBound..<text.endIndex])) as! Text. I apply this fix, but still see the same compiler error with the same suggested fix.
A few workarounds that I've tried:
Changing the return type from some View to Text. This creates a different error Cannot convert value of type '_ConditionalContent<Text, Text>' to specified type 'Text'. I didn't really explore this further, as it does make sense that the return value is reliant on that conditional.
Returning a Group rather than a Text, which causes additional errors throughout the SwiftUI file
Neither of these solutions feel very "Swifty". What is another way to go about this? Am I misunderstanding something in SwiftUI?
There are a few things to clarify here:
The + overload of Text only works between Texts which is why it's saying it cannot convert some View (your return type) to Text. Text + Text == Text, Text + some View == ☠️
Changing the return type to Text doesn't work for you because you're using #ViewBuilder, remove #ViewBuilder and it'll work fine.
Why? #ViewBuilder allows SwiftUI to defer evaluation of the closure until later but ensures it'll result in a specific view type (not AnyView). In the case where your closure returns either a Text or an Image this is handy but in your case where it always results in Text there's no need, #ViewBuilder forces the return type to be ConditionalContent<Text, Text> so that it could have different types.
Here's what should work:
private static func attributedTextView(text: String) -> Text {
if let range = text.range(of: "[0-9]+d[0-9]+", options: .regularExpression) {
//The unattributed text
return Text(text[text.startIndex..<range.lowerBound]) +
//Append the attributed text
Text(text[range]).bold() +
//Search for additional instances of text that needs attribution
AttributedTextView(text: String(text[range.upperBound..<text.endIndex]))
} else {
//If the searched text is not found, add the rest of the string to the end
return Text(text)
}
}
I made it static too because there's no state here it's a pure function and lowercased it so it was clear it was a function not a type (the function name looks like a View type).
You'd just call it Self.attributedTextView(text: ...)

Is there a faster method to find words beginning with string inside a string?

I have a field called keywords on Core Data that stores keywords separated by spaces, like:
car nascar race daytona crash
I have a list populated by core data. Every element on that list has keywords.
I have a search field on that view.
I want that list to be filtered as the user types.
If the user types c the app will check elements that have keywords beginning with c. In that case, the element mentioned above will be shown because it has car and crash, both beginning with c.
In order to check that, I created this extension
extension String {
func containsWordStartingWith(insensitive searchWord: String) -> Bool {
let lowercaseSelf = self.lowercased().trimmingCharacters(in: .whitespaces)
let lowercaseSearch = searchWord.lowercased().trimmingCharacters(in: .whitespaces)
let array = lowercaseSelf.components(separatedBy: " ")
return array.contains(where: {$0.hasPrefix(lowercaseSearch)})
}
}
This works but is slow as hell and typing characters on the search bar makes the app stall.
How can I improve that with something faster?
First thing I would do is split the single keywords string into a Set of actual keywords. If possible you should even store it in Core Data that way, so there's no need for a split step.
let keywords = "car nascar race daytona crash"
let keywordSet = Set(keywords.split(separator: " "))
Now the utility method you want is trivial and fast:
func keywordSet(_ set : Set<Substring>, containsWordStartingWith s: Substring) -> Bool {
for keyword in set {
if keyword.hasPrefix(s) { return true }
}
return false
}
Testing:
keywordSet(keywordSet, containsWordStartingWith:"c")

Swift 3 String has no member components [duplicate]

So I'm trying to prepare myself for coding interviews by doing HackerRank's test case samples. If you're familiar with the process, you usually take a standard input that has various lines of strings and you extract the information based on what the question is asking. I have come across numerous questions where they will give you a line (as a String) with n number of integers separated by a space (i.e. 1 2 3 4 5). In order to solve the problem I need to extrapolate an array of Int ([Int]) from a String. I came up with this nifty method:
func extractIntegers(_ s: String) -> [Int] {
let splits = s.characters.split { [" "].contains(String($0)) }
return splits.map { Int(String($0).trimmingCharacters(in: .whitespaces))! }
}
So I code it in my Playground and it works fantastic, I even run multiple test cases I make up, and they all pass with flying colors...then I copy the code to HackerRank and try running it for submission. And I get this:
solution.swift:16:29: error: value of type 'String' has no member 'trimmingCharacters'
return splits.map { Int(String($0).trimmingCharacters(in: .whitespaces))! }
So... okay maybe HR hasn't updated everything for Swift 3 yet. No big deal! I have an idea for an even cleaner solution! Here it is:
func extractIntegers(_ s: String) -> [Int] {
return s.components(separatedBy: " ").map { Int($0)! }
}
....AAAAANDDD of course:
solution.swift:15:12: error: value of type 'String' has no member 'components'
return s.components(separatedBy: " ").map { Int($0)! }
So now I'm forced to use a really sloppy method where I loop through all the characters, check for spaces, append substrings from ranges between spaces into an array, and then map that array and return it.
Does anyone have any other clean ideas to work around HR's inadequacies with Swift? I would like any recommendations I can get!
Thanks in advance!
The String methods
func trimmingCharacters(in set: CharacterSet) -> String
func components(separatedBy separator: String) -> [String]
are actually methods of the NSString class, defined in the Foundation
framework, and "bridged" to Swift. Therefore, to make your code compile,
you have go add
import Foundation
But a slightly simplified version of your first method compiles
with pure Swift, without importing Foundation. I handles leading, trailing, and intermediate whitespace:
func extractIntegers(_ s: String) -> [Int] {
let splits = s.characters.split(separator: " ").map(String.init)
return splits.map { Int($0)! }
}
let a = extractIntegers(" 12 234 -567 4 ")
print(a) // [12, 234, -567, 4]
Update for Swift 4 (and simplified):
func extractIntegers(_ s: String) -> [Int] {
return s.split(separator: " ").compactMap { Int($0) }
}

Is there a built-in Swift function to pad Strings at the beginning?

The String function padding(toLength:withPad:startingAt:) will pad strings by adding padding characters on the end to "fill out" the string to the desired length.
Is there an equivalent function that will pad strings by prepending padding characters at the beginning?
This would be useful if you want to right-justify a substring in a monospaced output string, for example.
I could certainly write one, but I would expect there to be a built-in function, seeing as how there is already a function that pads at the end.
You can do this by reversing the string, padding at the end, end then reversing again…
let string = "abc"
// Pad at end
string.padding(toLength: 7, withPad: "X", startingAt: 0)
// "abcXXXX"
// Pad at start
String(String(string.reversed()).padding(toLength: 7, withPad: "X", startingAt: 0).reversed())
// "XXXXabc"
Since Swift string manipulations are a rehash of the old NSString class, I suppose Apple never bothered to complete the feature set and just gave us toll free bridging as mana from the gods.
Or, since Objective-C never shied away from super verbose yet cryptic code, they expect us to use the native function twice :
let a = "42"
"".padding(toLength:10, withPad:a.padding(toLength:10, withPad:"0", startingAt:0), startingAt:a.characters.count)
// 0000000042
.
[EDIT] Objective-C ranting aside, the solution is a bit more subtle than that and adding some more useful padding methods to the String type is probably going to make things easier to use and maintain:
For example:
extension String
{
func padding(leftTo paddedLength:Int, withPad pad:String=" ", startingAt padStart:Int=0) -> String
{
let rightPadded = self.padding(toLength:max(count,paddedLength), withPad:pad, startingAt:padStart)
return "".padding(toLength:paddedLength, withPad:rightPadded, startingAt:count % paddedLength)
}
func padding(rightTo paddedLength:Int, withPad pad:String=" ", startingAt padStart:Int=0) -> String
{
return self.padding(toLength:paddedLength, withPad:pad, startingAt:padStart)
}
func padding(sidesTo paddedLength:Int, withPad pad:String=" ", startingAt padStart:Int=0) -> String
{
let rightPadded = self.padding(toLength:max(count,paddedLength), withPad:pad, startingAt:padStart)
return "".padding(toLength:paddedLength, withPad:rightPadded, startingAt:(paddedLength+count)/2 % paddedLength)
}
}