Programování pro iOS - 14. Udělejme gesto - MujMAC.cz - Apple, Mac OS X, Apple iPod

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:

Soutěž

Sponzorem soutěže je:

IDIF

 

Odkud pochází fotografka Anne Erhard?

V dnešní soutěži hrajeme o:

Seriály

Více seriálů



Software

Programování pro iOS - 14. Udělejme gesto

3. listopadu 2010, 00.00 | Jak přidat do kreslícího prográmku z minula podporu gest? Toť obsahem čtrnácté lekce našeho kurzu programování.

Náš konkrétní projekt, jehož součástí je i specifický kreslicí kód, se neobejde bez rámce. V daleko běžnějších případech, kdy chceme interpretovat jen doteková gesta, ale neimplementujeme vlastní metodu drawRect:, můžeme použít standardní rámec UIView a nemusíme vůbec mít jeho vlastní podtřídu.

Jak je to možné? Snadno: všechny zprávy touches.... withEvent: jsou totiž korektně předávány po řetězu responderů "nahoru", dokud se nenarazí na objekt, který jim rozumí. A tento řetěz je jednoduchý:

• rámec ve standardní implementaci metod touches.... withEvent: jen zprávu předá řídicímu objektu – existuje-li –, nebo nadřízenému rámci;

• řídicí objekt ve standardní implementaci metod touches.... withEvent: zprávu předá nadřízenému rámci svého rámce;

• pokud již nadřízený rámec neexistuje, zpráva se předá oknu;

• okno zprávu předá aplikaci.

Pokud by tedy v celém našem projektu nikde – tedy nikde v našem vlastním kódu – nebyla implementována metoda touchesBegan:withEvent: a uživatel by se dotkl obrazovky, nejprve by tuto zprávu dostal rámec. Ten by ji ve standardní knihovní implementaci prostě předal svému řídicímu objektu – ten prakticky v každém projektu pro iOS existuje, u nás jde o třídu <JménoProjektu>ViewController. Jeho knihovní implementace by předala zprávu oknu, to aplikaci, a ta by ji teprve "zahodila".

Nyní je již tedy také asi zřejmé, proč firma Apple v dokumentaci důtklivě upozorňuje, že pokud v rámci (nebo v řídicím objektu rámce nebo v okně nebo v aplikaci – to je jedno) implementujeme některou z metod touches.... withEvent:, měli bychom implementovat všechny čtyři, ty, jež nepotřebujeme, jako prázdné: pokud bychom některé z nich ponechali ve standardní knihovní implementaci, dále by události předávaly nahoru po řetězu responderů – a některý z tamních objektů by mohl být značně "zmaten" z toho, že by dostával neúplnou sekvenci událostí (např. touchesEnded... bez odpovídající touchesBegan...).

Jen pro úplnost je vhodné hned dodat, že pokud bychom náhodou implementovali tyto metody ve vlastní podtřídě některé z tříd, jež události jen nepředávají dále, nýbrž které je samy zpracovávají – např. UIButton – stačí implementovat jen ty z nich, jež nás zajímají; zato ale v nich nesmíme zapomenout na zvolání původní implementace pomocí [super touches...]).

Vidíme tedy, že můžeme stejně dobře implementovat metody touches.... withEvent: v řídicích objektech jako v rámcích samotných a bude to fungovat stejně dobře. V běžných projektech je to výhodnější: nemusíme se vůbec obtěžovat implementací vlastních rámců; naopak jejich řídicí objekty (podtřídy UIViewController) implementujeme stejně prakticky vždy.

Mimochodem – řídicí objekty rámců mají v UIKitu velmi důležité postavení, a budeme se jim věnovat podrobněji jen co skončíme s dotekovým ovládáním.

Pojďme tedy z cvičných důvodů přemístit všechny čtyři metody touches.... withEvent: z rámce do řídicího objektu. Jeho rozhraní v souboru <JménoProjektu>ViewController.h bude vypadat tedy nějak takto – proměnnou lines změníme z obyčejné instanční proměnné na atribut, aby k ní bylo možné přistupovat zvenku:

@interface ExampleViewController:UIViewController {
    CGPoint start;
}
@property (readonly) NSMutableArray *lines;
@end

Proč "readonly"? Jednoduše proto, že nám přístup "ke čtení" stačí a jiný nepotřebujeme – na vše ostatní již máme kód hotový, jen pro přístup k proměnné budeme potřebovat přístupovou metodu. Její vytvoření si vyžádáme pomocí jediného řádku s direktivou @synthesize v implementaci (díky standardní podpoře atributů v Objective C 2).

