¿Cómo perder margen / relleno en UITextView?

488

Tengo un UITextViewen mi aplicación iOS, que muestra una gran cantidad de texto.

Luego estoy paginando este texto usando el parámetro de margen de desplazamiento de UITextView.

Mi problema es que el relleno del archivo UITextViewconfunde mis cálculos, ya que parece ser diferente según el tamaño de fuente y la tipografía que uso.

Por lo tanto, planteo la pregunta: ¿es posible eliminar el relleno que rodea el contenido de la UITextView?

¡Esperamos escuchar sus respuestas!

sniurkst
fuente
99
¡Tenga en cuenta que este control de calidad tiene casi diez años! Con más de 100,000 vistas, ya que es uno de los problemas más estúpidos en iOS . Solo FTR que puse en la solución actual, 2017, razonablemente simple / habitual / aceptada a continuación como respuesta.
Fattie
1
Todavía recibo actualizaciones sobre esto, después de haber escrito una solución codificada en 2009 cuando acababa de lanzar IOS 3.0. Acabo de editar la respuesta para indicar claramente que tiene años de antigüedad e ignorar el estado aceptado.
Michael

Respuestas:

384

Actualizado para 2019

Es uno de los errores más tontos en iOS.

La clase dada aquí UITextViewFixedes generalmente la solución más razonable en general.

Aquí está la clase:

@IBDesignable class UITextViewFixed: UITextView {
    override func layoutSubviews() {
        super.layoutSubviews()
        setup()
    }
    func setup() {
        textContainerInset = UIEdgeInsets.zero
        textContainer.lineFragmentPadding = 0
    }
}

¡No olvide desactivar scrollEnabled en el Inspector!

  1. La solución funciona correctamente en storyboard

  2. La solución funciona correctamente en tiempo de ejecución

Eso es todo, ya terminaste.

En general, eso debería ser todo lo que necesita en la mayoría de los casos .

Incluso si está cambiando la altura de la vista de texto sobre la marcha , UITextViewFixedgeneralmente hace todo lo que necesita.

(Un ejemplo común de cambiar la altura sobre la marcha es cambiarlo a medida que el usuario escribe).

Aquí está el UITextView roto de Apple ...

captura de pantalla de IB con UITextView

Aqui esta UITextViewFixed:

captura de pantalla de IB con UITextViewFixed

Tenga en cuenta que, por supuesto, debe

apague scrollEnabled en el Inspector!

¡No olvides desactivar scrollEnabled! :)


Algunos problemas adicionales

(1) En algunos casos inusuales, por ejemplo, algunos casos de tablas con alturas de celda flexibles y dinámicamente cambiantes, Apple hace algo extraño: agregan espacio adicional en la parte inferior . ¡No realmente! Esta tendría que ser una de las cosas más irritantes en iOS.

Aquí hay una "solución rápida" para agregar a lo anterior que generalmente ayuda con esa locura.

...
        textContainerInset = UIEdgeInsets.zero
        textContainer.lineFragmentPadding = 0

        // this is not ideal, but you can sometimes use this
        // to fix the "extra space at the bottom" insanity
        var b = bounds
        let h = sizeThatFits(CGSize(
           width: bounds.size.width,
           height: CGFloat.greatestFiniteMagnitude)
       ).height
       b.size.height = h
       bounds = b
 ...

(2) A veces, para solucionar otro error sutil de Apple, debes agregar esto:

override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
    super.setContentOffset(contentOffset, animated: false)
}

(3) Podría decirse que deberíamos agregar:

contentInset = UIEdgeInsets.zero

justo después .lineFragmentPadding = 0de UITextViewFixed.

Sin embargo ... lo creas o no ... ¡ simplemente no funciona en el iOS actual! (Revisado en 2019). Puede ser necesario agregar esa línea en el futuro.

