UITextView, który rozwija się do tekstu przy użyciu automatycznego układu


163

Mam widok, który jest całkowicie programowo opracowany przy użyciu automatycznego układu. Mam UITextView w środku widoku z elementami powyżej i poniżej. Wszystko działa dobrze, ale chcę mieć możliwość rozwijania UITextView w miarę dodawania tekstu. Powinno to zepchnąć wszystko poniżej, gdy się rozszerzy.

Wiem, jak to zrobić na zasadzie „sprężyn i rozpórek”, ale czy jest na to sposób automatycznego układu? Jedynym sposobem, o którym mogę pomyśleć, jest usuwanie i ponowne dodawanie ograniczenia za każdym razem, gdy musi się rozwinąć.


5
Możesz utworzyć IBOutlet z ograniczeniem wysokości dla widoku tekstowego i po prostu zmodyfikować stałą, gdy chcesz, aby wzrosła, bez konieczności usuwania i dodawania.
rdelmar

4
2017, ogromna wskazówka, która często Cię podtrzymuje: DZIAŁA TYLKO JEŚLI USTAWISZ TO PO WIDOCZENIU> POJAWI SIĘ < jest to bardzo frustrujące, jeśli ustawisz tekst na ViewDidLoad - to nie zadziała!
Fattie

2
wskazówka nr 2: jeśli wywołasz [someView.superView layoutIfNeeded] TO może to zadziałać w viewWillAppear ();)
Yaro

Odpowiedzi:


30

Widok zawierający UITextView zostanie przypisany do jego rozmiaru setBoundsprzez AutoLayout. Więc to właśnie zrobiłem. W superviewie początkowo wszystkie inne ograniczenia są ustawione tak, jak powinny, a na koniec umieściłem jedno specjalne ograniczenie dla wysokości UITextView i zapisałem je w zmiennej instancji.

_descriptionHeightConstraint = [NSLayoutConstraint constraintWithItem:_descriptionTextView
                                 attribute:NSLayoutAttributeHeight 
                                 relatedBy:NSLayoutRelationEqual 
                                    toItem:nil 
                                 attribute:NSLayoutAttributeNotAnAttribute 
                                multiplier:0.f 
                                 constant:100];

[self addConstraint:_descriptionHeightConstraint];

W setBoundsmetodzie zmieniłem następnie wartość stałej.

-(void) setBounds:(CGRect)bounds
{
    [super setBounds:bounds];

    _descriptionTextView.frame = bounds;
    CGSize descriptionSize = _descriptionTextView.contentSize;

    [_descriptionHeightConstraint setConstant:descriptionSize.height];

    [self layoutIfNeeded];
}

4
Możesz również zaktualizować ograniczenie w „textViewDidChange:” w UITextViewDelegate
LK__,

2
Niezłe rozwiązanie. Zauważ, że możesz również utworzyć ograniczenie w Interface Builder i podłączyć je za pomocą gniazdka. W połączeniu z używaniem textViewDidChangetego powinno zaoszczędzić trochę kłopotów podczas tworzenia podklas i pisania kodu…
Bob Vork

3
Należy pamiętać, że textViewDidChange:nie zostanie wywołana podczas programowego dodawania tekstu do UITextView.
wanderlust

Sugestia @BobVork działa dobrze XCODE 11 / iOS13, ustaw stałą ograniczenia, gdy wartość widoku tekstu jest ustawiona w viewWillAppear i zaktualizuj w textViewDidChange.
ptc

549

Podsumowanie: Wyłącz przewijanie widoku tekstu i nie ograniczaj jego wysokości.

Aby zrobić to programowo, umieść następujący kod w viewDidLoad:

let textView = UITextView(frame: .zero, textContainer: nil)
textView.backgroundColor = .yellow // visual debugging
textView.isScrollEnabled = false   // causes expanding height
view.addSubview(textView)

// Auto Layout
textView.translatesAutoresizingMaskIntoConstraints = false
let safeArea = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
    textView.topAnchor.constraint(equalTo: safeArea.topAnchor),
    textView.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
    textView.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor)
])

