如何在iPad应用程序的常见撤销/重做UIPopoverController方案中向UIBarButtonItem添加UIGestureRecognizer?

40

问题

我的iPad应用程序在长按按钮后,无法将弹出窗口附加到按钮栏项上。但这似乎是撤消/重做的标准操作方式。其他应用程序如何实现呢?

背景

在我的UIKit(iPad)应用程序工具栏中有一个“撤消”按钮(UIBarButtonSystemItemUndo)。当我按下撤消按钮时,它会触发其操作,即undo:,并且正确执行。

然而,在iPad上,撤消/重做的“标准UE约定”是按下撤消执行撤消操作,但是如果“按住不放”该按钮,则会显示弹出控制器,用户可以选择“undo”或“redo”,直到控制器被关闭。

附加弹出控制器的正常方式是使用presentPopoverFromBarButtonItem:方法,我可以轻松配置此项。为了使其仅在长按后显示,我们必须设置视图以响应“长按”手势事件,如以下代码片段:

UILongPressGestureRecognizer *longPressOnUndoGesture = [[UILongPressGestureRecognizer alloc] 
       initWithTarget:self 
               action:@selector(handleLongPressOnUndoGesture:)];
//Broken because there is no customView in a UIBarButtonSystemItemUndo item
[self.undoButtonItem.customView addGestureRecognizer:longPressOnUndoGesture];
[longPressOnUndoGesture release];
通过这种方式,在视图上按住并保持,方法handleLongPressOnUndoGesture:将被调用,在该方法内我将配置并显示撤销/重做的弹出窗口。 到目前为止,一切都很好。
问题在于没有视图可供附加。self.undoButtonItem是一个UIButtonBarItem,而不是视图。
可能的解决方案:
1) [理想情况] 将手势识别器附加到按钮栏项上。可以将手势识别器附加到视图,但UIButtonBarItem不是视图。它确实有一个.customView属性,但是当buttonbaritem是标准系统类型(在本例中是)时,该属性为nil。
2) 使用另一个视图。我可以使用UIToolbar,但是那需要进行一些奇怪的命中测试,并且是所有的黑客,即使首先可能也无法实现。我想不到其他可用的替代视图。
3) 使用customView属性。像UIBarButtonSystemItemUndo这样的标准类型没有customView(为nil)。设置customView将删除它需要具有的标准内容。这将相当于重新实现UIBarButtonSystemItemUndo的所有外观和功能,即使可以做到也是如此。
问题:如何将手势识别器附加到此“按钮”上?更具体地说,在iPad应用程序中如何实现标准的按住并保持以显示重做弹出窗口?
想法?非常感谢,特别是如果有人实际上在他们的应用程序中使用了这个(我在想你,omni),并且愿意分享...
15个回答

25

注意:自iOS 11以来,此方法已不再适用

代替在工具栏的子视图列表中寻找UIBarButtonItem的视图的麻烦,您也可以尝试一下,在项目添加到工具栏后:

[barButtonItem valueForKey:@"view"];

这段代码使用了 Key-Value Coding 框架来访问 UIBarButtonItem 的私有 _view 变量,该变量保存了它所创建的视图。

我不确定这是否违反了苹果的私有 API 规定(这是一种访问公共类的私有变量的公共方法,而不是像访问私有框架以实现炫酷的只在苹果设备上生效的效果之类的),但它确实可以工作,并且非常容易实现。


很棒的回答 +1,API/合法性问题仍然存在。嗯嗯 - Dan Rosenstark
3
这是访问视图最简单和最可靠的方法。最终,此处建议的所有其他方法本质上都访问相同的变量,因此它们不比这个更“合法”-只是更难检测。是否有人尝试过提交使用此“valueForKey”方法的应用程序? - LPG
8
这样做更简单,而且_可能_更具有未来可扩展性。使用私有API的一个重大风险(除了被拒绝之外)是你的代码在未来可能会出现错误。将此代码包装在if ([[sortmodeBarButtonItem valueForKey:@"view"] respondsToSelector:@selector(addGestureRecognizer:)])中,可以使其更加稳健。 - William Denniss
3
这很有效。只需要在 viewDidAppear 中使用它,因为如果在早期调用它会返回 nil。有人使用过这种技术并成功地启动了他们的应用程序吗? - Dan Loughney
这是一个Objective-C中的UIBarButtonItem子类,它允许您为长按设置目标和操作,因此您不需要将该混乱放入视图控制器中。它会在长按触发时立即发送动作(而不是在touches ended上),如果工具栏按钮项被禁用,则方便地不会触发:https://pastebin.com/0a6DvNFM - Zac
1
自从IOS11以来,[barButtonItem valueForKey:@"view"]将返回nil。唯一的解决方案似乎是实现一个自定义视图。请参见laxman khanal的答案。 - EckhardN

13

这是一个老问题,但在谷歌搜索中仍经常出现,而且所有其他答案都过于复杂。

我有一个按钮栏,带有按钮栏项,当按下时调用action:forEvent:方法。

在该方法中,添加以下代码:

bool longpress=NO;
UITouch *touch=[[[event allTouches] allObjects] objectAtIndex:0];
if(touch.tapCount==0) longpress=YES;

如果是单击,tapCount为1。如果是双击,tapCount为2。如果是长按,则tapCount为0。


你如何使一个UIBarButtonItem调用action:forEvent:方法?它不是只能调用action:sender:方法吗? - arlomedia
啊,我没有在任何地方找到这个记录,但是我刚刚将UIBarButtonItem的操作设置为myMethod:forEvent:并且它起作用了,将事件对象传递给我的方法。不幸的是,直到我释放点击时才调用该方法,因此尽管我可以检查tapCount以查看是否完成了长按,但我无法在点击变成长按的那一刻执行操作。 - arlomedia
2
Utopian值得10+颗星。在5+年的iPhone开发后,我(再次!)遇到了这个问题,我尝试了valueForKeyUILongPressGestureRecognizer方法,但显然在iOS 8上已经不起作用了,但是action:forEvent方法仍然有效。太好了!谢谢!只需要稍微澄清一下:您需要将操作方法更改为**-(IBAction)button_hit:(id)sender forEvent:(UIEvent *)event { ... }并使用...@selector(button_hit:forEvent:)**添加目标事件或按照通常的方式从XCode连接。 - Marcus
这绝对是最佳解决方案,也适用于iOS8.3。 - Gerrit Beuze
4
表现很好。需要注意的是,我认为这与 UILongPressGestureRecognizer 稍有不同,因为它首先在长按后松开触摸 FYI。 - Steve Moser

9

选项1确实可行。不幸的是,找到UIBarButtonItem创建的UIView很麻烦。以下是我找到它的方法:

[[[myToolbar subviews] objectAtIndex:[[myToolbar items] indexOfObject:myBarButton]] addGestureRecognizer:myGesture];

这比应该做的更困难,但显然是为了阻止人们随意修改按钮的外观。

请注意,固定/灵活空间被视为视图!

为了处理空格,您必须有一些方法来检测它们,但遗憾的是 SDK 没有简单的方法来实现此目的。以下是一些解决方案:

1)将 UIBarButtonItem 的标记值设置为其在工具栏上从左到右的索引。但我认为这需要太多的手动操作来保持同步。

2)将任何空格的 enabled 属性设置为 NO。然后使用此代码片段为您设置标记值:

NSUInteger index = 0;
for (UIBarButtonItem *anItem in [myToolbar items]) {
    if (anItem.enabled) {
        // For enabled items set a tag.
        anItem.tag = index;
        index ++;
    }
}

// Tag is now equal to subview index.
[[[myToolbar subviews] objectAtIndex:myButton.tag] addGestureRecognizer:myGesture];

当然,如果你因为其他原因禁用了按钮,这可能存在潜在的问题。

3)手动编码工具栏并自己处理索引。由于您将自己构建UIBarButtonItem,因此您将事先知道它们将位于子视图中的哪个索引上。如果必要,您可以将此想法扩展到预先收集UIView以供以后使用。


我在这个问题上遇到了一个奇怪的问题: 系统在重新排序我的工具栏子视图,我找不到任何原因。 我采取了按它们的frame.origin.x(有效地)对子视图排序的方法。此外,系统将一个UIView插入到工具栏子视图中,该UIView具有负x和y。 我发誓这不是我的问题...(顺便说一下,这是使用4.3 SDK。) - JLundell
非常好的答案--感谢@v01d。我用它来派生一个UIToolbar(Gesture)类别来封装这个(在这里看到我的答案)。请注意,每次分配给toolbar.items或使用[toolbar setItems]时,您都必须重新添加手势识别器,因为底层UIView似乎每次都会被重新创建。 - natbro

7

