Comment faire défiler UITextView en tapant/éditant

UPDATE Cela semble être un problème avec IOS 7 seulement. Une excellente solution de contournement a été ajoutée à la réponse acceptée.

j'ai créé un contrôle personnalisé qui contient UITextView et UILabel qui contient le titre du textview c'est à dire mon contrôle. Mon contrôle change automatiquement la taille pour adapter le textview et le titre. Avant que cela n'arrive, je change la taille du textview pour l'adapter au texte. Cela fonctionne de manière optimale.

j'ai ajouté la fonctionnalité de sorte que le textview défile automatiquement vers la dernière ligne. Ou c'est du moins ce que j'essaie. Cela fonctionne très bien tant que la dernière ligne contient autre chose que du texte vide. Si le texte est vide, il roule vers le bas de sorte que vous ne pouvez voir environ la moitié du curseur.

Qu'est-ce que je fais de mal?

pour que vous puissiez mieux le comprendre j'ai fait quelques images:

C'est moi en train de taper un mot et de faire des linebreaks. (Pas encore assez pour le faire faites défiler jusqu')

Before making a line break

Et la je fais un saut de ligne. (en appuyant sur entrée) regardez de près comment le curseur est divisé en deux. Telle est la question!

The Issue

j'ai fait la prochaine image pour que vous puissiez voir exactement ce à quoi je m'attendais.

What I Want!

27
demandé sur chrs 2013-08-06 05:44:54

14 réponses

problèmes avec d'autres réponses:

  • lorsque vous scannez seulement pour "\n", Si vous tapez une ligne de texte qui dépasse la largeur de la vue de texte, alors le défilement ne se produira pas.
  • quand toujours contentOffset dans textViewDidChange: si vous modifiez le milieu du texte que vous ne voulez pas faire défiler vers le bas.

La solution est d'ajouter à l'affichage de texte délégué:

- (void)textViewDidChange:(UITextView *)textView {
    CGRect line = [textView caretRectForPosition:
        textView.selectedTextRange.start];
    CGFloat overflow = line.origin.y + line.size.height
        - ( textView.contentOffset.y + textView.bounds.size.height
        - textView.contentInset.bottom - textView.contentInset.top );
    if ( overflow > 0 ) {
        // We are at the bottom of the visible text and introduced a line feed, scroll down (iOS 7 does not do it)
        // Scroll caret to visible area
        CGPoint offset = textView.contentOffset;
        offset.y += overflow + 7; // leave 7 pixels margin
        // Cannot animate with setContentOffset:animated: or caret will not appear
        [UIView animateWithDuration:.2 animations:^{
            [textView setContentOffset:offset];
        }];
    }
}
54
répondu davidisdk 2013-10-09 15:59:57

j'ai essayé de mettre dans votre textViewDidChange: un extrait de la forme:

if([textView.text hasSuffix:@"\n"])
    [self.textView setContentOffset:CGPointMake(0,INT_MAX) animated:YES];

ce n'est pas vraiment propre, je travaille à trouver de meilleures choses, mais pour l'instant ça marche :D

UPDATE: Comme il s'agit d'un bug qui ne se produit que sur iOS 7 (Beta 5, pour l'instant), vous pouvez faire un contournement avec ce code:

if([textView.text hasSuffix:@"\n"]) { 
    double delayInSeconds = 0.2; 
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); 
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 
        CGPoint bottomOffset = CGPointMake(0, self.textView.contentSize.height - self.textView.bounds.size.height); 
        [self.textView setContentOffset:bottomOffset animated:YES]; 
    }); 
}

Puis, sur iOS 6, vous pouvez choisir soit de régler le délai à 0,0 ou d'utiliser le contenu du bloc.

8
répondu Vik 2013-08-09 10:20:00

Utilisation De Swift 3: -

let line : CGRect = textView.caretRect(for: (textView.selectedTextRange?.start)!)
    print("line = \(line)")

    let overFlow = line.origin.y + line.size.height - (textView.contentOffset.y + textView.bounds.size.height - textView.contentInset.bottom - textView.contentInset.top)

    print("\n OverFlow = \(overFlow)")

    if (0 < overFlow)
    {
        // We are at the bottom of the visible text and introduced a line feed, scroll down (iOS 7 does not do it)
        // Scroll caret to visible area

        var offSet : CGPoint = textView.contentOffset

        print("offSet = \(offSet)")

        //leave 7 pixels margin
        offSet.y += (overFlow + 7)

        //Cannot animate with setContentOffset:animated: or caret will not appear

        UIView.animate(withDuration: 0.3, animations: {
            textView.setContentOffset(offSet, animated: true)
        })
    }
4
répondu kishor0011 2016-12-23 13:06:27

j'ai utilisé le code suivant dans la balise textViewDidChange: méthode et ça semble bien fonctionner.