Aby to zrobić w programie Interface Builder, wybierz widok tekstu, odznacz opcję Przewijanie włączone w Inspektorze atrybutów i dodaj ograniczenia ręcznie.

Uwaga: jeśli masz inne widoki powyżej / poniżej swojego widoku tekstu, rozważ użycie UIStackViewdo ich rozmieszczenia.


12
Działa dla mnie (iOS 7 @ xcode 5). Jedynym zastrzeżeniem jest to, że po prostu odznaczenie „scrolling enabled” w IB nie pomoże. Musisz to zrobić programowo.
Kenneth Jiang,

10
Jak ustawić maksymalną wysokość w widoku tekstu, a następnie przewinąć widok tekstu, gdy jego zawartość przekroczy wysokość widoku tekstu?
ma11hew 28

3
@iOSGeek po prostu utwórz podklasę UITextView i nadpisz w intrinsicContentSizeten sposób - (CGSize)intrinsicContentSize { CGSize size = [super intrinsicContentSize]; if (self.text.length == 0) { size.height = UIViewNoIntrinsicMetric; } return size; }. Powiedz mi, czy to działa dla Ciebie. Sam tego nie przetestowałem.
manuelwaldner

1
Ustaw tę właściwość i umieść UITextView w UIStackView. Voilla - Self Resizing textView!)
Nik Kov

3
Ta odpowiedź jest złota. Z Xcode 9 działa również, jeśli wyłączysz pole wyboru w IB.
biografia z

48

Oto rozwiązanie dla osób, które wolą to wszystko robić za pomocą automatycznego układu:

Inspektor rozmiaru:

  1. Ustaw pionowy priorytet odporności na kompresję zawartości na 1000.

  2. Obniż priorytet wysokości wiązania, klikając „Edytuj” w obszarze Ograniczenia. Po prostu zrób to mniej niż 1000.

wprowadź opis obrazu tutaj

W Inspektorze atrybutów:

  1. Odznacz „Przewijanie włączone”

To działa doskonale. Nie ustawiłem wysokości. Wymagane było jedynie zwiększenie priorytetu odporności na ściskanie pionowe UItextview do 1000. UItextView ma kilka wstawek, dlatego autolayout wyświetla błędy, ponieważ UItextview będzie potrzebować co najmniej 32 wysokości, nawet jeśli nie ma w nim treści, gdy przewijanie jest wyłączone.
nr5

Aktualizacja: ograniczenie wysokości jest wymagane, jeśli chcesz, aby wysokość wynosiła minimum 20 lub cokolwiek innego. W innym przypadku 32 będzie uważane za minimalną wysokość.
nr5

@Nil Cieszę się, że to pomaga.
Michael Revlis

@MichaelRevlis, To działa świetnie. ale gdy jest duży tekst, tekst nie jest wyświetlany. możesz zaznaczyć tekst do innego przetwarzania, ale nie jest on widoczny. czy możesz mi w tym pomóc. ? dzieje się tak, gdy odznaczam włączoną opcję przewijania. kiedy włączam przewijanie widoku tekstu, wyświetla się idealnie, ale chcę, aby widok tekstu nie był przewijany.
Bhautik Ziniya

@BhautikZiniya, czy próbowałeś zbudować swoją aplikację na prawdziwym urządzeniu? Myślę, że to może być symulator wyświetlający błąd. Mam UITextView z czcionką o rozmiarze 100. Jest dobrze wyświetlany na prawdziwym urządzeniu, ale teksty są niewidoczne na symulatorze.
Michael Revlis

38

UITextView nie zapewnia intrinsicContentSize, więc musisz ją podklasować i podać. Aby rosła automatycznie, unieważnij intrinsicContentSize w layoutSubviews. Jeśli używasz czegokolwiek innego niż domyślny contentInset (czego nie polecam), może być konieczne dostosowanie obliczenia intrinsicContentSize.

@interface AutoTextView : UITextView

@end

#import "AutoTextView.h"

