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

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

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

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

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

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

Вопрос

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

Код

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

Контроллер просмотра

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

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(). Заметного эффекта не было.

позволяетNonContiguousLayout

Это тоже не повлияло. (Предлагается здесь.)

textView.layoutManager.allowsNonContiguousLayout = false

setNeedsLayout

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

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

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

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

Размер по размеру

Выполнение 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. Попробуйте прокрутить вид.

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

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


person Suragch    schedule 14.05.2016    source источник


Ответы (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))
    }
}
person ZYiOS    schedule 18.05.2016
comment
Тестовый проект теперь доступен на Github: github.com/suragch/TestVerticalTextView. - person Suragch; 19.05.2016
comment
Я не совсем уверен, что происходит после ваших правок. Текстовое представление больше не зависает. Он просто становится прокручиваемым без изменения размера. Возможно, вы что-то знаете, если есть другой способ изменить размер текстового представления. - person Suragch; 19.05.2016
comment
Небольшое обновление @Suragch для layoutSubviews UIVerticalTextView, кажется, работает. - person ZYiOS; 23.05.2016
comment
Удивительно! Это работает так, как и должно быть. Это намного лучше, чем мое решение для взлома ниже, потому что оно не прыгает после задержки. - person Suragch; 23.05.2016

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

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

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

person Suragch    schedule 19.05.2016