El hecho de que UITextViewse rompa en iOS es una de las cosas más extrañas en toda la informática móvil. ¡Diez años de aniversario de esta pregunta y aún no se ha solucionado!

Finalmente, aquí hay un consejo algo similar para el campo de texto : https://stackoverflow.com/a/43099816/294884

Sugerencia completamente al azar: cómo agregar el "..." al final

A menudo está utilizando un UITextView "como un UILabel". Por lo tanto, desea que trunque el texto usando puntos suspensivos "..."

Si es así, agregue una tercera línea de código en la "configuración":

 textContainer.lineBreakMode = .byTruncatingTail

Sugerencia práctica si desea altura cero, cuando no hay texto

A menudo utiliza una vista de texto para mostrar solo texto. Entonces, el usuario en realidad no puede editar nada. Utiliza líneas "0" para indicar que la vista de texto cambiará automáticamente la altura dependiendo de cuántas líneas de texto.

Eso es genial, pero si no hay texto , ¡lamentablemente obtienes la misma altura que si hubiera una línea de texto ! La vista de texto nunca "desaparece".

ingrese la descripción de la imagen aquí

Si desea que "desaparezca", simplemente agregue esto

override var intrinsicContentSize: CGSize {
    var i = super.intrinsicContentSize
    print("for \(text) size will be \(i)")
    if text == "" { i.height = 1.0 }
    print("   but we changed it to \(i)")
    return i
}

ingrese la descripción de la imagen aquí

(Lo hice '1' para que quede claro lo que está sucediendo, '0' está bien).

¿Qué hay de UILabel?

Cuando solo muestra texto, UILabel tiene muchas ventajas sobre UITextView. UILabel no sufre los problemas descritos en esta página de control de calidad. De hecho, la razón por la que todos "nos rendimos" y usamos UITextView es que es difícil trabajar con UILabel. En particular, es ridículamente difícil agregar relleno , correctamente, a UILabel. De hecho, aquí hay una discusión completa sobre cómo "finalmente" agregar correctamente el relleno a UILabel: https://stackoverflow.com/a/58876988/294884 En algunos casos, si está haciendo un diseño difícil con celdas de altura dinámica, a veces es mejor hacerlo de la manera difícil con UILabel.

Fattie
fuente
1
Lo hice por self.textView.isScrollEnabled = falsedentro viewDidLayoutSubviews()y eso también funcionó. A Apple simplemente le gusta hacernos saltar a través de aros :)
AnBisw
1
La mejor solución para corregir todos mis errores absurdos de autoejecución UITextView en mis celdas, encabezado y pie de página UITableView con altura dinámica (UITableViewAutomaticDimension). ¡Excelente!
Peter Kreinz el
1
He publicado una respuesta actualizada a la respuesta de @ Fattie que me ayudó a deshacerme de todas las inserciones utilizando el truco especial de habilitar / deshabilitar translatesAutoresizingMaskIntoConstraints. Con esta respuesta sola, no pude eliminar todos los márgenes (algunos márgenes inferiores extraños se resistieron a desaparecer) en una jerarquía de vistas usando el diseño automático. También se ocupa del cálculo de los tamaños de vista usando systemLayoutSizeFitting, que anteriormente devolvió un tamaño no válido debido al UITextView
error
1
1up para IBDesignable. También votaría por el texto del
marcador de
1
Tenía vistas de texto en la tabla, las que tenían una sola línea de texto no calculaban su altura correctamente (distribución automática). Para solucionarlo, tuve que anular didMoveToSuperview y llamar a la configuración allí también.
El Horrible
792

Para iOS 7.0, descubrí que el truco contentInset ya no funciona. Este es el código que utilicé para deshacerme del margen / relleno en iOS 7.

Esto lleva el borde izquierdo del texto al borde izquierdo del contenedor:

textView.textContainer.lineFragmentPadding = 0

Esto hace que la parte superior del texto se alinee con la parte superior del contenedor.

textView.textContainerInset = .zero

