web-dev-qa-db-ja.com

iPadアプリの一般的な元に戻す/やり直しのUIPopoverControllerスキームのように、UIGestureRecognizerをUIBarButtonItemに追加するにはどうすればよいですか?

問題

私の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アプリに標準の長押しして表示をやり直すポップオーバーを実装するにはどうすればよいですか?

アイデア?特に誰かが実際にこれをアプリで動作させていて(私はあなたのことを考えています、オムニ)、共有したい場合は特にありがとうございます...

39
SG.

注:これはiOS 11以降では機能しなくなりました

ツールバーのサブビューリストでUIBarButtonItemのビューを見つけようとするという混乱の代わりに、アイテムがツールバーに追加されたら、これを試すこともできます。

[barButtonItem valueForKey:@"view"];

これは、Key-Valueコーディングフレームワークを使用して、UIBarButtonItemのプライベート_view変数にアクセスし、作成したビューを保持します。

確かに、これがAppleのプライベートAPIの観点からどこに当てはまるかはわかりません(これは、パブリッククラスのプライベート変数にアクセスするために使用されるパブリックメソッドです-プライベートフレームワークにアクセスして、Appleのみの派手な効果などを作成するのとは異なります)。それは機能しますが、かなり痛みはありません。

25
Tustin2121

これは古い質問ですが、それでもグーグル検索で出てきて、他のすべての答えは非常に複雑です。

ボタンバーアイテムを含むボタンバーがあり、押されたときにaction:forEvent:メソッドを呼び出します。

そのメソッドで、次の行を追加します。

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

シングルタップの場合、tapCountは1です。ダブルタップの場合、tapCountは2です。長押しの場合、tapCountはゼロです。

12
utopian

オプション1は確かに可能です。残念ながら、UIBarButtonItemが作成するUIViewを見つけるのは面倒です。これが私がそれを見つけた方法です:

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

これは本来あるべきよりも難しいですが、これは明らかに、ボタンのルックアンドフィールで人々がだまされないように設計されています。

固定/フレキシブルスペースはnotビューとしてカウントされることに注意してください!

スペースを処理するには、スペースを検出する何らかの方法が必要です。残念ながら、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を事前に収集することができます。

9
v01d

サブビューを模索する代わりに、独自にボタンを作成し、カスタムビューでボタンバーアイテムを追加できます。次に、GRをカスタムボタンに接続します。

7
Ben Stiglitz

この質問は1年以上前のものですが、それでもかなり厄介な問題です。私はバグレポートをApple(rdar:// 9982911)に提出しましたが、同じように感じている人は誰でもそれを複製することをお勧めします。

5
mc.schroeder

あなたもこれを簡単に行うことができます...

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

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

@ 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
Michael

私はこれが古いことを知っていますが、許容できる解決策を見つけるために壁に頭をぶつけて夜を過ごしました。 customViewプロパティを使用したくなかったのは、ボタンの色合い、無効な色合いなどの組み込み機能がすべて削除され、UIBarButtonItemsがヒットボックスをかなり広げている間、長押しするとそのような小さなヒットボックスが表示されるためです。方法。私はこのソリューションを思いついたのですが、これは本当にうまく機能し、実装するのはほんの少しの苦痛だと思います。

私の場合、バーの最初の2つのボタンは長押しすると同じ場所に移動するため、特定のXポイントの前に押されたことを検出する必要がありました。長押しジェスチャレコグナイザーをUIToolbarに追加し(UINavigationBarに追加しても機能します)、2番目のボタンの直後に幅1ピクセルのUIBarButtonItemを追加しました。ビューが読み込まれると、そのUIBarButtonItemに1ピクセル幅のUIViewをcustomViewとして追加します。これで、長押しが発生したポイントをテストして、Xがカスタムビューのフレームの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がそれらのスペーサーの両方の間。

2
Steve E

私はベンが提案したのと同じようなことを試みました。 UIButtonを使用してカスタムビューを作成し、それをUIBarButtonItemのcustomViewとして使用しました。このアプローチについて私が気に入らなかったことがいくつかありました。

  • UIToolBarの親指のように突き出ないようにボタンのスタイルを設定する必要がありました
  • UILongPressGestureRecognizerを使用すると、「Touch up Inside」のクリックイベントが発生しなかったようです(これは、私の側でプログラミングエラーである可能性があります)。

代わりに、私はせいぜいハックなものに落ち着きましたが、それは私にとってはうまくいきます。私はXCode4.2を使用しており、以下のコードでARCを使用しています。 CustomBarButtonItemViewという新しいUIViewControllerサブクラスを作成しました。 CustomBarButtonItemView.xibファイルで、UIToolBarを作成し、ツールバーに単一のUIBarButtonItemを追加しました。次に、ツールバーをボタンのほぼ幅まで縮小しました。次に、ファイルの所有者ビュープロパティをUIToolBarに接続しました。

Interface Builder view of CustomBarButtonViewController

次に、ViewControllerのviewDidLoad:メッセージで、2つのUIGestureRecognizersを作成しました。 1つ目はクリックアンドホールド用のUILongPressGestureRecognizerで、2つ目は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のプロパティを作成して、カスタムBarButtonへのアクセスを許可し、それをViewControllerクラスに格納することをお勧めします。 longPressGesturedメッセージが送信されると、ボタンのシステムアイコンを変更できます。

私が見つけた1つの落とし穴は、customviewプロパティがビューをそのまま取得することです。 CustomBarButtonItemView.xibからカスタムUIBarButtonitemを変更して、ラベルを@ "really long string"に変更すると、ボタンは自動的にサイズ変更されますが、表示されているボタンの左端の部分のみがビューに表示されます。 UIGestuerRecognizerインスタンスによって監視されています。

2
Joel

IOS 11まで、let barbuttonView = barButton.value(forKey: "view") as? UIViewは、ジェスチャーを簡単に追加できるbarButtonのビューへの参照を提供しますが、iOS 11では状況がまったく異なり、上記のコード行は次のようになります。 nilしたがって、キー「ビュー」のビューにタップジェスチャを追加しても意味がありません。

UIBarItemsにはcustomViewプロパティがあるため、タップジェスチャを追加できる心配はありません。私たちにできることは、高さと幅が24 ptのボタンを作成し(Appleヒューマンインターフェイスガイドラインに従って)、カスタムビューを新しく作成したボタンとして割り当てることです。次のコードは実行に役立ちますシングルタップのアクションとバーボタンを5回タップするアクション。

[〜#〜] note [〜#〜]この目的のために、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)          
}
2
laxman khanal

@ voi1dの2番目のオプションの回答は、UIBarButtonItemのすべての機能を書き直したくない人にとって最も便利です。私はこれをカテゴリにまとめて、次のことができるようにしました。

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

興味がある場合に備えて、少しエラー処理を行います。注:setItemsを使用してツールバーにアイテムを追加または削除するたびに、ジェスチャレコグナイザーを再度追加する必要があります。UIToolbarは、アイテム配列を調整するたびに、保持しているUIViewを再作成すると思います。

IToolbar + Gesture.h

#import <UIKit/UIKit.h>

@interface UIToolbar (Gesture)

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

@end

IToolbar + 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
1
natbro

私はそれが最善の解決策ではないことを知っていますが、私は私のために働いたかなり簡単な解決策を投稿するつもりです。

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
Kádi

すぐに使用できます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
kasyanov-ms

@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
Klaas

これは私が思いついた最もスウィフトにやさしく、最もハックの少ない方法です。 iOS12で動作します。

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
}
0
Jayden Irwin