NSNotification, NSNotificationCenter, NSNotificationQueue

Úkolem tří tříd NSNotification... je zajistit předávání zpráv i v případech, kdy objekt který zprávu odesílá neví, které objekty ji mají dostat. Můžeme to vyjádřit také jinak -- zatímco v klasickém mechanismu předávání zpráv volí jak obsah zprávy tak i jejího příjemce odesilatel, umožňuje OpenStep aby odesilatel zvolil pouze obsah zprávy, zatímco příjemce sám určí podle obsahu které všechny zprávy chce dostávat. To je velmi praktické nejméně ve třech případech:

není zapotřebí navazovat přímé spojení mezi samostatnými programovými moduly. Vazba mezi takovými moduly může být realizována pouze prostřednictvím smluvených jmen zpráv; to zvyšuje flexibilitu takových modulů a snižuje pravděpodobnost programových chyb.

V levém horním rohu obrázku vidíme klasický přístup, kdy spolu objekty A a B komunikují přímo. Jsou-li oba objekty pevnými částmi jediného modulu, je to samozřejmě optimální; jestliže se však jedná o objekty z různých modulů, které se mohou dynamicky spojovat a oddělovat, mohou nastat při chybě programátora nepříjemné problémy -- typické situace vidíme níže v levé části obrázku: korektní navázání spojení z objektu B na objekt A, ale chybné spojení v opačném směru, nebo odstranění objektu B aniž by byla zároveň zrušena vazba uvnitř objektu A. Při vazbě prostřednictvím třídy NSNotification nic takového nehrozí -- žádné explicitní vazby mezi objekty totiž nejsou. Oba objekty pouze vysílají a přijímají zprávy "Xyz"; o korektní doručení -- je-li vůbec komu doručovat -- se postará sám systém.

můžeme snadno, bezpečně a bez zvláštní programátorské práce zajistit rozeslání zprávy více objektům:

V levé části obrázku opět vidíme klasický přístup, při kterém objekt musí využít služeb třídy NS(Mutable)Array a starat se o udržování jejího obsahu (obrázek ukazuje i nebezpečí nekorektního obsahu pole -- čtvrtý objekt B byl zrušen, ale pole o tom "neví"). Pravá část obrázku opět ukazuje bezproblémový přístup prostřednictvím třídy NSNotification -- o nic se nemusíme starat; objekt A prostě odešle zprávu "Xyz" a systém ji předá všem objektům, které mají o zprávu "Xyz" zájem.

třetím případem je vlastně kombinace obou předchozích možností: často narazíme na případ, kdy akce provedená nad jedním objektem má vliv na řadu objektů z jiných modulů; udržování explicitních vazeb mezi těmito objekty by přitom neúnosně zkomplikovalo celou aplikaci. Podívejme se na reálný případ, na kterém jsem nedávno pracoval:

Vidíme zde uživatelské rozhraní, za kterým se skrývají čtyři samostatné moduly: každé z oken má vlastního, samostatného správce; čtvrtým modulem je správce tabulky v levém horním okně, který je nezávislý na zobrazené části aplikace a dokáže "svou" tabulku zobrazit kdykoli a kdekoli. Jestliže nyní zavřeme okno "MCNB_0:Parametry...", musí se to dozvědět (a) samozřejmě správce tohoto okna, (b) Finder, protože další vyhledávání uvnitř zavřeného okna nemá smysl, (c) správce okna v pozadí (v něm je hierarchický přehled všech ostatních oken, a ten musí být udržován) a (d) správce tabulky, který musí tuto konkrétní instanci tabulky uvolnit. Bez třídy NSNotification to znamená udržování explicitních vazeb mezi všemi čtyřmi moduly, komplikované rozesílání zpráv a řadu "pastí", kde může malé přehlédnutí programátora vést k nepříjemným problémům: což není-li okno finderu právě otevřeno? To je třeba explicitně ošetřit, protože neexistujícímu finderu nemůžeme posílat zprávu.... S využitím třídy NSNotification naproti tomu stačí, ohlásí-li okno že je zavíráno; všichni (existující) správci tuto informaci dostanou automaticky.

Programátorské rozhraní je velmi jednoduché. Stačí, ukážeme-li si použití třídy NSNotificationCenter -- zbývající dvě třídy jsou pouze pomocné:

// chceme-li _odeslat_ zprávu, nalezneme
// standardního správce:
id nc=[NSNotificationCenter defaultCenter];
// a zašleme mu zprávu, určenou jménem
// a objektem, který je za zprávu "zodpovědný":
[nc postNotificationName:@"Xyz" object:self];

