NSRange para Range <String.Index>

258

Como posso converter NSRangepara Range<String.Index>no Swift?

Eu quero usar o seguinte UITextFieldDelegatemétodo:

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

textField.text.stringByReplacingCharactersInRange(???, withString: string)

insira a descrição da imagem aqui

János
fonte
71
Obrigado, Apple, por nos fornecer a nova classe Range, mas depois não atualizar nenhuma classe de utilitário de cadeia, como NSRegularExpression, para usá-las!
Puzzl

Respostas:

269

A NSStringversão (em oposição à Swift String) de replacingCharacters(in: NSRange, with: NSString)aceita um NSRange, portanto, uma solução simples é converter Stringpara o NSStringprimeiro . Os nomes dos métodos de delegação e substituição são ligeiramente diferentes no Swift 3 e 2, portanto, dependendo do Swift que você estiver usando:

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)
}
Alex Pretzlav
fonte
14
Isso não funcionará se textFieldcontiver caracteres unicode de unidade com vários códigos, como emoji. Como é um campo de entrada, o usuário pode digitar emoji (😂), outros caracteres da página 1 de caracteres com múltiplos códigos de unidade, como bandeiras (🇪🇸).
Zaph
2
@ Zaph: como o intervalo é proveniente do UITextField, escrito em Obj-C contra o NSString, suspeito que apenas intervalos válidos com base nos caracteres unicode da string sejam fornecidos pelo retorno de chamada do delegado e, portanto, é seguro usá-lo , mas eu não testei pessoalmente.
Alex Pretzlav
2
@ Zaph, isso é verdade, o que quero dizer é que UITextFieldDelegateo textField:shouldChangeCharactersInRange:replacementString:método não fornece um intervalo que começa ou termina dentro de um caractere unicode, pois o retorno de chamada é baseado em um usuário digitando caracteres do teclado e não é possível digitar apenas parte de um caractere unicode emoji. Observe que se o delegado fosse escrito em Obj-C, ele teria o mesmo problema.
precisa saber é o seguinte
4
Não é a melhor resposta. O código é mais fácil de ler, mas o @ martin-r tem a resposta correta.
314
3
Na verdade, vocês devem fazer isso(textField.text as NSString?)?.stringByReplacingCharactersInRange(range, withString: string)
Wanbok Choi
361

A partir do Swift 4 (Xcode 9), a biblioteca padrão do Swift fornece métodos para converter entre os intervalos de string do Swift ( Range<String.Index>) e os NSStringintervalos ( NSRange). Exemplo:

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]) // 🇩🇪

Portanto, a substituição de texto no método delegado do campo de texto agora pode ser feita como

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)
        // ...
    }
    // ...
}

(Respostas anteriores para Swift 3 e anteriores :)

A partir do Swift 1.2, String.Indextem um inicializador

init?(_ utf16Index: UTF16Index, within characters: String)

que pode ser utilizado para converter NSRangea Range<String.Index>correctamente (incluindo todos os casos de Emojis, Indicadores da região ou outros agregados estendidos) grafema sem conversão para um intermediário NSString:

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
    }
}

Esse método retorna um intervalo opcional de cadeias porque nem todos os NSRanges são válidos para uma determinada cadeia Swift.

O UITextFieldDelegatemétodo delegado pode então ser escrito como

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
}

A conversão inversa é

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)
    }
}

Um teste simples:

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)) // 🇩🇪

Atualização para o Swift 2:

A versão Swift 2 do rangeFromNSRange()já foi dada por Serhii Yakovenko nesta resposta , eu a incluo aqui por completo:

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
    }
}

A versão do Swift 2 NSRangeFromRange()é

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))
    }
}

Atualização para 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
    }
}

Exemplo:

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)) // 🇩🇪
Martin R
fonte
7
Esse código não funciona corretamente para caracteres que consistem em mais de um ponto de código UTF-16, por exemplo, Emojis. - Por exemplo, se o campo de texto contiver o texto "👿" e você excluir esse caractere, rangeserá (0,2)porque NSRangese refere aos caracteres UTF-16 em um NSString. Mas conta como um caractere Unicode em uma string Swift. - a let end = advance(start, range.length)partir da resposta mencionada falha nesse caso com a mensagem de erro "erro fatal: não é possível incrementar endIndex".
Martin R
8
@MattDiPasquale: Claro. Minha intenção era responder à pergunta literal "Como posso converter o NSRange em Range <String.Index> no Swift" de uma maneira segura para Unicode (esperando que alguém possa achar útil, o que não é o caso até agora :(
Martin R
5
Obrigado, mais uma vez, por fazer o trabalho da Apple para eles. Sem pessoas como você e o Stack Overflow, não há como eu desenvolver o iOS / OS X. A vida é muito curta.
Womble
1
@ jiminybob99: Command-clique em Stringpara saltar para a referência da API, leia todos os métodos e comentários, em seguida, tentar coisas diferentes até que ele funciona :)
Martin R
1
Para let from16 = utf16.index(utf16.startIndex, offsetBy: nsRange.location, limitedBy: utf16.endIndex), eu acho que você realmente quer limitá-lo por utf16.endIndex - 1. Caso contrário, você pode começar o final da string.
Michael Tsai
18

Você precisa usar em Range<String.Index>vez do clássico NSRange. O jeito que eu faço (talvez exista uma maneira melhor) é pegar a corda e String.Indexmovê-la advance.

Não sei qual intervalo você está tentando substituir, mas vamos fingir que deseja substituir os dois primeiros caracteres.

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)
Emilie
fonte
18