@implementation AutoTextView

- (void) layoutSubviews
{
    [super layoutSubviews];

    if (!CGSizeEqualToSize(self.bounds.size, [self intrinsicContentSize])) {
        [self invalidateIntrinsicContentSize];
    }
}

- (CGSize)intrinsicContentSize
{
    CGSize intrinsicContentSize = self.contentSize;

    // iOS 7.0+
    if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0f) {
        intrinsicContentSize.width += (self.textContainerInset.left + self.textContainerInset.right ) / 2.0f;
        intrinsicContentSize.height += (self.textContainerInset.top + self.textContainerInset.bottom) / 2.0f;
    }

    return intrinsicContentSize;
}

@end

1
To działało dobrze dla mnie. Ponadto ustawiłbym „scrollEnabled” na NIE w Interface Builder dla tego widoku tekstowego, ponieważ pokazuje on już całą swoją zawartość bez konieczności przewijania. Co ważniejsze, jeśli znajduje się w rzeczywistym widoku przewijania, możesz doświadczyć przeskakiwania kursora w pobliżu dolnej części widoku tekstu, jeśli nie wyłączysz przewijania w samym widoku tekstu.
idStar

To działa dobrze. Co ciekawe, gdy nadpisujesz, setScrollEnabled:aby zawsze ustawić go na NO, otrzymasz nieskończoną pętlę dla stale rosnącego wewnętrznego rozmiaru zawartości.
Pascal

IOS 7.0 potrzebne, ja też zadzwonić [textView sizeToFit]w textViewDidChange:zwrotnego Delegat Auto Układ rosnąć lub kurczyć TextView (i przeliczyć żadnych ograniczeń utrzymaniu) po każdym naciśnięciu klawisza.
followben

6
Wydaje się, że nie jest to już konieczne w iOS 7.1 Aby naprawić i zapewnić kompatybilność wsteczną, zawiń warunek -(void)layoutSubviewsi zawiń cały kod za -(CGSize)intrinsicContentSizepomocą version >= 7.0f && version < 7.1f+else return [super intrinsicContentSize];
David James

1
@DavidJames jest to nadal konieczne w iOS 7.1.2, nie wiem, gdzie testujesz.
Softlion

28

Możesz to zrobić poprzez scenorys, po prostu wyłącz „Przewijanie włączone” :)

StoryBoard


2
Najlepsza odpowiedź na to pytanie
Ivan Byelko

To najlepsze rozwiązanie, gdy pracujesz z Auto Layout.
JanApotheker

1
Ustawienia automatycznego układu (wiodący, kończący, górny, dolny, BEZ wysokości / szerokości ) + przewijanie wyłączone. i gotowe. Dzięki!
Farhan Arshad,

15

Zauważyłem, że nie jest to zupełnie rzadkie w sytuacjach, w których nadal możesz potrzebować isScrollEnabled ustawione na true, aby umożliwić rozsądną interakcję z interfejsem użytkownika. Prostym przypadkiem jest to, gdy chcesz zezwolić na automatycznie rozwijany widok tekstu, ale nadal ograniczyć jego maksymalną wysokość do czegoś rozsądnego w UITableView.

Oto podklasa UITextView, którą wymyśliłem, która umożliwia automatyczne rozszerzanie z automatycznym układem, ale nadal można ograniczyć do maksymalnej wysokości i która będzie zarządzać przewijaniem widoku w zależności od wysokości. Domyślnie widok będzie się rozszerzał w nieskończoność, jeśli masz ustawione ograniczenia w ten sposób.

import UIKit

