39

问题

在我的 iPad 应用程序中,我无法在按住事件后将弹出框附加到按钮栏项目。但这似乎是撤消/重做的标准。其他应用程序如何做到这一点?

背景

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

但是,iPad 上撤消/重做的“标准 UE 约定”是按下撤消执行撤消,但按住按钮会显示一个弹出控制器,其中用户选择“撤消”或“重做”,直到控制器被解除。

附加弹出框控制器的常规方法是使用 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)并且想要分享......

4

15 回答 15

25

注意:这不再适用于 iOS 11

代替试图在工具栏的子视图列表中找到 UIBarButtonItem 的视图的混乱,您也可以尝试这个,一旦项目被添加到工具栏:

[barButtonItem valueForKey:@"view"];

这使用键值编码框架来访问 UIBarButtonItem 的私有 _view 变量,它保存它创建的视图。

诚然,我不知道这在 Apple 的私有 API 方面属于哪里(这是用于访问公共类的私有变量的公共方法 - 不像访问私有框架来制作奇特的 Apple 专用效果或任何东西),但是它确实有效,而且相当无痛。

于 2012-02-21T02:09:36.390 回答
13

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

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

在该方法中,添加以下行:

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

如果是单击,则 tapCount 为 1。如果是双击,则 tapCount 为二。如果是长按,则 tapCount 为零。

于 2014-02-24T23:14:51.263 回答
9

选项1确实是可能的。不幸的是,找到 UIBarButtonItem 创建的 UIView 是一件痛苦的事情。我是这样找到它的:

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

这比它应该做的要困难,但这显然是为了阻止人们在按钮的外观和感觉上胡闹。

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

为了处理空格,您必须有某种方法来检测它们,遗憾的是 SDK 根本没有简单的方法来做到这一点。有一些解决方案,这里有一些:

1) 将 UIBarButtonItem 的标签值设置为它在工具栏上从左到右的索引。这需要太多的手动工作才能使其与 IMO 保持同步。

2) 将任何空间的启用属性设置为 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 以供以后使用。

于 2010-06-19T03:16:20.760 回答
7

您可以自己创建按钮并添加带有自定义视图的按钮栏项目,而不是四处寻找子视图。然后将 GR 连接到自定义按钮。

于 2010-07-26T19:00:04.990 回答
5

虽然这个问题现在已经有一年多了,但这仍然是一个非常烦人的问题。我已经向 Apple ( rdar://9982911 ) 提交了错误报告,我建议其他有相同感受的人复制它。

于 2011-08-19T12:09:23.450 回答
3

你也可以简单地这样做......

let longPress = UILongPressGestureRecognizer(target: self, action: "longPress:")
navigationController?.toolbar.addGestureRecognizer(longPress)

func longPress(sender: UILongPressGestureRecognizer) {
let location = sender.locationInView(navigationController?.toolbar)
println(location)
}
于 2015-02-03T12:05:42.727 回答
3

在 iOS 11 之前,let barbuttonView = barButton.value(forKey: "view") as? UIView将为我们提供 barButton 视图的引用,我们可以在其中轻松添加手势,但在 iOS 11 中情况完全不同,上面的代码行将以nil结尾,因此将点击手势添加到视图中关键“视图”是没有意义的。

不用担心,我们仍然可以向 UIBarItems 添加点击手势,因为它有一个属性 customView。我们可以做的是创建一个高度和宽度为 24 pt(根据 Apple 人机界面指南)的按钮,然后将自定义视图分配为新创建的按钮。下面的代码将帮助您执行一个操作以单击一次,另一个操作用于轻击栏按钮 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)          
}
于 2017-12-26T05:12:13.303 回答
2

我尝试了类似于 Ben 建议的方法。我用 UIButton 创建了一个自定义视图,并将其用作 UIBarButtonItem 的 customView。有几件事我不喜欢这种方法:

  • 按钮的样式需要设置为不会像 UIToolBar 上的拇指酸痛一样突出
  • 使用 UILongPressGestureRecognizer 我似乎没有收到“Touch up Inside”的点击事件(这可能/很可能是我的编程错误。)

相反,我充其量只接受了一些骇人听闻的东西,但它对我有用。我使用的是 XCode 4.2,我在下面的代码中使用了 ARC。我创建了一个名为 CustomBarButtonItemView 的新 UIViewController 子类。在 CustomBarButtonItemView.xib 文件中,我创建了一个 UIToolBar 并向工具栏添加了一个 UIBarButtonItem。然后我将工具栏缩小到几乎按钮的宽度。然后我将 File's Owner 视图属性连接到 UIToolBar。

CustomBarButtonViewController 的界面生成器视图

然后在我的 ViewController 的 viewDidLoad: 消息中,我创建了两个 UIGestureRecognizers。第一个是用于单击并按住的 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 文件连接)中发生单击时,点击手势会调用 buttonPressed: 消息。如果按钮被按住 longPressGestur 被触发。

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

我发现的一个问题是 customview 属性按原样获取视图。如果您从 CustomBarButtonItemView.xib 更改自定义 UIBarButtonitem 以将标签更改为@"really long string",例如,按钮将自行调整大小,但仅显示按钮的最左侧部分位于 UIGestuerRecognizer 实例正在监视的视图中。

于 2011-11-04T19:55:00.483 回答
2