Implementaci metod také přemístíme z rámce do odpovídajícího souboru <JménoProjektu>ViewController.m; nezapomeneme s nimi vzít i patřičnou metodu dealloc (implementovali jste ji sami od sebe, ačkoli jsme o ní minule výslovně nemluvili? Ne? Ale to už by dnes mělo být dávno samozřejmé!), a na třech místech, kde se rámec obrací sám na sebe pomocí self, přidáme atribut view, který nám vždy získá rámec v jeho řídicím objektu.

@implementation ExampleViewController
@synthesize lines;
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    start=[[touches anyObject] locationInView:self.view];
}
-(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
    if (!lines) lines=[NSMutableArray new];
    [lines addObject:
     [NSArray arrayWithObjects:
      [NSValue valueWithCGPoint:start],
      [NSValue valueWithCGPoint:
       [[touches anyObject] locationInView:self.view]
       ],nil]];
    [self.view setNeedsDisplay];
}
-(void)touchesMoved:touches withEvent:event {}
-(void)touchesCancelled:touches withEvent:event {}
-(void)dealloc {
    [lines release],lines=nil;
    [super dealloc];
}
@end

Poslední, co musíme udělat, je upravit kód metody drawRect: (která nám jako jediná zbyla v implementaci našeho rámce View) tak, aby měl přístup k proměnné lines v řídicím objektu. Hlavním problémem zde je otázka, jak v rámci najdeme řídicí objekt? Mohli bychom si samozřejmě přidat mezi jeho instanční proměnné odpovídající "outlet" a "nadrátovat" jej v Interface Builderu; můžeme ale využít jednoduššího a hezčího triku.

Víme-li totiž, že řídicí objekt je v řetězu responderů hned za rámcem – a to víme, prostě si pomocí standardní služby vyžádáme následující responder: to musí nutně být náš řídicí objekt. Vypadat by to mohlo asi takto:

#import "ExampleViewController.h"
@implementation View
-(void)drawRect:(CGRect)rect {
    [[UIColor whiteColor] set];
    UIRectFill(rect);
    CGContextRef gc=UIGraphicsGetCurrentContext();
    CGContextSetStrokeColorWithColor(gc,
        [UIColor blueColor].CGColor);
    ExampleViewController *vc=
        (ExampleViewController*)self.nextResponder;
    for (NSArray *a in vc.lines) {
        CGPoint pts[2]={
            [[a objectAtIndex:0] CGPointValue],
            [[a objectAtIndex:1] CGPointValue]
        };
        CGContextStrokeLineSegments(gc,pts,2);
    }
}
@end

A to je celé: můžeme projekt sestavit a vyzkoušet, a hned uvidíme, že funguje stejně dobře jako předtím.

Stejně dobře bychom mohli metody touches.... withEvent: implementovat ve vlastní podtřídě okna nebo aplikace; toho se ale v praxi využívá jen naprosto výjimečně.

A teď už se můžeme pustit do gest!

Jednoduchý dotek

Samozřejmě, vůbec nejjednodušší je interpretace jednoduchého doteku tam, kde nepotřebujeme odlišovat ostatní gesta: prostě požadovanou akci umístíme do metody touchesBegan:withEvent: (nebo touchesEnded:withEvent: – v tomto případě je to celkem jedno, zda k události dojde při položení nebo při zvednutí prstu), a o nic jiného se nestaráme.

To ale není náš případ – ne v projektu, s kterým si právě hrajeme, a obecně ne ve většině aplikací. Zde je problém v tom, že musíme vzájemně rozlišit dotek a začátek – nebo konec – tažení!

Jak na to? Inu, poměrně snadno: podíváme se, zda se poloha prstu v průběhu celého tahu významně změnila. Pokud ano, patrně šlo o tažení; ne-li, interpretujeme celou akci jako dotek.

Ukažme si, jak by to vypadalo, kdybychom chtěli "tapnutím" smazat naposled nakreslenou čáru (samozřejmě, že jako uživatelské rozhraní je to naprostý nesmysl a později to změníme). V takovém případě stačí změnit jen metodu touchesEnded:withEvent:, zhruba nějak takto:

static inline CGFloat dist(CGPoint a,CGPoint b) {
    CGFloat x=b.x-a.x,y=b.y-a.y;
    return sqrt(x*x+y*y);
}
-(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
    CGPoint pt=[[touches anyObject] locationInView:self.view];
    if (dist(pt,start)<8) {
        // jednoduchý dotek
        if (lines.count) [lines removeLastObject];
    } else {
        // tažení
        if (!lines) lines=[NSMutableArray new];
        [lines addObject:
         [NSArray arrayWithObjects:
          [NSValue valueWithCGPoint:start],
          [NSValue valueWithCGPoint:pt],nil]];
    }
    [self.view setNeedsDisplay];
}

Vícenásobný dotek