class FlexibleTextView: UITextView {
    // limit the height of expansion per intrinsicContentSize
    var maxHeight: CGFloat = 0.0
    private let placeholderTextView: UITextView = {
        let tv = UITextView()

        tv.translatesAutoresizingMaskIntoConstraints = false
        tv.backgroundColor = .clear
        tv.isScrollEnabled = false
        tv.textColor = .disabledTextColor
        tv.isUserInteractionEnabled = false
        return tv
    }()
    var placeholder: String? {
        get {
            return placeholderTextView.text
        }
        set {
            placeholderTextView.text = newValue
        }
    }

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        isScrollEnabled = false
        autoresizingMask = [.flexibleWidth, .flexibleHeight]
        NotificationCenter.default.addObserver(self, selector: #selector(UITextInputDelegate.textDidChange(_:)), name: Notification.Name.UITextViewTextDidChange, object: self)
        placeholderTextView.font = font
        addSubview(placeholderTextView)

        NSLayoutConstraint.activate([
            placeholderTextView.leadingAnchor.constraint(equalTo: leadingAnchor),
            placeholderTextView.trailingAnchor.constraint(equalTo: trailingAnchor),
            placeholderTextView.topAnchor.constraint(equalTo: topAnchor),
            placeholderTextView.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override var text: String! {
        didSet {
            invalidateIntrinsicContentSize()
            placeholderTextView.isHidden = !text.isEmpty
        }
    }

    override var font: UIFont? {
        didSet {
            placeholderTextView.font = font
            invalidateIntrinsicContentSize()
        }
    }

    override var contentInset: UIEdgeInsets {
        didSet {
            placeholderTextView.contentInset = contentInset
        }
    }

    override var intrinsicContentSize: CGSize {
        var size = super.intrinsicContentSize

        if size.height == UIViewNoIntrinsicMetric {
            // force layout
            layoutManager.glyphRange(for: textContainer)
            size.height = layoutManager.usedRect(for: textContainer).height + textContainerInset.top + textContainerInset.bottom
        }

        if maxHeight > 0.0 && size.height > maxHeight {
            size.height = maxHeight

            if !isScrollEnabled {
                isScrollEnabled = true
            }
        } else if isScrollEnabled {
            isScrollEnabled = false
        }

        return size
    }

    @objc private func textDidChange(_ note: Notification) {
        // needed incase isScrollEnabled is set to true which stops automatically calling invalidateIntrinsicContentSize()
        invalidateIntrinsicContentSize()
        placeholderTextView.isHidden = !text.isEmpty
    }
}

Jako bonus istnieje możliwość dołączania tekstu zastępczego podobnego do UILabel.


7

Możesz to również zrobić bez tworzenia podklas UITextView. Spójrz na moją odpowiedź na pytanie Jak dopasować UITextView do jego zawartości w systemie iOS 7?

Użyj wartości tego wyrażenia:

[textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)].height

zaktualizować constantSpośród textView„s wysokości UILayoutConstraint.


3
Gdzie to robisz i czy robisz cokolwiek innego, aby upewnić się, że UITextView jest skonfigurowany? Robię to, a widok tekstu sam się rozmiaruje, ale sam tekst jest odcinany w połowie wysokości UITextView. Tekst jest ustawiany programowo tuż przed wykonaniem powyższych czynności.
chadbag

Uważam, że jest to bardziej przydatne niż sztuczka scrollEnabled = NO, jeśli chcesz ustawić szerokość ręcznie.
jchnxu

3

Ważna uwaga:

Ponieważ UITextView jest podklasą UIScrollView, podlega właściwości automaticAdjustsScrollViewInsets UIViewController.

Jeśli konfigurujesz układ, a TextView jest pierwszym widokiem podrzędnym w hierarchii UIViewControllers, jego contentInsets zostanie zmodyfikowany, jeśli parametr automaticAdjustsScrollViewInsets ma wartość true, co czasami powoduje nieoczekiwane zachowanie w układzie automatycznym.

Jeśli więc masz problemy z automatycznym układem i widokami tekstu, spróbuj ustawić automaticallyAdjustsScrollViewInsets = falsena kontrolerze widoku lub przesunąć textView do przodu w hierarchii.


2

To bardziej bardzo ważny komentarz

Kluczem do zrozumienia, dlaczego odpowiedź firmy Vitaminwater działa, są trzy rzeczy:

  1. Wiedz, że UITextView jest podklasą klasy UIScrollView
  2. Dowiedz się, jak działa ScrollView i jak jest obliczany jego contentSize. Aby uzyskać więcej, zobacz tę tutaj odpowiedź i jej różne rozwiązania i komentarze.
  3. Dowiedz się, czym jest contentSize i jak jest obliczany. Zobacz tutaj i tutaj . Może również pomóc, że ustawienie contentOffsetjest prawdopodobnie niczym innym jak:

func setContentOffset(offset: CGPoint)
{
    CGRect bounds = self.bounds
    bounds.origin = offset
    self.bounds = bounds
}

Aby uzyskać więcej informacji, zobacz objc scrollview i zrozumienie scrollview


Łącząc te trzy razem, łatwo zrozumiesz, że musisz zezwolić na wewnętrzną contentSize textView na pracę z ograniczeniami AutoLayout elementu textView, aby sterować logiką. To prawie tak, jakbyś textView działał jak UILabel

Aby tak się stało, musisz wyłączyć przewijanie, co w zasadzie oznacza rozmiar scrollView, rozmiar contentSize, aw przypadku dodania containerView, rozmiar containerView byłby taki sam. Kiedy są takie same, NIE masz przewijania. I byś to zrobił . Mając oznacza, że ​​nie przewinąłeś w dół. Ani jeden punkt mniej! W rezultacie tekst textView zostanie rozciągnięty.0 contentOffset0 contentOffSet

Nie jest też nic warta, co oznacza, że ​​granice scrollView i ramka są identyczne. Jeśli przewinąć 5 punktów wówczas contentOffset byłoby , w czasie, gdy będzie równa0 contentOffset5scrollView.bounds.origin.y - scrollView.frame.origin.y5


1

Rozwiązanie Plug and Play - Xcode 9

Układ automatyczny podobnie jak UILabel, z detekcją łącza , wybór tekstu , edycji i przewijania z UITextView.

Obsługuje automatycznie

  • Bezpieczna strefa
  • Wstawki treści
  • Wypełnienie fragmentów linii
  • Wstawki kontenera tekstu
  • Ograniczenia
  • Widoki stosu
  • Ciągi przypisane
  • Cokolwiek.

Wiele z tych odpowiedzi dało mi 90%, ale żadna nie była niezawodna.

Wrzuć tę UITextViewpodklasę i jesteś dobry.


#pragma mark - Init

- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer
{
    self = [super initWithFrame:frame textContainer:textContainer];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (void)commonInit
{
    // Try to use max width, like UILabel
    [self setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
    
    // Optional -- Enable / disable scroll & edit ability
    self.editable = YES;
    self.scrollEnabled = YES;
    
    // Optional -- match padding of UILabel
    self.textContainer.lineFragmentPadding = 0.0;
    self.textContainerInset = UIEdgeInsetsZero;
    
    // Optional -- for selecting text and links
    self.selectable = YES;
    self.dataDetectorTypes = UIDataDetectorTypeLink | UIDataDetectorTypePhoneNumber | UIDataDetectorTypeAddress;
}

#pragma mark - Layout

- (CGFloat)widthPadding
{
    CGFloat extraWidth = self.textContainer.lineFragmentPadding * 2.0;
    extraWidth +=  self.textContainerInset.left + self.textContainerInset.right;
    if (@available(iOS 11.0, *)) {
        extraWidth += self.adjustedContentInset.left + self.adjustedContentInset.right;
    } else {
        extraWidth += self.contentInset.left + self.contentInset.right;
    }
    return extraWidth;
}

- (CGFloat)heightPadding
{
    CGFloat extraHeight = self.textContainerInset.top + self.textContainerInset.bottom;
    if (@available(iOS 11.0, *)) {
        extraHeight += self.adjustedContentInset.top + self.adjustedContentInset.bottom;
    } else {
        extraHeight += self.contentInset.top + self.contentInset.bottom;
    }
    return extraHeight;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    
    // Prevents flashing of frame change
    if (CGSizeEqualToSize(self.bounds.size, self.intrinsicContentSize) == NO) {
        [self invalidateIntrinsicContentSize];
    }
    
    // Fix offset error from insets & safe area
    
    CGFloat textWidth = self.bounds.size.width - [self widthPadding];
    CGFloat textHeight = self.bounds.size.height - [self heightPadding];
    if (self.contentSize.width <= textWidth && self.contentSize.height <= textHeight) {
        
        CGPoint offset = CGPointMake(-self.contentInset.left, -self.contentInset.top);
        if (@available(iOS 11.0, *)) {
            offset = CGPointMake(-self.adjustedContentInset.left, -self.adjustedContentInset.top);
        }
        if (CGPointEqualToPoint(self.contentOffset, offset) == NO) {
            self.contentOffset = offset;
        }
    }
}

- (CGSize)intrinsicContentSize
{
    if (self.attributedText.length == 0) {
        return CGSizeMake(UIViewNoIntrinsicMetric, UIViewNoIntrinsicMetric);
    }
    
    CGRect rect = [self.attributedText boundingRectWithSize:CGSizeMake(self.bounds.size.width - [self widthPadding], CGFLOAT_MAX)
                                                    options:NSStringDrawingUsesLineFragmentOrigin
                                                    context:nil];
    
    return CGSizeMake(ceil(rect.size.width + [self widthPadding]),
                      ceil(rect.size.height + [self heightPadding]));
}

0

Odpowiedź firmy Vitaminwater działa dla mnie.

Jeśli tekst w widoku tekstowym podskakuje w górę iw dół podczas edycji, po ustawieniu [textView setScrollEnabled:NO];ustawSize Inspector > Scroll View > Content Insets > Never .

Mam nadzieję, że to pomoże.


0

Umieść ukryty UILabel pod swoim widokiem tekstu. Linie etykiet = 0. Ustaw ograniczenia UITextView tak, aby były równe UILabel (centerX, centerY, width, height). Działa, nawet jeśli pozostawisz zachowanie przewijania textView.


-1

BTW, zbudowałem rozwijający się UITextView przy użyciu podklasy i nadpisywania wewnętrznego rozmiaru zawartości. Odkryłem błąd w UITextView, który możesz chcieć zbadać we własnej implementacji. Oto problem:

Rozwijany widok tekstu zmniejszy się, aby pomieścić rosnący tekst, jeśli będziesz wpisywać pojedyncze litery na raz. Ale jeśli wkleisz do niego fragment tekstu, nie urosnie, ale tekst przewinie się w górę, a tekst na górze zniknie z widoku.

Rozwiązanie: Zastąp setBounds: w swojej podklasie. Z jakiegoś nieznanego powodu wklejanie spowodowało, że wartość bounds.origin.y była non-zee (33 w każdym przypadku, który widziałem). Więc przesłoniłem setBounds: aby zawsze ustawiać bounds.origin.y na zero. Naprawiono problem.


-1

Oto szybkie rozwiązanie:

Ten problem może wystąpić, jeśli ustawiono właściwość clipsToBounds na wartość false w widoku tekstu. Jeśli go po prostu usuniesz, problem zniknie.

myTextView.clipsToBounds = false //delete this line

-3
Obj C:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

@property (nonatomic) UITextView *textView;
@end



#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

@synthesize textView;

- (void)viewDidLoad{
    [super viewDidLoad];
    [self.view setBackgroundColor:[UIColor grayColor]];
    self.textView = [[UITextView alloc] initWithFrame:CGRectMake(30,10,250,20)];
    self.textView.delegate = self;
    [self.view addSubview:self.textView];
}

- (void)didReceiveMemoryWarning{
    [super didReceiveMemoryWarning];
}

- (void)textViewDidChange:(UITextView *)txtView{
    float height = txtView.contentSize.height;
    [UITextView beginAnimations:nil context:nil];
    [UITextView setAnimationDuration:0.5];

    CGRect frame = txtView.frame;
    frame.size.height = height + 10.0; //Give it some padding
    txtView.frame = frame;
    [UITextView commitAnimations];
}

@end
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.