Trabajando con puntos de código Unicode en Swift

Si no está interesado en los detalles de Mongolia, pero solo quiere una respuesta rápida sobre el uso y la conversión de valores Unicode en Swift, salte a la primera parte de la respuesta aceptada .


Fondo

Quiero hacer que el texto Unicode para el mongol tradicional se use en las aplicaciones de iOS . La mejor solución a largo plazo es utilizar una fuente inteligente AAT que represente este complejo script. ( Dichas fonts existen pero su licencia no permite la modificación y el uso no personal). Sin embargo, dado que nunca he hecho una fuente, y mucho menos toda la lógica de renderizado para una fuente AAT, solo planeo hacer la renderización en Swift por ahora. Tal vez en una fecha posterior pueda aprender a hacer una fuente inteligente.

Externamente UITextView texto Unicode, pero internamente (para mostrar en una UITextView ) Convertiré el Unicode a glifos individuales que están almacenados en una fuente tonta (codificada con valores PUA Unicode). Por lo tanto, mi motor de renderizado debe convertir los valores Unicode mongoles (rango: U + 1820 a U + 1842) en valores de glifos almacenados en el PUA (rango: U + E360 a U + E5CF). De todos modos, este es mi plan ya que es lo que hice en Java en el pasado , pero tal vez deba cambiar mi forma de pensar.

Ejemplo

La siguiente imagen muestra su escrito dos veces en mongol usando dos formas diferentes para la letra u (en rojo). (Mongol está escrito verticalmente con letras conectadas como letras cursivas en inglés).

enter image description here

En Unicode estas dos cadenas se expressían como

 var suForm1: String = "\u{1830}\u{1826}" var suForm2: String = "\u{1830}\u{1826}\u{180B}" 

El Selector de variación libre (U + 180B) en suForm2 es reconocido (correctamente) por Swift String para ser una unidad con el u (U + 1826) que lo precede. Swift lo considera un personaje único, un grupo de grafemas ampliado. Sin embargo, a los fines de realizar la renderización yo mismo, necesito diferenciar u (U + 1826) y FVS1 (U + 180B) como dos puntos de código UTF-16 distintos.

Para fines de visualización interna, convertiría las cadenas Unicode anteriores a las siguientes cadenas de glifos renderizados:

 suForm1 = "\u{E46F}\u{E3BA}" suForm2 = "\u{E46F}\u{E3BB}" 

Pregunta

He estado jugando con Swift String y Character . Hay muchas cosas prácticas sobre ellos, pero dado que en mi caso particular trato exclusivamente con unidades de código UTF-16, me pregunto si debería usar el antiguo NSString lugar de Swift’s String . Me doy cuenta de que puedo usar String.utf16 para obtener puntos de código UTF-16, pero la conversión a String no es muy buena .

¿Sería mejor seguir con la String y el Character o debería usar NSString y unichar ?

Lo que he leído

  • Documentación de cadenas y caracteres
  • Cuerdas en Swift
  • NSString y Unicode

Las actualizaciones de esta pregunta se han ocultado para limpiar la página. Ver el historial de edición.

Actualizado para Swift 3

Cadena y carácter

Para casi todos los que en el futuro visiten esta pregunta, String y Character serán la respuesta para ti.

Establezca los valores Unicode directamente en el código:

 var str: String = "I want to visit 北京, Москва, मुंबई, القاهرة, and 서울시. 😊" var character: Character = "🌍" 

Use hexadecimal para establecer valores

 var str: String = "\u{61}\u{5927}\u{1F34E}\u{3C0}" // a大🍎π var character: Character = "\u{65}\u{301}" // é = "e" + accent mark 

Tenga en cuenta que el personaje de Swift puede estar compuesto por múltiples puntos de código Unicode, pero parece ser un solo carácter. Esto se llama Cluster Grafema Extendido.

Ver esta pregunta también

Convierte a valores Unicode:

 str.utf8 str.utf16 str.unicodeScalars // UTF-32 String(character).utf8 String(character).utf16 String(character).unicodeScalars 

Convertir desde valores hexadecimales Unicode:

 let hexValue: UInt32 = 0x1F34E // convert hex value to UnicodeScalar guard let scalarValue = UnicodeScalar(hexValue) else { // early exit if hex does not form a valid unicode value return } // convert UnicodeScalar to String let myString = String(scalarValue) // 🍎 