// chceme-li _dostat_ zprávu, musíme určit
// zprávu Objective C kterou dostaneme když
// se požadovaná zpráva objeví. K tomu slouží
// tzv. selector -- zakódované jméno zprávy:
id nc=[NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(xyz:)
name:@"Xyz" object:nil];
// kdykoli kterýkoli objekt odešle zprávu "Xyz",
// dostaneme zprávu Objective C xyz:, jejímž
// parametrem budou všechny údaje odeslané
// se zprávou "Xyz". Chceme-li, můžeme si také
// vyžádat doručení _jakékoli_ zprávy vydané
// objektem který známe:
[nc addObserver:self selector:@selector(xyz:)
name:nil object:anObject];
// nebo dokonce můžeme chtít přijímat
// naprosto všechny zprávy od kohokoli:
[nc addObserver:self selector:@selector(xyz:)
name:nil object:nil];



NSString

Objekty třídy NS(Mutable)String v OpenStepu reprezentují textové řetězce -- slouží tedy ke stejnému účelu, ke kterému v klasickém C sloužila pole znaků. Schopnosti třídy NSString však daleko přesahují možnosti klasických knihoven ANSI C; základem již je samotná schopnost třídy NSString využívat optimálního kódování znaků od prostého osmibitového kódování až po UNICODE. Ukažme si několik základních operací nad řetězci:

// možností vytvořit řetězec je řada:
id a=@"Toto je statický objekt třídy NSString";
char *xx="Můžeme samozřejmě využít i proměnné \"char *\"";
id b=[NSMutableString stringWithCString:xx];
char *yy="I\0když\0obsahují\0nulové\0znaky\0!"
id c=[NSString stringWithCString:yy length:30];
// řetězec lze načíst přímo ze souboru:
id d=[NSString stringWithContentsOfFile:@"/tmp/something.text"];
// nebo vytvořit pomocí "printf"-formátu:
id e=[NSString stringWithFormat:@"total:%d, %s, %@, %5.3f\n",1,"ahoj",a,3.14159];
// můžeme si také vyžádat automatický "překlad" prostřednictvím
// překladové tabulky v aktivním adresáři lproj (viz popis NSBundle
// v minulém dílu):
id f=[NSString localizedStringWithFormat:@"%d files",ff];
// výše uvedený příklad vytvoří např. při aktivní češtině a
// odpovídající položce v tabulce stringů řetězec
// @"15 souborů".

Řetězce často generují i jiné třídy -- např. libovolný objekt OpenStepu vrátí NSString, obsahující jeho popis, na základě zprávy description. Jiným hezkým příkladem je NSArray -- objekty této třídy umějí vygenerovat řetězec daný kombinací všech obsažených prvků a libovolného oddělovače:

NSArray *a=[NSArray arrayWithObjects:@"A",@"B",@"C",nil];
NSLog(@"%@",[a componentsJoinedByString:@" a "]);
// vypíše "A a B a C"
NSLog(@"DOS path: %@",[a componentsJoinedByString:@"\\"]);
// vypíše "DOS path: A\B\C"

Základní služby pro práci s řetězci samozřejmě zahrnují nejrůznější kombinace a rozklady -- ukažme si několik příkladů, využívajících řetězce a-f, vytvořené v prvním příkladu:

NSLog(@"%@",[f stringByAppendingString:f]);
// vypíše "15 souborů15 souborů"
NSLog(@"%@",[f stringByAppendingFormat:@" je prostě %@",f]);
// vypíše "15 souborů je prostě 15 souborů"
NSArray a=[e componentsSeparatedByString:@", "];
// vytvoří pole @"total:1",@"ahoj",@"Toto ... NSString",@"3.142"
NSLog(@"%@",[c substringFromIndex:29]);
// vypíše "!"
[b deleteCharactersInRange:(NSRange){8,8}];
// b obsahuje "Můžeme proměnné "char *""
[b appendString:@" použít"];
// b obsahuje "Můžeme proměnné "char *" použít"
[b insertString:@"snadno " atIndex:7];
// b obsahuje "Můžeme snadno proměnné "char *" použít"

Pro označování částí řetězců a pro vyhledávání slouží typ NSRange -- obyčejná struktura, obsahující dvě čísla, pozici a délku:

NSLog(@"%@",[a substringFromRange:(NSRange){8,8}]);
// vypíše "statický"
NSRange r=[a rangeOfString:@"je"];
NSLog(@"\"je\" je na pozici %d, před ním je \"%@\"",
r.location,[a substringToIndex:r.location]);
// vypíše ""je" je na pozici 5, před ním je "Toto ""