我尝试了@voi1d 的解决方案,效果很好,直到我更改了添加了长按手势的按钮的标题。更改标题似乎会为替换原始按钮的按钮创建一个新的 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 更改(并因此创建一个新视图)时,我的自定义手势就会被附加,所以它总是有效的!

于 2012-01-25T13:16:21.990 回答
2

我知道这已经过时了,但我花了一个晚上把头撞在墙上,试图找到一个可以接受的解决方案。我不想使用 customView 属性,因为它会摆脱所有内置功能,如按钮色调、禁用色调,并且长按会受到如此小的命中框,而 UIBarButtonItems 会将其命中框展开相当大方法。我想出了这个我认为效果很好的解决方案,实施起来只是有点痛苦。

在我的情况下,如果长按,我栏上的前 2 个按钮会转到同一个位置,所以我只需要检测在某个 X 点之前发生了按下。我将长按手势识别器添加到 UIToolbar(如果将其添加到 UINavigationBar 也可以),然后在第二个按钮之后添加了一个 1 像素宽的额外 UIBarButtonItem。当视图加载时,我将一个像素宽的 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 是在这两个垫片之间。

于 2017-04-20T08:55:35.827 回答
1

@voi1d 的第二个选项答案对于那些不想重写UIBarButtonItem's 的所有功能的人来说是最有用的。我将其包装在一个类别中,以便您可以执行以下操作:

[myToolbar addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton];

如果您有兴趣,请进行一些错误处理。注意:每次使用添加或删除工具栏中的项目时setItems,您都必须重新添加任何手势识别器——我猜 UIToolbar 每次调整项目数组时都会重新创建持有的 UIView。

UIToolbar+手势.h

#import <UIKit/UIKit.h>

@interface UIToolbar (Gesture)

- (void)addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton;

@end

UI工具栏+手势.m

#import "UIToolbar+Gesture.h"

@implementation UIToolbar (Gesture)

- (void)addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton {
  NSUInteger index = 0;
  NSInteger savedTag = barButton.tag;

  barButton.tag = NSNotFound;
  for (UIBarButtonItem *anItem in [self items]) {
    if (anItem.enabled) {
      anItem.tag = index;
      index ++;
    }
  }
  if (NSNotFound != barButton.tag) {
    [[[self subviews] objectAtIndex:barButton.tag] addGestureRecognizer:recognizer];
  }
  barButton.tag = savedTag;
}

@end
于 2012-01-22T17:24:37.980 回答
0

This is the most Swift-friendly and least hacky way I came up with. Works in iOS 12.

Swift 5

var longPressTimer: Timer?

let button = UIButton()
button.addTarget(self, action: #selector(touchDown), for: .touchDown)
button.addTarget(self, action: #selector(touchUp), for: .touchUpInside)
button.addTarget(self, action: #selector(touchCancel), for: .touchCancel)
let undoBarButton = UIBarButtonItem(customView: button)

@objc func touchDown() {
    longPressTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(longPressed), userInfo: nil, repeats: false)
}

@objc func touchUp() {
    if longPressTimer?.isValid == false { return } // Long press already activated
    longPressTimer?.invalidate()
    longPressTimer = nil
    // Do tap action
}

@objc func touchCancel() {
    longPressTimer?.invalidate()
    longPressTimer = nil
}

@objc func longPressed() {
    // Do long press action
}
于 2018-10-21T04:19:51.930 回答
0

我知道这不是最好的解决方案,但我将发布一个对我有用的相当简单的解决方案。

我创建了一个简单的扩展UIBarButtonItem

fileprivate extension UIBarButtonItem {
    var view: UIView? {
        return value(forKey: "view") as? UIView
    }
    func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
        view?.addGestureRecognizer(gestureRecognizer)
    }
}

在此之后,您可以简单地将手势识别器添加到 ViewControllerviewDidLoad方法中的项目中:

@IBOutlet weak var myBarButtonItem: UIBarButtonItem!

func setupLongPressObservation() {
    let recognizer = UILongPressGestureRecognizer(
        target: self, action: #selector(self.didLongPressMyBarButtonItem(recognizer:)))
    myBarButtonItem.addGestureRecognizer(recognizer)
}
于 2017-05-08T09:38:25.423 回答
0

准备使用UIBarButtonItem子类:

@objc protocol BarButtonItemDelegate {
    func longPress(in barButtonItem: BarButtonItem)
}

class BarButtonItem: UIBarButtonItem {
    @IBOutlet weak var delegate: BarButtonItemDelegate?
    private let button = UIButton(type: .system)

    override init() {
        super.init()
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    private func setup() {
        let recognizer = UILongPressGestureRecognizer(
            target: self,
            action: #selector(longPress)
        )
        button.addGestureRecognizer(recognizer)
        button.setImage(image, for: .normal)
        button.tintColor = tintColor
        customView = button
    }

    override var action: Selector? {
        set {
            if let action = newValue {
                button.addTarget(target, action: action, for: .touchUpInside)
            }
        }
        get { return nil }
    }

    @objc private func longPress(sender: UILongPressGestureRecognizer) {
        if sender.state == .began {
            delegate?.longPress(in: self)
        }
    }
}
于 2019-03-20T16:33:05.627 回答
0

@utopians 在 Swift 4.2 中回答

@objc func myAction(_ sender: UIBarButtonItem, forEvent event:UIEvent) {
    let longPressed:Bool = (event.allTouches?.first?.tapCount).map {$0 == 0} ?? false

    ... handle long press ...
}
于 2018-05-14T20:33:53.030 回答