web-dev-qa-db-ja.com

UIControlを正しくサブクラス化する方法は?

UIButtonまたはそのようなものは必要ありません。 UIControlを直接サブクラス化し、独自の非常に特別なコントロールを作成したいと思います。

しかし、何らかの理由で、オーバーライドするメソッドが呼び出されることはありません。ターゲットアクションのものが動作し、ターゲットは適切なアクションメッセージを受け取ります。しかし、私のUIControlサブクラス内でタッチ座標をキャッチする必要があり、そうする唯一の方法はこれらの人をオーバーライドするようです:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"begin touch track");
    return YES;
}

- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event {
    NSLog(@"continue touch track");
    return YES;
}

UIControlUIViewinitWithFrame:の指定イニシャライザでインスタンス化されていても、呼び出されません。

私が見つけることができるすべての例では、サブクラス化のベースとして常にUIButtonまたはUISliderを使用していますが、UIControlに近づきたいのです。遅延なしのタッチ座標。

65
HelloMoon

私はこの質問が古いことを知っていますが、私は同じ問題を抱えていたので、2セントを与えるべきだと思いました。

コントロールにサブビューがある場合、beginTrackingWithTouchtouchesBeganなどは、これらのサブビューがタッチイベントを飲み込んでいるために呼び出されない場合があります。

これらのサブビューでタッチを処理したくない場合は、userInteractionEnabledNOに設定して、サブビューがイベントを単に渡すようにすることができます。その後、touchesBegan/touchesEndedをオーバーライドし、そこですべてのタッチを管理できます。

お役に立てれば。

65
pixelfreak

Swift

これらは、UIControlをサブクラス化する複数の方法です。親ビューがタッチイベントに反応するか、コントロールから他のデータを取得する必要がある場合、これは通常(1)ターゲットまたは(2)オーバーライドされたタッチイベントを持つデリゲートパターンを使用して行われます。完全を期すために、(3)ジェスチャレコグナイザーで同じことを行う方法も示します。これらの各メソッドは、次のアニメーションのように動作します。

enter image description here

次の方法のいずれかを選択するだけです。


方法1:ターゲットを追加する

UIControlサブクラスには、既に組み込まれているターゲットがサポートされています。親に大量のデータを渡す必要がない場合、これがおそらく必要なメソッドです。

MyCustomControl.Swift

import UIKit
class MyCustomControl: UIControl {
    // You don't need to do anything special in the control for targets to work.
}

ViewController.Swift

import UIKit
class ViewController: UIViewController {

    @IBOutlet weak var myCustomControl: MyCustomControl!
    @IBOutlet weak var trackingBeganLabel: UILabel!
    @IBOutlet weak var trackingEndedLabel: UILabel!
    @IBOutlet weak var xLabel: UILabel!
    @IBOutlet weak var yLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Add the targets
        // Whenever the given even occurs, the action method will be called
        myCustomControl.addTarget(self, action: #selector(touchedDown), forControlEvents: UIControlEvents.TouchDown)
        myCustomControl.addTarget(self, action: #selector(didDragInsideControl(_:withEvent:)),
                              forControlEvents: UIControlEvents.TouchDragInside)
        myCustomControl.addTarget(self, action: #selector(touchedUpInside), forControlEvents: UIControlEvents.TouchUpInside)
    }

    // MARK: - target action methods

    func touchedDown() {
        trackingBeganLabel.text = "Tracking began"
    }

    func touchedUpInside() {
        trackingEndedLabel.text = "Tracking ended"
    }

    func didDragInsideControl(control: MyCustomControl, withEvent event: UIEvent) {

        if let touch = event.touchesForView(control)?.first {
            let location = touch.locationInView(control)
            xLabel.text = "x: \(location.x)"
            yLabel.text = "y: \(location.y)"
        }
    }
}

