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

Фон и описание проблемы

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

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

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

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

Вопрос

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

Код

Я попытался вырезать весь посторонний код и просто уйти в соответствующие части как для диспетчера представлений, так и для пользовательского вертикального 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 и в основном текстовом представлении.

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

даже внутри a 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()
}

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

Задержка

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

A Дубликат?

Аналогичный звуковой вопрос - 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()
}

Test

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

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

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

Ответ 1

Можно ли дать нам пример проекта (на 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))
    }
}

Ответ 2

Я нашел приемлемое решение. Он включал

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

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

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

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

Нет автоматической компоновки

В классе UIVerticalTextView я закомментировал строки ограничения автоматической компоновки:

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, это не сработает. Поэтому, не тестируя его на всех устройствах, я не знаю, достаточно ли задержки. По этой причине я считаю это решение скорее хаком, чем истинным решением.

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