Ambas líneas son necesarias para eliminar completamente el margen / relleno.

usuario1687195
fuente
2
Esto funciona para mí por dos líneas, pero cuando llego a 5 líneas, el texto se está cortando.
livings124
77
Tenga en cuenta que la segunda línea también se puede escribir como: self.descriptionTextView.textContainerInset = UIEdgeInsetsZero;
jessepinho
19
Establecer lineFragmentPaddingen 0 es la magia que estaba buscando. No tengo idea de por qué Apple hace que sea tan difícil alinear el contenido de UITextView con otros controles.
phatmann
3
Esta es la respuesta correcta. Desplazarse horizontalmente no sucede con esta solución.
Michael
2
lineFragmentPaddingno tiene la intención de modificar los márgenes. De los documentos El relleno de fragmentos de línea no está diseñado para expresar márgenes de texto. En su lugar, debe usar inserciones en su vista de texto, ajustar los atributos de margen de párrafo o cambiar la posición de la vista de texto dentro de su supervista.
Martin Berger
256

Esta solución se escribió en 2009 cuando se lanzó IOS 3.0. Ya no se aplica.

Me encontré exactamente con el mismo problema, al final tuve que terminar usando

nameField.contentInset = UIEdgeInsetsMake(-4,-8,0,0);

donde nameField es a UITextView. La fuente que estaba usando era Helvetica 16 puntos. Es solo una solución personalizada para el tamaño de campo particular que estaba dibujando. Esto hace que el desplazamiento a la izquierda quede al ras con el lado izquierdo, y el desplazamiento superior donde lo quiero para el cuadro se dibuja.

Además, esto solo parece aplicarse a UITextViewsdonde está utilizando la alineación predeterminada, es decir.

nameField.textAlignment = NSTextAlignmentLeft;

Alinear a la derecha, por ejemplo, y el UIEdgeInsetsMake parece que no tiene ningún impacto en el borde derecho.

Al menos, el uso de la propiedad .contentInset le permite colocar sus campos con las posiciones "correctas" y acomodar las desviaciones sin compensar su UITextViews.

Miguel
fuente
1
Sí, funciona en iOS 7. Asegúrese de que UIEdgeInset esté configurado con los valores correctos. Puede ser diferente a UIEdgeInsetsMake (-4, -8,0,0).
aplicación_
15
En IOS 7 encontré que UIEdgeInsetsMake (0, -4,0, -4) funcionó mejor.
Racura
2
Sí, esos son números de ejemplo. Dije "Es solo una solución personalizada para el tamaño de campo particular que estaba dibujando". Principalmente estaba tratando de ilustrar que tienes que jugar con los números para tu situación de diseño única.
Michael
3
UITextAlignmentLeft está en desuso en iOS 7. Utilice NSTextAlignmentLeft.
Jordi Kroon
77
No entiendo por qué la gente vota una respuesta que utiliza valores codificados. Es MUY probable que se rompa en futuras versiones de iOS y es simplemente una mala idea.
ldoogy
77

A partir de algunas de las buenas respuestas ya dadas, aquí hay una solución puramente basada en Storyboard / Interface Builder que funciona en iOS 7.0+

Establezca los atributos de tiempo de ejecución definidos por el usuario de UITextView para las siguientes claves:

textContainer.lineFragmentPadding
textContainerInset

Interface Builder

azdev
fuente
44
Esta es claramente la mejor respuesta, especialmente si necesita una solución solo XIB
yano
16
Copiar / pegar: textContainer.lineFragmentPadding | textContainerInset
Luca De Angelis
3
Esto es lo que hace una respuesta hermosa: fácil y funciona bien, incluso después de 3 años :)
Shai Mishali
46

En iOS 5 UIEdgeInsetsMake(-8,-8,-8,-8);parece funcionar muy bien.

Engin Kurutepe
fuente
41