- (void)textViewDidChange:(UITextView *)textView {
    CGPoint bottomOffset = CGPointMake(0, self.theTextView.contentSize.height - self.theTextView.bounds.size.height);
    [self.theTextView setContentOffset:bottomOffset animated:YES];
}

cela semble faire défiler l'UITextView légèrement plus loin pour que votre curseur ne soit pas coupé.

3
répondu hgwhittle 2013-08-08 16:05:06

réponse acceptée lors de L'utilisation de Xamarin / Monotouch ressemblera à

        textView.Changed += (object sender, EventArgs e) =>
        {

            var line = textView.GetCaretRectForPosition(textView.SelectedTextRange.start);
            var overflow = line.Top + line.Height -
                           (textView.ContentOffset.Y
                           + textView.Bounds.Size.Height
                           - textView.ContentInset.Bottom
                           - textView.ContentInset.Top);
            if (overflow > 0)
            {
                var offset = textView.ContentOffset;
                offset = new PointF(offset.X, offset.Y + overflow + 7);
                UIView.Animate(0.2f, () =>
                    {
                        textView.SetContentOffset(offset, false);
                    });
            }
        };
3
répondu Alex Sorokoletov 2017-05-23 12:10:32

La suite de la modification de Vik réponse a bien fonctionné pour moi:

if([_textView.text hasSuffix:@"\n"])
{
    if (_textView.contentSize.height - _textView.bounds.size.height > -30)
    {
        double delayInSeconds = 0.2;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
        {
            CGPoint bottomOffset = CGPointMake(0, _textView.contentSize.height - _textView.bounds.size.height);
            [_textView setContentOffset:bottomOffset animated:YES];
        });
    }
}
2
répondu shersa1986 2013-09-24 10:16:33

j'ai trouvé que si vous placez le code suivant dans viewWillAppear, il permettra de résoudre cela, et quelques autres questions qui UITextView semble avoir dans les bêtas:

[self.textView.layoutManager ensureLayoutForTextContainer:self.textView.textContainer];

1
répondu tharris 2013-08-18 22:38:00

Quelqu'un a-t-il signalé un bogue à apple concernant cette question? Cela ressemble à un bug assez évident qui est très facile à reproduire. Si personne ne répond, je vais déposer un radar avec un projet d'essai.

1
répondu tyler 2013-10-15 00:16:55

je pense que le meilleur moyen est de déterminer la position réelle du curseur pour voir si le défilement doit se produire.

- (void)textViewDidChange:(UITextView *)textView {
    // check to see if the cursor is at the end of the text
    if (textView.text.length == textView.selectedRange.location) {
        // find the caret position
        CGRect caret = [textView caretRectForPosition:textView.selectedTextRange.start];

        // determine the height of the visible text window
        UIEdgeInsets textInsets = textView.textContainerInset;
        CGFloat textViewHeight = textView.frame.size.height - textInsets.top - textInsets.bottom;
        // need to subtract the textViewHeight to correctly get the offset
        // that represents the top of the text window above the cursor
        textView.contentOffset = CGPointMake(textView.contentOffset.x, caret.origin.y - textViewHeight);
    }
}

le code ci-dessus va déterminer si le signe d'insertion est à la fin du texte. Si ce n'est pas le cas, ça ne défilera pas. Si c'est le cas (indépendamment de ce qu'est le dernier caractère), il va déterminer le déport correct à faire défiler et puis effectuer le défilement.

1
répondu mikeho 2015-06-26 22:27:36
- (void)textViewDidChange:(UITextView *)textView {
    CGRect frame = textView.frame;
    frame.size.height = textView.contentSize.height;
    textView.frame = frame;    
}

Cela fonctionne très bien pour moi. Si vous voulez créer une petite "bordure" entre le curseur et la zone de texte réelle, vous pouvez toujours ajouter quelques pixels à la hauteur. Comme ceci:

    frame.size.height = textView.contentSize.height+14;
0
répondu JustAnotherCoder 2013-08-06 12:52:36

la solution dans la réponse acceptée est inutilisable.

disons qu'il y a 1000 mots dans le textView et que le caractère final est "\n". Si vous éditez la première ligne de textView,hasSuffix:@"\n" retour YES et le textView défile immédiatement vers le bas du document.

ou, commencez avec un textView vide et tapez un mot, puis appuyez sur retour. Le texte défile vers le bas.

============  ============   ============   ============
 Te|           Text |         Text           
                              |


                                             Text
                                             |
============  ============   ============   ============

c'est Peut-être une meilleure solution, mais ce n'est pas parfait. Il vérifie si le signe d'insertion est en dessous d'un point maximum, puis passe au point maximum s'il est:

-(void)textViewDidChange:(UITextView *)textView {

    // Get caret frame
    UITextPosition *caret = [textView positionFromPosition:textView.beginningOfDocument offset:textView.selectedRange.location];
    CGRect caretFrame     = [textView caretRectForPosition:caret];

    // Get absolute y position of caret in textView
    float absCaretY       = caretFrame.origin.y - textView.contentOffset.y;

    // Set a max y for the caret (in this case the textView is resized to avoid the keyboard and an arbitrary padding is added)
    float maxCaretY       = textView.frame.size.height - 70;

    // Get how far below the maxY the caret is
    float overflow        = absCaretY - maxCaretY;

    // No need to scroll if the caret is above the maxY
    if (overflow < 0)
        return;

    // Need to add a delay for this to work
    double delayInSeconds = 0.2;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){

        // Scroll to the maxCaretY
        CGPoint contentOffset = CGPointMake(0, textView.contentOffset.y + overflow);
        [textView setContentOffset:contentOffset animated:YES];
    });
}
0
répondu ntesler 2013-09-27 07:23:55

Essayez d'utiliser

   textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
   textView.autoresizingSubviews = YES;

Il a résolu le problème pour moi pour iOS7.

0
répondu Shilpi 2013-12-13 06:22:33

sur iOS10 dans mon UITextView autosizing la clé pour moi était

// my method called on text change

- (void)updateLayout {

    [self invalidateIntrinsicContentSize];

    [UIView animateWithDuration:0.33 animations:^{

        [self.superview layoutIfNeeded];

        CGPoint bottomOffset = CGPointMake(0, self.contentSize.height - self.bounds.size.height);
        [self setContentOffset:bottomOffset animated:NO];

    } completion:nil];

}

toute la classe

#import "AutosizeTextView.h"

@implementation AutosizeTextView

- (instancetype)initWithFrame:(CGRect)frame {

    if (self = [super initWithFrame:frame]) {
        [self setup];
    }
    return self;
}

- (void)awakeFromNib {

    [super awakeFromNib];

    [self setup];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:self];
}

- (void)setText:(NSString *)text {
    [super setText:text];
    [self updateLayout];
}

- (CGSize)intrinsicContentSize {
    CGRect textRect = [self.layoutManager usedRectForTextContainer:self.textContainer];
    CGFloat height = textRect.size.height + self.textContainerInset.top + self.textContainerInset.bottom;
    return CGSizeMake(UIViewNoIntrinsicMetric, height);
}


////////////////////////////////////////////////////////////////////////
#pragma mark - Private
////////////////////////////////////////////////////////////////////////

- (void)setup {

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidChangeNotification:) name:UITextViewTextDidChangeNotification object:self];
    self.textContainer.lineFragmentPadding = 0;
    self.textContainerInset = UIEdgeInsetsMake(4, 4, 4, 4);

}

- (void)updateLayout {

    [self invalidateIntrinsicContentSize];

    [UIView animateWithDuration:0.33 animations:^{

        [self.superview layoutIfNeeded];

        CGPoint bottomOffset = CGPointMake(0, self.contentSize.height - self.bounds.size.height);
        [self setContentOffset:bottomOffset animated:NO];

    } completion:nil];

}

////////////////////////////////////////////////////////////////////////
#pragma mark - Notification
////////////////////////////////////////////////////////////////////////

- (void)textDidChangeNotification:(NSNotification *)notification {

    [self updateLayout];

}

@end
0
répondu Peter Lapisu 2016-10-28 12:40:15

Dans Swift 3

enter image description here

Définir la référence de sortie & délégué de textview

class ViewController: UIViewController , UITextViewDelegate{

@IBOutlet var txtViewRef: UITextView!

Dans le viewDidLoad ensemble de délégué et de Notification de changement de KeyboardFrame ou Masquer le clavier

 override func viewDidLoad() {
    super.viewDidLoad()

    txtViewRef.delegate = self
    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.updateTextView(notification:)), name: Notification.Name.UIKeyboardWillChangeFrame, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.updateTextView(notification:)), name: Notification.Name.UIKeyboardWillHide, object: nil)    
}

Créer une Fonction updateTextView Dans lesquels nous sommes le cadre du clavier et de l'évolution de l'encart de contenu et de défilement de l'indicateur de défilement et de la textview

func updateTextView(notification : Notification)
{
    let userInfo = notification.userInfo!
    let keyboardEndFrameScreenCoordinates = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
    let keyboardEndFrame = self.view.convert(keyboardEndFrameScreenCoordinates, to: view.window)

    if notification.name == Notification.Name.UIKeyboardWillHide{
        txtViewRef.contentInset = UIEdgeInsets.zero
    }
    else
    {
        txtViewRef.contentInset = UIEdgeInsetsMake(0, 0, keyboardEndFrame.height, 0)
        txtViewRef.scrollIndicatorInsets = txtViewRef.contentInset
    }

    txtViewRef.scrollRangeToVisible(txtViewRef.selectedRange)

}
0
répondu Mili Shah 2017-05-25 09:17:19