Uno de los puntos que más desconcierta a quienes pasan a Swift desde otro lenguaje son los strings. text[0] no funciona, text[2..<5] tampoco, y hay que crear un tipo de índice independiente para escribir text.index(text.startIndex, offsetBy: 2). En Python, esto se resolvería con un solo carácter.
No es que el equipo de Swift no pudiera diseñar la API. Es justo lo contrario: revela honestamente que el concepto de «el carácter n de un string» no es sencillo. Otros lenguajes ocultan esta complejidad y a veces devuelven respuestas incorrectas. Swift eligió devolver siempre la respuesta correcta, aunque resulte menos cómodo. En esta sexta y última entrega de Fundamentos de Swift, exploramos por qué los strings son realmente difíciles.
El flujo que puede fallar al convertir un string en número se trata junto con su modelo de errores en Cómo elegir entre throws, try y Result en Swift.
Por qué «¿Cuántos caracteres tiene?» es difícil — el mundo de Unicode
Empecemos con esta pregunta: ¿cuántos caracteres tiene «café»? Parece obvio que 4, pero para un ordenador hay dos respuestas. é puede almacenarse como un único punto de código compuesto (U+00E9), o como e (U+0065) más un acento combinante (U+0301). Se ven igual, pero internamente los puntos de código son 1 y 2.
El coreano hace que este problema resulte aún más evidente. Una sílaba coreana puede ser un único punto de código de sílaba precompuesta (U+AC01), o una combinación de tres letras jamo. Si has trabajado con nombres de archivo de macOS, quizá hayas visto nombres coreanos guardados en NFD (Normalization Form D — forma de normalización Unicode que descompone las letras en unidades jamo) aparecer descompuestos en otro sistema. El mismo «carácter» puede tener uno o tres puntos de código.
Los emojis añaden otra capa. El emoji de familia 👨👩👧👦 une cuatro emojis de personas mediante joiners de ancho cero (ZWJ, Zero Width Joiner — caracteres invisibles que agrupan emojis como uno solo), con un total de siete puntos de código. En unidades de código UTF-16 son 11. Para el ojo humano, sin embargo, es un carácter.
Por eso el estándar Unicode define una unidad independiente para «un carácter percibido por una persona»: el clúster de grafemas extendido (extended grapheme cluster). Tanto é como una sílaba coreana compuesta y el emoji de familia son un clúster de grafemas.
Elecciones de los lenguajes — ¿es más rápido dar una respuesta incorrecta o más lento dar la correcta?
Ya podemos comparar cómo gestiona cada lenguaje el «carácter n».
"👨👩👧👦".length de JavaScript es 11 porque cuenta unidades de código UTF-16. Java funciona igual. len de Python 3 devuelve 7 porque cuenta puntos de código. Todos contradicen la intuición humana de «un carácter». Si cortas ingenuamente un string con emojis, puedes partir un emoji por la mitad y obtener texto corrupto.
"👨👩👧👦".count de Swift es 1 porque Character representa un clúster de grafemas, no un punto de código. count es igual independientemente de cómo se almacene é, y == devuelve true teniendo en cuenta la normalización. Siempre responde según lo que ve una persona.
Pero esa precisión tiene un coste. Los clústeres de grafemas tienen longitud variable, así que encontrar el carácter n exige recorrer desde el principio y comprobar cada límite. Por eso los strings de Swift no tienen índices enteros. Una API que hiciera parecer O(1) text[7] ocultaría un coste real de O(n). Usar text[i] dentro de un bucle lo convierte en O(n²), y Swift decidió no fomentar ese patrón. El tipo opaco String.Index expresa mediante el tipo que esa posición se obtuvo contando. El principio de la primera entrega de la serie de filosofía —no ocultar los costes— también se repite en los strings.
Caja de herramientas práctica — trabajar con strings sin índices
Con el principio claro, pasemos a la práctica. Aunque no haya índices enteros, las herramientas seguras resuelven la mayoría de las tareas.
Para recortar por ambos extremos, usa prefix y suffix. text.prefix(10)devuelven de forma segura los primeros 10 caracteres (grafemas), o los disponibles si son menos, sin producir un fallo. No necesitas calcular índices de substring para crear textos de vista previa.
Para buscar, usa range(of:) y firstIndex(of:). Cuando necesitas una posición, normalmente es la de un contenido concreto. Inserta directamente el Range o Index obtenido en el subscript; no hace falta convertirlo a entero.
Para dividir, usa split. text.split(separator: ",")sustituye el recorrido manual de índices.
Si de verdad necesitas el carácter n, usa Array. Cuando un algoritmo necesita acceso aleatorio por carácter, lo habitual es convertirlo a Array(text). Pagas O(n) una vez y después cada acceso cuesta O(1). Es mucho mejor que llamar a index(offsetBy:) en cada iteración.
Si necesitas bytes, especifica la vista. A veces necesitas contar bytes, no caracteres, por ejemplo para el tráfico de red o el límite de una columna de DB. Swift hace explícita la unidad con text.utf8.count, text.utf16.count y text.unicodeScalars.count. Recuerda que NSString.length, presente en UserDefaults o API antiguas, cuenta UTF-16 y puede diferir de count de Swift.
Hay otra trampa: los índices de strings distintos no son compatibles. Usar en b un String.Index obtenido de a puede provocar un fallo o un resultado incorrecto. Un índice pertenece a «ese string en ese momento». Si modificas el string, considera inválidos los índices anteriores.
Por qué esto es especialmente importante para desarrolladores coreanos
Al crear servicios coreanos, este conocimiento deja de ser teórico y se vuelve práctico.
El límite de caracteres de un apodo es un ejemplo típico. Al validar «hasta 10 caracteres», si el servidor (otro lenguaje) y el cliente (Swift) cuentan con unidades distintas, un apodo aceptado por el cliente puede ser rechazado por el servidor. Si el servidor limita bytes UTF-8, el coreano ocupa 3 bytes por carácter, así que 10 caracteres son 30 bytes. Primero hay que acordar qué se cuenta; en Swift, elige conscientemente count (grafemas) o utf8.count (bytes).
La descomposición jamo es otro problema. Por la cuestión NFD anterior, un string coreano procedente de un sistema externo puede verse igual y tener puntos de código distintos. == de Swift considera la normalización, por lo que normalmente funciona, pero al usar strings externos como claves de diccionario basadas en hash o pasarlos a otro lenguaje, es más seguro normalizarlos explícitamente a NFC (Normalization Form C — forma que almacena las letras jamo combinadas en caracteres precompuestos) con precomposedStringWithCanonicalMapping.
Para funciones como la búsqueda por consonante inicial en búsquedas y autocompletado, hay que descomponer los grafemas en jamo; la vista unicodeScalars es el punto de partida. Si entiendes las vistas, el requisito se reduce a «recorrer otra vista», no a «necesitar una biblioteca especial».
Resultados confirmados mediante ejecución directa
En Apple Swift 6.3.3 comparamos la forma precompuesta 한 con la forma compuesta creada a partir de tres jamo 한. El string compuesto tiene tres Unicode scalar, pero String.count lo cuenta como un carácter, y ambas representaciones resultaron iguales en la comparación de equivalencia canónica.
string=characters:1,scalars:3,canonically-equal:true
Después de comprobarlo, muestro la unidad en los nombres de variables al implementar límites de longitud. Para characterLimit uso count; para el tamaño de transferencia, utf8.count, y escribo la misma unidad en el contrato del servidor. Si acordamos un único length sin unidad, el cliente y el servidor pueden desalinearse fácilmente con coreano y emojis.
Resumen
- El «carácter n» es difícil porque un carácter Unicode (un clúster de grafemas) está formado por un número variable de puntos de código. é, el coreano compuesto y los emojis ZWJ son casos representativos.
- El length de otros lenguajes puede contar unidades o puntos de código y contradecir la intuición. count de Swift cuenta grafemas y siempre responde según la perspectiva humana.
- El precio es que el acceso aleatorio cuesta O(n), así que Swift eliminó los índices enteros para no ocultar ese coste.
- En la práctica, prefix/suffix, range(of:) y split resuelven casi todo; usa una conversión a Array para acceso aleatorio y especifica la vista utf8 para contar bytes.
- En servicios coreanos, los puntos clave son acordar la unidad de validación de longitud y gestionar la normalización NFC/NFD.
Con esto termina la serie básica de seis entregas. A partir de ahora comienza la serie intermedia. El primer tema será el corazón de la gestión de memoria de Swift: ARC (Automatic Reference Counting) y cómo elegir entre weak y unowned. Es la entrega principal de la historia de referencias circulares anunciada en la parte de closures.
Lecturas recomendadas
- [Fundamentos de Swift #5] Mapa completo del manejo de errores en Swift: cuándo usar throws, try?, try! o Result
- [Fundamentos de Swift #4] Cómo usar guard correctamente en Swift: salidas tempranas que tumban la pirámide de la fatalidad
- Patrón Coordinator de iOS: separar el código de transición de pantallas de los view controllers
Fuentes y verificación
- Swift StringApple Developer Documentation · Documentación oficial · Consultado 26 de agosto de 2026Respalda: Modelo de colección Character de String, equivalencia canónica Unicode, vistas de strings y API de índices
- The Swift Programming Language: Strings and CharactersSwift.org · Documentación oficial · Consultado 26 de agosto de 2026Respalda: Clústeres de grafemas extendidos, representación de coreano compuesto y coste de count e indexación entera

![Imagen de portada de [Fundamentos de Swift #6] ¿Por qué no se puede usar text[0] con strings?](/assets/images/posts/18529e8e-628f-4b41-8e48-680d2ff6f480/1.jpg)