Definitivamente evitaría cualquier respuesta que implique valores codificados, ya que los márgenes reales pueden cambiar con la configuración del tamaño de fuente del usuario, etc.

Aquí está la respuesta de @ user1687195, escrita sin modificar el textContainer.lineFragmentPadding(porque los documentos indican que este no es el uso previsto).

Esto funciona muy bien para iOS 7 y versiones posteriores.

self.textView.textContainerInset = UIEdgeInsetsMake(
                                      0, 
                                      -self.textView.textContainer.lineFragmentPadding, 
                                      0, 
                                      -self.textView.textContainer.lineFragmentPadding);

Este es efectivamente el mismo resultado, solo un poco más limpio, ya que no hace un mal uso de la propiedad lineFragmentPadding.

ldoogy
fuente
Intenté otra solución basada en esta respuesta y funciona muy bien. La solución es:self.textView.textContainer.lineFragmentPadding = 0;
Bakyt Abdrasulov el
1
@BakytAbdrasulov por favor lea mi respuesta. Si bien su solución funciona, no está de acuerdo con los documentos (consulte el enlace en mi respuesta). lineFragmentPaddingno está destinado a controlar los márgenes. Por eso se supone que debes usar textContainerInset.
ldoogy
21

Solución Storyboard o Interface Builder que utiliza atributos de tiempo de ejecución definidos por el usuario :

Las capturas de pantalla son de iOS 7.1 y iOS 6.1 con contentInset = {{-10, -5}, {0, 0}}.

Atributos de tiempo de ejecución definidos por el usuario

salida

Uladzimir
fuente
1
Trabajó para mí en iOS 7.
kubilay
Solo para atributos de tiempo de ejecución definidos por el usuario: ¡increíble!
hris.to
16

Todas estas respuestas abordan la pregunta del título, pero quería proponer algunas soluciones para los problemas presentados en el cuerpo de la pregunta del OP.

Tamaño del contenido del texto

Una forma rápida de calcular el tamaño del texto dentro de UITextViewes utilizar NSLayoutManager:

UITextView *textView;
CGSize textSize = [textView usedRectForTextContainer:textView.textContainer].size;

Esto proporciona el contenido desplazable total, que puede ser más grande que el UITextViewmarco del archivo. Descubrí que esto es mucho más preciso que textView.contentSizeya que en realidad calcula cuánto espacio ocupa el texto. Por ejemplo, dado un vacío UITextView:

textView.frame.size = (width=246, height=50)
textSize = (width=10, height=16.701999999999998)
textView.contentSize = (width=246, height=33)
textView.textContainerInset = (top=8, left=0, bottom=8, right=0)

Altura de la línea

UIFonttiene una propiedad que le permite obtener rápidamente la altura de la línea para la fuente dada. Para que pueda encontrar rápidamente la altura de la línea del texto en su UITextViewcon:

UITextView *textView;
CGFloat lineHeight = textView.font.lineHeight;

Cálculo del tamaño de texto visible

Determinar la cantidad de texto que es realmente visible es importante para manejar un efecto de "paginación". UITextViewtiene una propiedad llamada textContainerInsetque en realidad es un margen entre UITextView.frameel texto real y el propio. Para calcular la altura real del marco visible, puede realizar los siguientes cálculos:

UITextView *textView;
CGFloat textViewHeight = textView.frame.size.height;
UIEdgeInsets textInsets = textView.textContainerInset;
CGFloat textHeight = textViewHeight - textInsets.top - textInsets.bottom;

Determinación del tamaño de paginación

Por último, ahora que tiene el tamaño de texto visible y el contenido, puede determinar rápidamente cuáles deberían ser sus compensaciones restando el textHeightde textSize:

// where n is the page number you want
CGFloat pageOffsetY = textSize - textHeight * (n - 1);
textView.contentOffset = CGPointMake(textView.contentOffset.x, pageOffsetY);

