Why does "\n" in a swift string behave differently in slightly different situations? - swift

I have a custom keyboard extension that inputs data from a BLE device into a text field.
func getdata(data:Data){
...
processing data from BLE device
...
dataToSend = "...\n"
textDocumentProxy.insertText(dataToSend)
}
When this function is used to insert text in different applications it behaves differently. For example, in notes, the line feed ("\n") seems to work correctly and insert a new line. But when the data is being inserted in an email or a numbers sheet, it does not work correctly and instead of inserting a new line, it inserts a tab ("\t").
I also have a function that inserts a new line character
func newLine(){
textDocumentProxy.insertText("\n")
}
that works as expected regardless of what application I am using. Does anyone know why "\n" by itself works correctly but when at the end of a string has different behavior?
For completeness, I have tried calling newLine() at the end of getdata() thinking there may be an issue with inserting "\n" at the end of a string but the results were the same.

There are another newLine characters available but I can't just simply paste them here (Because they make a new lines).
using this extension:
extension CharacterSet {
var allCharacters: [Character] {
var result: [Character] = []
for plane: UInt8 in 0...16 where self.hasMember(inPlane: plane) {
for unicode in UInt32(plane) << 16 ..< UInt32(plane + 1) << 16 {
if let uniChar = UnicodeScalar(unicode), self.contains(uniChar) {
result.append(Character(uniChar))
}
}
}
return result
}
}
you can access all characters in any CharacterSet. There is a character set called newlines. Use one of them to fulfill your requirements:
let newlines = CharacterSet.newlines.allCharacters
for newLine in newlines {
textDocumentProxy.insertText(String(newLine))
}
Then store the one you tested and worked everywhere and use it anywhere.
Note that you can't relay on the index of the character set. It may change.

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: Simple method to replace a single character in a String?

I wanted to replace the first character of a String and got it to work like this:
s.replaceSubrange(Range(NSMakeRange(0,1),in:s)!, with:".")
I wonder if there is a simpler method to achieve the same result?
[edit]
Get nth character of a string in Swift programming language doesn't provide a mutable substring. And it requires writing a String extension, which isn't really helping when trying to shorten code.
To replace the first character, you can do use String concatenation with dropFirst():
var s = "πŸ˜ƒhello world!"
s = "." + s.dropFirst()
print(s)
Result:
.hello world!
Note: This will not crash if the String is empty; it will just create a String with the replacement character.
Strings work very differently in Swift than many other languages. In Swift, a character is not a single byte but instead a single visual element. This is very important when working with multibyte characters like emoji (see: Why are emoji characters like πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘¦ treated so strangely in Swift strings?)
If you really do want to set a single random byte of your string to an arbitrary value as you expanded on in the comments of your question, you'll need to drop out of the string abstraction and work with your data as a buffer. This is sort of gross in Swift thanks to various safety features but it's doable:
var input = "Hello, world!"
//access the byte buffer
var utf8Buffer = input.utf8CString
//replace the first byte with whatever random data we want
utf8Buffer[0] = 46 //ascii encoding of '.'
//now convert back to a Swift string
var output:String! = nil //buffer for holding our new target
utf8Buffer.withUnsafeBufferPointer { (ptr) in
//Load the byte buffer into a Swift string
output = String.init(cString: ptr.baseAddress!)
}
print(output!) //.ello, world!

What is the best way to test if a CharacterSet contains a Character in Swift 4?