Základní problém detekce vícenásobného doteku – "double tap, triple tap..." – naštěstí řeší standardní knihovny, a my se jím proto nemusíme zabývat: počet klepnutí na totéž místo předtím, než byl aktivován aktuální tah, máme vždy k dispozici v rámci aktuálního tahu jako jeho atribut tapCount.

V jednodušším případě, kdy nám nevadí interpretace jednoduchého doteku, tedy stačí prostě tento atribut použít – například (opět šílené uživatelské rozhraní, avšak dobrá ilustrace problému) takto, pokud bychom chtěli rušit poslední čáru trojím dotykem, a ignorovat ostatní:

-(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
    UITouch *t=[touches anyObject];
    CGPoint pt=[t locationInView:self.view];
    if (dist(pt,start)<8) {
        if (t.tapCount==3) {
            if (lines.count) [lines removeLastObject];
        } else
            NSLog(@"Multi tap of %u taps ignored!",t.tapCount);
    } else {
        if (!lines) lines=[NSMutableArray new];
        [lines addObject:
         [NSArray arrayWithObjects:
          [NSValue valueWithCGPoint:start],
          [NSValue valueWithCGPoint:pt],nil]];
    }
    [self.view setNeedsDisplay];
}

Když tento kód vyzkoušíme, bude fungovat celkem korektně: jedno nebo dvojí klepnutí se ignoruje; trojí smaže poslední čáru.

Jenže... zkusíme-li klepnout čtyřikrát nebo pětkrát, poslední čára se také smaže; to jsme asi nechtěli! Podíváme-li se do aplikačního logu, hned odhalíme příčinu:

Example[21897:207] Multi tap of 1 taps ignored!
Example[21897:207] Multi tap of 2 taps ignored!
Example[21897:207] Multi tap of 4 taps ignored!
Example[21897:207] Multi tap of 5 taps ignored!

Problém je v tom, že i v případě vícenásobného "tapnutí" se nejprve interpretuje jednoduché, pak dvojité, pak trojité... a tak dále.

Pokud požadujeme, aby dvojité (trojité...) klepnutí nejprve neprovedlo akci jednoduchého (dvojitého...), máme problém, který není úplně jednoduché vyřešit. V zásadě zde musíme implementovat následující logiku:

• kdykoli nastane N-násobné klepnutí, neprovedeme odpovídající akci, ale naplánujeme její provedení "za chvilku";

• nastane-li N+1-násobné klepnutí, plán akce zrušíme.

Zhruba nějak takto:

static inline CGFloat dist(CGPoint a,CGPoint b) {
    CGFloat x=b.x-a.x,y=b.y-a.y;
    return sqrt(x*x+y*y);
}
-(void)performMultipleTap:(NSNumber*)n {
    if (n.intValue==3) {
        if (lines.count) {
            [lines removeLastObject];
            [self.view setNeedsDisplay];
        }
    } else
        NSLog(@"Multi tap of %u taps ignored!",n.intValue);
}
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    UITouch *t=[touches anyObject];
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    start=[t locationInView:self.view];
}
-(void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
    UITouch *t=[touches anyObject];
    CGPoint pt=[t locationInView:self.view];
    if (dist(pt,start)<8)
        [self performSelector:@selector(performMultipleTap:)
            withObject:[NSNumber numberWithInt:t.tapCount]
            afterDelay:.3];
    else {
        if (!lines) lines=[NSMutableArray new];
        [lines addObject:
         [NSArray arrayWithObjects:
          [NSValue valueWithCGPoint:start],
          [NSValue valueWithCGPoint:pt],nil]];
        [self.view setNeedsDisplay];
    }
}

Ovládání aplikace ovšem bude jaksi "gumové", než se vícenásobné klepnutí provede, chvíli to potrvá. Tomu nelze v tomto případě nijak zabránit: mechanismus interpretující klepnutí prostě nemá na vybranou, po klepnutí musí čekat, aby se ukázalo, zda uživatel klepl znovu nebo ne.

Závěr je jednoznačný: pokud je to jen trochu možné, měli bychom vždy navrhovat uživatelské rozhraní tak, aby vícenásobné klepnutí bylo rozšířením méněnásobného: pokud tedy např. jednoduché klepnutí označí objekt, mělo by dvojnásobné s označeným objektem počítat a nad ním pracovat. Tím – a jenom tím – se tomuto problému vyhneme.

Příště si ukážeme interpretaci některých dalších standardních gest.

Obsah seriálu (více o seriálu):

Tématické zařazení:

 » Rubriky  » Informace  

 » Rubriky  » Agregator  

 » Rubriky  » Tipy a Triky  

 » Rubriky  » Začínáme s  

 » Rubriky  » Software  

 

 

 

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: