自動レイアウトを使用してテキストに展開するUITextView


163

プログラムで自動レイアウトを使用して完全にレイアウトされたビューがあります。ビューの中央にUITextViewがあり、その上下にアイテムがあります。すべてが正常に動作しますが、テキストが追加されたときにUITextViewを展開できるようにしたいと考えています。これにより、拡張時に下にあるすべてが押し下げられます。

私はこれを「スプリングとストラット」の方法で行う方法を知っていますが、これを行う自動レイアウト方法はありますか?私が考えることができる唯一の方法は、成長する必要があるたびに制約を削除して再度追加することです。


5
テキストビューの高さの制約に合わせてIBOutletを作成し、定数を大きくしたいときに変更するだけで、削除して追加する必要はありません。
rdelmar 2013年

4
2017年、大きなヒントになることが多いヒント:VIEWDID> APPEAR < の後に設定した場合にのみ機能しますViewDidLoadでテキストを設定すると、イライラします-機能しません!
Fattie

2
ヒントNo2:[someView.superView layoutIfNeeded]を呼び出すと、viewWillAppear()で機能する場合があります;)
Yaro

回答:


30

UITextViewを含むビューには、AutoLayout setBoundsによってサイズが割り当てられます。だから、これは私がやったことです。スーパービューは最初は他のすべての制約を適切に設定し、最後にUITextViewの高さに対して1つの特別な制約を設定して、インスタンス変数に保存しました。

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

[self addConstraint:_descriptionHeightConstraint];

setBounds方法、それから一定の値を変更しました。

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

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

    [_descriptionHeightConstraint setConstant:descriptionSize.height];

    [self layoutIfNeeded];
}

4
UITextViewDelegateの 'textViewDidChange:'で制約を更新することもできます
LK__

2
素晴らしい解決策。Interface Builderで制約を作成し、アウトレットを使用して接続することもできます。textViewDidChangeこれを使用すると、手間のかかるサブクラス化とコードの記述を節約できます…
Bob Vork

3
覚えておいてtextViewDidChange:、プログラムUITextViewにテキストを追加するときに呼び出されることはありません。
wanderlust

@BobVorkの提案はXCODE 11 / iOS13でうまく機能し、viewWillAppearでテキストビューの値を設定し、textViewDidChangeで更新するときに制約定数を設定します。
ptc

549

概要:テキストビューのスクロールを無効にし、高さを制限しないでください。

これをプログラムで行うには、次のコードをに配置します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)
])

Interface Builderでこれを行うには、テキストビューを選択し、属性インスペクタでスクロール有効をオフにして、制約を手動で追加します。

注:テキストビューの上/下に他のビューがある場合は、a UIStackViewを使用してそれらすべてを配置することを検討してください。


12
私のために動作します(iOS 7 @ xcode 5)。注意点は、IBで「スクロール可能」のチェックを外すだけでは役に立たないということです。プログラムで行う必要があります。
Kenneth Jiang

10
テキストビューに最大の高さを設定し、コンテンツがテキストビューの高さを超えた後にテキストビューをスクロールするにはどうすればよいですか?
ma11hew28 2016

3
@iOSGeekはUITextViewサブクラスを作成し、intrinsicContentSizeこのように上書きするだけ- (CGSize)intrinsicContentSize { CGSize size = [super intrinsicContentSize]; if (self.text.length == 0) { size.height = UIViewNoIntrinsicMetric; } return size; }です。それがあなたのために働いているかどうか教えてください。自分でテストしませんでした。
manuelwaldner 2017年

1
このプロパティを設定し、UITextViewをUIStackViewに配置します。Voilla-textViewのサイズを自動変更!)
Nik Kov

3
この答えは金です​​。Xcode 9では、IBのチェックボックスを無効にしても機能します。
バイオ

48

自動レイアウトですべてを実行したい人のためのソリューションは次のとおりです。

サイズインスペクターで:

  1. コンテンツの圧縮耐性の優先度を垂直に1000に設定します。

  2. 制約の「編集」をクリックして、制約の高さの優先度を下げます。1000未満にしてください。

ここに画像の説明を入力してください

属性インスペクターで:

  1. 「スクロール可能」のチェックを外します

これは完全に機能しています。高さは設定していません。UItextviewの垂直圧縮耐性の優先度を1000に上げることだけが必要でした。UItextViewにはインセットがいくつかあります。そのため、スクロールが無効になっている場合、コンテンツが含まれていなくても、UItextviewには少なくとも32の高さが必要になるため、autolayoutでエラーが発生します。
nr5

更新:高さを20などに最小にする場合は、高さの制約が必要です。それ以外の場合、32は最小高さと見なされます。
nr5

@Nil Gladがお役に立てば幸いです。
マイケルレブリス2017

@MichaelRevlis、これは素晴らしい作品です。しかし、大きなテキストがある場合、テキストは表示されません。他の処理を行うテキストを選択できますが、表示されません。それで私を助けてくれませんか。?スクロールが有効になっているオプションをオフにすると発生します。テキストビューのスクロールを有効にすると、完全に表示されますが、テキストビューがスクロールしないようにします。
Bhautik Ziniya 2017年

@BhautikZiniya、実際のデバイスでアプリを構築しようとしましたか?バグを表示するシミュレータかもしれません。テキストのフォントサイズが100のUITextViewがあります。実際のデバイスでは問題なく表示されますが、シミュレータではテキストが表示されません。
マイケル・レブリス2017年

38

UITextViewは組み込みコンテンツサイズを提供しないため、それをサブクラス化して提供する必要があります。自動的に拡大するには、layoutSubviewsの組み込みコンテンツサイズを無効にします。デフォルトのcontentInset(お勧めしません)以外のものを使用する場合は、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
これは私にはうまくいきました。さらに、このテキストビューのインターフェイスビルダーでは、スクロールする必要なくすべてのコンテンツがすでに表示されているため、 'scrollEnabled'をNOに設定します。さらに重要なことに、実際のスクロールビュー内にある場合、テキストビュー自体のスクロールを無効にしないと、テキストビューの下部近くでカーソルがジャンプすることがあります。
idStar 2013年

これはうまくいきます。興味深いことに、オーバーライドsetScrollEnabled:して常にそれを設定NOすると、増大する固有のコンテンツサイズに対して無限ループが発生します。
Pascal

iOSの7.0に、私はまた、呼び出すために必要な[textView sizeToFit]textViewDidChange:自動レイアウトが拡大または縮小のTextViewを(と依存制約を再計算)するために、各キーストロークの後に、デリゲートコールバック。
followben

6
これはiOS 7.1では不要であると思われます。下位互換性を修正して提供するには、条件-(void)layoutSubviewsをラップすべてのコード-(CGSize)intrinsicContentSizeversion >= 7.0f && version < 7.1f+ でラップしますelse return [super intrinsicContentSize];
David James

1
@DavidJamesこれはiOS 7.1.2でも必要です。どこでテストしているかわかりません。
Softlion、2014年

28

ストーリーボードを使用してそれを行うことができます。「スクロール有効」を無効にするだけです:)

StoryBoard


2
この質問に対する最良の回答
Ivan Byelko

これは、自動レイアウトを使用する場合に最適なソリューションです。
JanApotheker

1
自動レイアウト設定(先頭、末尾、上、下、高さ/幅なし)+スクロールは無効です。そこに行きます。ありがとう!
Farhan Arshad、2018

15

妥当なUIインタラクションを可能にするためにisScrollEnabledをtrueに設定する必要がある状況では、これがまったく珍しいことではないことがわかりました。これの単純なケースは、自動拡張テキストビューを許可したいが、UITableViewで最大高さを適切なものに制限したい場合です。

ここに私が思いついたUITextViewのサブクラスがあります。これは自動レイアウトで自動拡張を可能にしますが、最大の高さに制限することができ、高さに応じてビューがスクロール可能かどうかを管理します。制約がそのように設定されている場合、デフォルトではビューは無期限に拡張されます。

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

おまけとして、UILabelと同様のプレースホルダーテキストを含めることができます。


7

サブクラス化せずにそれを行うこともできUITextViewます。見てい私はiOSの7上のコンテンツにUITextViewのサイズはどうすればよいの私の答えを?

この式の値を使用します。

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

更新するconstanttextViewの高さをUILayoutConstraint


3
UITextViewがすべてセットアップされていることを確認するために、これをどこで実行していますか?私はこれを行っており、テキストビュー自体のサイズは変更されていますが、テキスト自体がUITextViewの高さの半分までカットされています。テキストは、上記のものを実行する直前にプログラムで設定されます。
chadbag 2013

幅を手動で設定したい場合、これはscrollEnabled = NOトリックよりも一般的に便利であることがわかりました。
jchnxu 14

3

注意すべき重要なこと:

UITextViewはUIScrollViewのサブクラスであるため、UIViewControllerのautomaticallyAdjustsScrollViewInsetsプロパティの影響を受けます。

レイアウトを設定していて、TextViewがUIViewControllers階層の最初のサブビューである場合、automaticAdjustsScrollViewInsetsがtrueの場合、そのcontentInsetsが変更され、自動レイアウトで予期しない動作が発生することがあります。

したがって、自動レイアウトとテキストビューに問題がある場合はautomaticallyAdjustsScrollViewInsets = false、ビューコントローラーで設定するか、textViewを階層の前方に移動してみてください。


2

これは非常に重要なコメントです

ビタミンウォーターの答えが機能する理由を理解するための鍵は、3つのことです。

  1. UITextViewがUIScrollViewクラスのサブクラスであることを知っている
  2. ScrollViewの仕組みとそのcontentSizeの計算方法を理解します。詳細については、こちらの回答とそのさまざまなソリューションおよびコメントを参照してください 。
  3. contentSizeとは何か、およびその計算方法を理解します。こちらこちらをご覧ください。また、設定contentOffsetおそらく他に何もないことを助けるかもしれません:

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

詳細については、objc scrollviewおよびscrollviewの理解を参照してください


3つを組み合わせると、textViewの固有のcontentSizeがtextViewのAutoLayout制約に沿って動作してロジックを駆動できるようにする必要があることは簡単に理解できます。まるでtextViewがUILabelのように機能しているようです

これを実現するには、スクロールを無効にする必要があります。つまり、scrollViewのサイズ、contentSizeのサイズ、およびcontainerViewを追加する場合、containerViewのサイズはすべて同じになります。それらが同じ場合、スクロールはありません。そして、あなたは持っているでしょう。持っているということは、スクロールダウンしていないということです。1ポイントも下がっていません!その結果、textViewはすべて引き伸ばされます。0 contentOffset0 contentOffSet

また 、scrollViewの境界とフレームが同一であることを意味するものもありません。5ポイント下にスクロールすると、contentOffsetはになりますが、0 contentOffset5scrollView.bounds.origin.y - scrollView.frame.origin.y5


1

プラグアンドプレイソリューション-Xcode 9

自動レイアウトだけのようUILabelで、リンク検出テキストの選択編集およびスクロールUITextView

自動的に処理

  • 安全地帯
  • コンテンツインセット
  • 行フラグメントのパディング
  • テキストコンテナーインセット
  • 制約
  • スタックビュー
  • 属性付き文字列
  • なんでも。

これらの回答の多くは90%を達成しましたが、誰もが間違いなく使えるものはありませんでした。

このUITextViewサブクラスにドロップして、あなたは大丈夫です。


#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

ビタミンウォーターの答えは私のために働いています。

テキストビューのテキストが編集中に上下にバウンドする場合は、を設定した後[textView setScrollEnabled:NO];、を設定しSize Inspector > Scroll View > Content Insets > Neverます。

それが役に立てば幸い。


0

非表示のUILabelをテキストビューの下に配置します。ラベル行=0。UITextViewの制約をUILabel(centerX、centerY、幅、高さ)と等しくなるように設定します。textViewのスクロール動作を残しても機能します。


-1

ところで、私はサブクラスを使用して拡張UITextViewを作成し、固有のコンテンツサイズをオーバーライドしました。UITextViewのバグを発見しました。このバグを独自の実装で調査する必要があるかもしれません。ここに問題があります:

一度に1文字ずつ入力すると、拡大するテキストビューは拡大するテキストに対応するために縮小します。しかし、そこに大量のテキストを貼り付けると、テキストは大きくなりませんが、テキストは上にスクロールし、上部のテキストは見えなくなります。

解決策:サブクラスでsetBounds:をオーバーライドします。何らかの理由により、貼り付けによってbounds.origin.yの値が非ゼーションになります(私が目にしたすべての場合で33)。そのため、setBounds:をオーバーライドして、bounds.origin.yを常にゼロに設定しました。問題を修正しました。


-1

ここに簡単な解決策があります:

この問題は、テキストビューのclipsToBoundsプロパティをfalseに設定した場合に発生することがあります。単に削除すれば、問題はなくなります。

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.