Prostřednictvím přepínačů si můžeme vyžádat i hledání odzadu, hledání bez ohledu na velikost písmen nebo hledání pouze od zadané pozice; rozsah prohledávaného řetězce také můžeme omezit. Pro základní a nejčastěji potřebná porovnávání jsou samozřejmě k dispozici hotové metody:

if ([a hasPrefix:@"Toto"]) // platí
if ([c hasSuffix:@"!"]) // platí
if ([d isEqual:e]) // neplatí

Zajímavá je i možnost vyžádat si nejdelší společný prefix dvou řetězců; i zde máme možnost volit zda se má nebo nemá brát v úvahu velikost písmen:

NSLog(@"%@",[a commonPrefixWithString:e options:NSCaseInsensitiveSearch]);
// vypíše "Tot"

Prozatím jsme se vůbec nezabývali vnitřním kódováním řetězce. To je v objektovém prostředí samozřejmé -- do vnitřního kódování nám přece nic není, a zajímají nás pouze zprávy, které je objekt schopen zpracovat. Kódování však může být zajímavé ze dvou důvodů -- předně z použitého kódování vyplývá rozsah znaků, které řetězec může obsahovat; druhým důvodem může být programová volba kódování pro konkrétní účel -- např. pro ukládání do souboru bude asi nejvýhodnější kódování, které zabere nejméně místa.

Základním kódováním pro třídu NSString je UNICODE v tom smyslu, že řetězce reprezentované objekty třídy NSString mohou obsahovat libovolné znaky UNICODE, a že metody pro přímý přístup do řetězce (např. metoda characterAtIndex:) operují právě s šestnáctibitovými kódy znaků podle standardu UNICODE. Pro další kódování máme k dispozici předdefinovaný typ NSStringEncoding, který reprezentuje kódování, a následující metody:

// vypíšeme všechna kódování, která jsou k dispozici
NSStringEncoding *en=[NSString availableStringEncodings];
while (en)
NSLog(@"%@",[NSString localizedNameOfStringEncoding:en++]);
// zjistíme které kódování odpovídá běžným Céčkovým řetězcům
en=[NSString defaultCStringEncoding];
// ověříme lze-li do něj převést string d bez ztráty informace
if ([d canBeConvertedToEncoding:en])
// a ano-li, převedeme jej:
newd=[d dataUsingEncoding:en];
else {
// ne-li, vyhledáme nejúspornější bezztrátové kódování
en=[d smallestEncoding];
// a použijeme jej:
newd=[d dataUsingEncoding:en];
}
// pro použití ve standardním C jsou k dispozici
// pomocné převáděcí metody
void std_func(int i,float f,char *c)
{
printf("%d, %e, %s",i,f,c);
}
NSString *s=@"3.141592654";
std_func([s intValue],[s floatValue],[s cString]);
// vypíše "3, 3.141593e+00, 3.141592654"

Tím samozřejmě možnosti NSStringů zdaleka nekončí; na úrovni tohoto článku by však nemělo smysl podrobně popisovat všechny metody. Proto se již seznámíme jen se samostatnou skupinou metod, které slouží pro práci s názvy souborů a adresářů -- samozřejmě korektně v konkrétním hostitelském operačním systému, takže programátor se nemusí starat o to oddělují-li se jednotlivé položky lomítkem, obráceným lomítkem nebo třeba dvojtečkou:

// příklady buďtež z Unixu:
NSString *p=@"/Users/oc/Apps/Test.app";
NSLog(@"%@",[p lastPathComponent]);
// vypíše "Test.app"
NSLog(@"%@",[p pathExtension]);
// vypíše "app"
NSLog(@"%@",[p stringByAppendingPathComponent:@"Czech.lproj"]);
// vypíše "/Users/oc/Apps/Test.app/Czech.lproj"
NSLog(@"%@",[p stringByDeletingLastPathComponent]);
// vypíše "/Users/oc/Apps"
NSLog(@"%@",[p stringByAbbreviatingWithTildeInPath]);
// pro uživatele "oc" vypíše "~/Apps/Test.app"
NSString *p=@"~/Apps/Test.app";
NSLog(@"%@",[p stringByExpandingTildeInPath]);
// pro uživatele Steve vypíše "/Users/Steve/Apps/Test.app"
NSString *p=@"/oc/RootTemp";
NSLog(@"%@",[p stringByResolvingSymlinksInPath]);
// u mě vypíše "/private/tmp", protože
// /oc/RootTemp je link ("zástupce")
NSString *p=@"~/../oc/./RootTemp/./TmpFile";
NSLog(@"%@",[p stringByStandardizingPath]);
// vypíše "/Users/oc/RootTemp/TmpFile"