  • アクションメソッド名について特別なものはありません。私はそれらを何とでも呼ぶことができました。ターゲットを追加したときとまったく同じように、メソッド名のスペルを注意する必要があります。そうしないと、クラッシュします。
  • didDragInsideControl:withEvent:の2つのコロンは、2つのパラメーターがdidDragInsideControlメソッドに渡されることを意味します。コロンの追加を忘れた場合、または正しい数のパラメーターがない場合、クラッシュします。
  • TouchDragInsideイベントのヘルプについて この回答 に感謝します。

他のデータを渡す

カスタムコントロールに何らかの値がある場合

class MyCustomControl: UIControl {
    var someValue = "hello"
}

ターゲットアクションメソッドでアクセスしたい場合は、コントロールへの参照を渡すことができます。ターゲットを設定するときは、アクションメソッド名の後にコロンを追加します。例えば:

myCustomControl.addTarget(self, action: #selector(touchedDown), forControlEvents: UIControlEvents.TouchDown) 

touchedDown(コロンなし)ではなく、touchedDown:(コロンあり)であることに注意してください。コロンは、パラメーターがアクションメソッドに渡されることを意味します。アクションメソッドで、パラメーターがUIControlサブクラスへの参照であることを指定します。その参照を使用して、コントロールからデータを取得できます。

func touchedDown(control: MyCustomControl) {
    trackingBeganLabel.text = "Tracking began"

    // now you have access to the public properties and methods of your control
    print(control.someValue)
}

方法2:パターンを委任し、タッチイベントをオーバーライドする

UIControlをサブクラス化すると、次のメソッドにアクセスできます。

  • beginTrackingWithTouchは、指がコントロールの境界内で最初にタッチダウンしたときに呼び出されます。
  • continueTrackingWithTouchは、指がコントロールを横切ってスライドし、さらにコントロールの境界外にスライドするときに繰り返し呼び出されます。
  • endTrackingWithTouchは、指が画面から離れたときに呼び出されます。

タッチイベントの特別な制御が必要な場合、または親とやり取りするデータ通信が多い場合、この方法はターゲットを追加するよりもうまく機能する可能性があります。

方法は次のとおりです。

MyCustomControl.Swift

import UIKit

// These are out self-defined rules for how we will communicate with other classes
protocol ViewControllerCommunicationDelegate: class {
    func myTrackingBegan()
    func myTrackingContinuing(location: CGPoint)
    func myTrackingEnded()
}

class MyCustomControl: UIControl {

    // whichever class wants to be notified of the touch events must set the delegate to itself
    weak var delegate: ViewControllerCommunicationDelegate?

    override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {

        // notify the delegate (i.e. the view controller)
        delegate?.myTrackingBegan()

        // returning true means that future events (like continueTrackingWithTouch and endTrackingWithTouch) will continue to be fired
        return true
    }

    override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {

        // get the touch location in our custom control's own coordinate system
        let point = touch.locationInView(self)

        // Update the delegate (i.e. the view controller) with the new coordinate point
        delegate?.myTrackingContinuing(point)

        // returning true means that future events will continue to be fired
        return true
    }

    override func endTrackingWithTouch(touch: UITouch?, withEvent event: UIEvent?) {

        // notify the delegate (i.e. the view controller)
        delegate?.myTrackingEnded()
    }
}

ViewController.Swift

これは、View Controllerがデリゲートとして設定され、カスタムコントロールからのタッチイベントに応答する方法です。

import UIKit
class ViewController: UIViewController, ViewControllerCommunicationDelegate {

    @IBOutlet weak var myCustomControl: MyCustomControl!
    @IBOutlet weak var trackingBeganLabel: UILabel!
    @IBOutlet weak var trackingEndedLabel: UILabel!
    @IBOutlet weak var xLabel: UILabel!
    @IBOutlet weak var yLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        myCustomControl.delegate = self
    }

    func myTrackingBegan() {
        trackingBeganLabel.text = "Tracking began"
    }

    func myTrackingContinuing(location: CGPoint) {
        xLabel.text = "x: \(location.x)"
        yLabel.text = "y: \(location.y)"
    }

    func myTrackingEnded() {
        trackingEndedLabel.text = "Tracking ended"
    }
}

  • デリゲートパターンの詳細については、 this answer を参照してください。
  • これらがカスタムコントロール自体内でのみ使用されている場合、これらのメソッドでデリゲートを使用する必要はありません。イベントがどのように呼び出されるかを示すprintステートメントを追加することもできます。その場合、コードは次のように簡略化されます