// examples
CGFloat page1Offset = 0;
CGFloat page2Offset = textSize - textHeight
CGFloat page3Offset = textSize - textHeight * 2

Utilizando todos estos métodos, no toqué mis inserciones y pude ir al cursor o en cualquier parte del texto que quisiera.

mikeho
fuente
12

puedes usar textContainerInsetpropiedad de UITextView:

textView.textContainerInset = UIEdgeInsetsMake (10, 10, 10, 10);

(arriba a la izquierda, abajo a la derecha)

Himanshu padia
fuente
1
Me pregunto por qué esto no se asigna como la mejor respuesta aquí. El textContainerInset realmente satisfizo mis necesidades.
KoreanXcodeWorker
Actualizar el valor inferior de la textContainerInsetpropiedad no me funciona cuando quiero cambiarlo a un nuevo valor; consulte stackoverflow.com/questions/19422578/… .
Evan R
@MaggiePhillips No es la mejor respuesta porque los valores codificados no pueden ser la forma correcta de hacerlo, y se garantiza que fallarán en algunos casos. Debe tener en cuenta lineFragmentPadding.
ldoogy
11

Para iOS 10, la siguiente línea funciona para la eliminación del relleno superior e inferior.

captionTextView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0)
Anson Yao
fuente
Xcode 8.2.1. sigue siendo el mismo problema que se menciona en esta respuesta. Mi solución fue editar los valores como UIEdgeInsets (arriba: 0, izquierda: -4.0, abajo: 0, derecha: -4.0).
Darkwonder
UIEdgeInset.zerofunciona utilizando XCode 8.3 y iOS 10.3 Simulator
Simon Warta
10

Último Swift:

self.textView.textContainerInset = .init(top: -2, left: 0, bottom: 0, right: 0)
self.textView.textContainer.lineFragmentPadding = 0
Urvish Modi
fuente
Gran respuesta. Esta respuesta elimina el recuadro superior adicional. textView.textContainerInset = UIEdgeInsets.zero no elimina 2 píxeles del recuadro superior.
korgx9
10

Aquí hay una versión actualizada de la muy útil respuesta de Fattie. Agrega 2 líneas importantes que me ayudaron a que el diseño funcionara en iOS 10 y 11 (y probablemente también en las más bajas):

@IBDesignable class UITextViewFixed: UITextView {
    override func layoutSubviews() {
        super.layoutSubviews()
        setup()
    }
    func setup() {
        translatesAutoresizingMaskIntoConstraints = true
        textContainerInset = UIEdgeInsets.zero
        textContainer.lineFragmentPadding = 0
        translatesAutoresizingMaskIntoConstraints = false
    }
}

¡Las líneas importantes son las dos translatesAutoresizingMaskIntoConstraints = <true/false>declaraciones!

¡Esto sorprendentemente elimina todos los márgenes en todas mis circunstancias!

Si bien textViewno es el primer respondedor, puede suceder que haya un margen inferior extraño que no se pueda resolver utilizando el sizeThatFitsmétodo que se menciona en la respuesta aceptada.

Al tocar el textView de repente, el extraño margen inferior desapareció y todo parecía como debería, pero solo tan pronto como el textView tiene firstResponder .

Así que leí aquí en SO que habilitar y deshabilitar translatesAutoresizingMaskIntoConstraintsayuda al configurar el marco / límites manualmente entre las llamadas.

Afortunadamente, esto no solo funciona con la configuración del marco, sino con las 2 líneas de setup()intercalado entre las dos translatesAutoresizingMaskIntoConstraintsllamadas.

Esto, por ejemplo, es muy útil al calcular el marco de una vista usando systemLayoutSizeFittingaUIView . ¡Devuelve el tamaño correcto (que anteriormente no tenía)!

Como en la respuesta original mencionada:
¡No olvide desactivar scrollEnabled en el Inspector!
Esa solución funciona correctamente en storyboard, así como en tiempo de ejecución.

