SwiftでNSRange
をRange<String.Index>
に変換する方法を教えてください。
私は次のUITextFieldDelegate
メソッドを使いたいです。
func textField(textField: UITextField!,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String!) -> Bool {
textField.text.stringByReplacingCharactersInRange(???, withString: string)
(Swift Stringとは対照的に)replacingCharacters(in: NSRange, with: NSString)
のNSString
バージョンはNSRange
を受け入れます。したがって、簡単な解決策の1つは、最初にString
をNSString
に変換することです。 Swift 3と2では、デリゲートメソッドと置換メソッドの名前が少し異なります。したがって、使用しているSwiftによって異なります。
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)
}
func textField(textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String) -> Bool {
let nsString = textField.text as NSString?
let newString = nsString?.stringByReplacingCharactersInRange(range, withString: string)
}
Swift 4(Xcode 9)以降、Swift標準ライブラリには、Swift文字列範囲(Range<String.Index>
)とNSString
範囲(NSRange
)の間で変換を行うためのメソッドが用意されています。例:
let str = "a????b????????c"
let r1 = str.range(of: "????????")!
// String range to NSRange:
let n1 = NSRange(r1, in: str)
print((str as NSString).substring(with: n1)) // ????????
// NSRange back to String range:
let r2 = Range(n1, in: str)!
print(str[r2]) // ????????
したがって、テキストフィールドデリゲートメソッドでのテキスト置換は、次のようになります。
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
if let oldString = textField.text {
let newString = oldString.replacingCharacters(in: Range(range, in: oldString)!,
with: string)
// ...
}
// ...
}
(Swift 3以前の古い答え:)
Swift 1.2では、String.Index
に初期化子があります。
init?(_ utf16Index: UTF16Index, within characters: String)
これは、NSRange
への中間変換なしで、NSString
をRange<String.Index>
に正しく変換するために使用できます(Emojis、Regional Indicators、またはその他の拡張書記素クラスタのすべてのケースを含みます)。
extension String {
func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
let from16 = advance(utf16.startIndex, nsRange.location, utf16.endIndex)
let to16 = advance(from16, nsRange.length, utf16.endIndex)
if let from = String.Index(from16, within: self),
let to = String.Index(to16, within: self) {
return from ..< to
}
return nil
}
}
このメソッドは、オプションの文字列範囲を返します。これは、すべてのNSRange
が特定のSwift文字列に対して有効であるとは限らないためです。
UITextFieldDelegate
デリゲートメソッドは次のように書くことができます。
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if let swRange = textField.text.rangeFromNSRange(range) {
let newString = textField.text.stringByReplacingCharactersInRange(swRange, withString: string)
// ...
}
return true
}
逆変換は
extension String {
func NSRangeFromRange(range : Range<String.Index>) -> NSRange {
let utf16view = self.utf16
let from = String.UTF16View.Index(range.startIndex, within: utf16view)
let to = String.UTF16View.Index(range.endIndex, within: utf16view)
return NSMakeRange(from - utf16view.startIndex, to - from)
}
}
簡単なテスト:
let str = "a????b????????c"
let r1 = str.rangeOfString("????????")!
// String range to NSRange:
let n1 = str.NSRangeFromRange(r1)
println((str as NSString).substringWithRange(n1)) // ????????
// NSRange back to String range:
let r2 = str.rangeFromNSRange(n1)!
println(str.substringWithRange(r2)) // ????????
Swift 2用のアップデート:
Swift 2版のrangeFromNSRange()
はすでにSerhii Yakovenkoによってこの回答で与えられています、完全を期すためにここに含めています。
extension String {
func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
let from16 = utf16.startIndex.advancedBy(nsRange.location, limit: utf16.endIndex)
let to16 = from16.advancedBy(nsRange.length, limit: utf16.endIndex)
if let from = String.Index(from16, within: self),
let to = String.Index(to16, within: self) {
return from ..< to
}
return nil
}
}
NSRangeFromRange()
のSwift 2バージョンは
extension String {
func NSRangeFromRange(range : Range<String.Index>) -> NSRange {
let utf16view = self.utf16
let from = String.UTF16View.Index(range.startIndex, within: utf16view)
let to = String.UTF16View.Index(range.endIndex, within: utf16view)
return NSMakeRange(utf16view.startIndex.distanceTo(from), from.distanceTo(to))
}
}
Swift 3(Xcode 8)用に更新:
extension String {
func nsRange(from range: Range<String.Index>) -> NSRange {
let from = range.lowerBound.samePosition(in: utf16)
let to = range.upperBound.samePosition(in: utf16)
return NSRange(location: utf16.distance(from: utf16.startIndex, to: from),
length: utf16.distance(from: from, to: to))
}
}
extension String {
func range(from nsRange: NSRange) -> Range<String.Index>? {
guard
let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
let to16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location + nsRange.length, limitedBy: utf16.endIndex),
let from = from16.samePosition(in: self),
let to = to16.samePosition(in: self)
else { return nil }
return from ..< to
}
}
例:
let str = "a????b????????c"
let r1 = str.range(of: "????????")!
// String range to NSRange:
let n1 = str.nsRange(from: r1)
print((str as NSString).substring(with: n1)) // ????????
// NSRange back to String range:
let r2 = str.range(from: n1)!
print(str.substring(with: r2)) // ????????
あなたは古典的なNSRange
の代わりにRange<String.Index>
を使う必要があります。私のやり方(おそらくもっと良い方法があるでしょう)は、文字列のString.Index
をadvance
で動かすことです。
どの範囲を置き換えようとしているのかわかりませんが、最初の2文字を置き換えたいとしましょう。
var start = textField.text.startIndex // Start at the string's start index
var end = advance(textField.text.startIndex, 2) // Take start index and advance 2 characters forward
var range: Range<String.Index> = Range<String.Index>(start: start,end: end)
textField.text.stringByReplacingCharactersInRange(range, withString: string)
Martin Rによるこの答え は、Unicodeを考慮しているので正しいようです。
しかし、投稿時(Swift 1)、彼のコードはSwift 2.0(Xcode 7)ではコンパイルされません、なぜならそれらはadvance()
関数を削除したからです。更新版は以下の通りです。
extension String {
func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
let from16 = utf16.startIndex.advancedBy(nsRange.location, limit: utf16.endIndex)
let to16 = from16.advancedBy(nsRange.length, limit: utf16.endIndex)
if let from = String.Index(from16, within: self),
let to = String.Index(to16, within: self) {
return from ..< to
}
return nil
}
}
extension String {
func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
if let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex),
let to16 = utf16.index(from16, offsetBy: nsRange.length, limitedBy: utf16.endIndex),
let from = String.Index(from16, within: self),
let to = String.Index(to16, within: self) {
return from ..< to
}
return nil
}
}
extension String {
func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
return Range(nsRange, in: self)
}
}
NSRange
をRange<String.Index>
に変換する方法を具体的に尋ねたので、これはEmilieの答えと似ています。
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let start = advance(textField.text.startIndex, range.location)
let end = advance(start, range.length)
let swiftRange = Range<String.Index>(start: start, end: end)
...
}
@ Emilieによるすばらしい答えのリフであり、代替/競合する答えではありません。
(Xcode 6-Beta 5)
var original = "????????????This is a test"
var replacement = "!"
var startIndex = advance(original.startIndex, 1) // Start at the second character
var endIndex = advance(startIndex, 2) // point ahead two characters
var range = Range(start:startIndex, end:endIndex)
var final = original.stringByReplacingCharactersInRange(range, withString:replacement)
println("start index: \(startIndex)")
println("end index: \(endIndex)")
println("range: \(range)")
println("original: \(original)")
println("final: \(final)")
出力:
start index: 4
end index: 7
range: 4..<7
original: ????????????This is a test
final: ????????!his is a test
インデックスが複数のコード単位を占めることに注意してください。フラグ(REGIONAL INDICATOR SYMBOL LETTERS ES)は8バイト、(FACE WITH TEYS OF JOY)は4バイトです。 (この特定のケースでは、バイト数がUTF-8、UTF-16、およびUTF-32表現で同じであることがわかります。)
関数でそれを包む:
func replaceString(#string:String, #with:String, #start:Int, #length:Int) ->String {
var startIndex = advance(original.startIndex, start) // Start at the second character
var endIndex = advance(startIndex, length) // point ahead two characters
var range = Range(start:startIndex, end:endIndex)
var final = original.stringByReplacingCharactersInRange(range, withString: replacement)
return final
}
var newString = replaceString(string:original, with:replacement, start:1, length:2)
println("newString:\(newString)")
出力:
newString: !his is a test
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let strString = ((textField.text)! as NSString).stringByReplacingCharactersInRange(range, withString: string)
}
Swift 2.0ではfunc textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
を仮定しています:
var oldString = textfield.text!
let newRange = oldString.startIndex.advancedBy(range.location)..<oldString.startIndex.advancedBy(range.location + range.length)
let newString = oldString.stringByReplacingCharactersInRange(newRange, withString: string)
最もクリーンなSwift2のみの解決策は、NSRangeにカテゴリを作成することです。
extension NSRange {
func stringRangeForText(string: String) -> Range<String.Index> {
let start = string.startIndex.advancedBy(self.location)
let end = start.advancedBy(self.length)
return Range<String.Index>(start: start, end: end)
}
}
そして、テキストフィールドのデリゲート関数からそれを呼び出します。
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let range = range.stringRangeForText(textField.text)
let output = textField.text.stringByReplacingCharactersInRange(range, withString: string)
// your code goes here....
return true
}
これが私の最善の努力です。しかし、これは間違った入力引数をチェックまたは検出することはできません。
extension String {
/// :r: Must correctly select proper UTF-16 code-unit range. Wrong range will produce wrong result.
public func convertRangeFromNSRange(r:NSRange) -> Range<String.Index> {
let a = (self as NSString).substringToIndex(r.location)
let b = (self as NSString).substringWithRange(r)
let n1 = distance(a.startIndex, a.endIndex)
let n2 = distance(b.startIndex, b.endIndex)
let i1 = advance(startIndex, n1)
let i2 = advance(i1, n2)
return Range<String.Index>(start: i1, end: i2)
}
}
let s = "????????????"
println(s[s.convertRangeFromNSRange(NSRange(location: 4, length: 2))]) // Proper range. Produces correct result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 4))]) // Proper range. Produces correct result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 2))]) // Improper range. Produces wrong result.
println(s[s.convertRangeFromNSRange(NSRange(location: 0, length: 1))]) // Improper range. Produces wrong result.
結果。
????
????????
????????
????????
NSRange
からのNSString
は、UTF-16コード単位をカウントします。そしてSwift String
からのRange<String.Index>
は不透明相対型で、等価性とナビゲーション操作のみを提供します。これは意図的に隠されたデザインです。
Range<String.Index>
はUTF-16コード単位オフセットにマップされているように見えますが、それは単なる実装の詳細であり、保証についての言及はありませんでした。つまり、実装の詳細はいつでも変更できます。 SwiftのString
の内部表現はあまり明確に定義されていません、そして私はそれに頼ることができません。
NSRange
値はString.UTF16View
インデックスに直接マッピングできます。しかし、それをString.Index
に変換する方法はありません。
Swift String.Index
は、Unicode書記素クラスタであるSwift Character
を反復するためのインデックスです。次に、正しい書記素クラスタを選択する適切なNSRange
を提供する必要があります。上記の例のように間違った範囲を指定した場合、適切な書記素クラスタ範囲が把握できないため、誤った結果が生成されます。
String.Index
がUTF-16コード単位オフセットであるという保証がある場合、問題は単純になります。しかし起こりそうもない。
とにかく逆変換は正確に行うことができます。
extension String {
/// O(1) if `self` is optimised to use UTF-16.
/// O(n) otherwise.
public func convertRangeToNSRange(r:Range<String.Index>) -> NSRange {
let a = substringToIndex(r.startIndex)
let b = substringWithRange(r)
return NSRange(location: a.utf16Count, length: b.utf16Count)
}
}
println(convertRangeToNSRange(s.startIndex..<s.endIndex))
println(convertRangeToNSRange(s.startIndex.successor()..<s.endIndex))
結果。
(0,6)
(4,2)
受け入れられた答えで私はオプションが厄介だと思います。これはSwift 3でも動作し、絵文字でも問題ないようです。
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
guard let value = textField.text else {return false} // there may be a reason for returning true in this case but I can't think of it
// now value is a String, not an optional String
let valueAfterChange = (value as NSString).replacingCharacters(in: range, with: string)
// valueAfterChange is a String, not an optional String
// now do whatever processing is required
return true // or false, as required
}
extension StringProtocol where Index == String.Index {
func nsRange(of string: String) -> NSRange? {
guard let range = self.range(of: string) else { return nil }
return NSRange(range, in: self)
}
}
Swift 3.0 Beta公式ドキュメントでは、セクションのタイトルString.UTF16Viewでこの状況に対する標準的な解決策を提供しています。 NSStringの文字タイトル