Esta resposta de Martin R parece estar correta porque é responsável por Unicode.

No entanto, no momento da publicação (Swift 1), seu código não é compilado no Swift 2.0 (Xcode 7), porque eles removeram a advance()função. A versão atualizada está abaixo:

Swift 2

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
    }
}

Swift 3

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
    }
}

Swift 4

extension String {
    func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
        return Range(nsRange, in: self)
    }
}
Serhii Yakovenko
fonte
7

Isso é semelhante à resposta de Emilie, no entanto, uma vez que você perguntou especificamente como converter o NSRangepara Range<String.Index>você faria algo assim:

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) 
     ...

}
James Jones
fonte
2
A exibição de caracteres e a exibição UTF16 de uma sequência podem ter comprimentos diferentes. Essa função está usando índices UTF16 (é o que NSRangefala, embora seus componentes sejam inteiros) na visualização de caracteres da sequência, que pode falhar quando usada em uma sequência com "caracteres" que levam mais de uma unidade UTF16 para expressar.
Nate Cook
6

Um riff sobre a ótima resposta de @Emilie, não uma resposta substituta / concorrente.
(Xcode6-Beta5)

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)")

Resultado:

start index: 4
end index:   7
range:       4..<7
original:    🇪🇸😂This is a test
final:       🇪🇸!his is a test

Observe que os índices são responsáveis ​​por várias unidades de código. O sinalizador (LETRAS DE SÍMBOLOS DO INDICADOR REGIONAL ES) é de 8 bytes e o (CARA COM LÁGRIMAS DE ALEGRIA) é de 4 bytes. (Nesse caso específico, verifica-se que o número de bytes é o mesmo para as representações UTF-8, UTF-16 e UTF-32.)

Envolvendo-o em uma função:

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)")

Resultado:

newString: !his is a test
zaph
fonte
4
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {

       let strString = ((textField.text)! as NSString).stringByReplacingCharactersInRange(range, withString: string)

 }
Darshan Panchal
fonte
2

No Swift 2.0, assumindo 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)
Brenden
fonte
1

Aqui está o meu melhor esforço. Mas isso não pode verificar ou detectar argumentos de entrada incorretos.

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.

Resultado.

😂
🇪🇸
🇪🇸
🇪🇸

Detalhes

NSRangedas NSStringcontagens de unidades de código UTF-16 . E Range<String.Index>do Swift Stringé um tipo relativo opaco que fornece apenas operações de igualdade e navegação. Este é um design intencionalmente oculto.

Embora Range<String.Index>pareça estar mapeado para o deslocamento da unidade de código UTF-16, isso é apenas um detalhe da implementação, e não encontrei nenhuma menção a nenhuma garantia. Isso significa que os detalhes da implementação podem ser alterados a qualquer momento. A representação interna do Swift Stringnão está bem definida, e não posso confiar nela.

NSRangevalores podem ser mapeados diretamente para String.UTF16Viewíndices. Mas não há método para convertê-lo String.Index.

Swift String.Indexé o índice para iterar o Swift, Characterque é um cluster de grafema Unicode . Em seguida, você deve fornecer o apropriado, NSRangeque seleciona os clusters de grafema corretos. Se você fornecer um intervalo errado, como no exemplo acima, ele produzirá um resultado errado, pois não foi possível descobrir o intervalo adequado do cluster grafema.

Se houver uma garantia de que o deslocamento da unidade de código String.Index é UTF-16, o problema se tornará simples. Mas é improvável que isso aconteça.

Conversão inversa

De qualquer forma, a conversão inversa pode ser feita com precisão.

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))

Resultado.

(0,6)
(4,2)
Eonil
fonte
Em Swift 2, return NSRange(location: a.utf16Count, length: b.utf16Count)deve ser alterado para #return NSRange(location: a.utf16.count, length: b.utf16.count)
Elliot
1

Descobri que a única solução mais limpa do swift2 é criar uma categoria no 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)
    }
}

E, em seguida, chame-o de para a função delegada do campo de texto:

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
}
Danny Bravo
fonte
Percebi que meu código não estava mais funcionando após a atualização mais recente do Swift2. Atualizei minha resposta para trabalhar com o Swift2.
Danny Bravo
0

A documentação oficial do Swift 3.0 beta forneceu sua solução padrão para esta situação sob o título String.UTF16View na seção UTF16View Elements Match NSString Characters title

user6511559
fonte
Será útil se você fornecer links na sua resposta.
m5khan
0

Na resposta aceita, acho os opcionais pesados. Isso funciona com o Swift 3 e parece não ter problemas com emojis.

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
}
Murray Sagal
fonte
0
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)
    }
}
Zheng
fonte