13

I'm working on a text view that replaces placeholders with UITextFields. I pass it an object (a struct or a dictionary) with text containing multiple instances of a placeholder token. The dictionary also contains an array of fields that we want to collect data from. My goal is to place UITextFields (or other views) throughout my text, and hide the tokens.

Using NSLayoutManager methods to calculate the location of my placeholder tokens in the text containers, I convert those points to CGRects and then exclusion paths to, flow my text around the text fields. Here's what that looks like :

func createAndAssignExclusionPathsForInputTextFields () {
 var index = 0
 let textFieldCount = self.textFields.count
 var exclusionPaths : [UIBezierPath] = []
 while index < textFieldCount {
 let textField : AgreementTextField = self.textFields[index]
 let location = self.calculatePositionOfPlaceholderAtIndex(index)
 let size = textField.intrinsicContentSize()
 textField.frame = CGRectMake(location.x, location.y, size.width, size.height)
 exclusionPaths.append(textField.exclusionPath())
 index = index + 1
 }
 self.textContainer.exclusionPaths = exclusionPaths
}

// ...

func calculatePositionOfPlaceholderAtIndex(textIndex : NSInteger) -> CGPoint {
 let layoutManager : NSLayoutManager = self.textContainer.layoutManager!
 let delimiterRange = self.indices[textIndex]
 let characterIndex = delimiterRange.location
 let glyphRange = self.layoutManager.glyphRangeForCharacterRange(delimiterRange, actualCharacterRange:nil)
 let glyphIndex = glyphRange.location
 let rect = layoutManager.lineFragmentRectForGlyphAtIndex(glyphIndex, effectiveRange: nil, withoutAdditionalLayout: true)
 let remainingRect : UnsafeMutablePointer<CGRect> = nil
 let textContainerRect = self.textContainer.lineFragmentRectForProposedRect(rect, atIndex: characterIndex, writingDirection: .LeftToRight, remainingRect: remainingRect)
 let position = CGPointMake(textContainerRect.origin.x, textContainerRect.origin.y)
 return position
}

At this point, I have three issues:

  1. Once I assign an exclusion path to the textContainer, the calculated glyph positions for the other placeholders are now all wrong.
  2. The calculatePositionOfPlaceholderAtIndex method is giving me pretty good y values, but the x values are all 0.
  3. I haven't been able successfully hide the placeholder tokens.

So, to solve first issue on my list, I tried adding the exclusion path before calculating the next one, by changing createAndAssignExclusionPathsForInputTextFields:

func createAndAssignExclusionPathsForInputTextFields () {
 var index = 0
 let textFieldCount = self.textFields.count
 while index < textFieldCount {
 let textField : AgreementTextField = self.textFields[index]
 let location = self.calculatePositionOfPlaceholderAtIndex(index)
 let size = textField.intrinsicContentSize()
 textField.frame = CGRectMake(location.x, location.y, size.width, size.height)
 self.textContainer.exclusionPaths.append(textField.exclusionPath())
 index = index + 1
 }
}

Now, my calculated positions are all returning 0, 0. Not what we want. Adding an exclusion path understandably makes the calculated locations invalid, but getting 0, 0 back for every rect method isn't helpful.

How can I ask the layout manager to re-calculate the position for glyphs on screen after adding an exclusion path or hiding a glyph?

EDIT: Per Alain T's answer, I tried the following with no luck:

 func createAndAssignExclusionPathsForInputTextFields () {
 var index = 0
 let textFieldCount = self.textFields.count
 var exclusionPaths : [UIBezierPath] = []
 while index < textFieldCount {
 let textField : AgreementTextField = self.textFields[index]
 let location = self.calculatePositionOfPlaceholderAtIndex(index)
 let size = textField.intrinsicContentSize()
 textField.frame = CGRectMake(location.x, location.y, size.width, size.height)
 exclusionPaths.append(textField.exclusionPath())
 self.textContainer.exclusionPaths = exclusionPaths
 self.layoutManager.ensureLayoutForTextContainer(self.textContainer)
 index = index + 1
 }
 self.textContainer.exclusionPaths = exclusionPaths
}
asked Dec 15, 2015 at 18:18
5
  • I haven't figured this out. Instead, I've implemented a subclass of UITextField that does something similar. Commented Jan 13, 2016 at 18:38
  • can you hook up a sample xcode project and share it? I would like to give this a try. Commented Jan 16, 2016 at 7:06
  • I don't have a sample project at the moment, but I'll see if I can make one. Commented Jan 18, 2016 at 17:04
  • @ShahiM, the new AgreementView is here: gist.github.com/MosheBerman/1a990d15863737047968 Commented Jan 20, 2016 at 17:41
  • 1
    @ShahiM The full contents of the class described in the question: gist.github.com/MosheBerman/d65408b75dc28a7046e0 Commented Jan 20, 2016 at 17:43

