Как правильно подклассифицировать 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;
}

Они никогда не вызываются, хотя UIControl с помощью инициализатора обозначений из UIView, initWithFrame:

Все примеры, которые я могу найти, всегда используют UIButton или UISlider качестве основы для UISlider подклассов, но я хочу приблизиться к UIControl так как это источник того, что я хочу: быстрые и незадействованные координаты касания.

Ответы

Ответ 1

Я знаю, что этот вопрос древний, но у меня была та же проблема, и я думал, что должен дать свои 2 цента.

Если ваш элемент управления вообще имеет какие-либо подъязыки, beginTrackingWithTouch, touchesBegan и т.д. не могут быть вызваны, потому что эти подпрограммы поглощают события касания.

Если вы не хотите, чтобы эти подзаголовки обрабатывали штрихи, вы можете установить userInteractionEnabled в NO, так что subviews просто передает событие через. Затем вы можете переопределить touchesBegan/touchesEnded и управлять всеми своими контактами.

Надеюсь, что это поможет.

Ответ 2

Swift

Это более чем один способ подкласса UIControl. Когда родительское представление должно реагировать на события касания или получать другие данные из элемента управления, это обычно делается с использованием (1) целей или (2) шаблона делегата с переопределенными событиями касания. Для полноты я также покажу, как (3) сделать то же самое с распознающим жестом. Каждый из этих методов будет вести себя следующим образом:

введите описание изображения здесь

Вам нужно только выбрать один из следующих способов.


Способ 1: Добавить цель

Подкласс

A 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: означают, что два метода передаются методу 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 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

Таким образом, контроллер представления настроен как делегат и отвечает на события касания от нашего настраиваемого элемента управления.

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

Примечания

  • Чтобы узнать больше о шаблоне делегата, см. этот ответ.
  • Нет необходимости использовать делегат с этими методами, если они используются только самим настраиваемым элементом управления. Я мог бы просто добавить оператор 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 выше.

Ответ 3

Я долго и долго искал решение этой проблемы, и я не думаю, что она есть. Однако при ближайшем рассмотрении документации я думаю, что может быть недоразумением, что begintrackingWithTouch:withEvent: и continueTrackingWithTouch:withEvent: должны быть вызваны вообще...

UIControl документация говорит:

Возможно, вы захотите расширить UIControlподкласс по двум основным причинам:

Наблюдение или изменение отправки сообщения о действиях для целей определенные события Чтобы сделать это, переопределите sendAction:to:forEvent:, оцените передаваемый в селектор, целевой объект или "Обратите внимание" на битовую маску и действуйте как требуется.

Чтобы обеспечить пользовательское отслеживание (например, чтобы изменить выделение внешний вид) Чтобы сделать это, переопределите один или все следующие методы: begintrackingWithTouch:withEvent: continueTrackingWithTouch:withEvent: endTrackingWithTouch:withEvent:.

Критическая часть этого, что не очень ясно на мой взгляд, заключается в том, что он говорит, что вы можете расширить подкласс UIControl - НЕ вы можете напрямую расширить UIControl. Возможно, что begintrackingWithTouch:withEvent: и continueTrackingWithTouch:withEvent: не должны вызываться в ответ на прикосновения и что прямые подклассы UIControl должны вызывать их так, чтобы их подклассы могли отслеживать отслеживание.

Итак, мое решение состоит в том, чтобы переопределить touchesBegan:withEvent: и touchesMoved:withEvent: и вызвать их оттуда следующим образом. Обратите внимание, что для этого элемента управления не задействовано multi-touch, и я не забочусь о завершении штрихов и затрагивает отмененные события, но если вы хотите быть полными/тщательными, вы, вероятно, тоже должны их реализовать.

- (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: - они могут быть вызваны из супер методов, я его не тестировал.

Ответ 4

Я думаю, что вы забыли добавить [супер] призывы к касаниюBegan/touchesEnded/touchesMoved. Методы вроде

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

не работают, если вы перекрываете touchBegan/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");
}

Ответ 5

Самый простой подход, который я часто использую, - это расширение UIControl, но использование унаследованного метода addTarget для получения обратных вызовов для различных событий. Ключ заключается в том, чтобы прослушивать как отправителя, так и событие, чтобы вы могли узнать больше информации о фактическом событии (например, о месте, где произошло событие).

Итак, просто подкласс UIControl и затем в методе init (убедитесь, что ваш initWithCoder также настроен, если вы используете nibs), добавьте следующее:

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

Конечно, вы можете выбрать любое из стандартных событий управления, включая UIControlEventAllTouchEvents. Обратите внимание, что селектору будут переданы два объекта. Первый - это контроль. Вторая информация о событии. Вот пример использования сенсорного события для переключения кнопки в зависимости от того, нажал ли пользователь слева и справа.

- (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 уже поддерживает (подправить, перетащить и т.д.)

Пример кода пользовательского элемента управления здесь: Custom UISwitch (примечание: это не регистрируется с помощью селектора buttonPressed: forEvent:, но вы можете понять это из приведенного выше кода)

Ответ 6

У меня возникли проблемы с UIControl, не отвечающим на beginTrackingWithTouch и continueTrackingWithTouch.

Я обнаружил, что моя проблема была, когда я сделал initWithFrame:CGRectMake(). Я сделал фрейм маленьким (область, которая реагирует) и имела только пару точек, где она работала. Я сделал рамку того же размера, что и элемент управления, а затем в любое время, когда я нажал на любой элемент управления, на который он ответил.

Ответ 7

Obj C

Я нашел связанную статью с 2014 года https://www.objc.io/issues/13-architecture/behaviors/.

Интересно, что его подход состоит в том, чтобы использовать IB и инкапсулировать логику обработки событий внутри выделенного объекта (они называют его поведением), таким образом удаляя логику из вашего view/viewController, делая его легче.