Содержимое UITextView становится неуместным с дополнительным пространством после изменения размера

Предпосылки и описание проблемы

Я сделал вертикальный текстовый вид для использования с монгольским языком. Это пользовательское текстовое представление, состоящее из трех слоев: дочернее UITextViewвид контейнера (который поворачивается на 90 градусов и переворачивается) для удержания UITextViewи родительский вид. (Смотрите здесь и здесь для получения дополнительной информации.)

Представление увеличивается в зависимости от размера содержимого основного текстового представления, если оно находится между минимальным и максимальным размером. Тем не менее, в течение последних нескольких дней я пытался исправить ошибку, при которой добавлялось дополнительное пространство, а содержимое сдвигалось влево (это было бы вверх по координатам основного текстового представления). Это можно наблюдать на следующем изображении. Желтое представление - это пользовательское представление текста (называется inputWindow в представлении кода контроллера ниже.)

После того, как я коснусь ввода несколько раз, чтобы увеличить размер представления содержимого, добавляется дополнительное пространство. Попытка прокрутить вид ничего не делает. (Прокрутка работает после того, как ширина достигает максимума, а размер содержимого превышает размер кадра.) Это выглядит так, как будто содержимое находилось в середине прокрутки, когда оно было заморожено до того, как его можно было разместить в правильном положении. Если я вставлю другой символ (например, пробел), представление содержимого обновится до правильной позиции.

Вопрос

Что мне нужно изменить? Или как мне вручную форсировать UITextView показать его содержимое просмотра в правильном месте?

Код

Я попытался вырезать весь посторонний код и просто оставить в соответствующих частях как для View Controller, так и для Custom Vertical TextView. Если есть что-то еще, что я должен включить, дайте мне знать.

Контроллер представления

Контроллер представления обновляет ограничения размера в настраиваемом текстовом представлении при изменении размера представления содержимого.

import UIKit
class TempViewController: UIViewController, KeyboardDelegate {

    let minimumInputWindowSize = CGSize(width: 80, height: 150)
    let inputWindowSizeIncrement: CGFloat = 50

    // MARK:- Outlets
    @IBOutlet weak var inputWindow: UIVerticalTextView!
    @IBOutlet weak var topContainerView: UIView!
    @IBOutlet weak var keyboardContainer: KeyboardController!
    @IBOutlet weak var inputWindowHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var inputWindowWidthConstraint: NSLayoutConstraint!


    override func viewDidLoad() {
        super.viewDidLoad()

        // get rid of space at beginning of textview
        self.automaticallyAdjustsScrollViewInsets = false

        // setup keyboard
        keyboardContainer.delegate = self
        inputWindow.underlyingTextView.inputView = UIView()
        inputWindow.underlyingTextView.becomeFirstResponder()
    }

    // KeyboardDelegate protocol
    func keyWasTapped(character: String) {
        inputWindow.insertMongolText(character) // code omitted for brevity
        increaseInputWindowSizeIfNeeded()
    }
    func keyBackspace() {
        inputWindow.deleteBackward() // code omitted for brevity
        decreaseInputWindowSizeIfNeeded()
    }

    private func increaseInputWindowSizeIfNeeded() {

        if inputWindow.frame.size == topContainerView.frame.size {
            return
        }

        // width
        if inputWindow.contentSize.width > inputWindow.frame.width &&
            inputWindow.frame.width < topContainerView.frame.size.width {
            if inputWindow.contentSize.width > topContainerView.frame.size.width {
                //inputWindow.scrollEnabled = true
                inputWindowWidthConstraint.constant = topContainerView.frame.size.width
            } else {
                self.inputWindowWidthConstraint.constant = self.inputWindow.contentSize.width
            }
        }

        // height
        if inputWindow.contentSize.width > inputWindow.contentSize.height {
            if inputWindow.frame.height < topContainerView.frame.height {
                if inputWindow.frame.height + inputWindowSizeIncrement < topContainerView.frame.height {
                    // increase height by increment unit
                    inputWindowHeightConstraint.constant = inputWindow.frame.height + inputWindowSizeIncrement
                } else {
                    inputWindowHeightConstraint.constant = topContainerView.frame.height
                }
            }
        }
    }