I'm looking for a way, in Swift 4, to test if a Character is a member of an arbitrary CharacterSet. I have this Scanner class that will be used for some lightweight parsing. One of the functions in the class is to skip any characters, at the current position, that belong to a certain set of possible characters.
class MyScanner {
let str: String
var idx: String.Index
init(_ string: String) {
str = string
idx = str.startIndex
}
var remains: String { return String(str[idx..<str.endIndex])}
func skip(charactersIn characters: CharacterSet) {
while idx < str.endIndex && characters.contains(str[idx])) {
idx = source.index(idx, offsetBy: 1)
}
}
}
let scanner = MyScanner("fizz buzz fizz")
scanner.skip(charactersIn: CharacterSet.alphanumerics)
scanner.skip(charactersIn: CharacterSet.whitespaces)
print("what remains: \"\(scanner.remains)\"")
I would like to implement the skip(charactersIn:) function so that the above code would print buzz fizz.
The tricky part is characters.contains(str[idx])) in the while - .contains() requires a Unicode.Scalar, and I'm at a loss trying to figure out the next step.
I know I could pass in a String to the skip function, but I'd like to find a way to make it work with a CharacterSet, because of all the convenient static members (alphanumerics, whitespaces, etc.).
How does one test a CharacterSet if it contains a Character?
Not sure if it's the most efficient way but you can create a new CharSet and check if they are sub/super-sets (Set comparison is rather quick)
let newSet = CharacterSet(charactersIn: "a")
// let newSet = CharacterSet(charactersIn: "\(character)")
print(newSet.isSubset(of: CharacterSet.decimalDigits)) // false
print(newSet.isSubset(of: CharacterSet.alphanumerics)) // true
Swift 4.2
CharacterSet extension function to check whether it contains Character:
extension CharacterSet {
func containsUnicodeScalars(of character: Character) -> Bool {
return character.unicodeScalars.allSatisfy(contains(_:))
}
}
Usage example:
CharacterSet.decimalDigits.containsUnicodeScalars(of: "3") // true
CharacterSet.decimalDigits.containsUnicodeScalars(of: "a") // false
I know that you wanted to use CharacterSet rather than String, but CharacterSet does not (yet, at least) support characters that are composed of more than one Unicode.Scalar. See the "family" character (πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘¦) or the international flag characters (e.g. "πŸ‡―πŸ‡΅" or "πŸ‡―πŸ‡²") that Apple demonstrated in the string discussion in WWDC 2017 video What's New in Swift. The multiple skin tone emoji also manifest this behavior (e.g. πŸ‘©πŸ» vs πŸ‘©πŸ½).
As a result, I'd be wary of using CharacterSet (which is a "set of Unicode character values for use in search operations"). Or, if you want to provide this method for the sake of convenience, be aware that it will not work correctly with characters represented by multiple unicode scalars.
So, you might offer a scanner that provides both CharacterSet and String renditions of the skip method:
class MyScanner {
let string: String
var index: String.Index
init(_ string: String) {
self.string = string
index = string.startIndex
}
var remains: String { return String(string[index...]) }
/// Skip characters in a string
///
/// This rendition is safe to use with strings that have characters
/// represented by more than one unicode scalar.
///
/// - Parameter skipString: A string with all of the characters to skip.
func skip(charactersIn skipString: String) {
while index < string.endIndex, skipString.contains(string[index]) {
index = string.index(index, offsetBy: 1)
}
}
/// Skip characters in character set
///
/// Note, character sets cannot (yet) include characters that are represented by
/// more than one unicode scalar (e.g. πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘¦ or πŸ‡―πŸ‡΅ or πŸ‘°πŸ»). If you want to test
/// for these multi-unicode characters, you have to use the `String` rendition of
/// this method.
///
/// This will simply stop scanning if it encounters a multi-unicode character in
/// the string being scanned (because it knows the `CharacterSet` can only represent
/// single-unicode characters) and you want to avoid false positives (e.g., mistaking
/// the Jamaican flag, πŸ‡―πŸ‡², for the Japanese flag, πŸ‡―πŸ‡΅).
///
/// - Parameter characterSet: The character set to check for membership.
func skip(charactersIn characterSet: CharacterSet) {
while index < string.endIndex,
string[index].unicodeScalars.count == 1,
let character = string[index].unicodeScalars.first,
characterSet.contains(character) {
index = string.index(index, offsetBy: 1)
}
}
}
Thus, your simple example will still work:
let scanner = MyScanner("fizz buzz fizz")
scanner.skip(charactersIn: CharacterSet.alphanumerics)
scanner.skip(charactersIn: CharacterSet.whitespaces)
print(scanner.remains) // "buzz fizz"
But use the String rendition if the characters you want to skip might include multiple unicode scalars:
let family = "πŸ‘©\u{200D}πŸ‘©\u{200D}πŸ‘§\u{200D}πŸ‘¦" // πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘¦
let boy = "πŸ‘¦"
let charactersToSkip = family + boy
let string = boy + family + "foobar" // πŸ‘¦πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘¦foobar
let scanner = MyScanner(string)
scanner.skip(charactersIn: charactersToSkip)
print(scanner.remains) // foobar
As Michael Waterfall noted in the comments below, CharacterSet has a bug and doesn’t even handle 32-bit Unicode.Scalar values correctly, meaning that it doesn’t even handle single scalar characters properly if the value exceeds 0xffff (including emoji, amongst others). The String rendition, above, handles these correctly, though.