如何在UIBarButtonItem中添加UIGestureRecognizer,就像在iPad应用程序中常见的undo / redo UIPopoverController方案一样?

问题描述 投票:39回答:15

问题

在我的iPad应用程序中,仅在按下并保持事件后才能将弹出窗口附加到按钮栏项目。但这似乎是撤消/重做的标准。其他应用程序如何做到这一点?

背景

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

然而,在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:将被调用,在这个方法中,我将配置并显示undo / redo的弹出框。到现在为止还挺好。

这个问题是没有附加视图。 self.undoButtonItem是一个UIButtonBarItem,而不是一个视图。

可能的解决方案

1)[理想]将手势识别器附加到按钮栏项目。可以将手势识别器附加到视图,但UIButtonBarItem不是视图。它确实具有.customView的属性,但当buttonbaritem是标准系统类型时(在这种情况下它是),该属性为nil。

2)使用另一个视图。我可以使用UIToolbar,但这需要一些奇怪的命中测试,并且如果可能的话首先是一个全面的黑客攻击。我无法想到使用其他替代视图。

3)使用customView属性。像UIBarButtonSystemItemUndo这样的标准类型没有customView(它是nil)。设置customView将删除它需要的标准内容。这将相当于重新实现UIBarButtonSystemItemUndo的所有外观和功能,如果可能的话。

如何将手势识别器附加到此“按钮”?更具体地说,如何在iPad应用程序中实现标准的按住 - 显示 - 重做 - 弹出窗口?

想法?非常感谢你,特别是如果有人真的在他们的应用程序中工作(我在想你,omni)并希望分享...

cocoa-touch ipad uibarbuttonitem undo-redo uigesturerecognizer
15个回答
24
投票

注意:这不再适用于iOS 11

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

[barButtonItem valueForKey:@"view"];

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

当然,我不知道Apple的私有API事件在哪里(这是用于访问公共类的私有变量的公共方法 - 不像访问私有框架来制作纯粹的Apple效果或任何东西),但是它确实有效,而且非常轻松。


2
投票

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

不用担心我们仍然可以向UIBarItems添加点击手势,因为它有一个属性customView。我们可以做的是创建一个高度和宽度为24磅的按钮(根据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)          
}

1
投票

@ voi1d的第二个选项答案对于那些不想重写UIBarButtonItem的所有功能的人来说是最有用的。我把它包装在一个类别中,这样你就可以:

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

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

UIToolbar + Gesture.h

#import <UIKit/UIKit.h>

@interface UIToolbar (Gesture)

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

@end

UIToolbar + Gesture.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

0
投票

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

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

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

在此之后,您只需将手势识别器添加到ViewController的viewDidLoad方法中的项目:

@IBOutlet weak var myBarButtonItem: UIBarButtonItem!

func setupLongPressObservation() {
    let recognizer = UILongPressGestureRecognizer(
        target: self, action: #selector(self.didLongPressMyBarButtonItem(recognizer:)))
    myBarButtonItem.addGestureRecognizer(recognizer)
}

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 ...
}

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)
        }
    }
}

0
投票

这是我提出的最友好,最不忠实的方式。适用于iOS 12。

斯威夫特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
}

12
投票

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

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

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

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

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


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以供以后使用。


7
投票

您可以自己创建按钮,然后使用自定义视图添加按钮栏项,而不是在子视图中进行搜索。然后将GR连接到您的自定义按钮。


5
投票

虽然这个问题已经超过一年了,但这仍然是一个非常烦人的问题。我已经向Apple提交了一份错误报告(rdar:// 9982911),我建议其他任何感觉相同的人都会复制它。


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
投票

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

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

相反,我最终解决了一些hackish,但它对我有用。我使用的是XCode 4.2,我在下面的代码中使用了ARC。我创建了一个名为CustomBarButtonItemView的新UIViewController子类。在CustomBarButtonItemView.xib文件中,我创建了一个UIToolBar,并在工具栏中添加了一个UIBarButtonItem。然后我将工具栏缩小到几乎按钮的宽度。然后我将File的Owner视图属性连接到UIToolBar。

然后在我的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:消息。如果按住按钮,则会触发longPressGestured。

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

我发现的一个问题是customview属性按原样查看。如果您从CustomBarButtonItemView.xib更改自定义UIBarButtonitem以将标签更改为@“真正长字符串”,例如按钮将自行调整大小,但只有显示的按钮的最左侧部分位于UIGestuerRecognizer实例正在观看的视图中。


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发生变化(从而创建一个新视图),我的自定义手势就会被附加,所以它总是有效!


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是否是两个间隔物之间​​。

© www.soinside.com 2019 - 2024. All rights reserved.