Eso es todo, ¡ahora estás realmente listo!

Fab1n
fuente
5

Para swift 4, Xcode 9

Utilice la siguiente función para cambiar el margen / relleno del texto en UITextView

func público UIEdgeInsetsMake (_ arriba: CGFloat, _ izquierda: CGFloat, _ abajo: CGFloat, _ derecha: CGFloat) -> UIEdgeInsets

entonces en este caso es

 self.textView?.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0)
Shan Ye
fuente
3

Al hacer la solución insertada, todavía tenía relleno en el lado derecho y en la parte inferior. También la alineación del texto estaba causando problemas. La única forma segura de fuego que encontré fue poner la vista de texto dentro de otra vista que está recortada a límites.

rp90
fuente
3

Aquí hay una pequeña extensión fácil que eliminará el margen predeterminado de Apple de cada vista de texto en su aplicación.

Nota: Interface Builder seguirá mostrando el margen anterior, pero su aplicación funcionará como se esperaba.

extension UITextView {

   open override func awakeFromNib() {
      super.awakeFromNib();
      removeMargins();
   }

   /** Removes the Apple textview margins. */
   public func removeMargins() {
      self.contentInset = UIEdgeInsetsMake(
         0, -textContainer.lineFragmentPadding,
         0, -textContainer.lineFragmentPadding);
   }
}
Cal S
fuente
1

Para mí (iOS 11 y Xcode 9.4.1) lo que funcionó mágicamente fue configurar la propiedad textView.font al UIFont.preferred(forTextStyle:UIFontTextStyle) estilo y también la primera respuesta mencionada por @Fattie. Pero la respuesta @Fattie no funcionó hasta que configuré la propiedad textView.font, de lo contrario, UITextView sigue comportándose de manera errática.

Nikhil Pandey
fuente
1

En caso de que alguien busque la última versión de Swift, el siguiente código funciona bien con Xcode 10.2 y Swift 4.2

yourTextView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
Bhavin_m
fuente
0

He encontrado un enfoque más, obtener vista con texto de las subvistas de UITextView y configurarlo en el método layoutSubview de una subclase:

- (void)layoutSubviews {
    [super layoutSubviews];

    const int textViewIndex = 1;
    UIView *textView = [self.subviews objectAtIndex:textViewIndex];
    textView.frame = CGRectMake(
                                 kStatusViewContentOffset,
                                 0.0f,
                                 self.bounds.size.width - (2.0f * kStatusViewContentOffset),
                                 self.bounds.size.height - kStatusViewContentOffset);
}
Nikita
fuente
0

El desplazamiento textView también afecta la posición del texto y hace que parezca que no está centrado verticalmente. Logré centrar el texto en la vista desactivando el desplazamiento y configurando el recuadro superior en 0:

    textView.scrollEnabled = NO;
    textView.textContainerInset = UIEdgeInsetsMake(0, textView.textContainerInset.left, textView.textContainerInset.bottom, textView.textContainerInset.right);

Por alguna razón aún no lo he descubierto, el cursor todavía no está centrado antes de que comience a escribir, pero el texto se centra inmediatamente cuando empiezo a escribir.

Alex
fuente
0

En caso de que desee establecer una cadena HTML y evitar el relleno inferior, asegúrese de no utilizar etiquetas de bloque, es decir, div, p.

En mi caso, esta fue la razón. Puede probarlo fácilmente reemplazando las ocurrencias de las etiquetas de bloque con, por ejemplo, la etiqueta span.

Dawid Płatek
fuente
0

Para SwiftUI

Si está haciendo su propio TextView usando UIViewRepresentabley desea controlar el relleno, en su makeUIViewfunción, simplemente haga:

uiTextView.textContainerInset = UIEdgeInsets(top: 10, left: 18, bottom: 0, right: 18)

o lo que quieras

P. Ent
fuente
-4
[firstNameTextField setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter];
to0nice4eva
fuente