Poruszając się w tematach związanych z iOS wreszcie przyszła pora na zagadnienia związane z … aplikacjami na iOS. Na pierwszy ogień musiały pójść takie podstawy jak: kod związany z uruchamianiem aplikacji i przygotowaniem ekranu, układanie kontrolek na ekranie i łączenie ich z kodem, idea MVC na platformie Apple, nawigacja między ekranami obejmująca pasek nawigacyjny u góry lub zakładki na dole ekranu.
Co chciałbym dziś skomentować? Znajdzie się kilka tematów:
- jeszcze coś z Objective-C: w nowszych odmianach znika potrzeba pisania @synthesize, warto też rozumieć atrybuty propercji, bardzo istotne są protokoły odpowiadające jakby interfejsom, a także delegatom (zdaje się, że gdzieś zawieruszyły mi się wcześniej), skrótowy zapis inicjalizacji tablicy w nowszych wersjach języka, coś na kształt refleksji z programowanym opóźnieniem czasowym, kategorie klasowe (odpowiedniki method extensions)
- MVC w Apple: widok potrafiący sam się rysować (trochę mam skojarzenia z Windows Forms), podwidoki, którymi często są bardziej specjalizowane formy - kontrolki; na porządku dziennym jest tutaj kontroler trzymający referencje do widoków i kontrolek i zarządzający nimi, interfejs graficzny możemy definiować albo w kodzie kontrolera (coś na kształt code-behind Windows Forms czy rzadziej technologii XAML) oraz przy pomocy designera w plikach XML *.xib (niestety ten XML jak podejrzałem jest znacznie bardziej rozdmuchany niż XAML i bardziej skomplikowany, czasami operuje jakimiś identyfikatorami liczbowymi, wydaje mi się bardziej służyć do zapisywania pracy designera niż do ręcznej edycji). Robienie referencji w kodzie kontrolera do elementów w XML w XAML polega na nadaniu x:Name. W XCode trzeba element przeciągnąć w odpowiedni sposób na kod interfejsu kontrolera, trochę bardziej niebanalne. Podpięcie akcji do przycisku też nie odbywa się tak jak w Visual Studio, trzeba umiejętnie przeciągnąć przycisk z designera na kod implementacji kontrolera. Dodatkowo jeśli chcemy, by kontroler rozmawiał z bardziej złożoną kontrolką, musi wtedy zazwyczaj implementować protokół (podobnie było z interfejsem w aktywności w Androidzie), co w przypadku pliku .xib wiąże się z przeciągnięciem kontrolki na właściciela pliku jako delegat.
- lista: tym razem znalazłem nieco prostszy sposób podpinania UITableView pod źródło danych, jak używamy UITableViewController to część rzeczy zostanie zautomatyzowana, w tym łączenie widoku z kodem, ale i tak potrzebujemy oprogramować co najmniej trzy metody, by ukazała się najprostsza lista ze stringami. Udziwnienia wynikają po części z dość jawnego uwidocznienia w każdym przypadku grupowania i wirtualizacji. Programista nadpisująć metodę określa ilość sekcji, musi też w swoim kodzie w razie stwierdzenia braku komórki utworzyć ją na nowo, co jest pomyślane na wypadek przewijania większych ilości danych. Jednak przy kilku elementach również trzeba brak obsługi takich rzeczy oprogramować, co moim zdaniem nie jest wygodne.
- UIWebView - rzecz używana nie tylko przez aplikacje hybrydowe firm trzecich, ale podobno i przez samego Apple w niektórych systemowych aplikacjach. Ładowanie html, adresu to rzeczy znane także z rozwiązań Microsoft. Nie wiem czemu w iOS położono taki nacisk na przechwytywanie nawigacji z HTML, co pozwala wywołać kod Objective-C przy odmowie zezwolenia. Z poziomu Objective-C można też przekazać kawałek kodu JavaScript do wykonania. W Windows/Windows Phone mamy podobne rozwiązania, które są bardziej uniwersalne (np. wywoływanie kodu aplikacji nie tylko w odpowiedzi na nawigację). Podpięcie narzędzi Web przeglądarki Safari jest czymś, co się prosiło i dobrze, że jest. W Windows/Windows Phone możemy diagnozować wykonywanie strony Web w emulatorze za pomocą przeglądarki Internet Explorer.
- nawigacja - ze świata Hub i Pivot-a cofnąłem się do świata prostego paska nawigacji i zakładek (zakładki też są w Android). Ale zacznijmy od samego nawigowania. W WinRT XAML nawiguje się do typu strony, ewentualnie przekazuje parametry. W Caliburn Micro tworzy się instancję view modelu i to ją przekazuje się do nawigacji. W iOS tworzy się instancję view kontrolera i ustawia jako bieżącą prezentację, albo kładziemy na stosie kontrolerów, jeśli używamy UINavigationController. Możemy też nic nie robić, jeśli korzystamy z UITabBarController. W przypadku UINavigationController i UITabBarController musimy je wcześniej zbudować i odpowiednio ustawić. Niby wszystko logiczne, ale mamy dwa byty - widoki i kontrolery – niekoniecznie oferujące separację interfejsu od logiki (jaką mamy w MVVM w XAML czy Angularze, a nie mamy w Android w Javie czy w XAML bez MVVM), niewizualne kontrolery sterują nawigacją, natomiast na związane z nimi wizualizacje wydaje się, że mamy dość ograniczony wpływ. W Windows Phone Silverlight był pasek aplikacyjny, który miał bardzo ograniczoną funkcjonalność i to z nim kojarzą mi się elementy wizualne iOS, przynajmniej ich część. W WinRT XAML a tym bardziej WinJS Microsoft uczynił pasek nawigacyjny zwykłą kontrolką, z którą możemy zrobić co chcemy bez żadnych ograniczeń. W iOS czuję wciąż mocno skrępowany…
Poniżej notatki, do których wpadły dwa screenshoty z moich improwizacji.
Intro
iOS
- bazuje na Unix
- pierwsze wydanie w 2007
- bazuje na MacOSX
- multi-touch UI
- urządzenia: iPhone, iPod Touch, iPad
- symulator na MacOSX
- oficjalne IDE: Xcode (free w App Store)
Architektura iOS
- aplikacja
- Cocoa Touch
- GameKit, MapKit, iAd
- UIKit (korzysta z Cocoa Touch)
- Media
- Core Services
- Foundation (np. obsługa wejścia/wyjścia, Bluetooth)
- Core OS
Niektóre API w postaci bibliotek C (AddressBook)
Jak uruchamiana jest aplikacja iOS ?
- start procesu Unix - wywołanie main
- UIApplicationMain
- uruchamia message loop dla wejścia od użytkownika
- parametr AppDelegate
- interakcja poprzez wywołanie didFinishLaunchingWithOptions
- typowo ładuje widok (lub kontroler widoku) do wyświetlenia
#import <UIKit/UKit.h>
#import “AppDelegate.h”
int main(int argc, char *argv[])
{
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
Projekt
- Jeden projekt na aplikację
- XCode może obsługiwać też projekty z Cordovą
UIResponder
- klasa bazowa dla wszystkich elementów wizualnych, także dla AppDelegate
- obsługa zdarzeń dotykowych, także bardziej zawansowanych gestów
Proste API dla dotyku
- - (void) touchesBegan: (NSSet *)touches withEvent: (UIEvent *)event;
- także: tochesMoved, touchesEnded, touchesCancelled
- nadpisanie w UIResponder
XCode
- Debug nawigator: +: Add Exception Breakpoint…, Break: On Throw - zatrzymanie się na wyjątku w kodzie
Symulator
- lokalizacja, pamięć, przychodzące rozmowy…
- część API wymaga fizycznego urządzenia (np. camera)
Objective-C
- etykiety przez parametrami są opcjonalne i wchodzą w skład nazwy metody
- propercje
@implementation X
NSString* _name;
- (NSString *) name {
return _name;
}
- (void) setName: (NSString *) name {
_name = name;
}
@end
- @synthesize - zlecenie kompilatorowi wygenerowania metod dostępu do propercji, opcjonalne słowo od XCode 4.4, gdzie kompilator generuje metody dostępu automatycznie bez używania @synthesize
- zarządzanie pamięcią
- liczenie referencji
- retain inkrementuje licznik dla obiekt
- release dekrementuje licznik referencji
- programista jest odpowiedzialny za czyszczenie referencji (gdy liczba referencji jest zero obiekt może być zwolniony)
- metoda release czyści pamięć
- słowo kluczowe autorelease we wcześniejszych wersjach XCode
- obecnie zazwyczaj zostawiamy to kompilatorowi dzięki Automatic Reference Counting (ARC)
- zarządzanie pamięcią przy implementacji propercji jest istotne, zachowanie implementacji wpływa na liczenie referencji do obiektu
- liczenie referencji
- popularne atrybuty propercji
- readonly - tylko getter
- nonatomic - nie jest thread safe
- weak - nie inkrementuje licznika referencji
- retain - inkrementuje licznik referencji
@interface X : NSObject
@property (nonatomic, weak) UIColor* color;
@end
- protokoły
- zbiór metod obowiązkowych lub opcjonalnych do zaimplementowania
- słowa kluczowe optional i required
- opcjonalność jest domyślna, jeśli nie ma słowa kluczowego
- specyfikacja protokołów w definicji interfejsu klasy (wygląda jak składnia generyków, interfejs może zadeklarować więcej niż jeden protokół)
@protocol Y
- (void) display: (NSString*) msg;
@end
@interface X: NSObject<Y>
- (void) display: (NSString*) msg;
@end
@protocol UIApplicationDelegate<NSObject>
@optional
…
- słowo kluczowe id
- może być użyte, kiedy nazwa klasa nie jest znana
@protocol Y;
@interface X: NSObject
@property (nonatomic, weak) UIColor* theColor;
@property (nonatomic, strong) id<Y> delegate;
@end
@protocol Y<NSObject>
- (void) setTheColor: (UIColor*) color;
@end
- typy numeryczne
- NSInteger, NSUInteger - definicje typedef bazujące na typach C int i uint
- NSDecimalNumber - klasa Objective-C
- BOOL
- definicja typedef
- YES (1)
- NO (0)
- można używać TRUE/FALSE, ale większość kodu używa YES/NO
- definicja typedef
- kolekcje
- zwykle po dwa warianty dla każdego typu
- NSArray
- od XCode 4.5 przy inicjalizacji można stosować uproszczoną składnię:
- NSArray
- zwykle po dwa warianty dla każdego typu
NSArray* array = @[[UIColor redColor], [UIColor greenColor]];
- NSMutableArray
- NSDictionary
- NSMutableDictionary
- …
- nie ma generyków, obiekty pobierane z kolekcji trzeba rzutować
- enumeracja z “for”
- klasa musi wspierać protokół NSFastEnumeration
- for (UITouch* touch in touches) {}
- szybka i bezpieczna (wyjątek jest wyrzucany, jeśli kolekcja jest modyfikowana przez inny wątek)
- refleksja z opóźnieniem
- [obj performSelector: @selector(setTheColor) withObject: color afterDelay: 5]
MVC
kontroler ViewController
- hybrydowy kontroler
- zawsze ma związany z nim widok, może trzymać podwidoki
widok UIView - powiązany jest z nim ViewController
UIScreen –> UIWindow –> UIView –> UIButton, …
można stworzyć klasę dziedziczącą po UIView w Xcode i nadpisać w niej metodę drawRect (surowe rysowanie)
- (void) drawRect: (CGRect) rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef redColor = [UIColor redColor].CGColor;
CGContextSetFillColorWithColor(context, redColor);
CGContextFillRect(context, self.bounds);
}
w implementacji AppDelegate w metodzie didFinishLaunchingWithOptions:
…
MyUIView *view = [[MyUIView alloc] initWithFrame: CGRectMake(20,30,100,100)];
[self.window.rootViewController.view addSubview:view]
…
ViewController
- każdy ViewController może być rootem, nie ma specjalnej klasy
- UIWindow spodziewa się jednego, zazwyczaj tworzonego i ustawianego w AppDelegate
- klasa bazowa dla wszystkich view kontrolerów w iOS, może programowo tworzyć i zarządzać swoim widokiem i podwidokami
tworzymy klasę dziedziczącą po UIViewController (opcja: target na iPad, opcja: z XIB dla UI) i w jego metodzie viewDidLoad dodajemy:
MyUIView *view = [[MyUIView alloc] initWithFrame: CGRectMake(20,30,100,100)];
UILabel* label = [[UILabel alloc] initWithFrame: CGRectMake(130, 10, 200, 100)];
label.text = @”Ina different stylee”;
[self.view addSubview:view];
[self.view addSubview:label];
a w AppDelegate.m w didFinishLaunchingWithOptions wstawiamy:
self.window.rootViewController = [[MyViewController alloc] init ];
Interfejs możemy też budować w bardziej wygodny sposób przy pomocy wizualnego interface builder’a. Generuje on pliki nib (Next-Step Interface Builder):
- pliki XML z rozszerzeniem .xib
- zwykle opisują obiekty UIView
Przy kreowaniu view kontrolera zaznaczamy opcję tworzącą dodatkowo plik XIB.
Xcode: prawym przyciskiem Open As… –> Source code - podgląd XML
Widoki UIView
Framework dostarcza klas dziedziczących po UIView, których zwykle używa się do budowy UI (za wyjątkiem gier)
Łączenie UI z kodem w kontrolerze.
Jak w jednym z poprzednich odcinków zaznaczam obiekt w designerze, naciskam CTRL i przeciągam do kodu z interfejsem. Tworzę element będący referencją do obiektu tworzonego na podstawie pliku nib. Połączenie między nimi to outlet (podobnie jak x:Name w XAML). Teraz mogę operować w kontrolerze na kontrolkach np. w viewDidLoad:
self.label.text = @”Pro Forma”;
Popularne UI View
- używane jako cały widok
- UITableView
- lista elementów
- dedykowany UITableViewController
- implementacja protokołów
- UITableViewDataSource
- metody wymagane: numberOfRowsInSection, cellForRowAtIndexPath
- metody opcjonalne
- UITableViewDelegate
- metody opcjonalne: najczęściej didSelectRowAtIndexPath
- UITableViewDataSource
- nie jest wymagany, ale bardzo pomocny
- implementacja protokołów
- UITableView
Tworzymy plik dziedziczący po UITableViewController z plikiem XIB. Style = Grouped włącza widok grupowy, z podziałem na sekcje w UITableView.
AppDelegate, w metodzie didFinishLaunchingWithOptions:
self.window.rootViewController = [[TTViewController alloc] initWithNibName:@"TTViewController" bundle:nil ];
@interface TTViewController: UITableViewController
@property (strong, nonatomic) NSArray* data;
@end
@implementation TTViewController
…
- (void) viewDidLoad
{
…
self.data = @[@”Kaenżet”, @”Pro Forma”, @”Kult”, @”Zuch Kazik”];
}
…
- (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView
{
return 1;
}
- (NSInteger) tableView: (UITableView *) tableView numberOfRowsInSection: (NSInteger) section
{
return self.data.count;
}
- (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath*) indexPath
{
static NSString *CellIdentifier = @”Cell”;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: CellIdentifier forIndexPath:indexPath];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: CellIdentifier];
}
NSInteger row = indexPath.row;
NSString* str = [self.data objectAtIndex: row];
cell.textLabel.text = str;
return cell;
}
…
@end
- UIWebView
- używane przez wiele aplikacji Apple np. AppStore, Music
- używane przez PhoneGap / Cordovę
- JavaScript to Native
- implementujemy w view kontrolerze protokół UIWebViewDelegate
- shouldStartLoadWithRequest - wywoływane dla każdego nawigacyjnego zdarzenia
- klikanie na <a>
- zmiana window.location
- inspekcja żądań nawigacji (true - zezwalamy; false - nie zezwalamy, możliwość wykonania kodu w iOS)
- iOS to JavaScript
- stringByEvaluatingJavaScriptFromString - wykonanie kodu JavaScript wewnątrz uruchomionej strony
- ładowanie danych
- loadData
- loadHTMLString
- loadRequest
- debugowanie
- metoda klasowa _enableRemoteInspector - włącza narzędzia debugujące WebKit dla UIWebView w symulatorze, nie można jej pozostawić do publikacji w appstore
- http://localhost:9999/
- od Safari 6 i Xcode 4.5 wbudowane w Safari: Develop –> iPhone Simulator (menu Develop włącza się w Safari-> Preferences –> Advanced)
- (BOOL) webView: (UIWebView *)webView shouldStartLoadWithRequest: (NSURLRequest *) request
navigationType: (UIWebViewNavigationType) navigationType {
NSString* path = request.mainDocumentURL.relativePath; //request.URL.path
if ([path isEqualToString: @”/xxx”]) {
…
return FALSE;
}
return TRUE;
}
…
[mWebView stringByEvaluatingJavaScriptFromString: @”document.querySelector(‘#msg’).innerText=’ina different stylee’”];
…
kontroler viewDidLoad:
[self.webView loadHTMLString: @”<html>…</html>” baseURL: nil];
Wiązanie protokołu UIWebViewDelegate z widokiem XIB - zaznaczone WebView łączymy wizualnie delegat z File’s Owner, wcześniej deklarujemy obsługę protokołu UIWebViewDelegate przez kontroler
- kontrolki używane typowo jako podwidoki
- UIButton
- UILabel
- UISlider
- UIActivityIndicatorView
- UIProgressView
- UITextField
- UISwitch
- UIImageView
- UIPopoverController
- tylko na iPad
Nawigacja
Odpowiadają za nią kontrolery ViewController, nie UIWindow czy UIView.
Nawigacja oparta na view kontrolerach używa właściwości Objective-C zwanymi klasowymi kategoriami (duck typing) do wstrzyknięcia właściwości i metod do innych definicji interfejsów klas (coś ala extension methods).
Wstrzykiwanie paska z zakładkami
interface UIViewController (UITabBarControllerItem)
@property (nonatomic, retain) UITabBarItem *tabBarItem;
@property (nonatomic, readonly, retain) UITabBarController *tabBarController;
@end
UINavigationController
- zarządza stosem view kontrolerów (i powiązanymi z nimi widokami)
- używamy jako rootViewController
- rysuje pasek nawigacyjny
- umieszcza ViewController na szczycie stosu
- przycisk + (chyba że wyłączymy przycisk back)
- możemy
- zmieniać przyciski na pasku nawigacyjnym
- dodawać przyciski do paska nawigacyjnego
- pokazywać i dostosowywać toolbar na dole widoku
Protokół paska nawigacyjnego
@protocol UINavigationControllerDelegate <NSObject>
@optional
- (void) navigationController: (UINavigationController *) navigationController willShowViewController: (UIViewController *) viewController animated: (BOOL) animated;
- (void) navigationController: (UINavigationController *) navigationController didShowViewController: (UIViewController *) viewController animated: (BOOL) animated;
@end
Tworzymy pierwszy ekran z dwoma przyciskami oraz dwa ekrany. Podobnie jak w jednym z wcześniejszych odcinków przycisk z kodem łączymy przeciągając wizualnie z Ctrl na implementację odpowiedniego kontrolera.
- (IBAction) navigateToDocuments: (id) sender {
DocumentsTableViewController* vc = [[DocumentsTableViewController alloc] init];
//[self presentViewController: vc animated: YES completion: nil]; //bez kontrolera nawigacyjnego
[self.navigationController pushViewController: vc animated: YES];
}
AppDelegate.m w didFinishLaunchingWithOptions
ViewController* vc = [[ViewController alloc] initWithNibName: @”ViewController” bundle: nil];
self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController: vc];
Na pasku nawigacyjnym u góry po przejściu do wskazanego ekranu pojawia się po lewej stronie przycisk Back.
Pasek nawigacyjny wyświetla tytuły ekranów, jeśli w view kontrolerach ustawimy właściwość title (w viewDidLoad). Na przycisku Back pojawia się nazwa ekranu, do którego możemy powrócić.
self.title = @”Dokumenty”;
Do danego widoku możemy dodać Bar Button Item (ustawiamy Identifier i Title). Łączymy z interfejsem (referencja) i implementacją (akcja) w kodzie przez przeciągnięcie. W odpowiednim kontrolerze w viewDidLoad możemy ustawić nowo dodany przycisk:
self.navigationItem.rightBarButtonItem = self.editButton;
UITabController
- zakładki na dole ekranu
Protokół paska z zakładkami: UITabBarControllerDelegate
AppDelegate.h
@property (strong, nonatomic) UITabBarController *viewController;
AppDelegate.m
self.viewController = [[UITabBarController alloc] init];
self.viewController.viewControllers = @[vc, …];
Brak komentarzy:
Prześlij komentarz