Содержимое 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()
}
Тестовое задание
- Нажмите "вставить текст" несколько раз. (Обратите внимание, что текст задом наперед, потому что само приложение использует зеркальный шрифт для компенсации перевернутого текста.)
- Нажмите "Новая строка" пять раз.
- Попробуйте прокрутить вид.
Обратите внимание, что содержимое не на своем месте и что представление не будет прокручиваться.
Что мне нужно сделать, чтобы решить эту проблему?
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))
}
}
Я нашел приемлемое решение. Это связано
- отмена автоматического макета (в рамках самого пользовательского текстового представления) и
- добавление задержки перед обновлением.
В примере проекта это дает следующий результат.
Содержимое текстового представления обновляет свою позицию до правильного местоположения.
Нет автоматического макета
в 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
это не работает Поэтому, не тестируя его на всех устройствах, я не могу узнать, достаточно ли велика моя задержка. По этой причине я считаю это решение скорее хакерским, нежели истинным решением.
В любом случае, я не могу присудить награду самому себе, поэтому, если кто-то сможет найти лучшее решение, я буду рад его услышать.