Интерполяция UITextFields с UITextView с использованием Text Kit?

Я работаю над текстовым представлением, которое заменяет заполнители на 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 для каждого метода rect бесполезно.

Как я могу попросить менеджера компоновки пересчитать положение глифов на экране после добавления пути исключения или скрытия глифа?

EDIT: Согласно ответу Алена Т., я безуспешно пробовал следующее:

    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
}

person Moshe    schedule 15.12.2015    source источник
comment
Я не понял этого. Вместо этого я реализовал подкласс UITextField, который делает что-то подобное.   -  person Moshe    schedule 13.01.2016
comment
Можете ли вы подключить образец проекта xcode и поделиться им? Я хотел бы попробовать.   -  person ShahiM    schedule 16.01.2016
comment
На данный момент у меня нет образца проекта, но я посмотрю, смогу ли я его сделать.   -  person Moshe    schedule 18.01.2016
comment
@ShahiM, новый вид соглашения находится здесь: gist.github.com/MosheBerman/1a990d15863737047968   -  person Moshe    schedule 20.01.2016
comment
@ShahiM Полное содержимое класса, описанного в вопросе: gist.github.com/MosheBerman/d65408b75dc28a7046e0   -  person Moshe    schedule 20.01.2016


Ответы (2)


Я могу только вносить предложения, так как сам новичок в TextKit, но что-то бросилось мне в глаза в вашем коде, и я подумал, что упомяну об этом, если это может помочь.

В вашей функции createAndAssignExclusionPathsForInputTextFields вы обрабатываете свои поля в порядке массива self.textFields.

Когда вы устанавливаете значение self.textContainer.exclusionPaths в конце функции, менеджер компоновки (потенциально) переформатирует текст вокруг всех ваших исключений, тем самым делая недействительными некоторые ваши вычисления, которые были выполнены без учета влияния других исключения.

Обойти это можно, отсортировав массив self.textFields так, чтобы они соответствовали потоку текста (возможно, они уже в таком порядке, я не могу сказать). Затем вы должны очистить все исключения из self.textContainer.exclusionPaths и добавить их обратно по одному, чтобы перед тем, как вы вычислите следующее исключение, менеджер компоновки переформатировал текст вокруг ранее добавленного исключения. (Я полагаю, что он делает это каждый раз, когда вы устанавливаете исключения, но если это не так, вам может потребоваться вызвать функцию layoutManager sureLayoutForManager)

Вы должны делать это в порядке потока текста, чтобы каждое исключение добавлялось к области текста, которая не сделает недействительным любое предыдущее исключение.

Оптимизация этого, чтобы избежать полной очистки/перестроения исключений, также возможна, но я бы посоветовал получить рабочий базовый уровень, прежде чем пытаться это сделать.

person Alain T.    schedule 16.01.2016
comment
Похоже, гарантировать, чтоLayoutForManager, возможно, было тем, чего мне не хватало. Я посмотрю и дам вам знать. - person Moshe; 18.01.2016
comment
Итак, есть несколько методов sureLayoutFor.... Кажется, они не работают для меня. - person Moshe; 20.01.2016
comment
Мои поля действительно в порядке. - person Moshe; 20.01.2016
comment
Затем я считаю, что добавление их по ходу (вместо установки массива в конце) должно решить большую часть проблемы, если макет корректируется между каждым добавлением. примечание: вы также должны очистить их в начале. С другой стороны, я мог бы быть полностью в левом поле, поскольку я не пробовал это сам. К сожалению, только что заметили ваш измененный алгоритм, и, похоже, он это делает. Надо будет посмотреть повнимательнее, когда вернусь с работы. - person Alain T.; 21.01.2016
comment
Я тоже это пробовал. Что-то приводит к тому, что макет становится недействительным и не пересчитывается на основе путей исключения. - person Moshe; 21.01.2016

Возможно, вместо этого вам следует использовать sureGlyphsForCharacterRange или наряду с sureLayoutForManager. Мне нужно было бы создать более полную тестовую программу, чтобы выяснить это наверняка (сейчас у меня недостаточно свободного времени).

Однако я рассмотрел альтернативный способ иметь редактируемые поля и нередактируемый текст в 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      
}

Это оставляет еще несколько вещей для управления, таких как переход между полями, начальная позиция курсора и некоторое поведение курсора для ввода форматированного текста, но все они кажутся достаточно простыми.

Если вы не слишком далеко зашли по пути исключений и ваши требования не ограничивают, возможно, это может помочь устранить препятствие.

person Alain T.    schedule 21.01.2016
comment
У меня на самом деле есть этот другой метод, который работает довольно хорошо сейчас. Смотрите мои комментарии к вопросу. Это немного сложнее, если вы хотите правильно обрабатывать выделение. - person Moshe; 21.01.2016