    import UIKit
    class MyCustomControl: UIControl {
    
        override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
            print("Began tracking")
            return true
        }
    
        override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool {
            let point = touch.locationInView(self)
            print("x: \(point.x), y: \(point.y)")
            return true
        }
    
        override func endTrackingWithTouch(touch: UITouch?, withEvent event: UIEvent?) {
            print("Ended tracking")
        }
    }
    

方法3:ジェスチャー認識を使用する

ジェスチャレコグナイザーの追加はどのビューでも実行でき、UIControlでも機能します。上の例と同様の結果を得るには、UIPanGestureRecognizerを使用します。次に、イベントが発生したときにさまざまな状態をテストすることで、何が起こっているのかを判断できます。

MyCustomControl.Swift

import UIKit
class MyCustomControl: UIControl {
    // nothing special is required in the control to make it work
}

ViewController.Swift

import UIKit
class ViewController: UIViewController {

    @IBOutlet weak var myCustomControl: MyCustomControl!
    @IBOutlet weak var trackingBeganLabel: UILabel!
    @IBOutlet weak var trackingEndedLabel: UILabel!
    @IBOutlet weak var xLabel: UILabel!
    @IBOutlet weak var yLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()

        // add gesture recognizer
        let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(gestureRecognized(_:)))
        myCustomControl.addGestureRecognizer(gestureRecognizer)
    }

    // gesture recognizer action method
    func gestureRecognized(gesture: UIPanGestureRecognizer) {

        if gesture.state == UIGestureRecognizerState.Began {

            trackingBeganLabel.text = "Tracking began"

        } else if gesture.state == UIGestureRecognizerState.Changed {

            let location = gesture.locationInView(myCustomControl)

            xLabel.text = "x: \(location.x)"
            yLabel.text = "y: \(location.y)"

        } else if gesture.state == UIGestureRecognizerState.Ended {
            trackingEndedLabel.text = "Tracking ended"
        }
    }
}

  • action: "gestureRecognized:"のアクションメソッド名の後にコロンを追加することを忘れないでください。コロンは、パラメーターが渡されることを意味します。
  • コントロールからデータを取得する必要がある場合は、上記の方法2のようにデリゲートパターンを実装できます。
24
Suragch

私はこの問題の解決策を長く探していましたが、解決策はないと思います。ただし、ドキュメントを詳しく調べると、begintrackingWithTouch:withEvent:continueTrackingWithTouch:withEvent:が呼び出されることになっているというのは誤解かもしれないと思います...

UIControlのドキュメントによると:

UIControlサブクラスを拡張するには、2つの基本的な理由があります。

特定のイベントのターゲットへのアクションメッセージのディスパッチを監視または変更するにはこれを行うには、sendAction:to:forEvent:をオーバーライドし、渡されたセレクター、ターゲットオブジェクト、または「ノート」ビットマスクを評価し、必要に応じて続行します。

カスタムトラッキング動作を提供するには(たとえば、ハイライトの外観を変更するため)、これを行うには、beginTrackingWithTouch:withEvent:continueTrackingWithTouch:withEvent:endTrackingWithTouch:withEvent:のいずれかまたはすべてのメソッドをオーバーライドします。

私の見解ではあまり明確ではないが、これの重要な部分は、UIControlサブクラスを拡張したい場合があるということです-UIControlを直接拡張したくない場合があります。 beginTrackingWithTouch:withEvent:continuetrackingWithTouch:withEvent:はタッチに応答して呼び出されることは想定されておらず、UIControlの直接サブクラスはそれらを呼び出すことになっており、theirサブクラス追跡を監視できます。

したがって、これに対する私の解決策は、touchesBegan:withEvent:およびtouchesMoved:withEvent:をオーバーライドし、そこから次のように呼び出すことです。このコントロールではマルチタッチが有効になっていないことに注意してください。また、タッチの終了やキャンセルされたイベントのタッチについては気にしませんが、完全/完全にしたい場合はおそらくそれらも実装する必要があります。

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
    [super touchesBegan:touches withEvent:event];
    // Get the only touch (multipleTouchEnabled is NO)
    UITouch* touch = [touches anyObject];
    // Track the touch
    [self beginTrackingWithTouch:touch withEvent:event];
}

- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
    [super touchesMoved:touches withEvent:event];
    // Get the only touch (multipleTouchEnabled is NO)
    UITouch* touch = [touches anyObject];
    // Track the touch
    [self continueTrackingWithTouch:touch withEvent:event];
}

UIControlEvent*を使用して、コントロールに関連するsendActionsForControlEvents:メッセージも送信する必要があることに注意してください。これらは、スーパーメソッドから呼び出すことができますが、テストしていません。

20
jhabbott

TouchesBegan/touchesEnded/touchesMovedに[スーパー]コールを追加するのを忘れたと思います。のようなメソッド

(BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event    
(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event

touchesBegin/touchesEndedをこのようにオーバーライドすると機能しません:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
   NSLog(@"Touches Began");
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
   NSLog(@"Touches Moved");
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
   NSLog(@"Touches Ended");
}

だが!メソッドが次のようであれば、すべて正常に動作します。

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
   [super touchesBegan:touches withEvent:event];
   NSLog(@"Touches Began");
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
   [super touchesMoved:touches withEvent:event];
     NSLog(@"Touches Moved");
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
   [super touchesEnded:touches withEvent:event];
   NSLog(@"Touches Ended");
}
9
tt.Kilew

私がよく使用する最も簡単なアプローチは、UIControlを拡張することですが、継承されたaddTargetメソッドを使用して、さまざまなイベントのコールバックを受信します。重要なのは、送信者とイベントの両方をリッスンして、実際のイベントに関する詳細情報(イベントが発生した場所など)を確認できるようにすることです。

したがって、単にUIControlをサブクラス化してから、initメソッドで(nibsを使用している場合はinitWithCoderも設定してください)、次を追加します。

[self addTarget:self action:@selector(buttonPressed:forEvent:) forControlEvents:UIControlEventTouchUpInside];

もちろん、UIControlEventAllTouchEventsを含む標準のコントロールイベントのいずれかを選択できます。セレクターには2つのオブジェクトが渡されることに注意してください。最初はコントロールです。 2番目は、イベントに関する情報です。ユーザーが左右を押したかどうかに応じて、タッチイベントを使用してボタンを切り替える例を次に示します。

- (IBAction)buttonPressed:(id)sender forEvent:(UIEvent *)event
{
    if (sender == self.someControl)
    {        
        UITouch* touch = [[event allTouches] anyObject];
        CGPoint p = [touch locationInView:self.someControl];
        if (p.x < self. someControl.frame.size.width / 2.0)
        {
            // left side touch
        }
        else
        {
            // right side touch
        }
    }
}

確かに、これは非常に単純化されたコントロール用であり、十分な機能が得られないポイントに到達する可能性がありますが、このポイントまでのカスタムコントロールのすべての目的で機能し、私は通常同じことを気にしているので本当に使いやすいですUIControlがすでにサポートしているイベントの制御(タッチアップ、ドラッグなど)

ここにカスタムコントロールのコードサンプル: カスタムUISwitch (注:これはbuttonPressed:forEvent:セレクターでは登録されませんが、上記のコードから把握できます)

8
Duane Homick

UIControlがbeginTrackingWithTouchおよびcontinueTrackingWithTouchに応答しない問題がありました。

私が問題を見つけたのは、initWithFrame:CGRectMake()を実行したときでした。フレームを小さく(反応する領域)にし、動作するポイントスポットが2つしかないことでした。フレームをコントロールと同じサイズにした後、コントロールのどこかを押すと応答しました。

3
digitalmasters

Obj C

2014年の関連記事を見つけました https://www.objc.io/issues/13-architecture/behaviors/

興味深いのは、そのアプローチはIBを使用し、指定されたオブジェクト内にイベント処理ロジックをカプセル化することです(ビヘイビアと呼びます)。したがって、view/viewControllerからロジックを削除して軽量化します。

0
supergegs