// snad nejzajímavější metoda dokáže v
// systému souborů vyhledat všechna jména,
// která odpovídají zadané specifikaci:
NSArray *a;
[@"~/Mailboxes/Ne" completePathIntoString:nil
caseSensitive:YES
matchesIntoArray:&a
filterTypes:nil];
NSLog(@"%@",a);
// vypíše "("~/Mailboxes/NeXT.mbox", "~/Mailboxes/NewEncoding.mbox")"

Nakonec si na jednoduchém příkladu ukážeme použití třídy NSScanner, která slouží k procházení textem a vybírání jeho jednotlivých částí. Typicky je samozřejmě používána nad stringy načtenými ze souboru nebo získanými od jiných procesů prostřednictvím sítě. Zároveň si okrajově ukážeme služby třídy NSCharacterSet, jejíž objekty reprezentují množiny znaků:

id s=@"12 chlapů na mrtvého bedně, Johoho!";
id scan=[NSScanner scannerWithString:s];
int i;
NSString *s1,*s2;

// načteme číslo
[scan scanInt:&i];
// načteme text dokud jsou v něm...
[scan scanCharactersFromSet:
// ... jen písmena
[NSCharacterSet letterCharacterSet]
intoString:&s1];
// přeskočíme vše až k nejbližšímu...
[scan scanUpToCharactersFromSet:
// ... velkému písmenu
[NSCharacterSet uppercaseLetterCharacterSet]
intoString:NULL];
// načteme vše dokud nenarazíme na "ho"
[scan scanUpToString:@"ho"
intoString:&s2];
NSLog(@"%d, \"%@\", \"%@\"",i,s1,s2);
// vypíše "12, "chlapů", "Jo""



NSUserDefaults

Dříve než se začneme seznamovat s konkrétními službami třídy NSUserDefaults si musíme vysvětlit, co to vlastně jsou a jak fungují v OpenStepu uživatelské předvolby -- pokud vím, žádný jiný systém zatím podobnou službu nenabízí, takže většina čtenářů tohoto článku zřejmě nebude mít s ničím podobným zkušenosti.

Základním prvkem předvoleb je dvojice klíč-hodnota, podobně, jako u třídy NSDictionary. Klíč vždy určuje jméno předvolby (např. "BarvaTextuVHlavnímOkně"), zatímco hodnota určuje skutečnou hodnotu předvolby ("Black"). Dvojice jsou uloženy v tzv. doménách; doménu si můžeme představit jako NSDictionary, obsahující libovolný počet dvojic. Systém uživatelských předvoleb pak spravuje libovolný počet pojmenovaných domén v předem zadaném pořadí; chceme-li vyhledat hodnotu k zadanému klíči, prohledává třída NSUserDefaults postupně domény podle jejich pořadí, až nalezne odpovídající dvojici. Celá skupina domén přitom může obsahovat hodnoty specifické pro každého uživatele.

Neurčíme-li jinak, obsahuje objekt třídy NSUserDefaults následující domény v uvedeném pořadí:

argumenty: doména argumentů je vytvořena při spuštění aplikace na základě parametrů příkazové řádky a slouží především pro ladicí účely -- jejím prostřednictvím můžeme specifikovat jakékoli požadované předvolby pouze pro jediné spuštění aplikace.

aplikace: aplikační doména obsahuje všechny předvolby, specifické pro danou aplikaci. Je druhá v pořadí; to znamená, že předvolby v ní uložené mají přednost před předvolbami uloženými ve všech ostatních doménách vyjma domény argumentů.

globální: globální doména obsahuje předvolby, které uživatel specifikuje pro všechny aplikace -- je-li např. v globální doméně uvedena dvojice <"NSApplicationFontSize",12>, budou všechny aplikace -- které nemají jiné nastavení v aplikační doméně ani v doméně argumentů -- používat standardně dvanáctibodové písmo.

