Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITextView's content getting misplaced with extra space after resizing

Background and description of the problem

I made a vertical text view to be used with Mongolian. It is a custom text view that is made of three layers of views: a child UITextView, a container view (which is rotated 90 degrees and flipped) to hold the UITextView, and the parent view. (See here and here for more background info.)

The view increases its size according to the content size of the underlying text view as long as it is between a minimum and maximum size. However, for the past few days I have been struggling to fix a bug where an extra space is added and the content is shifted left (that would be up on the underlying text view's coordinates). This can be observed in the following image. The yellow view is the custom text view (called inputWindow in the view controller code below.)

enter image description here

After I tap enter a few times to increase the size of the content view, an extra space is added. Trying to scroll the view does nothing. (Scrolling does work after the width reaches it's max and the content size is bigger than the frame size.) It is as if the content was in middle of scrolling when it got frozen in place before it could be placed in the correct position. If I insert another character (like a space) then the content view updates itself to the correct position.

Question

What do I need to change? Or how do I manually force the underlying UITextView to show its content view in the correct location?

Code

I have tried to cut out all of the extraneous code and just leave in the relevant parts for both the View Controller and the Custom Vertical TextView. If there is anything else I should include, let me know.

View Controller

The view controller updates the size constraints on the custom text view when it's content view size changes.

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

Custom Vertical Text View

This custom view is basically a shell around a UITextView to allow it to be rotated and flipped for the proper viewing of traditional Mongolian.

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
    }

}

What I have tried

Many of the ideas for things I have tried have come from How do I size a UITextView to its content? Specifically, I have tried:

Setting the frame instead of auto layout

In the custom view layoutSubviews() method I did

textView.frame = rotationView.bounds

and I didn't add the constraints in setup(). There was no noticeable effect.

allowsNonContiguousLayout

This also had no effect. (Suggested here.)

textView.layoutManager.allowsNonContiguousLayout = false

setNeedsLayout

I've tried various combinations of setNeedsLayout and setNeedsDisplay on the inputWindow and the underlying text view.

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

even inside a dispatch_async so that it gets run on the next run loop.

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

sizeToFit

Doing sizeToFit on the next run loop after updating the width constraint looked promising at first, but it still didn't solve the problem. At times the content would freeze and at other times it would be scrollable. It doesn't always freeze at the same spot every time.

self.inputWindowWidthConstraint.constant = self.inputWindow.contentSize.width
dispatch_async(dispatch_get_main_queue()) {
    self.inputWindow.underlyingTextView.sizeToFit()
}

enter image description here

Delay

I've been looking at scheduling a delayed event, but this feels like a hack.

A Duplicate?

A similar sounding question is UITextview gains an extra line when it should not. However, it is in Objective-C so I can't tell very well. It is also 6 years old with no answer.

This answer also mentions an extra space on iPhone 6+ (my test image above was iPhone 6, not 6+). However, I think I tried the suggestions in that answer. That is, I did

var _f = self.inputWindow.underlyingTextView.frame
_f.size.height = self.inputWindow.underlyingTextView.contentSize.height
self.inputWindow.underlyingTextView.frame = _f

to no noticeable effect.

Update: A Basic Reproducible Project

In order to make this problem as reproducible as possible, I made a standalone project. It is available on Github here. The storyboard layout looks like this:

enter image description here

The yellow UIView class is the inputWindow and should be set to UIVerticalTextView. The light blue view is the topContainerView. And the buttons below replace the keyboard.

Add the autolayout constraints shown. The Input Window's width constrain is 80 and the height constraint is 150.

Hook up the outlets and actions to the View Controller code below. This view controller code completely replaces the view controller code I used in my original example above.

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

Use the same code as for the UIVerticalTextView in the original example but with the addition of the following two methods.

func insertMongolText(unicode: String) {
    textView.insertText(unicode)
}

func deleteBackward() {
    textView.deleteBackward()
}

Test

  1. Tap "insert text" a few times. (Note that the text is backwards because the actual app uses a mirrored font to compensate for the flipped text view.)
  2. Tap "new line" five times.
  3. Try to scroll the view.

Observe that the content is misplaced and that the view will not scroll.

What do I need to do to fix this problem?

like image 581
Suragch Avatar asked May 14 '16 10:05

Suragch


1 Answers

Is it possible to give us an example project(on github)?

Can you test with a bit change to below code of your UIVerticalTextView file:

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))
    }
}
like image 52
ZYiOS Avatar answered Nov 14 '22 08:11

ZYiOS