检测用户在iOS上点击NSTextAttachment
的最佳方法是什么?
我认为其中一种方法是检查carret位置上的角色是否是NSAttachmentCharacter
,但它似乎并不正确。
我也尝试过UITextViewDelegate
方法:-(BOOL)textView:(UITextView *)textView shouldInteractWithTextAttachment:(NSTextAttachment *)textAttachment inRange:(NSRange)characterRange
但是当textView.editable=YES
没有调用它
委托方法确实有效,但只有附件在图像属性中有图像并且如果editable = NO!因此,如果你有一个图像粘贴到来自其他地方的attributionString,似乎数据最终存储在fileWrapper中,下次你将attributedString放回textView时,image属性为nil,布局管理器或其他任何东西都会得到图片来自fileWrapper。
在文档的某处,它确实提到NSTextAttachment中没有方法来保持图像属性的持久性。
要测试此操作,请尝试从Photo应用程序复制照片并将其粘贴到textView中,现在如果您按住手指,则应该会弹出默认菜单。现在,如果你保存这个富文本,比如说进入一个核心数据实体,然后检索它,图像属性将是零,但图像数据将在attachment.fileWrapper.regularFileContents
这很痛苦,我很想知道工程师的意图。所以你有两个选择。
请记住,这样做的副作用是使fileWrapper无效。我想调整图像的大小,但也保留原始图像,所以我没有松开全分辨率。我认为这样做的唯一方法可能是继承NSTextAttachment。
编辑:
我想出了如何创建自定义NSTextAttachments - 这里是一个链接感兴趣的http://ossh.com.au/design-and-technology/software-development/implementing-rich-text-with-images-on-os-x-and-ios/
编辑2:要在编辑模式下自定义菜单,请参阅以下Apple文档,问题是“touchEnded”似乎永远不会被调用,因此您可能必须尝试使用touchesBegan。但请注意,不要干扰默认的编辑行为。
请注意,在下面的代码中,您需要在// 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];
}
斯威夫特3回答:
func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange) -> Bool {
return true
}
确保你的textView isEditable = false
,isSelectable = true
和isUserInteractionEnabled = true
。 Duncan的答案没有提及isUserInteractionEnabled
,这必须是true
,否则它不会工作。
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)
}
}
也许这种方法可以用于链接上的点击。
我已经将Drew的手势识别器here改为子类UITapGestureRecognizer
而不是UIGestureRecognizer
。
这提供了一个优点,因为它仅检测离散的抽头而不是滚动的开始。
import UIKit
import UIKit.UIGestureRecognizerSubclass
// Modified from: 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 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)
}
}
Josh的answer几乎是完美的。但是,如果您在输入结束后点击UITextView的空白区域,则glyphIndex(for:in:fractionOfDistanceThroughGlyph)将返回字符串中的最终字形。如果这是您的附件,则会错误地评估为true。
Apple的文档说:如果没有字形在点之下,则返回最近的字形,其中最接近的字形根据鼠标选择的要求定义。希望确定该点是否实际位于返回的字形范围内的客户端应该通过调用boundingRect(forGlyphRange:in :)来测试该点是否落在该方法返回的矩形中。
所以,这是一个调整版本(Swift 5,XCode 10.2),它对检测到的字形的边界执行额外的检查。我相信一些characterIndex测试现在是多余的,但它们不会伤害任何东西。
一个警告:字形似乎延伸到包含它们的行的高度。如果在横向图像附件旁边有高图像图像附件,则在横向图像上方的空白处的点按仍将评估为true。
import UIKit
import UIKit.UIGestureRecognizerSubclass
// Thanks to https://stackoverflow.com/a/52883387/658604
// 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)
}
}
使用hitTest来触摸子类UITextView。这避免了弄乱标准编辑功能的问题。从位置获取字符索引,然后检查附件的字符。