Como subclassificar correctamente o 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;
}
Eles nunca são chamados, mesmo que ... o UIControl
está instanciado com o inicializador designates de UIView
, initWithFrame:
.
todos os exemplos que consigo encontrar usam sempre um UIButton
ou UISlider
como base para subclassificação, mas quero aproximar-me de UIControl
uma vez que essa é a fonte para o que eu quero: coordenadas de Toque rápidas e não marcadas.
7 answers
Se o seu controlo tiver alguma visão negativa, beginTrackingWithTouch
, touchesBegan
, etc pode não ser chamado porque essas visões estão engolindo os eventos de toque.
Se não quiser que essas visualizações manipulem os toques, pode definir userInteractionEnabled
para NO
, para que as visualizações passem simplesmente o evento. Então você pode anular touchesBegan/touchesEnded
e gerenciar todos os seus toques lá.
Swift
Estas são mais do que uma forma de subclasse. Quando uma visão pai precisa reagir a eventos de toque ou obter outros dados do controle, isso é geralmente feito usando (1) alvos ou (2) o padrão de delegado com eventos de toque sobrepostos. Para completar, também mostrarei como (3) Fazer a mesma coisa com um reconhecedor de gestos. Cada um destes métodos se comportará como a seguinte animação:Só precisas de escolher um dos seguinte.
Método 1: Adicionar um alvo
A subclasse aUIControl
tem suporte para alvos já incorporados. Se você não precisa passar um monte de dados para o pai, este é provavelmente o método que você quer.
Eustomcontrol.swift
import UIKit
class MyCustomControl: UIControl {
// You don't need to do anything special in the control for targets to work.
}
Vercontrolador.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)"
}
}
}
Notas
- Não há nada de especial nos nomes dos métodos de Acção. Podia ter-lhes chamado qualquer coisa. Só tenho de ser cuidado para soletrar o nome do método exactamente como eu fiz onde adicionei o alvo. Caso contrário, vais ter um acidente.
- os dois pontos em
didDragInsideControl:withEvent:
significa que dois parâmetros estão a ser passados para o métododidDragInsideControl
. Se você se esquecer de adicionar um cólon ou se você não tiver o número correto de Parâmetros, você vai ter um acidente. - graças a esta resposta para a ajuda com o evento
TouchDragInside
.
Transmissão de outros dados
Se TEM algum valor no seu costume controlo
class MyCustomControl: UIControl {
var someValue = "hello"
}
Que você quer acessar no método de ação alvo, então você pode passar em uma referência ao controle. Quando estiver a definir o alvo, adicione dois pontos após o nome do método de Acção. Por exemplo:
myCustomControl.addTarget(self, action: #selector(touchedDown), forControlEvents: UIControlEvents.TouchDown)
Repare que é touchedDown:
(com dois pontos) e não touchedDown
(Sem dois pontos). O cólon significa que um parâmetro está sendo passado para o método de ação. No método de ação, especifique que o parâmetro é uma referência à sua subclasse UIControl
. Com essa referência, você pode obter dados do seu controle.
func touchedDown(control: MyCustomControl) {
trackingBeganLabel.text = "Tracking began"
// now you have access to the public properties and methods of your control
print(control.someValue)
}
Método 2: delegar padrões e anular eventos de Toque
Subclassing UIControl
dá-nos acesso aos seguintes métodos:
- Chama-se quando o dedo toca pela primeira vez dentro dos limites do controlo.
-
continueTrackingWithTouch
é chamado repetidamente como o dedo desliza através do controle e mesmo fora dos limites do controle. -
endTrackingWithTouch
chama-se quando o dedo levanta ecra.
Se você precisa de um controle especial dos eventos de toque ou se você tem um monte de comunicação de dados para fazer com o pai, então este método pode funcionar melhor do que adicionar metas.
Aqui está como fazê-lo.Eustomcontrol.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()
}
}
Vercontrolador.swift
É assim que o controlador de visualização é configurado para ser o delegado e responder a eventos de toque do nosso costume controlo.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"
}
}
Notas
- para saber mais sobre o padrão dos delegados, veja esta resposta.
-
Não é necessário utilizar um delegado com estes métodos se estes só estiverem a ser utilizados dentro do próprio controlo aduaneiro. Eu poderia ter apenas Adicionado uma declaração para mostrar como os eventos estão sendo chamados. Nesse caso, o código seria simplificado para
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") } }
Método 3: Usar um gesto Reconhecedor A adição de um reconhecedor de gestos pode ser feita em qualquer visão e também funciona em um {[[10]}. Para obter resultados semelhantes ao exemplo no topo, usaremos um
UIPanGestureRecognizer
. Depois, testando os vários estados quando um evento é disparado, podemos determinar o que está acontecendo.
Eustomcontrol.swift
import UIKit
class MyCustomControl: UIControl {
// nothing special is required in the control to make it work
}
Vercontrolador.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"
}
}
}
Notas
- Não se esqueça de adicionar o cólon após o nome do método de acção em
action: "gestureRecognized:"
. O cólon significa que os parâmetros estão a ser passados para dentro. - se você precisa obter dados do controle, você pode implementar o padrão de delegado como no método 2 acima.
begintrackingWithTouch:withEvent:
e {[2] } devem ser chamados de todo...
UIControl
a documentação diz:
É melhor estender um
UIControl
subclasse por duas razões básicas:Para observar ou modificar a expedição de mensagens de Acção para os objectivos para eventos particulares para fazer isso, substituir
Para fornecer um comportamento de rastreamento personalizado (por exemplo, para alterar o realce aparência) para fazer isso, sobreponha um ou todos os seguintes métodos:sendAction:to:forEvent:
, avalie o selector de passagem, objecto-alvo ou "Nota" bit mask e proceder como necessario.beginTrackingWithTouch:withEvent:
,continueTrackingWithTouch:withEvent:
,endTrackingWithTouch:withEvent:
.
A parte crítica disto, que não é muito clara em minha opinião, é que diz que você pode querer estender uma subclasse UIControl
- Não você pode querer estender UIControl
diretamente. É possível que beginTrackingWithTouch:withEvent:
e continuetrackingWithTouch:withEvent:
não devam ser chamados em resposta a toques e que UIControl
subclasses diretas devam chamá-los para que suas subclasses possam monitorar rastreamento.
touchesBegan:withEvent:
e {15]} e chamá-los de lá da seguinte forma. Note que o Multi-touch não está habilitado para este controle e que eu não me importo com toques terminados e toques cancelados eventos, mas se você quiser ser completo / completo você provavelmente deve implemente esses também.
- (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];
}
Note que você também deve enviar quaisquer mensagens UIControlEvent*
que sejam relevantes para o seu controlo usando sendActionsForControlEvents:
- estas podem ser chamadas a partir dos super métodos, Eu não testei.
A abordagem mais simples que muitas vezes uso é estender o UIControl, mas fazer uso do método adtarget herdado para receber chamadas para os vários eventos. A chave é ouvir tanto o remetente quanto o evento para que você possa descobrir mais informações sobre o evento real (como a localização de onde o evento ocorreu).
Por isso, basta subclasse o UIControl e , em seguida, no método init (certifique-se que o seu codificador init também está configurado se estiver a usar o nibs), adicione o seguinte:
[self addTarget:self action:@selector(buttonPressed:forEvent:) forControlEvents:UIControlEventTouchUpInside];
É claro que pode escolher qualquer um dos eventos de controlo padrão, incluindo UIControlEventAllTouchEvents. Repare que o selector vai passar por dois objectos. O primeiro é o controlo. A segunda é a informação sobre o evento. Aqui está um exemplo de usar o evento touch para comutar um botão, dependendo se o usuário pressionou à esquerda e à direita.
- (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
}
}
}
Certo, isto é para controles muito simplistas e você pode chegar a um ponto em que isso não lhe dará o suficiente. funcionalidade, mas isso tem funcionado para todos os meus propósitos para controles personalizados até este ponto e é realmente fácil de usar, uma vez que eu tipicamente me importo com os mesmos eventos de controle que o UIControl já suporta (toque, arrasto, etc...)
Amostra de código do controlo personalizado aqui: UISwitch personalizado (nota: isto não se regista com o selector buttonPressed: forEvent:, mas você pode descobrir isso a partir do código acima)
(BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
(BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
Não está a funcionar se substituir o touchesBegan / touchesEnded desta forma:
- (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");
}
Mas! Todos funcionam bem se os métodos forem como:
- (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");
}
continueTrackingWithTouch
.
Descobri que o meu problema era quando o fiz.fiz a moldura para pequena (a área que reage) e tinha apenas um par de pontos onde funcionava. Eu fiz a moldura do mesmo tamanho do controle e então sempre que eu pressionei em qualquer lugar no controle ela respondeu.
Obj C
Encontrei um artigo relacionado de 2014 https://www.objc.io/issues/13-architecture/behaviors/.
O interessante é que a sua abordagem é fazer uso do IB e encapsular a lógica de manipulação de eventos dentro de um objecto designado( eles chamam-lhe comportamentos), removendo assim a lógica do seu ponto de vista/vercontrolador tornando-a mais leve.
- caixa de amostras de Github: https://github.com/krzysztofzablocki/BehavioursExample