How to Convert Range in Nsrange

How to convert Range in NSRange?

Xcode 11 • Swift 5.1

import Foundation

extension RangeExpression where Bound == String.Index {
func nsRange<S: StringProtocol>(in string: S) -> NSRange { .init(self, in: string) }
}

let string = "Hello USA !!! Hello World !!!"
if let nsRange = string.range(of: "Hello World")?.nsRange(in: string) {
(string as NSString).substring(with: nsRange) // "Hello World"
}

You can also create the corresponding nsRange(of:) method extending StringProtocol:

extension StringProtocol {
func nsRange<S: StringProtocol>(of string: S, options: String.CompareOptions = [], range: Range<Index>? = nil, locale: Locale? = nil) -> NSRange? {
self.range(of: string,
options: options,
range: range ?? startIndex..<endIndex,
locale: locale ?? .current)?
.nsRange(in: self)
}
func nsRanges<S: StringProtocol>(of string: S, options: String.CompareOptions = [], range: Range<Index>? = nil, locale: Locale? = nil) -> [NSRange] {
var start = range?.lowerBound ?? startIndex
let end = range?.upperBound ?? endIndex
var ranges: [NSRange] = []
while start < end,
let range = self.range(of: string,
options: options,
range: start..<end,
locale: locale ?? .current) {
ranges.append(range.nsRange(in: self))
start = range.lowerBound < range.upperBound ? range.upperBound :
index(range.lowerBound, offsetBy: 1, limitedBy: endIndex) ?? endIndex
}
return ranges
}
}

let string = "Hello USA !!! Hello World !!!"

if let nsRange = string.nsRange(of: "Hello World") {
(string as NSString).substring(with: nsRange) // "Hello World"
}
let nsRanges = string.nsRanges(of: "Hello")
print(nsRanges) // "[{0, 5}, {19, 5}]\n"

NSRange from Swift Range?

Swift String ranges and NSString ranges are not "compatible".
For example, an emoji like counts as one Swift character, but as two NSString
characters (a so-called UTF-16 surrogate pair).

Therefore your suggested solution will produce unexpected results if the string
contains such characters. Example:

let text = "Long paragraph saying!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
let start = distance(text.startIndex, substringRange.startIndex)
let length = distance(substringRange.startIndex, substringRange.endIndex)
let range = NSMakeRange(start, length)

if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: range)
}
})
println(attributedString)

Output:


Long paragra{
}ph say{
NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}ing!{
}

As you see, "ph say" has been marked with the attribute, not "saying".

Since NS(Mutable)AttributedString ultimately requires an NSString and an NSRange, it is actually
better to convert the given string to NSString first. Then the substringRange
is an NSRange and you don't have to convert the ranges anymore:

let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: nsText)

nsText.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in

if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
}
})
println(attributedString)

Output:


Long paragraph {
}saying{
NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}!{
}

Update for Swift 2:

let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)

nsText.enumerateSubstringsInRange(textRange, options: .ByWords, usingBlock: {
(substring, substringRange, _, _) in

if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
}
})
print(attributedString)

Update for Swift 3:

let text = "Long paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)

nsText.enumerateSubstrings(in: textRange, options: .byWords, using: {
(substring, substringRange, _, _) in

if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.red, range: substringRange)
}
})
print(attributedString)

Update for Swift 4:

As of Swift 4 (Xcode 9), the Swift standard library
provides method to convert between Range<String.Index> and NSRange.
Converting to NSString is no longer necessary:

let text = "Long paragraph saying!"
let attributedString = NSMutableAttributedString(string: text)

text.enumerateSubstrings(in: text.startIndex..<text.endIndex, options: .byWords) {
(substring, substringRange, _, _) in
if substring == "saying" {
attributedString.addAttribute(.foregroundColor, value: NSColor.red,
range: NSRange(substringRange, in: text))
}
}
print(attributedString)

Here substringRange is a Range<String.Index>, and that is converted to the
corresponding NSRange with

NSRange(substringRange, in: text)

NSRange to Range String.Index

The NSString version (as opposed to Swift String) of replacingCharacters(in: NSRange, with: NSString) accepts an NSRange, so one simple solution is to convert String to NSString first. The delegate and replacement method names are slightly different in Swift 3 and 2, so depending on which Swift you're using:

Swift 3.0

func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {

let nsString = textField.text as NSString?
let newString = nsString?.replacingCharacters(in: range, with: string)
}

Swift 2.x

func textField(textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool {

let nsString = textField.text as NSString?
let newString = nsString?.stringByReplacingCharactersInRange(range, withString: string)
}

How to generate NSRange correctly for a given String to represent end of String?

There are many ways. I recommend making a Swift Range<String.Index>, and convert it to an NSRange:

NSRange(string.endIndex..., in: string)

See also: NSRange from Swift Range?

The reason why the incorrect behaviour happens, is because the location property in NSRange counts a different thing (UTF-16 code units) than what the Swift String.count property counts (the number of Swift Characters, which are extended grapheme clusters, in the string). Therefore, to fix this, we can also just pass the correct number to NSRange(location:length:):

NSRange(location: string.utf16.count, length: 0)
// or
NSRange(location: (string as NSString).length, length: 0)

But I find these less intuitive than just converting the Swift range to NSRange.

Convert String.Index to Int or Range String.Index to NSRange

If you look into the definition of String.Index you find:

struct Index : BidirectionalIndexType, Comparable, Reflectable {

/// Returns the next consecutive value after `self`.
///
/// Requires: the next value is representable.
func successor() -> String.Index

/// Returns the previous consecutive value before `self`.
///
/// Requires: the previous value is representable.
func predecessor() -> String.Index

/// Returns a mirror that reflects `self`.
func getMirror() -> MirrorType
}

So actually there is no way to convert it to Int and that for good reason. Depending on the encoding of the string the single characters occupy a different number of bytes. The only way would be to count how many successor operations are needed to reach the desired String.Index.

Edit The definition of String has changed over the various Swift versions but it's basically the same answer. To see the very current definition just CMD-click on a String definition in XCode to get to the root (works for other types as well).

The distanceTo is an extension which goes to a variety of protocols. Just look for it in the String source after the CMD-click.

Convert Range Int to Range String.Index

The Swift String method rangeOfString() returns an optional Range? which
does not have a location property but can be checked with conditional binding (if let).

And if you replace the NSString method stringByReplacingCharactersInRange()
by the Swift String method replaceRange() (or in this case
simply by removeRange()) then you can work purely with Range<Swift.Index>
without converting it to NSRange or Range<Int>.

func removeHTMLTags(source : String) -> String {

var sourceString = source
let HTMLTags = "<[^>]*>"

while let range = sourceString.rangeOfString(HTMLTags, options: .RegularExpressionSearch) {
sourceString.removeRange(range)
}

return sourceString;
}


Related Topics



Leave a reply



Submit