2 Answers 2

2
+125

I can only make suggestions as I am new at TextKit myself but something glared at me in your code and I thought I'd mention it in case it could help.

In your createAndAssignExclusionPathsForInputTextFields function, you are processing your fields in the order of your self.textFields array.

When you set the value of self.textContainer.exclusionPaths at the end of the function, the layout manager will (potentially) re-flow text around all your exclusions thus invalidating some of your calculation that were performed without taking into account the impact of other exclusions.

The way around this is to sort your self.textFields array so that they correspond to the text flow (they may already be in that order, I can't tell). Then, you must clear all the exclusions from self.textContainer.exclusionPaths and add them back one by one so that, before you calculate the next exclusion, the layout manager has reflowed the text around the previously added exclusion. (I believe it does it every time you set the exclusions but if it does not, you may need to call the layoutManager's ensureLayoutForManager function)

You must do this in text flow order so that every exclusion is added on a region of text that will not invalidate any previous exclusion.

Optimizing this to avoid a full clear/rebuild of exclusions is also possible but I would suggest getting to a working baseline before attempting that.

answered Jan 16, 2016 at 4:50
Sign up to request clarification or add additional context in comments.

5 Comments

Looks like the ensureLayoutForManager might have been what I was missing. I'll give a look and let you know.
So there are a few ensureLayoutFor... methods. They don't seem to work for me.
My fields are indeed in order.
Then, I believe that adding them as you go (instead of setting the array at the end) should fix most of the issue as long as the layout is readjusted between each addition. note: you must clear them at the beginning too. Then again I could be completely out in left field as I have not tried this myself. Oopps just noticed your modified algorithm and it does seem to do that. I'll have to look at it closer when I'm back from work.
I've tried that too. Something is causing the layout to become invalidated and not recalculate based on the exclusion paths.
0

Perhaps you should use ensureGlyphsForCharacterRange instead or as well as ensureLayoutForManager. I would need to build a more comprehensive test program to figure that out for sure (I don't have enough free time right now).

I did however look at an alternative way to have editable fields and non-editable text in a UITextView and came up with a text only approach (crude and incomplete but it's a starting point).

The idea is to use some text attributes to identify editable fields and make everything else non-editable using the UITextView's delegate.

// function to determine if attributes of a range of text allow editing
// (you decide which attributes correspond to editable text)
func editableText(attributes:[String:AnyObject]) -> Bool
{
 if let textColor = attributes[NSForegroundColorAttributeName] as? UIColor
 where textColor == UIColor.redColor() // example: red text is editable
 { return true }
 return false
}
// UITextViewDelegate's function to allow editing a specific text area (range)
func textView(textView: UITextView, var shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool
{
 // 0 length selection is an insertion point
 // will get the attibutes of the text preceding it as typing attributes
 if range.length == 0 
 && text != ""
 && editableText(textView.typingAttributes) 
 { return true }
 // in case we're on an insertion point at the very begining of an editable field
 // lets look at the next character
 range.length = max(1, range.length)
 // for non-zero ranges, all subranges must have the editable attributes to allow editing
 // (also setting typingAttributes to cover the insertion point at begining of field case)
 var canEdit = true
 textView.attributedText.enumerateAttributesInRange(range, options:[])
 { 
 if self.editableText(0ドル.0) 
 { textView.typingAttributes = 0ドル.0 }
 else
 { canEdit = false } 
 } 
 return canEdit 
}

This leave a few more things to manage such as tabbing between fields, initial cursor position and some cursor behaviour for formatted text entry but they all seem simple enough to do.

If you're not too far gone down the exclusions path and your requirements are not to constraining, perhaps that could help clear the roadblock.

answered Jan 21, 2016 at 3:31

1 Comment

I actually have that other method working pretty well now. See my comments to the question. It's a little trickier if you want to properly handle selection too.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.