    private func decreaseInputWindowSizeIfNeeded() {

        if inputWindow.frame.size == minimumInputWindowSize {
            return
        }

        // width
        if inputWindow.contentSize.width < inputWindow.frame.width &&
            inputWindow.frame.width > minimumInputWindowSize.width {

            if inputWindow.contentSize.width < minimumInputWindowSize.width {
                inputWindowWidthConstraint.constant = minimumInputWindowSize.width
            } else {
                inputWindowWidthConstraint.constant = inputWindow.contentSize.width
            }
        }

        // height
        if (2 * inputWindow.contentSize.width) <= inputWindow.contentSize.height && inputWindow.contentSize.width < topContainerView.frame.width {
            // got too high, make it shorter
            if minimumInputWindowSize.height < inputWindow.contentSize.height - inputWindowSizeIncrement {
                inputWindowHeightConstraint.constant = inputWindow.contentSize.height - inputWindowSizeIncrement
            } else {
                // Bump down to min height
                inputWindowHeightConstraint.constant = minimumInputWindowSize.height
            }
        }
    }
}

Пользовательский вертикальный вид текста

Этот пользовательский вид представляет собой оболочку вокругUITextView чтобы позволить ему вращаться и переворачивать для правильного просмотра традиционного монгольского.

import UIKit
@IBDesignable class UIVerticalTextView: UIView {

    var textView = UITextView()
    let rotationView = UIView()

    var underlyingTextView: UITextView {
        get {
            return textView
        }
        set {
            textView = newValue
        }
    }