不必在子视图中搜索按钮,您可以自己创建按钮并添加具有自定义视图的按钮栏项。然后将GR连接到您的自定义按钮。


5
虽然这个问题已经有一年多的时间了,但仍然是一个相当恼人的问题。我向苹果提交了一个错误报告(rdar://9982911),建议任何感觉相同的人都复制它。

3
在iOS 11之前,let barbuttonView = barButton.value(forKey: "view") as? UIView会给我们提供barButton的视图引用,我们可以在其中轻松添加手势,但是在iOS 11中,以上代码将以nil结束,因此向“view”键的视图添加点击手势是没有意义的。
不用担心,我们仍然可以向UIBarItems添加点击手势,因为它有一个customView属性。我们可以创建一个高度和宽度均为24个点的按钮(根据Apple人机界面指南),然后将自定义视图分配为新创建的按钮。下面的代码将帮助您执行单击一个操作并另一个操作来点击bar button 5次。 注意:为此,您必须已经引用了barButtonItem。
func setupTapGestureForSettingsButton() {
      let multiTapGesture = UITapGestureRecognizer()
      multiTapGesture.numberOfTapsRequired = 5
      multiTapGesture.numberOfTouchesRequired = 1
      multiTapGesture.addTarget(self, action: #selector(HomeTVC.askForPassword))
      let button = UIButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24))
      button.addTarget(self, action: #selector(changeSettings(_:)), for: .touchUpInside)
      let image = UIImage(named: "test_image")withRenderingMode(.alwaysTemplate)
      button.setImage(image, for: .normal)
      button.tintColor = ColorConstant.Palette.Blue
      settingButton.customView = button
      settingButton.customView?.addGestureRecognizer(multiTapGesture)          
}

3
您也可以简单地执行以下操作...
let longPress = UILongPressGestureRecognizer(target: self, action: "longPress:")
navigationController?.toolbar.addGestureRecognizer(longPress)

func longPress(sender: UILongPressGestureRecognizer) {
let location = sender.locationInView(navigationController?.toolbar)
println(location)
}

2

我尝试了@voi1d的解决方案,它很好地工作了,直到我更改了我添加长按手势的按钮的标题。更改标题似乎会为按钮创建一个新的UIView,用来替换原始的UIView,从而导致添加的手势在对按钮进行更改时停止工作(这在我的应用程序中经常发生)。

我的解决方案是子类化UIToolbar并覆盖addSubview:方法。我还创建了一个属性,用于保存我的手势目标的指针。以下是精确的代码:

- (void)addSubview:(UIView *)view {
    // This method is overridden in order to add a long-press gesture recognizer
    // to a UIBarButtonItem. Apple makes this way too difficult, but I am clever!
    [super addSubview:view];
    // NOTE - this depends the button of interest being 150 pixels across (I know...)
    if (view.frame.size.width == 150) {
        UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:targetOfGestureRecognizers 
                                                                                                action:@selector(showChapterMenu:)];
        [view addGestureRecognizer:longPress];
    }
}

在我的情况下,我感兴趣的按钮宽度为150像素(也是唯一一个),因此这是我使用的测试。这可能不是最安全的测试方法,但对我来说有效。显然,您需要想出自己的测试并提供自己的手势和选择器。
这种方法的好处是,每当我的UIBarButtonItem更改(从而创建新视图)时,我的自定义手势都会附加,因此它始终有效!

我喜欢这个方法,因为我的UIBarButtonItem经常无缘无故地随机更改。但是,通过将此方法与上面的[barButtonItem valueForKey:@"view"];方法相结合,它可以得到改进(变得更加清晰)。这样可以明确引用属于工具栏按钮项的视图。只需将要添加到工具栏的视图与barButtonItem报告所属的视图进行比较即可。非常可靠! - Rob Glassey

2
我尝试了Ben建议的类似方法。我创建了一个自定义视图,其中包含一个UIButton,并将其用作UIBarButtonItem的自定义视图。但是,这种方法有一些我不喜欢的地方:
  • 需要对按钮进行样式设置,以使其在UIToolBar上不突兀。
  • 使用UILongPressGestureRecognizer时,好像无法获取“Touch up Inside”事件(这可能是我的编程错误)。
因此,我采用了某种程度上有点hackish的方法,但它对我有效。我使用XCode 4.2并在下面的代码中使用ARC。我创建了一个名为CustomBarButtonItemView的新UIViewController子类。在CustomBarButtonItemView.xib文件中,我创建了一个UIToolBar,并向其中添加了一个UIBarButtonItem。然后,我将工具栏缩小到几乎与按钮相同的宽度。接着,我将File's Owner视图属性连接到UIToolBar。

