使用Text Kit插值UITextFields和UITextView?

13
我正在开发一个文本视图,用于替换占位符为UITextFields。我会传入一个包含多个占位符标记的文本(结构体或字典)。该字典还包含一个我们想要收集数据的字段数组。我的目标是在文本中放置UITextFields(或其他视图)并隐藏标记。
使用NSLayoutManager方法计算文本容器中占位符标记的位置后,将这些点转换为CGRects和排除路径,以便围绕文本字段流动文本。以下是此过程的示例:
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
}

目前,我有三个问题:
  1. 一旦我为textContainer分配了排除路径,其他占位符的计算字形位置现在都是错误的。
  2. calculatePositionOfPlaceholderAtIndex方法给出了相当不错的y值,但x值全部为0。
  3. 我无法成功隐藏占位符标记。
因此,为了解决我列出的第一个问题,我尝试在计算下一个排除路径之前添加排除路径,通过更改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
    }
}

现在,我的计算位置都返回0, 0。这不是我们想要的。很明显添加一个排除路径会使计算出的位置无效,但是每个矩形方法都返回0, 0并没有什么帮助。我该如何请求布局管理器在添加排除路径或隐藏字形后重新计算屏幕上字形的位置?
编辑:根据Alain T的答案,我尝试了以下方法,但没有成功:
    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
}

我还没有想出来。相反,我实现了一个UITextField的子类,它做了类似的事情。 - Moshe
你能否连接一个示例Xcode项目并分享吗?我想试一试。 - ShahiM
我目前没有样例项目,但我会看看能否制作一个。 - Moshe
@ShahiM,新的AgreementView在这里:https://gist.github.com/MosheBerman/1a990d15863737047968 - Moshe
1
@ShahiM 问题描述中提到的类的完整内容:https://gist.github.com/MosheBerman/d65408b75dc28a7046e0 - Moshe
2个回答

2

我只能提出建议,因为我自己对TextKit还不熟悉,但是在你的代码中有一些问题引起了我的注意,我想提一下,以防它有帮助。

在你的createAndAssignExclusionPathsForInputTextFields函数中,你按照self.textFields数组的顺序处理你的字段。

当你在函数末尾设置self.textContainer.exclusionPaths的值时,布局管理器将会(可能)重新排列所有你的排除内容周围的文本,从而使你之前执行的一些计算结果失效,因为这些结果没有考虑到其他排除内容的影响。

解决方法是按照文本流程对self.textFields数组进行排序(它们可能已经按照该顺序排列,但我无法确定)。然后,你必须清除self.textContainer.exclusionPaths中的所有排除内容,并逐个添加它们,以便在计算下一个排除内容之前,布局管理器已经在先前添加的排除内容周围重新排列了文本。(我相信它每次设置排除内容时都会这样做,但如果它没有这样做,你可能需要调用layoutManager的ensureLayoutForManager函数)

你必须按照文本流程顺序执行此操作,以便每个排除内容都添加在不会使任何先前的排除内容失效的文本区域上。

优化此操作以避免完全清除/重建排除内容也是可能的,但我建议先尝试实现一个工作基线。


看起来 ensureLayoutForManager 可能是我所缺少的。我会去看一下并告诉你。 - Moshe
所以有几个 ensureLayoutFor... 方法。它们对我似乎不起作用。 - Moshe
我的字段确实是有序的。 - Moshe
然后,我相信在添加它们时(而不是在最后设置数组),只要在每次添加之间重新调整布局,就应该可以解决大部分问题。注意:你也必须在开头清除它们。再说一遍,我可能完全错了,因为我自己没有尝试过这个方法。哦,我刚刚注意到你修改的算法,它似乎确实做到了这一点。等我下班回来后,我会仔细看看它。 - Alain T.
我也尝试过那个。某些因素导致布局无效,并且不会根据排除路径重新计算 - Moshe

0

也许你应该使用ensureGlyphsForCharacterRange,或者同时使用ensureLayoutForManager。我需要构建一个更全面的测试程序来确定这一点(我现在没有足够的空闲时间)。

然而,我研究了一种在UITextView中实现可编辑字段和不可编辑文本的替代方法,并提出了一种仅限于文本的方法(粗糙且不完整,但是这是一个起点)。

这个想法是使用一些文本属性来标识可编辑字段,并使用UITextView的委托使其他所有内容都不可编辑。

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

这还需要处理一些其他事情,例如在字段之间进行制表、初始光标位置以及格式化文本输入的一些光标行为,但它们似乎都足够简单。

如果您没有走得太远而且您的要求不是太苛刻,也许这可以帮助消除障碍。


我现在已经有另一种方法运作得非常好了。请看我的评论。如果您想要正确处理选择,这会有点棘手。 - Moshe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接