O alternativamente:

 let hexValue: UInt32 = 0x1F34E if let scalarValue = UnicodeScalar(hexValue) { let myString = String(scalarValue) } 

Algunos ejemplos más

 let value0: UInt8 = 0x61 let value1: UInt16 = 0x5927 let value2: UInt32 = 0x1F34E let string0 = String(UnicodeScalar(value0)) // a let string1 = String(UnicodeScalar(value1)) // 大let string2 = String(UnicodeScalar(value2)) // 🍎 // convert hex array to String let myHexArray = [0x43, 0x61, 0x74, 0x203C, 0x1F431] // an Int array var myString = "" for hexValue in myHexArray { myString.append(UnicodeScalar(hexValue)) } print(myString) // Cat‼🐱 

Tenga en cuenta que para UTF-8 y UTF-16 la conversión no siempre es tan fácil. (Consulte las preguntas sobre UTF-8 , UTF-16 y UTF-32 ).

NSString y unichar

También es posible trabajar con NSString y unichar en Swift, pero debe tener en cuenta que a menos que esté familiarizado con Objective C y sea bueno convirtiendo la syntax en Swift, será difícil encontrar una buena documentación.

Además, unichar es una matriz UInt16 y, como se mencionó anteriormente, la conversión de UInt16 a valores escalares Unicode no siempre es fácil (es decir, convertir pares sustitutos para cosas como emoji y otros caracteres en los planos de código superiores).

Estructura de cadena personalizada

Por las razones mencionadas en la pregunta, terminé sin utilizar ninguno de los métodos anteriores. En cambio, escribí mi propia estructura de cadenas, que básicamente era una matriz de UInt32 para contener los valores escalares Unicode.

De nuevo, esta no es la solución para la mayoría de las personas. Primero considere usar extensiones si solo necesita extender un poco la funcionalidad de String o Character .

Pero si realmente necesita trabajar exclusivamente con valores escalares Unicode, podría escribir una estructura personalizada.

Las ventajas son:

  • No es necesario cambiar constantemente entre los tipos ( String , Character , UnicodeScalar , UInt32 , etc.) cuando se manipula la cadena.
  • Una vez finalizada la manipulación de Unicode, la conversión final a String es fácil.
  • Fácil de agregar más métodos cuando sean necesarios
  • Simplifica la conversión de código de Java u otros idiomas

Las desventajas son:

  • hace que el código sea menos portátil y menos legible para otros desarrolladores de Swift
  • no tan bien probado y optimizado como los tipos Swift nativos
  • es otro archivo que debe incluirse en un proyecto cada vez que lo necesite

