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:
Informace
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):
- Nastal čas na kakao...
- Tak nejdřív kakao ochutnáme...
- Programovací jazyk C: velmi, velmi stručně
- Objective C: to si vysvětlíme podrobněji
- Co jsme si o Objective C ještě neřekli...
- Nastal čas na kakao - Vznik a zánik objektů
- Nastal čas na kakao - Kopírování objektů
- Nastal čas na kakao - Skryté podtřídy
- Nastal čas na kakao - Základní služby objektů
- Nastal čas na kakao - Jak správně psát v Objective C
- Nastal čas na kakao - Jak správně importovat
- Nastal čas na kakao - Podtřídy, delegáti, vkládání, jak se to rýmuje?
- Nastal čas na kakao - Využití kategorií namísto dědičnosti
- Nastal čas na kakao - Vkládání objektů a přesměrování zpráv
- Nastal čas na kakao - Inicializace a rušení objektů
- Nastal čas na kakao - Metody initWith... a designovaný inicializátor
- Nastal čas na kakao - Inicializace: tipy a triky
- Nastal čas na kakao - Accesory: přístup k proměnným instancí
- Nastal čas na kakao - Šedá je teorie, zelený je strom života...
- Nastal čas na kakao - Více o XCode: inspektory
- Nastal čas na kakao - Aplikace RSS2: datový model
- Nastal čas na kakao - Aplikace RSS: implementace datového modelu
- Nastal čas na kakao - Aplikace RSS: parsování XML
- Nastal čas na kakao - Interface Builder a uživatelské rozhraní
- Nastal čas na kakao - Interface Builder: atributy objektů
- Nastal čas na kakao - Interface Builder: atributy objektů
- Nastal čas na kakao - Druhý kontrolér a dokončení aplikace
- Nastal čas na kakao - Drobná vylepšení a zdokonalení...
- Nastal čas na kakao - Ladění
- Nastal čas na kakao - Třídy Foundation Kitu
- Nastal čas na kakao - Třídy Foundation Kitu (2)
- Nastal čas na kakao - Textové řetězce: NS(Mutable)String
- Nastal čas na kakao - Čísla, binární data a další...
- Nastal čas na kakao - Archivace objektů
- Nastal čas na kakao - Trocha magie, aneb distribuované objekty
- Nastal čas na kakao - Málem bychom zapomněli: NSAutoreleasePool
- Nastal čas na kakao - Zpracování výjimek: NSException
- Nastal čas na kakao - NSInvocation a černá magie
- Nastal čas na kakao - Kakao v Tygrovi
- Nastal čas na kakao - Notifikace: nepřímé předávání zpráv
- Nastal čas na kakao - NSUserDefaults
- Nastal čas na kakao - Co nového ve Foundation Kitu
- Nastal čas na kakao – s Intelem, s Intelem, jedeme do...
- Co nového v Xcode
- Začínáme s AppKitem
- Jak MVC v Kakau vypadá doopravdy?
- Jak MVC v Kakau vypadá doopravdy: dokončení
- Přehled tříd AppKitu
- Nastal čas na kakao - Přehled tříd AppKitu 2
- Přehled tříd AppKitu 3: zbývající třídy GUI
- Přehled tříd AppKitu 4: textový systém
- Nastal čas na kakao - Přehled tříd AppKitu 5: hlavně grafika
- Přehled tříd AppKitu 6: dokumentový systém
- Přehled tříd AppKitu 7: dokončení
- Pojmenované vlastnosti objektů
- Pojmenované vlastnosti objektů: implementace
- Pojmenované vlastnosti objektů: relace 1:N
- Pojmenované vlastnosti objektů: řazení jmen a agregační funkce
- Sledování změn objektů
- Sledování změn objektů – ukázka
- Sledování změn objektů – zdrojový kód
- Sledování změn objektů: kód modelu
- Sledování změn objektů: přímý přístup
- Kontroléry a vazby
- Vázání vazeb
- Další vazby s jednoduchým kontrolérem
- Implementace a použití převodu hodnot
- Validace hodnot
- Validace a chyby, a jedna hezká vazba...
- Práce s polem objektů
- Základní vazby NSArrayControlleru
- Převodníky, přepínače, placeholdery
- Mírná vylepšení v mezích zákona
- Objective C 2.0 - novinky z Leoparda
- NSTreeController
- Programování v Cocoa - Pár tipů a triků
- Programování v Cocoa - Základy kreslení
- Kterak nakreslit modrý obdélník...
- Další služby pro kreslení
- Obrázky a písmenka...
- Události a myš
- Lepší práce s myší
- Události klávesnice
- Input Management
- Příkazy a schránka
- Další události
- Táhni a padni
- Byli jsme na tahu; nyní padneme.
- Zvolme si, jak vhodit
- Drobnosti a chybičky
- Speciální případy tahání či házení
- Kterak táhnout něco, co neexistuje?
- Jak na sítě...
- NSURLConnection
- Safari za minutu
- Služby WebKitu
- Kakao v Leopardu
- Druhé Objective C
- Druhé Objective C: různé drobnosti
- Druhé Objective C: kategorie a protokoly
- Druhé Objective C: nový příkaz cyklu
- Druhé Objective C: atributy a accesory
- Druhé Objective C: atributy a accesory
- 64 je dvakrát 32
- Ubicumque dulce est, ibi et acidum invenies...
- Irbis: že prý žádné novinky?
- Blok sem, blok tam, nám už je to všechno jasné...
- Bloky jsou i v AppKitu
- Irbis a Foundation Kit
- Kde jsou má data?
- Kde jsou má data? V NSCache!
- Soubor, jméno, URL, jak se to rýmuje...
- Další podpora NSURL
- Zabíjení!
- A máme tady i...OS!
- Systémové prvky GUI
- Programování pro iOS 1. díl - Rozdíly mezi "i" a "Mac"
- Programování pro iOS - 2. Začínáme programovat
- Programování pro iOS - 3. základní ovladače a propojení GUI s kódem
- Programování pro iOS - 4. Varovná hlášení
- Programování pro iOS - 5. Rámce a jejich řídicí objekty
- Programování pro iOS - 6. Ukládání dat
- Programování pro iOS - 7. Správa paměti a starý restík
- Programování pro iOS - 8. Dokončení aplikace
- Programování pro iOS - 9. Jak dostat aplikaci do iPhone
- Programování pro iOS - 10. Instalace aplikace do cizího iPhone
- Programování pro iOS - 11. Jak dostat aplikaci do libovolného iPhone
- Programování pro iOS - 12. Touching!
- Programování pro iOS - 13. Kreslíme na iPhone
- Programování pro iOS - 14. Udělejme gesto
- Programování pro iOS - 15. Další gesta
- Programování pro iOS - 16. Více prstů, více zábavy
- Programování pro iOS - 17. Podpora standardních gest
- Programování pro iOS - 18. Recognizery v iOS
- Programování pro iOS - 19. Další standardní recognizery
- Programování pro iOS - 20. Co nového v iOSu
- Programování pro iOS - 21. "Multitasking"
- Programování pro iOS - 22. Nulla est honesta avaritia nisi temporis
- Programování pro iOS - 23. Jak se aktivovat, jsme-li v pozadí
- Programování pro iOS - 24. Zbývající drobnosti
- Programování pro iOS - 25. Řídicí objekty rámců
- Programování pro iOS - 26. Jak se dělá UIViewController
- Programování pro iOS - 27. Kde vzít rámce
- Programování pro iOS - 28. Základní služby
- Programování pro iOS - 29. Práce s rámci
- Programování pro iOS - 30. Rotace zařízení
- Programování pro iOS - 31. Správa paměti v rámcích
- Programování pro iOS - 32. Řídicí objekt pro tabulky
- Programování pro iOS - 33. Řídicí objekt pro strom
- Programování pro iOS - 33. Více o UINavigationControlleru
- Programování pro iOS - 35. Ještě jednou UINavigationController
- Programování pro iOS - 36. Po navigátoru taby
- Programování pro iOS - 37. Více o UITabBarControlleru
- Programování pro iOS - 38. Dokončení UITabBarControlleru
- Programování pro iOS - 39. UIPopoverController
- Programování pro iOS - 40. Další triky UIPopoverControlleru
- Programování pro iOS - 41. Zbývající služby UIPopoverControlleru
- Programování pro iOS - 42. UISplitViewController
- Programujeme v
iTunesXcode 4 - Programování pro iOS - 44. Předvolby Xcode 4
- Programování pro iOS - 45. Práce v Xcode 4
- Xcode 4: projekt a cíle
- Xcode 4: práce s cíli
- Xcode 4: Build Settings
- Xcode 4: Build Phases
- Xcode4: Build Phases podruhé
- Xcode 4: Co jsou to Build Rules?
- Xcode4: taje editoru
- Xcode4: automatické doplňování v editoru
- XIBy chyby
- Více o XIBech
- Editor XIBů
- Inspektory pro XIBy
- Vazby mezi objekty v XIBech
- Vazby mezi objekty v kódu
- Paletky Xcode pro XIBy
- Xcode 4: levý sloupec
- Xcode 4: okno Organizer
- Xcode 4: okno Organizer, část druhá
- Xcode 4: co je to Workspace?
- Xcode 4: základy schémat
- Xcode 4: akční schémata