调整大小

时间:2016-05-14 10:04:09

标签: ios swift uitextview mongolian-vertical-script

问题的背景和描述

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

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

enter image description here

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

问题

我需要改变什么?或者,如何手动强制基础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
            }
        }
    }
}

自定义垂直文字视图

这个自定义视图基本上是a shell around 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()中添加约束。没有明显的效果。

allowsNonContiguousLayout

这也没有效果。 (建议here。)

textView.layoutManager.allowsNonContiguousLayout = false

setNeedsLayout

我在inputWindow和基础文本视图上尝试了setNeedsLayoutsetNeedsDisplay的各种组合。

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()
}

enter image description here

延迟

我一直在关注scheduling a delayed event,但这感觉就像一个黑客。

重复?

类似的声音问题是UITextview gains an extra line when it should not。但是,它在Objective-C中,所以我不能很好地说出来。它也是6岁,没有答案。

This answer还提到了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 here上找到。故事板布局如下所示:

enter image description here

黄色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. 尝试滚动视图。
  4. 观察内容放错地方并且视图不会滚动。

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

2 个答案:

答案 0 :(得分: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))
    }
}

答案 1 :(得分:0)

我找到了一个可接受的解决方案。它涉及

  1. 取消自动布局(在自定义文本视图中)和
  2. 在更新前添加延迟。
  3. 在示例项目中,这会得到以下结果。

    enter image description here

    文本视图的内容将其位置更新为正确的位置。

    无自动布局

    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
    }
    

    延迟

    在宽度增加之后,我在调用setNeedsLayout之前添加了100毫秒的延迟。

    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则不起作用。因此,如果不在所有设备上进行测试,我无法知道延迟是否足够长。出于这个原因,我认为这个解决方案更像是一个黑客而不是一个真正的解决方案。

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