I have a UITextView and I want to line break each time a user is extending a limit of chars per line (let's say 30 chars per line is the maximum). And I want to save the word wrapping too so if a 30 limit is reached in the middle of a word, it should just go straight to the new line.
How should I approach this problem? I was hoping for a native solution but can't find anything related in the documentation.
You can use this workaround by using textViewDidChange delegate method from UITextViewDelegate to add newline after every 30 characters, like this
func textViewDidChange(_ textView: UITextView) {
if let text = textView.text {
let strings = string.components(withMaxLength: 30) // generating an array of strings with equally split parts
var newString = ""
for string in strings {
newString += "\(string)\n" //joining all the strings back with newline
}
textView.text = String(newString.dropLast(2)) //dropping the new line sequence at the end
}
}
You will need this extension to split String in equal parts for above code to work though:
extension String {
func components(withMaxLength length: Int) -> [String] {
return stride(from: 0, to: self.count, by: length).map {
let start = self.index(self.startIndex, offsetBy: $0)
let end = self.index(start, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
return String(self[start..<end])
}
}
}
Related
I would like to hide sensitive user data in strings by replacing a certain subrange with asterisks. For instance, replace all characters except the first and last three, turning
"sensitive info"
into
"sen********nfo".
I have tried this:
func hideInfo(sensitiveInfo: String) -> String {
let fIndex = sensitiveInfo.index(sensitiveInfo.endIndex, offsetBy: -4)
let sIndex = sensitiveInfo.index(sensitiveInfo.startIndex, offsetBy: 3)
var hiddenInfo = sensitiveInfo
let hiddenSubstring = String(repeating: "*", count: hiddenInfo.count - 6)
hiddenInfo.replaceSubrange(sIndex...fIndex, with: hiddenSubstring)
return hiddenInfo
}
and it works. But it seems overcomplicated. Is there a simpler and/or more elegant way of achieving this?
How about building the string with the first three characters (prefix(3)) the created asterisk substring and the last three characters (suffix(3))
func hideInfo(sensitiveInfo: String) -> String {
let length = sensitiveInfo.utf8.count
if length <= 6 { return sensitiveInfo }
return String(sensitiveInfo.prefix(3) + String(repeating: "*", count: length - 6) + sensitiveInfo.suffix(3))
}
I would like to change the formatting of the first line of text in an NSTextView (give it a different font size and weight to make it look like a headline). Therefore, I need the range of the first line. One way to go is this:
guard let firstLineString = textView.string.components(separatedBy: .newlines).first else {
return
}
let range = NSRange(location: 0, length: firstLineString.count)
However, I might be working with quite long texts so it appears to be inefficient to first split the entire string into line components when all I need is the first line component. Thus, it seems to make sense to use the firstIndex(where:) method:
let firstNewLineIndex = textView.string.firstIndex { character -> Bool in
return CharacterSet.newlines.contains(character)
}
// Then: Create an NSRange from 0 up to firstNewLineIndex.
This doesn't work and I get an error:
Cannot convert value of type '(Unicode.Scalar) -> Bool' to expected argument type 'Character'
because the contains method accepts not a Character but a Unicode.Scalar as a parameter (which doesn't really make sense to me because then it should be called a UnicodeScalarSet and not a CharacterSet, but nevermind...).
My question is:
How can I implement this in an efficient way, without first slicing the whole string?
(It doesn't necessarily have to use the firstIndex(where:) method, but appears to be the way to go.)
A String.Index range for the first line in string can be obtained with
let range = string.lineRange(for: ..<string.startIndex)
If you need that as an NSRange then
let nsRange = NSRange(range, in: string)
does the trick.
You can use rangeOfCharacter, which returns the Range<String.Index> of the first character from a set in your string:
extension StringProtocol where Index == String.Index {
var partialRangeOfFirstLine: PartialRangeUpTo<String.Index> {
return ..<(rangeOfCharacter(from: .newlines)?.lowerBound ?? endIndex)
}
var rangeOfFirstLine: Range<Index> {
return startIndex..<partialRangeOfFirstLine.upperBound
}
var firstLine: SubSequence {
return self[partialRangeOfFirstLine]
}
}
You can use it like so:
var str = """
some string
with new lines
"""
var attributedString = NSMutableAttributedString(string: str)
let firstLine = NSAttributedString(string: String(str.firstLine))
// change firstLine as you wish
let range = NSRange(str.rangeOfFirstLine, in: str)
attributedString.replaceCharacters(in: range, with: firstLine)
I'm trying to split the text of a string into lines no longer than 72 characters (to break lines to the usual Usenet quoting line length). The division should be done by replacing a space with a new line (choosing the closest space so that every line is <= 72 characters). [edited]
The text is present in a string and could also contain emoji or other symbols.
I have tried different approaches but the fact that I can not separate a word but I must necessarily separate the text where there is a space has not allowed me to find a solution for now.
Does anyone know how this result can be obtained in Swift? Also with Regular expressions if needed. [edited]
In other languages you can index a string with an integer. Not so in Swift: you must interact with its character index, which can be a pain in the neck if you are not familiar with it.
Try this:
private func split(line: Substring, byCount n: Int, breakableCharacters: [Character]) -> String {
var line = String(line)
var lineStartIndex = line.startIndex
while line.distance(from: lineStartIndex, to: line.endIndex) > n {
let maxLineEndIndex = line.index(lineStartIndex, offsetBy: n)
if breakableCharacters.contains(line[maxLineEndIndex]) {
// If line terminates at a breakable character, replace that character with a newline
line.replaceSubrange(maxLineEndIndex...maxLineEndIndex, with: "\n")
lineStartIndex = line.index(after: maxLineEndIndex)
} else if let index = line[lineStartIndex..<maxLineEndIndex].lastIndex(where: { breakableCharacters.contains($0) }) {
// Otherwise, find a breakable character that is between lineStartIndex and maxLineEndIndex
line.replaceSubrange(index...index, with: "\n")
lineStartIndex = index
} else {
// Finally, forcible break a word
line.insert("\n", at: maxLineEndIndex)
lineStartIndex = maxLineEndIndex
}
}
return line
}
func split(string: String, byCount n: Int, breakableCharacters: [Character] = [" "]) -> String {
precondition(n > 0)
guard !string.isEmpty && string.count > n else { return string }
var string = string
var startIndex = string.startIndex
repeat {
// Break a string into lines.
var endIndex = string[string.index(after: startIndex)...].firstIndex(of: "\n") ?? string.endIndex
if string.distance(from: startIndex, to: endIndex) > n {
let wrappedLine = split(line: string[startIndex..<endIndex], byCount: n, breakableCharacters: breakableCharacters)
string.replaceSubrange(startIndex..<endIndex, with: wrappedLine)
endIndex = string.index(startIndex, offsetBy: wrappedLine.count)
}
startIndex = endIndex
} while startIndex < string.endIndex
return string
}
let str1 = "Iragvzvyn vzzntvav chooyvpngr fh Vafgntenz r pv fbab gnagvffvzv nygev unfugnt, qv zvabe fhpprffb, pur nttertnab vzzntvav pba y’vzznapnovyr zntyvrggn"
let str2 = split(string: str1, byCount: 72)
print(str2)
Edit: this turns out to be more complicated than I thought. The updated answer improves upon the original by processing the text line by line. You may ask why I devise my own algorithm to break lines instead of components(separatedBy: "\n"). The reason is to preserve blank lines. components(...) will collapse consecutive blank lines into one.
What's the best way to go about removing the first six characters of a string? Through Stack Overflow, I've found a couple of ways that were supposed to be solutions but I noticed an error with them. For instance,
extension String {
func removing(charactersOf string: String) -> String {
let characterSet = CharacterSet(charactersIn: string)
let components = self.components(separatedBy: characterSet)
return components.joined(separator: "")
}
If I type in a website like https://www.example.com, and store it as a variable named website, then type in the following
website.removing(charactersOf: "https://")
it removes the https:// portion but it also removes all h's, all t's, :'s, etc. from the text.
How can I just delete the first characters?
In Swift 4 it is really simple, just use dropFirst(n: Int)
let myString = "Hello World"
myString.dropFirst(6)
//World
In your case: website.dropFirst(6)
Why not :
let stripped = String(website.characters.dropFirst(6))
Seems more concise and straightforward to me.
(it won't work with multi-char emojis either mind you)
[EDIT] Swift 4 made this even shorter:
let stripped = String(website.dropFirst(6))
length is the number of characters you want to remove (6 in your case)
extension String {
func toLengthOf(length:Int) -> String {
if length <= 0 {
return self
} else if let to = self.index(self.startIndex, offsetBy: length, limitedBy: self.endIndex) {
return self.substring(from: to)
} else {
return ""
}
}
}
It will remove first 6 characters from a string
var str = "Hello-World"
let range1 = str.characters.index(str.startIndex, offsetBy: 6)..<str.endIndex
str = str[range1]
print("the end time is : \(str)")
I've been updating some of my old code and answers with Swift 3 but when I got to Swift Strings and Indexing it has been a pain to understand things.
Specifically I was trying the following:
let str = "Hello, playground"
let prefixRange = str.startIndex..<str.startIndex.advancedBy(5) // error
where the second line was giving me the following error
'advancedBy' is unavailable: To advance an index by n steps call 'index(_:offsetBy:)' on the CharacterView instance that produced the index.
I see that String has the following methods.
str.index(after: String.Index)
str.index(before: String.Index)
str.index(String.Index, offsetBy: String.IndexDistance)
str.index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)
These were really confusing me at first so I started playing around with them until I understood them. I am adding an answer below to show how they are used.
All of the following examples use
var str = "Hello, playground"
startIndex and endIndex
startIndex is the index of the first character
endIndex is the index after the last character.
Example
// character
str[str.startIndex] // H
str[str.endIndex] // error: after last character
// range
let range = str.startIndex..<str.endIndex
str[range] // "Hello, playground"
With Swift 4's one-sided ranges, the range can be simplified to one of the following forms.
let range = str.startIndex...
let range = ..<str.endIndex
I will use the full form in the follow examples for the sake of clarity, but for the sake of readability, you will probably want to use the one-sided ranges in your code.
after
As in: index(after: String.Index)
after refers to the index of the character directly after the given index.
Examples
// character
let index = str.index(after: str.startIndex)
str[index] // "e"
// range
let range = str.index(after: str.startIndex)..<str.endIndex
str[range] // "ello, playground"
before
As in: index(before: String.Index)
before refers to the index of the character directly before the given index.
Examples
// character
let index = str.index(before: str.endIndex)
str[index] // d
// range
let range = str.startIndex..<str.index(before: str.endIndex)
str[range] // Hello, playgroun
offsetBy
As in: index(String.Index, offsetBy: String.IndexDistance)
The offsetBy value can be positive or negative and starts from the given index. Although it is of the type String.IndexDistance, you can give it an Int.
Examples
// character
let index = str.index(str.startIndex, offsetBy: 7)
str[index] // p
// range
let start = str.index(str.startIndex, offsetBy: 7)
let end = str.index(str.endIndex, offsetBy: -6)
let range = start..<end
str[range] // play
limitedBy
As in: index(String.Index, offsetBy: String.IndexDistance, limitedBy: String.Index)
The limitedBy is useful for making sure that the offset does not cause the index to go out of bounds. It is a bounding index. Since it is possible for the offset to exceed the limit, this method returns an Optional. It returns nil if the index is out of bounds.
Example
// character
if let index = str.index(str.startIndex, offsetBy: 7, limitedBy: str.endIndex) {
str[index] // p
}
If the offset had been 77 instead of 7, then the if statement would have been skipped.
Why is String.Index needed?
It would be much easier to use an Int index for Strings. The reason that you have to create a new String.Index for every String is that Characters in Swift are not all the same length under the hood. A single Swift Character might be composed of one, two, or even more Unicode code points. Thus each unique String must calculate the indexes of its Characters.
It is possible to hide this complexity behind an Int index extension, but I am reluctant to do so. It is good to be reminded of what is actually happening.
I appreciate this question and all the info with it. I have something in mind that's kind of a question and an answer when it comes to String.Index.
I'm trying to see if there is an O(1) way to access a Substring (or Character) inside a String because string.index(startIndex, offsetBy: 1) is O(n) speed if you look at the definition of index function. Of course we can do something like:
let characterArray = Array(string)
then access any position in the characterArray however SPACE complexity of this is n = length of string, O(n) so it's kind of a waste of space.
I was looking at Swift.String documentation in Xcode and there is a frozen public struct called Index. We can initialize is as:
let index = String.Index(encodedOffset: 0)
Then simply access or print any index in our String object as such:
print(string[index])
Note: be careful not to go out of bounds`
This works and that's great but what is the run-time and space complexity of doing it this way? Is it any better?
func change(string: inout String) {
var character: Character = .normal
enum Character {
case space
case newLine
case normal
}
for i in stride(from: string.count - 1, through: 0, by: -1) {
// first get index
let index: String.Index?
if i != 0 {
index = string.index(after: string.index(string.startIndex, offsetBy: i - 1))
} else {
index = string.startIndex
}
if string[index!] == "\n" {
if character != .normal {
if character == .newLine {
string.remove(at: index!)
} else if character == .space {
let number = string.index(after: string.index(string.startIndex, offsetBy: i))
if string[number] == " " {
string.remove(at: number)
}
character = .newLine
}
} else {
character = .newLine
}
} else if string[index!] == " " {
if character != .normal {
string.remove(at: index!)
} else {
character = .space
}
} else {
character = .normal
}
}
// startIndex
guard string.count > 0 else { return }
if string[string.startIndex] == "\n" || string[string.startIndex] == " " {
string.remove(at: string.startIndex)
}
// endIndex - here is a little more complicated!
guard string.count > 0 else { return }
let index = string.index(before: string.endIndex)
if string[index] == "\n" || string[index] == " " {
string.remove(at: index)
}
}
Create a UITextView inside of a tableViewController. I used function: textViewDidChange and then checked for return-key-input.
then if it detected return-key-input, delete the input of return key and dismiss keyboard.
func textViewDidChange(_ textView: UITextView) {
tableView.beginUpdates()
if textView.text.contains("\n"){
textView.text.remove(at: textView.text.index(before: textView.text.endIndex))
textView.resignFirstResponder()
}
tableView.endUpdates()
}