jazykové: domény odpovídající adresářům "lproj" (popsaným v předchozích dílech), v pořadí daném uživatelskou volbou (jejíž variantu např. v NEXTSTEPu ukazuje obrázek:

V těchto doménách budou samozřejmě obvykle uloženy předvolby související se zvoleným jazykem -- např. v doméně "Czech" bude třeba předvolba <"NSApplicationFont","HelveticaCE">, která zajistí, že aplikace která běží v češtině bude standardně využívat českou Helvetiku.

poslední doménou je tzv. doména registrační; ta slouží k určení standardních hodnot, které se použijí jestliže není požadovaná předvolba uložena nikde jinde.

Kromě toho můžeme samozřejmě vytvářet vlastní domény podle potřeby. Každá doména je buď persistentní nebo dočasná; obsah persistentních domén je uložen na disku, zatímco obsah dočasných domén zanikne ve chvíli ukončení aplikace. Mezi standardními doménami jsou dvě dočasné -- argumentová a registrační; všechny ostatní jsou persistentní. Vytváříme-li vlastní domény, závisí pouze na nás, jakého budou typu.

Ukažme si opět konkrétní příklad zdrojového textu. Jedna z prvních věcí které každá aplikace udělá bude pravděpodobně získání standardního objektu třídy NSUserDefaults obsahujícího standardní domény, a nastavení hodnot domény registrační (která je po startu aplikace samozřejmě prázdná):

NSUserDefaults *def;

// následující metoda je standardně volána ihned
// po startu aplikace (existuje-li)
-(void)applicationDidFinishLaunching:notification
{
id reg=[NSDictionary dictionaryWithObjects:
[NSArray arrayWithObjects:
@"12",@"YES",@"Pepa z depa",
nil] forKeys:
[NSArray arrayWithObjects:
@"Počet",@"JeToTak?",@"Jméno",
nil]
];
def=[NSUserDefaults standardUserDefaults];
[def registerDefaults:reg];
....
}

Kdekoli v aplikaci pak můžeme volně používat standardní hodnoty; pro tento účel máme k dispozici řadu zpráv, mezi kterými jsou např. tyto:

NSLog(@"Počet je %d",[def integerForKey:@"Počet"]);
NSLog([def boolForKey:@"JeToTak?"]?@"Je to pravda":@"Nejni to pravda!");
NSLog(@"Jméno: %@",[def stringForKey:@"Jméno"]);
id o=[def objectForKey:@"Tralala"];
if (o) ...
else NSLog(@"Default \"Tralala\" neexistuje");

Uvědomme si, co se v takovém případě vlastně děje: jakmile pošleme objektu 'def' např. zprávu integerForKey:@"Počet", začnou se prohledávat domény uživatele, který aplikaci spustil, v pořadí daném pořadím domén uložených v objektu 'def'. Standardně se tedy nejprve zjistí, nebyl-li při spuštění aplikace použit parametr "Počet=n" (argumentová doména), pak se klíč "Počet" postupně hledá v aplikační doméně, v doméně globální a v doménách jazykových. Jestliže není nalezen v žádné z nich, použije se nakonec hodnota 12 z domény registrační, do které byla uložena při startu aplikace.

Aplikace samozřejmě může měnit obsah své aplikační domény (jakmile se uživatel rozhodl některou z předvoleb v jejím rámci změnit):

if (userChanged1)
[def setBool:val1 forKey:@"JeToTak?"];
if (userChanged2)
[def setInteger:val2 forKey:@"Počet"];
if (userChanged3)
[def setObject:anObject forKey:@"Tralala"];

Změny hodnot v doménách ovšem mají vedlejší efekty -- předně, nový obsah persistentních domén je nutné uložit na disk; za další, ostatní objekty mohou používat tutéž předvolbu a při změně by tedy také měly změnit své chování. První problém je řešen tak, že si můžeme kdykoli uložení vyžádat zprávou synchronize. Ta navíc zajistí i znovunačtení ostatních persistentních domén pro případ, že je mezitím jiná aplikace změnila. Zapomeneme-li na to, nic závažného se neděje -- třída NSUserDefaults sama pravidelně zprávu synchronize vyvolává. Pro řešení druhého problému skvěle slouží třída NSNotificationCenter, se kterou jsme se seznámili na začátku tohoto článku: jakmile dojde ke změně předvoleb, je prostřednictvím této třídy rozeslána zpráva "NSUserDefaultsChanged". Pokud tedy některý objekt potřebuje být informován o aktuálním stavu předvoleb, stačí, vyžádá-li si zprávou addObserver... zasílání zpráv "NSUserDefaultsChanged".

Třída NSUserDefaults nabízí řadu dalších metod pro správu persistentních i dočasných domén, pro přidávání i odebírání domén ze standardního seznamu, pro rušení existujících domén a vytváření nových, pro přístup k předvolbám jiných uživatelů a tak dále. Nemá smysl je zde podrobně popisovat, protože jsou využívány jen zřídka.





(další článek)


Copyright (c) Ondra Čada