Interface Builder view of CustomBarButtonViewController

然后,在我的ViewController的viewDidLoad:消息中,我创建了两个UIGestureRecognizer。第一个是用于单击和长按的UILongPressGestureRecognizer,第二个是UITapGestureRecognizer。我似乎无法正确获取视图中UIBarButtonItem的操作,因此我使用UITapGestureRecognizer进行模拟。UIBarButtonItem会显示为已点击,而UITapGestureRecognizer则负责处理操作,就像设置了UIBarButtonItem的操作和目标一样。
- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib

    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestured)];

    UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(buttonPressed:)];

    CustomBarButtomItemView* customBarButtonViewController = [[CustomBarButtomItemView alloc] initWithNibName:@"CustomBarButtonItemView" bundle:nil];

    self.barButtonItem.customView = customBarButtonViewController.view;

    longPress.minimumPressDuration = 1.0;

    [self.barButtonItem.customView addGestureRecognizer:longPress];
    [self.barButtonItem.customView addGestureRecognizer:singleTap];        

}

-(IBAction)buttonPressed:(id)sender{
    NSLog(@"Button Pressed");
};
-(void)longPressGestured{
    NSLog(@"Long Press Gestured");
}

现在,当ViewController的barButtonItem(通过xib文件连接)被单击时,tap手势会调用buttonPressed:消息。如果按钮长按,则触发longPressGesture。

为了改变UIBarButton的外观,建议为CustomBarButtonItemView创建一个属性,以允许访问Custom BarButton并将其存储在ViewController类中。当发送longPressGesture消息时,您可以更改按钮的系统图标。

我发现其中一个陷阱是customview属性将视图保持不变。如果您修改CustomBarButtonItemView.xib中的自定义UIBarButtonitem,例如将标签更改为“really long string”,则按钮将调整大小,但仅在UIGestuer Recognizer实例所监视的视图的最左侧部分中显示。


2

我知道这已经过时了,但我花了一整晚的时间试图找到一个可接受的解决方案。我不想使用customView属性,因为那样会摆脱所有内置功能,如按钮色调、禁用色调,而且长按操作将受到很小的点击区域限制,而UIBarButtonItems则可以将其点击区域扩展出去。我想出了这个解决方案,我认为它非常有效,只需要稍微费点功夫来实现。

在我的情况下,工具栏上的前两个按钮在长按时都会跳转到同一个位置,所以我只需要检测在某个X点之前是否发生了按下事件。我将长按手势识别器添加到UIToolbar(如果您将其添加到UINavigationBar也可以),然后在第二个按钮右侧添加了一个额外的UIBarButtonItem,宽度为1像素。当视图加载时,我将一个单像素宽的UIView添加到该UIBarButtonItem的customView中。现在,我可以测试长按发生的点,并查看它的X是否小于customview框架的X。这是一段简单的Swift 3代码:

@IBOutlet var thinSpacer: UIBarButtonItem!

func viewDidLoad() {
    ...
    let thinView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 22))
    self.thinSpacer.customView = thinView

    let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressed(gestureRecognizer:)))
    self.navigationController?.toolbar.addGestureRecognizer(longPress)
    ...
}

func longPressed(gestureRecognizer: UIGestureRecognizer) {
    guard gestureRecognizer.state == .began, let spacer = self.thinSpacer.customView else { return }
    let point = gestureRecognizer.location(ofTouch: 0, in: gestureRecognizer.view)
    if point.x < spacer.frame.origin.x {
        print("Long Press Success!")
    } else {
        print("Long Pressed Somewhere Else")
    }
}

虽然不是最理想的方法,但对于我的使用情况来说还是足够简单。如果你需要在特定位置的特定按钮上指定长按,则会变得有点烦人,但是你应该能够用细小的间隔线将需要检测长按的按钮包围起来,然后只需检查你的点的X坐标是否在这两个间隔线之间。


这适用于iOS 11。使用细间隔符的替代方法(不幸的是会改变其他项目的对齐方式)是将触摸位置与常量进行比较。如果所涉及的按钮恰好位于最左侧或最右侧的极端位置,这就非常容易,例如 if (point.x > self.view.frame.size.width-60) - William Denniss

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