sobota, 20 grudnia 2014

iOS, a co to jest - odcinek szósty: podstawy iOS cz.1

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:

  1. 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)
  2. 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.
  3. 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.
  4. 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.
  5. 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
  • 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
  • kolekcje
    • zwykle po dwa warianty dla każdego typu
      • NSArray
        • od XCode 4.5 przy inicjalizacji można stosować uproszczoną składnię:

                                         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
        • nie jest wymagany, ale bardzo pomocny

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;

 

iOS Simulator Screen Shot 20 Dec 2014 11.28.21

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, …];

 

iOS Simulator Screen Shot 20 Dec 2014 12.10.44

Brak komentarzy: