Как обнаружить прикосновение к NSTextAttachment
Каков наилучший способ определить, когда пользователь нажимает на NSTextAttachment
на iOS?
Я думаю, что одним из способов будет проверка символа на позиции carret, является ли это NSAttachmentCharacter
, но это просто не кажется правильным.
Я также пробовал метод UITextViewDelegate
: -(BOOL)textView:(UITextView *)textView shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment inRange:(NSRange)characterRange
, но он не вызывается, когда textView.editable=YES
Ответы
Ответ 1
Метод делегата работает, но ТОЛЬКО, если вложение имеет изображение в атрибуте изображения И если editable = NO! Поэтому, если у вас есть образ, вставленный в атрибутString из другого места, кажется, что данные заканчиваются тем, что они хранятся в файлеWrapper, и в следующий раз, когда вы добавите атрибут attribring в textView, атрибут изображения равен нулю, а менеджер компоновки или что-то еще получает изображение из файла.
Где-то в документах упоминается, что в NSTextAttachment нет методов для сохранения атрибута изображения.
Чтобы протестировать эту попытку, скопируйте фотографию из приложения "Фото" и вставьте ее в свой текстовый рисунок, теперь, если вы держите палец на нем, вы увидите, что всплывает меню по умолчанию. Теперь, если вы сохраните этот богатый текст, скажите в объект Core Data и затем извлеките его, атрибут изображения будет равен нулю, но данные изображения будут находиться в attachment.fileWrapper.regularFileContents
Это боль, и я хотел бы знать намерения инженеров. Таким образом, у вас есть два варианта.
- Создайте свой собственный NSTextAttachment и включите методы для архивирования изображения и других настроек (ПОЖАЛУЙСТА, ПОКАЗЫВАЙТЕ, ЧТО СЛИШКОМ, КОГДА ВЫ НАПИСАЕТЕ ЭТО ОДИН ИЗ ВЫХОДА)
-
Каждый раз, прежде чем возвращать строку в textView, вы найдете все вложения и воссоздаете атрибут изображения следующим образом:
attachment.image = [UIImage imageWithData: attachment.fileWrapper.regularFileContents];
Помните, что побочным эффектом этого является недействительность файла. Я хочу изменить размер изображения, но также сохранить оригинал, чтобы я не потерял полное разрешение. Я думаю, что единственный способ сделать это может заключаться в подклассе NSTextAttachment.
ИЗМЕНИТЬ:
Я понял, как создать пользовательский NSTextAttachments - вот ссылка для заинтересованных http://ossh.com.au/design-and-technology/software-development/implementing-rich-text-with-images-on-os-x-and-ios/
ИЗМЕНИТЬ 2: Чтобы настроить меню, когда в режиме редактирования см. следующие документы Apple, проблема "touchEnded" никогда не кажется вызванной, поэтому вам, возможно, придется попробовать использовать touchhesBegan. Будьте осторожны, вы не вмешиваетесь в поведение редактирования по умолчанию, хотя.
https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/AddingCustomEditMenuItems/AddingCustomEditMenuItems.html
Обратите внимание, что в приведенном ниже коде вам нужно будет добавить код после комментария // selection management
, чтобы определить, какой символ был затронут, проверьте, является ли он особым символом вложения текста и
затем измените меню редактирования или выполните некоторые другие действия.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *theTouch = [touches anyObject];
if ([theTouch tapCount] == 2 && [self becomeFirstResponder]) {
// selection management code goes here...
// bring up edit menu.
UIMenuController *theMenu = [UIMenuController sharedMenuController];
CGRect selectionRect = CGRectMake (currentSelection.x, currentSelection.y, SIDE, SIDE);
[theMenu setTargetRect:selectionRect inView:self];
[theMenu setMenuVisible:YES animated:YES];
}
}
В качестве альтернативы вы можете добавить пользовательское меню, добавив пункт меню, а затем изменив метод canPerformAction.
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
LOG(@"canPerformAction: called");
if (action == @selector(viewImage)) {
// Check the selected character is the special text attachment character
return YES;
}
return NO;
}
Вот код добавления, но его немного суетливый. Второй метод просто отключает меню редактирования по умолчанию, если обнаружено вложение.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
FLOG(@"touchesBegan:withEvent: called");
if (self.selectedRange.location != NSNotFound) {
FLOG(@" selected location is %d", self.selectedRange.location);
int ch;
if (self.selectedRange.location >= self.textStorage.length) {
// Get the character at the location
ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location-1];
} else {
// Get the character at the location
ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location];
}
if (ch == NSAttachmentCharacter) {
FLOG(@" selected character is %d, a TextAttachment", ch);
} else {
FLOG(@" selected character is %d", ch);
}
}
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
FLOG(@"canPerformAction: called");
FLOG(@" selected location is %d", self.selectedRange.location);
FLOG(@" TextAttachment character is %d", NSAttachmentCharacter);
if (self.selectedRange.location != NSNotFound) {
int ch;
if (self.selectedRange.location >= self.textStorage.length) {
// Get the character at the location
ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location-1];
} else {
// Get the character at the location
ch = [[[self textStorage] string] characterAtIndex:self.selectedRange.location];
}
if (ch == NSAttachmentCharacter) {
FLOG(@" selected character is %d, a TextAttachment", ch);
return NO;
} else {
FLOG(@" selected character is %d", ch);
}
// Check for an attachment
NSTextAttachment *attachment = [[self textStorage] attribute:NSAttachmentAttributeName atIndex:self.selectedRange.location effectiveRange:NULL];
if (attachment) {
FLOG(@" attachment attribute retrieved at location %d", self.selectedRange.location);
return NO;
}
else
FLOG(@" no attachment at location %d", self.selectedRange.location);
}
return [super canPerformAction:action withSender:sender];
}
Ответ 2
Свифт 3:
func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange) -> Bool {
return true
}
Убедитесь, что у вас есть textView isEditable = false
, isSelectable = true
и isUserInteractionEnabled = true
. Ответ Duncan не упоминал isUserInteractionEnabled
, это должно быть true
, иначе оно не будет работать.
Вы можете сделать это программно (textView.isEditable = false) или через инспектор атрибутов:
![введите описание изображения здесь]()
Ответ 3
Apple сделать это действительно сложно. Как указывают другие, метод делегата вызывается, но только когда isEditable
имеет значение false
или когда пользователь нажимает и удерживает вложение. Если вы хотите получать информацию о простом взаимодействии касанием во время редактирования, забудьте об этом.
Я пошел вниз touchesBegan:
и hitTest:
путь, и с проблемами. Методы касания вызываются после того, как UITextView
уже обработал взаимодействие, а hitTest:
слишком грубый, потому что он портит статус первого респондента и так далее.
Мое решение, в конце концов, заключалось в распознавании жестов. Apple использует их внутренне, что объясняет, почему touchesBegan:
самом деле не является жизнеспособным: распознаватели жестов уже обработали событие.
Я создал новый класс распознавателя жестов для использования с UITextView
. Он просто проверяет местоположение крана и, если это вложение, обрабатывает его. Я делаю все другие распознаватели жестов подчиненными моему, чтобы мы сначала посмотрели на события, а остальные вступают в игру только в случае отказа нашего.
Класс распознавателя жестов находится ниже, вместе с расширением для добавления его в UITextView
. Я добавляю его в моем UITextView
подкласса в awakeFromNib
, как это. (Вам не нужно использовать подкласс, если у вас его нет.)
override func awakeFromNib() {
super.awakeFromNib()
let recognizer = AttachmentTapGestureRecognizer(target: self, action: #selector(handleAttachmentTap(_:)))
add(recognizer)
и я обрабатывать действие, вызывая существующий UITextViewDelegate
метод textView(_:,shouldInteractWith:,in:,interaction:)
. С таким же успехом вы можете поместить код обработки непосредственно в действие, а не использовать делегат.
@IBAction func handleAttachmentTap(_ sender: AttachmentTapGestureRecognizer) {
let _ = delegate?.textView?(self, shouldInteractWith: sender.attachment!, in: NSRange(location: sender.attachmentCharacterIndex!, length: 1), interaction: .invokeDefaultAction)
}
Вот основной класс.
import UIKit
import UIKit.UIGestureRecognizerSubclass
/// Recognizes a tap on an attachment, on a UITextView.
/// The UITextView normally only informs its delegate of a tap on an attachment if the text view is not editable, or a long tap is used.
/// If you want an editable text view, where you can short cap an attachment, you have a problem.
/// This gesture recognizer can be added to the text view, and will add requirments in order to recognize before any built-in recognizers.
class AttachmentTapGestureRecognizer: UIGestureRecognizer {
/// Character index of the attachment just tapped
private(set) var attachmentCharacterIndex: Int?
/// The attachment just tapped
private(set) var attachment: NSTextAttachment?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
attachmentCharacterIndex = nil
attachment = nil
let textView = view as! UITextView
if touches.count == 1, let touch = touches.first, touch.tapCount == 1 {
let point = touch.location(in: textView)
let glyphIndex: Int? = textView.layoutManager.glyphIndex(for: point, in: textView.textContainer, fractionOfDistanceThroughGlyph: nil)
let index: Int? = textView.layoutManager.characterIndexForGlyph(at: glyphIndex ?? 0)
if let characterIndex = index, characterIndex < textView.textStorage.length {
if NSAttachmentCharacter == (textView.textStorage.string as NSString).character(at: characterIndex) {
attachmentCharacterIndex = characterIndex
attachment = textView.textStorage.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment
state = .recognized
} else {
state = .failed
}
}
} else {
state = .failed
}
}
}
extension UITextView {
/// Add an attachment recognizer to a UITTextView
func add(_ attachmentRecognizer: AttachmentTapGestureRecognizer) {
for other in gestureRecognizers ?? [] {
other.require(toFail: attachmentRecognizer)
}
addGestureRecognizer(attachmentRecognizer)
}
}
Этот же подход можно предположительно использовать для постукивания по ссылкам.
Ответ 4
Я изменил распознаватель жестов Дрю здесь для подкласса UITapGestureRecognizer
а не UIGestureRecognizer
.
Это дает одно преимущество в том, что он обнаруживает только отдельные касания, а не начало прокрутки.
import UIKit
import UIKit.UIGestureRecognizerSubclass
// Modified from: /questions/447075/how-to-detect-touch-on-nstextattachment/26288505#26288505
/// Recognizes a tap on an attachment, on a UITextView.
/// The UITextView normally only informs its delegate of a tap on an attachment if the text view is not editable, or a long tap is used.
/// If you want an editable text view, where you can short cap an attachment, you have a problem.
/// This gesture recognizer can be added to the text view, and will add requirments in order to recognize before any built-in recognizers.
class AttachmentTapGestureRecognizer: UITapGestureRecognizer {
typealias TappedAttachment = (attachment: NSTextAttachment, characterIndex: Int)
private(set) var tappedState: TappedAttachment?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
tappedState = nil
guard let textView = view as? UITextView else {
state = .failed
return
}
if let touch = touches.first {
tappedState = evaluateTouch(touch, on: textView)
}
if tappedState != nil {
// UITapGestureRecognizer can accurately differentiate discrete taps from scrolling
// Therefore, let the super view evaluate the correct state.
super.touchesBegan(touches, with: event)
} else {
// User didn't initiate a touch (tap or otherwise) on an attachment.
// Force the gesture to fail.
state = .failed
}
}
/// Tests to see if the user has tapped on a text attachment in the target text view.
private func evaluateTouch(_ touch: UITouch, on textView: UITextView) -> TappedAttachment? {
let point = touch.location(in: textView)
let glyphIndex: Int? = textView.layoutManager.glyphIndex(for: point, in: textView.textContainer, fractionOfDistanceThroughGlyph: nil)
let index: Int? = textView.layoutManager.characterIndexForGlyph(at: glyphIndex ?? 0)
guard let characterIndex = index, characterIndex < textView.textStorage.length else {
return nil
}
guard NSTextAttachment.character == (textView.textStorage.string as NSString).character(at: characterIndex) else {
return nil
}
guard let attachment = textView.textStorage.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment else {
return nil
}
return (attachment, characterIndex)
}
}
Ответ 5
Используйте hitTest, чтобы получить штрих в подклассе UITextView. Это позволяет избежать проблем со стандартными функциями редактирования. Из позиции получите индекс символа, а затем проверьте символ для вложения.
Ответ 6
Джош ответил почти идеально. Однако, если вы нажмете в пустое пространство вашего UITextView после конца ввода, glyphIndex (for: in: фракцияOfDistanceThroughGlyph) вернет последний глиф в строке. Если это ваше вложение, оно будет неверно оценено как истинное.
Документы Apple гласят: Если глиф не находится под точкой, возвращается ближайший глиф, где ближайший определяется в соответствии с требованиями выбора мышью. Клиенты, которые хотят определить, находится ли точка на самом деле в пределах возвращенного глифа, должны следовать за этим вызовом boundingRect (forGlyphRange: in :) и проверить, попадает ли точка в прямоугольник, возвращенный этим методом.
Итак, вот настроенная версия (Swift 5, XCode 10.2), которая выполняет дополнительную проверку границ обнаруженного глифа. Я полагаю, что некоторые тесты CharacterIndex теперь излишни, но они ничего не повредят.
Одно предостережение: глифы, кажется, простираются до высоты строки, содержащей их. Если у вас есть высокое вложение портретного изображения рядом с вложением ландшафтного изображения, касания в пустое пространство над ландшафтным изображением все равно будут иметь значение true.
import UIKit
import UIKit.UIGestureRecognizerSubclass
// Thanks to /info/447075/how-to-detect-touch-on-nstextattachment/26288506#26288506
// and https://stackoverflow.com/a/49153247/658604
/// Recognizes a tap on an attachment, on a UITextView.
/// The UITextView normally only informs its delegate of a tap on an attachment if the text view is not editable, or a long tap is used.
/// If you want an editable text view, where you can short cap an attachment, you have a problem.
/// This gesture recognizer can be added to the text view, and will add requirments in order to recognize before any built-in recognizers.
class AttachmentTapGestureRecognizer: UITapGestureRecognizer {
typealias TappedAttachment = (attachment: NSTextAttachment, characterIndex: Int)
private(set) var tappedState: TappedAttachment?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
tappedState = nil
guard let textView = view as? UITextView else {
state = .failed
return
}
if let touch = touches.first {
tappedState = evaluateTouch(touch, on: textView)
}
if tappedState != nil {
// UITapGestureRecognizer can accurately differentiate discrete taps from scrolling
// Therefore, let the super view evaluate the correct state.
super.touchesBegan(touches, with: event)
} else {
// User didn't initiate a touch (tap or otherwise) on an attachment.
// Force the gesture to fail.
state = .failed
}
}
/// Tests to see if the user has tapped on a text attachment in the target text view.
private func evaluateTouch(_ touch: UITouch, on textView: UITextView) -> TappedAttachment? {
let point = touch.location(in: textView)
let glyphIndex: Int = textView.layoutManager.glyphIndex(for: point, in: textView.textContainer, fractionOfDistanceThroughGlyph: nil)
let glyphRect = textView.layoutManager.boundingRect(forGlyphRange: NSRange(location: glyphIndex, length: 1), in: textView.textContainer)
guard glyphRect.contains(point) else {
return nil
}
let characterIndex: Int = textView.layoutManager.characterIndexForGlyph(at: glyphIndex)
guard characterIndex < textView.textStorage.length else {
return nil
}
guard NSTextAttachment.character == (textView.textStorage.string as NSString).character(at: characterIndex) else {
return nil
}
guard let attachment = textView.textStorage.attribute(.attachment, at: characterIndex, effectiveRange: nil) as? NSTextAttachment else {
return nil
}
return (attachment, characterIndex)
}
}