11

问题的背景和描述

我制作了一个垂直文本视图以用于蒙古语。它是一个自定义文本视图,由三层视图组成:子UITextView视图、用于容纳 的容器视图(旋转 90 度并翻转)UITextView和父视图。(有关更多背景信息,请参见此处此处。)

视图根据底层文本视图的内容大小增加其大小,只要它在最小和最大大小之间。然而,在过去的几天里,我一直在努力修复一个错误,即添加了额外的空间并且内容向左移动(这将在底层文本视图的坐标上)。这可以在下图中观察到。黄色视图是自定义文本视图(inputWindow在下面的视图控制器代码中调用。)

在此处输入图像描述

在我点击 enter 几次以增加内容视图的大小后,添加了一个额外的空间。试图滚动视图什么也没做。(在宽度达到最大值并且内容大小大于框架大小后,滚动确实有效。)就好像内容处于滚动中间时它被冻结在适当的位置之前它可以放置在正确的位置。如果我插入另一个字符(如空格),则内容视图会自行更新到正确的位置。

问题

我需要改变什么?或者我如何手动强制底层证券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
            }
        }
    }
}

自定义垂直文本视图

这个自定义视图基本上是一个围绕 a 的外壳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
    }

}

我试过的

我尝试过的许多想法都来自How do I size a UITextView to its content? 具体来说,我尝试过:

设置框架而不是自动布局

layoutSubviews()我做的自定义视图方法中

textView.frame = rotationView.bounds

而且我没有在setup(). 没有明显的效果。

允许非连续布局

这也没有效果。(建议在这里。)

textView.layoutManager.allowsNonContiguousLayout = false

设置需求布局

setNeedsLayout我已经尝试了 inputWindow和setNeedsDisplay底层文本视图的各种组合。

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

甚至在 a 内部,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。

将出口和操作连接到下面的视图控制器代码。此视图控制器代码完全替换了我在上面的原始示例中使用的视图控制器代码。

视图控制器

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. 尝试滚动视图。

观察内容放错位置并且视图不会滚动。

我需要做什么来解决这个问题?

4

2 回答 2

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))
    }
}
于 2016-05-18T07:07:30.140 回答
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它不起作用。因此,如果不在所有设备上进行测试,我就无法知道我的延迟是否足够长。出于这个原因,我认为这个解决方案更像是一个 hack,而不是一个真正的解决方案。

无论如何,我不能将赏金奖励给自己,所以如果有人能提出更好的解决方案,我会很高兴听到。

于 2016-05-19T08:16:52.250 回答