    var contentSize: CGSize {
        get {
            // height and width are swapped because underlying view is rotated 90 degrees
            return CGSize(width: textView.contentSize.height, height: textView.contentSize.width)
        }
        set {
            textView.contentSize = CGSize(width: newValue.height, height: newValue.width)
        }
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override init(frame: CGRect){
        super.init(frame: frame)
        self.setup()
    }

    override func awakeFromNib() {
        super.awakeFromNib()
        self.setup()
    }

    func setup() {

        textView.backgroundColor = UIColor.yellowColor()
        self.textView.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(rotationView)
        rotationView.addSubview(textView)

        // add constraints to pin TextView to rotation view edges.
        let leadingConstraint = NSLayoutConstraint(item: self.textView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: rotationView, attribute: NSLayoutAttribute.Leading, multiplier: 1.0, constant: 0)
        let trailingConstraint = NSLayoutConstraint(item: self.textView, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: rotationView, attribute: NSLayoutAttribute.Trailing, multiplier: 1.0, constant: 0)
        let topConstraint = NSLayoutConstraint(item: self.textView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: rotationView, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 0)
        let bottomConstraint = NSLayoutConstraint(item: self.textView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: rotationView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 0)
        rotationView.addConstraints([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        rotationView.transform = CGAffineTransformIdentity
        rotationView.frame = CGRect(origin: CGPointZero, size: CGSize(width: self.bounds.height, height: self.bounds.width))
        rotationView.userInteractionEnabled = true
        rotationView.transform = translateRotateFlip()
    }

    func translateRotateFlip() -> CGAffineTransform {

        var transform = CGAffineTransformIdentity

        // translate to new center
        transform = CGAffineTransformTranslate(transform, (self.bounds.width / 2)-(self.bounds.height / 2), (self.bounds.height / 2)-(self.bounds.width / 2))
        // rotate counterclockwise around center
        transform = CGAffineTransformRotate(transform, CGFloat(-M_PI_2))
        // flip vertically
        transform = CGAffineTransformScale(transform, -1, 1)

        return transform
    }

}

Что я пробовал

Многие идеи для вещей, которые я попробовал, пришли из Как мне определить размер UITextView для его содержимого? В частности, я попытался:

Установка рамки вместо авто макета

В пользовательском представлении layoutSubviews() метод, который я сделал

textView.frame = rotationView.bounds

и я не добавил ограничения в setup(), Не было никакого заметного эффекта.

allowsNonContiguousLayout

Это также не имело никакого эффекта. (Предлагается здесь.)

textView.layoutManager.allowsNonContiguousLayout = false

setNeedsLayout

Я пробовал различные комбинации setNeedsLayout а также setNeedsDisplay на входное окно и текстовое представление.

inputWindow.setNeedsLayout()
inputWindow.underlyingTextView.setNeedsLayout()

даже внутри dispatch_async так что он запускается в следующем цикле выполнения.

dispatch_async(dispatch_get_main_queue()) {
    self.inputWindow.setNeedsLayout()
}

sizeToFit

дела sizeToFit на следующем цикле выполнения после обновления ограничения ширины сначала выглядело многообещающим, но это все равно не решило проблему. Временами контент зависает, а иногда прокручивается. Это не всегда замерзает в одном и том же месте каждый раз.

self.inputWindowWidthConstraint.constant = self.inputWindow.contentSize.width
dispatch_async(dispatch_get_main_queue()) {
    self.inputWindow.underlyingTextView.sizeToFit()
}

задержка

Я смотрел на планирование отложенного события, но это похоже на взлом.

Дубликат?

Подобный вопрос звучит так: UITextview получает дополнительную строку, когда не должен. Тем не менее, это в Objective-C, поэтому я не могу сказать очень хорошо. Ему тоже 6 лет без ответа.

В этом ответе также упоминается дополнительное место на iPhone 6+ (мое тестовое изображение выше было iPhone 6, а не 6+). Тем не менее, я думаю, что попробовал предложения в этом ответе. То есть я сделал

var _f = self.inputWindow.underlyingTextView.frame
_f.size.height = self.inputWindow.underlyingTextView.contentSize.height
self.inputWindow.underlyingTextView.frame = _f

без заметного эффекта.

Обновление: базовый воспроизводимый проект

Чтобы сделать эту проблему максимально воспроизводимой, я создал отдельный проект. Это доступно на Github здесь. Макет раскадровки выглядит следующим образом:

Желтый UIView класс inputWindow и должен быть установлен в UIVerticalTextView, Голубой вид topContainerView, А кнопки ниже заменяют клавиатуру.

Добавьте показанные ограничения автоматического размещения. Ограничение ширины окна ввода равно 80, а ограничение высоты - 150.

Подключите розетки и действия к коду View Controller ниже. Этот код контроллера представления полностью заменяет код контроллера представления, который я использовал в моем предыдущем примере выше.

Контроллер представления

import UIKit
class ViewController: UIViewController {

    let minimumInputWindowSize = CGSize(width: 80, height: 150)
    let inputWindowSizeIncrement: CGFloat = 50

    // MARK:- Outlets
    @IBOutlet weak var inputWindow: UIVerticalTextView!
    @IBOutlet weak var topContainerView: UIView!
    //@IBOutlet weak var keyboardContainer: KeyboardController!
    @IBOutlet weak var inputWindowHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var inputWindowWidthConstraint: NSLayoutConstraint!

    @IBAction func enterTextButtonTapped(sender: UIButton) {
        inputWindow.insertMongolText("a")
        increaseInputWindowSizeIfNeeded()
    }
    @IBAction func newLineButtonTapped(sender: UIButton) {
        inputWindow.insertMongolText("\n")
        increaseInputWindowSizeIfNeeded()
    }
    @IBAction func deleteBackwardsButtonTapped(sender: UIButton) {
        inputWindow.deleteBackward()
        decreaseInputWindowSizeIfNeeded()
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // get rid of space at beginning of textview
        self.automaticallyAdjustsScrollViewInsets = false

        // hide system keyboard but show cursor
        inputWindow.underlyingTextView.inputView = UIView()
        inputWindow.underlyingTextView.becomeFirstResponder()
    }

    private func increaseInputWindowSizeIfNeeded() {

        if inputWindow.frame.size == topContainerView.frame.size {
            return
        }

        // width
        if inputWindow.contentSize.width > inputWindow.frame.width &&
            inputWindow.frame.width < topContainerView.frame.size.width {
            if inputWindow.contentSize.width > topContainerView.frame.size.width {
                //inputWindow.scrollEnabled = true
                inputWindowWidthConstraint.constant = topContainerView.frame.size.width
            } else {
                self.inputWindowWidthConstraint.constant = self.inputWindow.contentSize.width
            }
        }

        // height
        if inputWindow.contentSize.width > inputWindow.contentSize.height {
            if inputWindow.frame.height < topContainerView.frame.height {
                if inputWindow.frame.height + inputWindowSizeIncrement < topContainerView.frame.height {
                    // increase height by increment unit
                    inputWindowHeightConstraint.constant = inputWindow.frame.height + inputWindowSizeIncrement
                } else {
                    inputWindowHeightConstraint.constant = topContainerView.frame.height
                }
            }
        }
    }

    private func decreaseInputWindowSizeIfNeeded() {

        if inputWindow.frame.size == minimumInputWindowSize {
            return
        }

        // width
        if inputWindow.contentSize.width < inputWindow.frame.width &&
            inputWindow.frame.width > minimumInputWindowSize.width {

            if inputWindow.contentSize.width < minimumInputWindowSize.width {
                inputWindowWidthConstraint.constant = minimumInputWindowSize.width
            } else {
                inputWindowWidthConstraint.constant = inputWindow.contentSize.width
            }
        }

        // height
        if (2 * inputWindow.contentSize.width) <= inputWindow.contentSize.height && inputWindow.contentSize.width < topContainerView.frame.width {
            // got too high, make it shorter
            if minimumInputWindowSize.height < inputWindow.contentSize.height - inputWindowSizeIncrement {
                inputWindowHeightConstraint.constant = inputWindow.contentSize.height - inputWindowSizeIncrement
            } else {
                // Bump down to min height
                inputWindowHeightConstraint.constant = minimumInputWindowSize.height
            }
        }
    }
}

UIVerticalTextView

Используйте тот же код, что и для UIVerticalTextView в исходном примере, но с добавлением следующих двух методов.

func insertMongolText(unicode: String) {
    textView.insertText(unicode)
}

func deleteBackward() {
    textView.deleteBackward()
}

Тестовое задание

  1. Нажмите "вставить текст" несколько раз. (Обратите внимание, что текст задом наперед, потому что само приложение использует зеркальный шрифт для компенсации перевернутого текста.)
  2. Нажмите "Новая строка" пять раз.
  3. Попробуйте прокрутить вид.

Обратите внимание, что содержимое не на своем месте и что представление не будет прокручиваться.

Что мне нужно сделать, чтобы решить эту проблему?

2 ответа

Решение

Можно ли привести пример проекта (на github)?

Можете ли вы протестировать с небольшим изменением приведенного ниже кода вашего файла UIVerticalTextView:

override func layoutSubviews() {
    super.layoutSubviews()

    rotationView.transform = CGAffineTransformIdentity
    rotationView.frame = CGRect(origin: CGPointZero, size: CGSize(width: self.bounds.height, height: self.bounds.width))
    rotationView.userInteractionEnabled = true
    rotationView.transform = translateRotateFlip()

    if self.textView.text.isEmpty == false {
        self.textView.scrollRangeToVisible(NSMakeRange(0, 1))
    }
}

Я нашел приемлемое решение. Это связано

  1. отмена автоматического макета (в рамках самого пользовательского текстового представления) и
  2. добавление задержки перед обновлением.

В примере проекта это дает следующий результат.

введите описание изображения здесь

Содержимое текстового представления обновляет свою позицию до правильного местоположения.

Нет автоматического макета

в UIVerticalTextView Класс I закомментировал строки ограничения автоматического размещения:

let leadingConstraint = NSLayoutConstraint(item: self.textView, attribute: NSLayoutAttribute.Leading, relatedBy: NSLayoutRelation.Equal, toItem: rotationView, attribute: NSLayoutAttribute.Leading, multiplier: 1.0, constant: 0)
let trailingConstraint = NSLayoutConstraint(item: self.textView, attribute: NSLayoutAttribute.Trailing, relatedBy: NSLayoutRelation.Equal, toItem: rotationView, attribute: NSLayoutAttribute.Trailing, multiplier: 1.0, constant: 0)
let topConstraint = NSLayoutConstraint(item: self.textView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: rotationView, attribute: NSLayoutAttribute.Top, multiplier: 1.0, constant: 0)
let bottomConstraint = NSLayoutConstraint(item: self.textView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: rotationView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 0)
rotationView.addConstraints([leadingConstraint, trailingConstraint, topConstraint, bottomConstraint])

Обратите внимание, что это ограничения автоматического макета в самом пользовательском представлении (используется для закрепления textView кадр к rotationView границы), а не ограничения автоматического макета из раскадровки.

Так что вместо автоматического макета я установил textView.frame = rotationView.bounds в layoutSubviews:

override func layoutSubviews() {
    super.layoutSubviews()

    // ...

    textView.frame = rotationView.bounds
}

задержка

После увеличения ширины я добавил задержку в 100 миллисекунд перед вызовом setNeedsLayout,

private func increaseInputWindowSizeIfNeeded() {

    // ...

    // width
    if inputWindow.contentSize.width > inputWindow.frame.width &&
        // ...
        } else {

            self.inputWindowWidthConstraint.constant = self.inputWindow.contentSize.width

            // ********** Added delay here *********
            let delay = 0.1
            let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC)))
            dispatch_after(time, dispatch_get_main_queue()) {
                self.inputWindow.setNeedsLayout()
            }
            // *************************************

        }
    }

Все еще ищу лучшее решение

настройка delay = 0.1 работает в симуляторе, но если я установлю delay = 0.05 это не работает Поэтому, не тестируя его на всех устройствах, я не могу узнать, достаточно ли велика моя задержка. По этой причине я считаю это решение скорее хакерским, нежели истинным решением.

В любом случае, я не могу присудить награду самому себе, поэтому, если кто-то сможет найти лучшее решение, я буду рад его услышать.

Другие вопросы по тегам