Puedes hacer tu propia, pero aquí es mía para referencia. La parte más difícil fue hacerlo Hashable .

 // This struct is an array of UInt32 to hold Unicode scalar values // Version 3.4.0 (Swift 3 update) struct ScalarString: Sequence, Hashable, CustomStringConvertible { fileprivate var scalarArray: [UInt32] = [] init() { // does anything need to go here? } init(_ character: UInt32) { self.scalarArray.append(character) } init(_ charArray: [UInt32]) { for c in charArray { self.scalarArray.append(c) } } init(_ string: String) { for s in string.unicodeScalars { self.scalarArray.append(s.value) } } // Generator in order to conform to SequenceType protocol // (to allow users to iterate as in `for myScalarValue in myScalarString` { ... }) func makeIterator() -> AnyIterator { return AnyIterator(scalarArray.makeIterator()) } // append mutating func append(_ scalar: UInt32) { self.scalarArray.append(scalar) } mutating func append(_ scalarString: ScalarString) { for scalar in scalarString { self.scalarArray.append(scalar) } } mutating func append(_ string: String) { for s in string.unicodeScalars { self.scalarArray.append(s.value) } } // charAt func charAt(_ index: Int) -> UInt32 { return self.scalarArray[index] } // clear mutating func clear() { self.scalarArray.removeAll(keepingCapacity: true) } // contains func contains(_ character: UInt32) -> Bool { for scalar in self.scalarArray { if scalar == character { return true } } return false } // description (to implement Printable protocol) var description: String { return self.toString() } // endsWith func endsWith() -> UInt32? { return self.scalarArray.last } // indexOf // returns first index of scalar string match func indexOf(_ string: ScalarString) -> Int? { if scalarArray.count < string.length { return nil } for i in 0...(scalarArray.count - string.length) { for j in 0.. ScalarString { var returnString = ScalarString() for scalar in self.scalarArray { if scalar != character { returnString.append(scalar) } } return returnString } func removeRange(_ range: CountableRange) -> ScalarString? { if range.lowerBound < 0 || range.upperBound > scalarArray.count { return nil } var returnString = ScalarString() for i in 0..= range.upperBound { returnString.append(scalarArray[i]) } } return returnString } // replace func replace(_ character: UInt32, withChar replacementChar: UInt32) -> ScalarString { var returnString = ScalarString() for scalar in self.scalarArray { if scalar == character { returnString.append(replacementChar) } else { returnString.append(scalar) } } return returnString } func replace(_ character: UInt32, withString replacementString: String) -> ScalarString { var returnString = ScalarString() for scalar in self.scalarArray { if scalar == character { returnString.append(replacementString) } else { returnString.append(scalar) } } return returnString } func replaceRange(_ range: CountableRange, withString replacementString: ScalarString) -> ScalarString { var returnString = ScalarString() for i in 0..= range.upperBound { returnString.append(scalarArray[i]) } else if i == range.lowerBound { returnString.append(replacementString) } } return returnString } // set (an alternative to myScalarString = "some string") mutating func set(_ string: String) { self.scalarArray.removeAll(keepingCapacity: false) for s in string.unicodeScalars { self.scalarArray.append(s.value) } } // split func split(atChar splitChar: UInt32) -> [ScalarString] { var partsArray: [ScalarString] = [] if self.scalarArray.count == 0 { return partsArray } var part: ScalarString = ScalarString() for scalar in self.scalarArray { if scalar == splitChar { partsArray.append(part) part = ScalarString() } else { part.append(scalar) } } partsArray.append(part) return partsArray } // startsWith func startsWith() -> UInt32? { return self.scalarArray.first } // substring func substring(_ startIndex: Int) -> ScalarString { // from startIndex to end of string var subArray: ScalarString = ScalarString() for i in startIndex.. ScalarString { // (startIndex is inclusive, endIndex is exclusive) var subArray: ScalarString = ScalarString() for i in startIndex.. String { var string: String = "" for scalar in self.scalarArray { if let validScalor = UnicodeScalar(scalar) { string.append(Character(validScalor)) } } return string } // trim // removes leading and trailing whitespace (space, tab, newline) func trim() -> ScalarString { //var returnString = ScalarString() let space: UInt32 = 0x00000020 let tab: UInt32 = 0x00000009 let newline: UInt32 = 0x0000000A var startIndex = self.scalarArray.count var endIndex = 0 // leading whitespace for i in 0.. [UInt32] { return self.scalarArray } } func ==(left: ScalarString, right: ScalarString) -> Bool { return left.scalarArray == right.scalarArray } func +(left: ScalarString, right: ScalarString) -> ScalarString { var returnString = ScalarString() for scalar in left.values() { returnString.append(scalar) } for scalar in right.values() { returnString.append(scalar) } return returnString } 
 //Swift 3.0 // This struct is an array of UInt32 to hold Unicode scalar values struct ScalarString: Sequence, Hashable, CustomStringConvertible { private var scalarArray: [UInt32] = [] init() { // does anything need to go here? } init(_ character: UInt32) { self.scalarArray.append(character) } init(_ charArray: [UInt32]) { for c in charArray { self.scalarArray.append(c) } } init(_ string: String) { for s in string.unicodeScalars { self.scalarArray.append(s.value) } } // Generator in order to conform to SequenceType protocol // (to allow users to iterate as in `for myScalarValue in myScalarString` { ... }) //func generate() -> AnyIterator { func makeIterator() -> AnyIterator { let nextIndex = 0 return AnyIterator { if (nextIndex > self.scalarArray.count-1) { return nil } return self.scalarArray[nextIndex + 1] } } // append mutating func append(scalar: UInt32) { self.scalarArray.append(scalar) } mutating func append(scalarString: ScalarString) { for scalar in scalarString { self.scalarArray.append(scalar) } } mutating func append(string: String) { for s in string.unicodeScalars { self.scalarArray.append(s.value) } } // charAt func charAt(index: Int) -> UInt32 { return self.scalarArray[index] } // clear mutating func clear() { self.scalarArray.removeAll(keepingCapacity: true) } // contains func contains(character: UInt32) -> Bool { for scalar in self.scalarArray { if scalar == character { return true } } return false } // description (to implement Printable protocol) var description: String { var string: String = "" for scalar in scalarArray { string.append(String(describing: UnicodeScalar(scalar))) //.append(UnicodeScalar(scalar)!) } return string } // endsWith func endsWith() -> UInt32? { return self.scalarArray.last } // insert mutating func insert(scalar: UInt32, atIndex index: Int) { self.scalarArray.insert(scalar, at: index) } // isEmpty var isEmpty: Bool { get { return self.scalarArray.count == 0 } } // hashValue (to implement Hashable protocol) var hashValue: Int { get { // DJB Hash Function var hash = 5381 for i in 0 ..< scalarArray.count { hash = ((hash << 5) &+ hash) &+ Int(self.scalarArray[i]) } /* for i in 0..< self.scalarArray.count { hash = ((hash << 5) &+ hash) &+ Int(self.scalarArray[i]) } */ return hash } } // length var length: Int { get { return self.scalarArray.count } } // remove character mutating func removeCharAt(index: Int) { self.scalarArray.remove(at: index) } func removingAllInstancesOfChar(character: UInt32) -> ScalarString { var returnString = ScalarString() for scalar in self.scalarArray { if scalar != character { returnString.append(scalar: scalar) //.append(scalar) } } return returnString } // replace func replace(character: UInt32, withChar replacementChar: UInt32) -> ScalarString { var returnString = ScalarString() for scalar in self.scalarArray { if scalar == character { returnString.append(scalar: replacementChar) //.append(replacementChar) } else { returnString.append(scalar: scalar) //.append(scalar) } } return returnString } // func replace(character: UInt32, withString replacementString: String) -> ScalarString { func replace(character: UInt32, withString replacementString: ScalarString) -> ScalarString { var returnString = ScalarString() for scalar in self.scalarArray { if scalar == character { returnString.append(scalarString: replacementString) //.append(replacementString) } else { returnString.append(scalar: scalar) //.append(scalar) } } return returnString } // set (an alternative to myScalarString = "some string") mutating func set(string: String) { self.scalarArray.removeAll(keepingCapacity: false) for s in string.unicodeScalars { self.scalarArray.append(s.value) } } // split func split(atChar splitChar: UInt32) -> [ScalarString] { var partsArray: [ScalarString] = [] var part: ScalarString = ScalarString() for scalar in self.scalarArray { if scalar == splitChar { partsArray.append(part) part = ScalarString() } else { part.append(scalar: scalar) //.append(scalar) } } partsArray.append(part) return partsArray } // startsWith func startsWith() -> UInt32? { return self.scalarArray.first } // substring func substring(startIndex: Int) -> ScalarString { // from startIndex to end of string var subArray: ScalarString = ScalarString() for i in startIndex ..< self.length { subArray.append(scalar: self.scalarArray[i]) //.append(self.scalarArray[i]) } return subArray } func substring(startIndex: Int, _ endIndex: Int) -> ScalarString { // (startIndex is inclusive, endIndex is exclusive) var subArray: ScalarString = ScalarString() for i in startIndex ..< endIndex { subArray.append(scalar: self.scalarArray[i]) //.append(self.scalarArray[i]) } return subArray } // toString func toString() -> String { let string: String = "" for scalar in self.scalarArray { string.appending(String(describing:UnicodeScalar(scalar))) //.append(UnicodeScalar(scalar)!) } return string } // values func values() -> [UInt32] { return self.scalarArray } } func ==(left: ScalarString, right: ScalarString) -> Bool { if left.length != right.length { return false } for i in 0 ..< left.length { if left.charAt(index: i) != right.charAt(index: i) { return false } } return true } func +(left: ScalarString, right: ScalarString) -> ScalarString { var returnString = ScalarString() for scalar in left.values() { returnString.append(scalar: scalar) //.append(scalar) } for scalar in right.values() { returnString.append(scalar: scalar) //.append(scalar) } return returnString }