Pieni App Delegate -testiohjelma

Moi,
kuten sanottua, opettelen parhaillaan applikaation sopeutumista erilaisiin keskeytyksiin, kuten puhelimen soimiseen, exit-napin painamiseen tai näytön lukitsemiseen. Kaikenlaisia selostuksia ja flow chartteja on nyt tulkittu, mutta emmental-aivot kaipasivat edelleen jotakin konkreettisempaa.

Tein siis pikkiriikkisen testiapplikaation, jonka ajattelin jakaa tässä, josko siitä joku muukin hyötyisi. Kyse ei ole siis mistään sen kummemmasta kuin valmis Xcoden ”Utility Application” -template, johon olen lisännyt NSLog-komentoja tyypillisimpiin applikaation eri vaiheisiin liittyviin delegate-metodeihin ym. – loki ei siis ole millään muotoa kattava, mutta perustoiminnoista päässee tämän avulla kärryille.

Huom! Ruudulla ei siis näy mitään muuta kuin perus Utility-template flipside-näkymineen – Xcoden Debugger Console (omppu-shift-R) pitää olla auki, jotta applikaatiosta saisi mitään irti.

Tässä Xcode-projekti zip-pakettina.

Asia konkretisoituu varsin mukavasti tiirailemalla konsolia applikaatiota käytellessä. Aika paljon konepellin alla tapahtuu, vaikka ulospäin ei näy juuri mitään. Esimerkiksi heti applikaatiota käynnistäessä pölähtää konsoliin tällainen raportti – MVC tarkoittaa MainViewControlleria, Flipside tietty kääntöpuolen ViewControlleria ja pelkät metodinimet viittaavat Application Delegaten metodeihin.

2011-10-19 23:17:26.485 AppDelegateHarjoitus[183:707] MVC - awakeFromNib
2011-10-19 23:17:26.496 AppDelegateHarjoitus[183:707] application:didFinishLaunchingWithOptions:
2011-10-19 23:17:26.516 AppDelegateHarjoitus[183:707] MVC - viewDidLoad
2011-10-19 23:17:26.520 AppDelegateHarjoitus[183:707] MVC - viewWillAppear
2011-10-19 23:17:26.525 AppDelegateHarjoitus[183:707] MVC - shouldAutorotateToInterfaceOrientation: Portrait
2011-10-19 23:17:26.529 AppDelegateHarjoitus[183:707] MVC - viewDidAppear
2011-10-19 23:17:26.535 AppDelegateHarjoitus[183:707] MVC - shouldAutorotateToInterfaceOrientation: Portrait
2011-10-19 23:17:26.543 AppDelegateHarjoitus[183:707] applicationDidBecomeActive:

Ihan mielenkiintoista oli huomata käytännössä esimerkiksi, että tuo awakeFromNib ilmoittaa olemassaolostaan ennen mitään muita viestejä, ja että tuo Autorotate-metodi tulee jostain syystä kutsutuksi kahdesti: sekä WillAppearin että DidAppearin jälkeen.

Jos App Delegaten ja View Controllereiden perustoiminnot vielä Dev Centerin kahlaamisen jälkeenkin vaikuttavat hieman hämäriltä, kuten minulla, tämän applikaation kanssa leikkiminen voi helpottaa asian hahmottamista.

Mainokset

Välihuomautus: ”FBCDN image is not allowed in stream…”

Moi pitkästä aikaa! En ole heittänyt lusikkaa nurkkaan, enkä myöskään devauspyyhettäni kehään, päinvastoin. Aika vain on ollut hieman kortilla viime kuukausina. Olen laatinut yhtä suhteellisen yksinkertaista taulukkonäkymiin ja -navigaatioon perustuvaa applikaatiota, mutta huomasin jossain vaiheessa, että sekin on turhan laaja ekaksi projektiksi ja näinä harvoina vapaina yön pikkutunteina edistettäväksi.

Palasin siis metaforiselle Moleskinelleni, valitsin jokseenkin järkevien softaideoideni pinosta kaikista simppeleimmän, ja aloin hommiin. Kyseessä on pikkuruinen yhden näkymän viihdeapplikaatio, ja se alkaa itse asiassa olla olennaisilta osiltaan valmis! Suosittelen muuten kaikille samankaltaisessa tilanteessa oleville: aloittakaa yhden screenin applikaatiolla! Tekemistä ja opettelua riittää ihan varmasti, ja toisekseen, osa maailman suosituimmista iOS-applikaatioista on (ainakin pintapuolisesti) simppeleitä yhden ruudun softia.

Tilanne softan kanssa alkaa olla sellainen, että jotakuinkin kaikki sellaiset ominaisuudet jotka eivät edellytä todellisella iVehkeellä testaamista, alkavat olla valmiina. Toisin sanoen, hurja kahdeksankympin sijoitus omaan Developer Programiin on lähempänä kuin koskaan. Jotta kaikki vapaa-aikani ei menisi omilla applikaatioraakileilla leikkiessä oikealla iPhonella, olen kuitenkin edellyttänyt itseltäni tämän ekan applikaation viimeistelemistä kaikilta muilta osiltaan ennen Developer-tilin avausta ja fyysisellä laitteella testaamista.

Yksi näistä ”muista osista” on ohjelmallinen Facebook-statuspäivitys, jota olen siis viime päivät (=yöt) opetellut. Ja se on myös tämän kirjoituksen pohjimmainen motivaattori.

Ja se varsinainen asia:

Siltä varalta, jos joku muukin sattumoisin haluaisi tehdä applikaatiollaan jotain niinkin poikkeuksellista kuin antaa käyttäjän liittää applikaatiosta lähettämänsä kuvan Facebook-statukseensa, eikä tunne tämän asian ympärillä kiehunutta kollektiivista devaajien raivoa, sanottakoon tässä: ei v**** onnistu.

Eli: kuvia voi uploadata FB:iin applikaatiosta käsin (helposti), käyttäjän seinälle voi laatia statuspäivityksen johon saa myös määritellä kuva-URLin, (helposti), näiden uploadattujen kuvien URLin pystyy selvittämään ohjelmallisesti FB graph API:n metodien avulla (pienellä perehtymisellä) mutta näitä nimenomaisia URLeja ei sitten saakaan käyttää statuspäivityksen kuva-URLeina (ei niin sitten millään).

Tulos on virheilmoitus, joka alkaa näin: ”FBCDN image is not allowed in stream…” – googlailemalla löytyy muutama tuhat devausblogia, jossa lainataan Facebookin ilmoitusta nähtävästi viime vuoden kesältä:

”We no longer allow stream stories to contain images that are hosted on the fbcdn.net domain. The images associated with these URLs aren’t always optimized for stream stories and occasionally resulted in errors, leading to a poor user experience. Make sure your stream attachments don’t reference images with this domain. You should host the images locally.”

Että se siitä. Eipä siinä muuten mitään, mutta jokseenkin turhauttaa pari viime yötä, jotka meni FB:n developer-dokumentteja kahlatessa ja yllämainittuja tekniikoita opetellessa. Että ehkäpä olisi voinut jossain getting started -osion kohdalla vaikka ilmoittaa, että kenties yleisimmin kaivattu toiminnallisuus sosiaaliselle applikaatiolle on kerrassaan kielletty FB:n toimesta.

Kylläpä taas hieman paremmin osaa arvostaa Applen Dev Centerin loistavasti laadittuja ja käyttäjälähtöisesti kirjoitettuja dokumentaatioita. Jobs 6 – Zuckerberg 0.

Ei mulla muuta, piti vain tulla vähän päästämään höyryjä.

Se liikkuu sittenkin!

Ensimmäinen varsinainen harjoitustyöni, viihdepainotteinen aurinkokuntasimulaattori, on lähtenyt ihan mukavasti käyntiin. Perusosat – kosketusnäytön tapahtumiin reagointi, säännöllisten tilannepäivitysten laskeminen ja ruutuun piirtäminen toimivat jotenkuten, joskaan eivät aivan bugeitta, tuskin ihan oikeaoppisesti, eivätkä taatusti ainakaan optimoidusti.

Helpoin osuus oli odotetusti Planet-luokan laatiminen, koska MVC-mallin model-osio toimii täysin itsenäisesti, eikä itselleni vaikeita kytkyjä iOS-käyttöliittymään tarvinnut siis vielä miettiä.  Xcoden new filestä vaan Objective-C class > Subclass of NSObject (joka on iOS-ohjelmoinnissa jonkinlainen ”kaikkien olioiden äiti”) ja tallennetaan nimillä Planet.h ja Planet.m. Tämän luokan muuttujien ja metodien määrittely muistutti eniten sitä ohjelmointia jota opettelin silloin joskus kun Ritari Ässä oli vielä kova juttu.

Planet.h

Header-tiedostossa oli #import <Foundation/Foundation.h> joten jätin sen siihen ja naputtelin perään muutamia vakioita kuten:

#define PLANET_GROWTH_RATIO 0.5
#define MAX_PLANET_RADIUS 50.0

Tämä on simppeli tapa määritellä nimettyjä vakioita. Muitakin tapoja näytti olevan, mutta tämä vaikutti tarpeisiini sopivalta.

Sitten määrittelin @interfacen, jossa esitellään luokan julkiset muuttujat, esimerkiksi nämä:

@interface Planet : NSObject {
    BOOL tooFar;
    float radius;
    float mass;
    NSTimeInterval birthday;
    // jne.
}

Noiden floatien sijaan suositeltiin käytettäväksi CGFloatia, kuulemma helpottaa 64-bittisyyteen siirtymistä, mutta kaipasin hämmentävän koodin joukkoon jotain ennestään tuttua, joten käytin float-määritystä. Tuo NSTimeInterval taisi olla sekin vain normaali float tms., mutta koska sitä käytetään nimenomaan ajan mittaamiseen se on ymmärrettävyyden vuoksi nimetty noin. En tiedä, olisiko jotenkin parempi käyttää näissäkin pointereita, mutta olen jotenkin vielä arka käyttämään niitä muuten kuin pakon edessä (olioihin viitatessa).

Sitten @propertyt (eli ne getter/setter-metodin automatisointikäskyt) samoille muuttujille tähän tapaan:

@property(nonatomic, readwrite, getter=isTooFar) BOOL tooFar;
@property(nonatomic, readwrite) float radius;
@property(nonatomic, readwrite) float mass;

Ja vielä metodien nimet:

-(id) initWithLocation: (CGPoint)theLocation birthday:(NSTimeInterval)theBirthday;
-(void) calculateGravityWithSolarSystem: (NSDictionary *)theSolarSystem;
//jne.

Tuo eka on alustusmetodi, joten siinä on paluuarvona (id), eli tässä tapauksessa planeetta-luokan instanssi. Tuo painovoima-metodi ei palauta mitään lopputulosta vaan muuttaa vain sen planeetta-instanssin sisäisiä arvoja, josta sitä kutsutaan,  joten paluuarvona on (void). Samaan tapaan kaikki tarvittavat metodit ja sitten loppuun vielä @end, niin päästään varsinaiseen metodien määrittelyyn.

Planet.m

Tässä jonkin verran karsittu listaus tiedoston alusta:

#import "Planet.h"
@implementation Planet

@synthesize tooFar;
@synthesize radius;
@synthesize mass;
// jne.

-(id) initWithLocation:(CGPoint)theLocation birthday:(NSTimeInterval)theBirthday
{    self = [super init]; //tämä rivi piti olla, se liittyi jotenkin periytymishommiin mutten nyt muista tarkemmin
    if (self) { //pitää varmistaa että edellisen rivin komento onnistui ennen kun aloitetaan alustelu
        self.xCenter = theLocation.x;
        self.yCenter = theLocation.y;
        self.birthday = theBirthday;
        self.tooFar = NO;
        self.radius = BABY_PLANET_RADIUS;
        self.diameter = self.radius*2;
        self.mass = (self.radius * self.radius * self.radius) / MASS_DIVIDER;
        self.xSpeed = 0;
        //jne.
    }
    return self;
}

-(void) calculateGravityWithSolarSystem: (NSDictionary *)theSolarSystem {
    float xDistance, yDistance, distance = 0.0;
    double multipliedMasses, distanceSqr, gravity, xGravity, yGravity = 0.0;
    // seuraavat kolme riviä käyvät läpi theSolarSystem -Dictionaryn
    // kaikki planeetat, eräänlainen looppi siis, kiitti vinkistä www.hopeinenomena.net
    NSEnumerator *enumerator = [theSolarSystem keyEnumerator];
    id key;
    while ((key = [enumerator nextObject])) {
        xDistance = ([[theSolarSystem objectForKey:key] xCenter] - self.xCenter);
        yDistance = ([[theSolarSystem objectForKey:key] yCenter] - self.yCenter);
        distance = sqrtf((xDistance * xDistance) + (yDistance * yDistance));
        if (distance != 0) {
            if (distance < (self.radius + [[theSolarSystem objectForKey:key] radius])){
                // Ympyröiden törmäystunnistus, hillitsee älytöntä kiihtymistä pakottamalla
                // etäisyysmuuttujan vähintään planeettojen säteiden summan suuruiseksi.
                // Ei kimpoamislaskelmia tms. vielä, väri vain muuttuu punaiseksi
                distance = (self.radius + [[theSolarSystem objectForKey:key] radius]);
                NSLog(@"Hit!");
                self.red = 1;
            }
            multipliedMasses = (self.mass * [[theSolarSystem objectForKey:key] mass]);
            distanceSqr = (distance * distance);
            gravity = multipliedMasses / distanceSqr;
            xGravity = gravity * xDistance / distance;
            yGravity = gravity * yDistance / distance;
            self.xForce += (xGravity);
            NSLog(@"distance: %f, gravity: %f, multipliedMasses: %f, distanceSqr: %f", distance, gravity, multipliedMasses, distanceSqr);
            NSLog(@"xDist: %f, xForce: %f, xGravity: %f", xDistance, self.xForce, xGravity);
            self.yForce += (yGravity);                    
            NSLog(@"yDist: %f, yForce: %f, yGravity: %f", yDistance, self.yForce, yGravity);

        }
    }
}
// jne.

Tuo calculateGravity… oli melko kinkkinen, mutta ei varsinaisten gravitaatiolaskelmien takia, vaan koska en meinannut millään osata tehdä luuppia. Samaan ongelmaan törmäsin myös myöhemmin, kun ajastinmetodin sisällä piti kutsua kaikkien planeettojen päivitystoimintoja vuorotellen. Onneksi  hopeinenomena.netin avuliaat toverit tulivat hätiin ja neuvoivat tuon enumerator-objektin käytön. Tästä tarkemmin vähän jäljempänä. Tuo Dictionary joka koodissa paljon toistuu, on muuten kuten taulukko, mutta siihen tallennetaan objektin kaveriksi aina key-arvo, jota käyttämällä päästään aina tarvittaessa käsiksi oikeaan objektiin.

SolarSystemView.h ja .m

Planeetat piirretään view-luokan drawRect-metodissa, jonka käyttöä aiemmin jo vähän opeteltiinkin. View-luokassa tarkkaillaan kuitenkin myös kosketusnäyttöä ja luodaan kosketusten perusteella uusia planeettoja. Header on lyhyt, kokonaisuudessaan tässä:

#import <UIKit/UIKit.h>
#import "Planet.h"
#import "SolarSystemViewController.h"
#import "SolarSystemAppDelegate.h"

@class SolarSystemViewController;

@interface SolarSystemView : UIView {
    SolarSystemViewController *solarSystemViewController;
    float dashes[2];
    NSMutableDictionary *solarSystem;
}
@property(nonatomic, retain) SolarSystemViewController *solarSystemViewController;
@property(nonatomic, readwrite, retain) NSMutableDictionary *solarSystem;

@end

Siinä pohjustetaan Viewin paritusta ViewControllerin kanssa, josta tarkemmin kohta. Ekaksi kurkataan kuitenkin pari juttua View.m:stä. Se alkaa näin:

#import "SolarSystemView.h"
@implementation SolarSystemView
@synthesize solarSystemViewController;
@synthesize solarSystem;

- (id)initWithFrame:(CGRect)frame {

    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor blackColor];
        dashes[0] = 2;
        dashes[1] = 5;

        solarSystem = [[NSMutableDictionary alloc] initWithCapacity:20];
    }

    return self;
}
//jatkuu...

Tuossa heti alussa on taas ilmiö, joka tulee Cocoan kanssa usein vastaan: tuota initWithFrameahan ei määritelty headerissa lainkaan, silti sen toiminta tuossa selvästi määritellään. Se on yksi näistä lukuisista vakiomuotoisista metodeista, joita iOS kutsuu automaattisesti sopivissa tilanteissa, ja jotka koodaaja uudelleenmäärittelee tekemään mitä sitten haluaakaan. Tässä tapauksessa määrittelemään taustavärin sekä vähän nolosti tuon dashes-taulukon tuossa, koska en osannut tehdä taulukkoa vakiona (tuota dashes-taulukkoa tarvitaan myöhemmin planeettojen ääriviivoissa käytetyn katkoviivan määrittelyyn). Ja sitten alustetaan tuo solarSystem -dictionary, johon kaikki luotavat planeetat tullaan laittamaan talteen.

Saman tiedoston drawRect-metodissa loopataan läpi kaikki kirjastoon tallennetut planeetat ja piirretään niiden perustella sopivankokoiset ympyrät oikeille paikoilleen, tähän tapaan:

// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    CGContextRef solarSystemContext = UIGraphicsGetCurrentContext();
    //CGContextClearRect (solarSystemContext,CGRectMake(0, 0, 320, 480));
    CGContextSetLineWidth(solarSystemContext, 2);
    CGContextSetLineDash (solarSystemContext, 0, dashes, 2);
    CGContextSetLineCap(solarSystemContext, kCGLineCapRound);
    NSString *key;
    for(key in solarSystem){
        CGContextSetRGBFillColor (solarSystemContext, [[solarSystem objectForKey: key] red], [[solarSystem objectForKey: key] green], [[solarSystem objectForKey: key] blue], [[solarSystem objectForKey: key] alphaValue]);
        CGContextSetRGBStrokeColor (solarSystemContext, [[solarSystem objectForKey: key] red], [[solarSystem objectForKey: key] green], [[solarSystem objectForKey: key] blue], [[solarSystem objectForKey: key] alphaValue]);    
        CGContextAddEllipseInRect (solarSystemContext, CGRectMake ([[solarSystem objectForKey: key] leftEdge], [[solarSystem objectForKey: key] topEdge], [[solarSystem objectForKey: key] diameter], [[solarSystem objectForKey: key] diameter]));
        CGContextDrawPath(solarSystemContext, kCGPathFillStroke);
    }
    [key release];
}

Tuossa olin näköjään osannut käyttää luuppia Dictionaryn läpikäyntiin noin yksinkertaisesti, miksihän en muistanut tuota tapaa siellä Planet-luokan koodissa tai myöhemmin siellä Timer-metodissa… no, tulipa treenattua erilaisia luuppaustapoja!

Edelleen, täällä SolarSystemView.m:ssä on määritelty uudestaan kosketusnäytön tapahtumiin reagointimetodit. Nämä voisivat ymmärtääkseni olla View Controllerinkin puolella kuten ajastinmetoditkin (oho, spoileri!), ja minulla on sellainen fiilis, että ehkä niiden tyylikkyyden nimissä pitäisikin, jotta View-objektille jäisi selkeästi pelkkä ruudun piirto. Mutta en ole ihan varma, joten ne nyt jäivät tähän – kumpikaan tapa ei siis ole varsinaisesti laiton. Mutta ne touches-metodit, tässä:

// Handles the start of a touch
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{    
    //NSUInteger numTaps = [[touches anyObject] tapCount];
    // Enumerate through all the touch objects.
    for (UITouch *touch in touches) {
        CGPoint touchLocation = [touch locationInView:self];
        NSString *addressToTouchThatCreatedAPlanet = [[NSString alloc] initWithFormat:@"%u", touch];
        Planet *aPlanet = [[Planet alloc] initWithLocation:touchLocation birthday:[touch timestamp]];

        [solarSystem setObject: aPlanet forKey: addressToTouchThatCreatedAPlanet];
        [aPlanet release];
        [addressToTouchThatCreatedAPlanet release];

        /*
        Tällä palautetaan osoite takaisin integeriksi
        unsigned int addressAsInteger = [addressToTouchThatCreatedAPlanet intValue];
        UITouch *testTouch = addressAsInteger;*/
        [self setNeedsDisplay];
    }    
}

// Handles the continuation of a touch.
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{  
    // Enumerates through all touch objects
    for (UITouch *touch in touches) {
        CGPoint touchLocation = [touch locationInView:self];

        Planet *aPlanet = [solarSystem objectForKey: [NSString stringWithFormat:@"%u", touch]];
        [aPlanet movePlanetToLocation:touchLocation];
    }

}

// Handles the end of a touch event.
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    // Enumerates through all touch object
    for (UITouch *touch in touches) {
        CGPoint touchLocation = [touch locationInView:self];
        CGPoint previousTouchLocation = [touch previousLocationInView:self];

        Planet *aPlanet = [solarSystem objectForKey: [NSString stringWithFormat:@"%u", touch] ];
        [aPlanet setPlanetFreeWithXSpeed: (touchLocation.x - previousTouchLocation.x) ySpeed:(touchLocation.y - previousTouchLocation.y)];
    }
}

-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    // Enumerates through all touch object
    for (UITouch *touch in touches) {
    // täällä pitäisi varautua siihen, jos touch-tapahtuma keskeytyy esim. puhelimen soidessa, mutta en ole viitsinyt :)
    }
}

Tuossa ekassa metodissa touchesBegan käy ilmi myös minkä takia päädyin Dictionaryyn planeettojen tallentamiseksi enkä settiin tai arrayhin. Kaunis ajatukseni oli tallentaa planeetan key-pariksi sen kosketuksen pointteriosoitteen, jolla kyseinen planeetta on luotu, jotta multitouch-tilanteessa planeetta osaisi pysytellä juuri oikean kosketuksen mukana. Itse asiassa tämä toimiikin, mutta samalla syntyi koodin toistaiseksi järein bugi. iOS nimittäin kierrättää kosketusten pointteriosoitteita aika tehokkaasti vanhojen kosketusten päätyttyä,  joten uudet planeetat alkavat ennen pitkää tallentua vanhojen päälle. Tämä pitäisi korjata käyttämällä dictionaryä vain kosketusten ajan, tai tallentaa koko kosketuksen osoitetieto vaikka tuonne planeetta-luokan muuttujaan ja tarkistaa touchesMoved-metodissa sieltä käsin. Tämä on vielä vähän hakusessa 🙂

SolarSystemViewController.h

Tämä on hyvin simppeli:

#import <UIKit/UIKit.h>
#import "SolarSystemView.h"
#import "Planet.h"

@class SolarSystemView;

@interface SolarSystemViewController : UIViewController {
    SolarSystemView *solarSystemView;
    NSTimer *solarSystemTimer;
}

@property (nonatomic, assign) SolarSystemView *solarSystemView;

-(void) updateSolarSystem:(NSTimer*)theTimer;

@end

Tuossa määriteltiin solarSystemView-instanssi, jotta näkymää päästään kontrolloimaan. Ja sitten tuo tärkeä NSTimer *solarSystemTimer -ajastinobjekti, jota tarvitaan säännöllisen tilannepäivitysrutiinin käynnistämiseen. ViewControllerin ainoa oma metodi onkin tuo updateSolarSystem, jota tuolta ajastimesta sitten säännöllisesti kutsutaan.

SolarSystemViewController.m

Täällä ei oikeastaan olekaan muuta kuin tuo updateSolarSystem-metodi sekä ViewController-templatessa valmiina tulleet metodit, joista tarvitsi uudelleenmääritellä vain tuo loadView

#import "SolarSystemViewController.h"
@implementation SolarSystemViewController
@synthesize solarSystemView;

-(void) updateSolarSystem: (NSTimer*)theTimer {
     NSMutableArray *keysForPlanetsToRemove = [NSMutableArray array];
     NSEnumerator *enumerator = [[solarSystemView solarSystem] keyEnumerator];
     id key;
     while ((key = [enumerator nextObject])) {

         if ([[[solarSystemView solarSystem] objectForKey:key] isTooFar])
         { [keysForPlanetsToRemove addObject:key];
         NSLog(@"%@", keysForPlanetsToRemove);}

         else {
             [[[solarSystemView solarSystem] objectForKey:key] growPlanetUnderConstruction];
             [[[solarSystemView solarSystem] objectForKey:key] calculateGravityWithSolarSystem:[solarSystemView solarSystem]];
             [[[solarSystemView solarSystem] objectForKey:key] movePlanet];    
         }
     }
    [[solarSystemView solarSystem] removeObjectsForKeys:keysForPlanetsToRemove];

    NSLog(@"Planeettoja: %u", [[solarSystemView solarSystem] count]);
    [solarSystemView setNeedsDisplay];
}

// Implement loadView to create a view hierarchy programmatically, without using a nib.
- (void)loadView {
    self.wantsFullScreenLayout = YES;
    SolarSystemView *view = [[SolarSystemView alloc] initWithFrame:[UIScreen mainScreen].applicationFrame];
    view.solarSystemViewController = self;
    self.view = view;
    self.solarSystemView = view;
    self.view.multipleTouchEnabled = YES;
    [view release];
    // Timerin ohjelmointi
    solarSystemTimer = [[NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(updateSolarSystem:) userInfo:nil repeats: YES] retain];

}

/*
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
    [super viewDidLoad];
}
*/

/*
// Override to allow orientations other than the default portrait orientation.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
    // Return YES for supported orientations.
    return (interfaceOrientation == UIInterfaceOrientationPortrait);
}
*/

- (void)didReceiveMemoryWarning {
    // Releases the view if it doesn't have a superview.
    [super didReceiveMemoryWarning];

    // Release any cached data, images, etc. that aren't in use.
}

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

- (void)dealloc {
    [super dealloc];
}

@end

Tämä rivi oli itselleni ehkä koko harjoituksen valaisevin:

solarSystemTimer = [[NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(updateSolarSystem:) userInfo:nil repeats: YES] retain];

Tuolla siis laitetaan käyntiin ajastin, joka lähettää kerran sadasosasekunnissa (jos pystyy) kohteelle self, eli tälle ViewControllerille itselleen metodikutsun updateSolarSystem. Tuossa siis vihdoin on se ”pääluuppi” tai ainakin niin lähellä sitä kuin Cocoa-maailmassa pääsee ilman todellisia hc-kikkoja. Ja tuolta loadView-metodin lopusta sille löytyi mielestäni ihan looginen paikkakin – ajastinta tarvitaan ekan kerran siinä vaiheessa kun eka näkymä on latautunut loppuun.

Jaha, kello on 4:22

Ehkä pitää muuten mainita, että tässä harjoituksessa jätin tarkoituksella kaikki Interface Builderilla wysiwyg-tyyliin laaditut näkymät kokonaan pois, ja kokeilin näkymän luomista ohjelmallisesti – osittain harjoittelun vuoksi ja osittain, koska IB-elementtien linkitys koodiin aiheuttaa vielä toistaiseksi enemmän hämmennystä kuin helpotusta. Kannattaa kuitenkin ehdottomasti käyttää Interface Builderia, jos tarvitset lähimainkaan Apple-tyylistä vakionavigointia – vakioelementit ovat aika pitkälle tuunattavissa, joten omaakin ilmettä niillä kyllä saa. Tähänkin projektiin varmaan käyttäisin IB-näkymiä esimerkiksi preferences-valikkoon ja planeettanäkymän päälle tuleviin pistelaskureihin tms.; pitää vain ensin opetella se linkitys paremmin.

Tuo NSLog on muuten aika välttämätön komento työstövaiheessa. Xcoden Run-valikosta löytyvä Console kannattaa pitää auki ja NSLogilla lähetellä työstövaihessa tarvittavaa tietoa sinne, jotta pääsee kärryille esim. ohjelmakoodin bugeista.

Korostan siis vielä, että näitä lähdekoodeja ei missään tapauksessa kannata pitää esimerkkitapauksina oikeaoppisesta Cocoa-ohjelmoinnista. Nämä ovat harjoitusvaiheen koodeja ja täynnä tehottomuuksia ja epäloogisuuksia. Otan mielelläni vastaan kaikenlaisia vinkkejä edistyneemmiltä iOS-koodaajilta!

Testatessa on kyllä ilmennyt harmillinen juttu, jota en osannut ihan ennakoida, nimittäin fysiikan lait. Planeetan saaminen toisen kiertoradalle onkin muuten aika pirun tarkkaa hommaa, eikä se oikein onnistu näppituntumalta, kuten olin mielessäni alunperin hahmotellut. Jos näkymän laittaisi zoomailemaan ulospäin tarvittaessa, jotta pystyisi hahmottamaan isomman palan avaruutta kerralla, voisi tämä helpottua, mutta varsinaisen aurinkokunnan kasaaminen edellyttäisi kyllä koodiin jonkinlaista tekoälyä, joka vaivihkaa avustaisi planeettoja toisiaan kiertäville radoille. Se on ehkä tässä vaiheessa turhan monimutkaista, joten tämä harjoitus taitaa nyt  jäädä odottelemaan taitojeni karttumista. Monenlaista muuta on kuitenkin kehitteillä, ei tämä tähän lopu!

Kurkista tästä applikaatiota toiminnassa.

Johan alkaa tapahtua!

Tässä vaiheessa opiskelua tulisi varmaan olla zeniläisen maltillinen ja pohdiskella muutama kuukausi luokkien, olioiden, instanssien, pointtereiden ja sensellaisten syvintä olemusta, ja käynnistää koneet vasta kun kaikki nämä käsitteet ovat omia taskuja tutummat. Eivät ole. Enkä pohdiskele. Haluan jotain näkymään ruutuun NYT!

Siispä vaihteeksi kahlaamaan iOS Dev Centeriä. Siellä on kasapäin hyviä oppaita, mutta samaa aihetta sivuavia tekstejä voi olla useampia ja jotkut niistä muistuttavat otsikkotasolla hämäävässä määrin toisiaan, joten kirjaan joitakin ylös tänne myös itselleni muistiinpanoiksi. Tämän aloitusoppaan iPhone-grafiikkaan koin itse hyödyllisimmäksi. Sen mukaan vaihtoehdot näyttivät lyhyesti sanoen olevan OpenGL ES tai Applen omat grafiikkaenginet Quartz (aka. Core Graphics), Core Animation, ja UIKit. OpenGL toi mieleen ”oikeat” 3d-pelit, joten hylkäsin sen toistaiseksi liian vaativana, vaikka oppaan mukaan sillä pystyy generoimaan myös 2d-grafiikkaa. Jos aikomuksena olisi tehdä vaativia rendauksia korkealla frameratella, tämä varmaan olisi ainoa oikea tapa, mutta kuten muistamme, tavoitehan on ihan jotain muuta – siis kääriä tukuittain rahaa köykäisillä pikkupeleillä.

UIKitin piirto-ominaisuudet taas näyttivät omiin tarpeisiini nähden äkkiseltään vähän turhankin korkean tason ohjelmoinnilta. Jos oikein ymmärsin, niin jokaikistä visuelementtiä (ympyrä, viiva, bittikarttakuva tms.) varten täytyy luoda ja hallita oma instanssinsa. Sain sellaisen mielikuvan, että tämä piirtotapa on laadittu yksinomaan toimimaan hyvin synkassa Cocoan valmiiden navigointiobjektien kanssa. Eli jos teet pääasiassa iOS:n omiin navigointielementteihin perustuvia softia, ja haluat vain hieman kustomoida jotain visua sieltä täältä, tämä tapa sopinee sinulle. Harkitsin tätä, koska näissä luokissa näytti olevan sisäänrakennettu törmäystunnistus, josta voisi olla hyötyä peleissä, mutta saavutettava framerate ei mahtaisi riittää edes pikkupelien tarpeisiin. Korjatkaa, jos olen väärässä!

Core Animation näytti ainakin tämän MoveMe-tutoriaalisoftan perusteella tukevan lähinnä ennalta määrättäviä siirtymiä, joten edes jollain asteella fysiikkaa mallintaviin pelisovelluksiin se lienee hieman kömpelö. Kaikennäköisiin näkymien välillä siirtymiin tämä framework vaikuttaisi olevan kuitenkin enemmän kuin paikoillaan, ja näitä samaisia metodeja iPhonen oma navigointi ymmärtääkseni käyttääkin.

Haluan siis piirtää melko simppeliä 2d-grafiikkaa, mutta hallita näkymää kuitenkin frame framelta. Vaihtoehdokseni näyttäisi siis jäävän Quartz 2d eli Core Graphics framework. Tämän erinomaisen oppaan perusteella Core Graphics vaikuttaisi itse asiassa aika pätevältä ja helppokäyttöiseltä grafiikkaengineltä – muunmuassa pdf-tiedostojen luonti ja Photoshopista tunnetut läpinäkyvyystilat (Multiply, Overlay jne.) näyttäisivät olevan Quartzissa sisäänrakennettuina ja muutamalla komentorivillä käytettävissä. Hyvä tietää vastaisen varalle. Juuri nyt kelpaisi silti vähempikin, vaikkapa nyt ympyrä!

Tein siis tähän tapaan (kun pari harhapolkua oikaistaan):

PS. Tuota esimerkkikoodien tulkintaa helpotti hurjasti, kun sain tietää että tuo usein toistuva omituinen syntaksi [munInstanssi munMetodi] on itse asiassa Objective-C:ssä ihan sama asia kuin itselleni paljon tutumpi munInstanssi.munMetodi -tyyli. En tiedä, josko joku sävyero on, mutta käytännössä näyttäis että noita tapoja voi käyttää miten vaan ristiin. En oikein tiedä, onko tuosta jotain suosituksia, kumpaa tapaa pitäisi käyttää.

PPS. Ai niin, ehkä jo huomasittekin, nuo Core Graphicsin komennothan olivat muuten ihan perinteisiä C-funktioita! Eli Apple koodaa itsekin muovailuvahalla silloin tällöin, vaikka ulospäin ollaan niin legoa, että.

Pelkoa ja inhoa kehitysympäristössä

Applella on valtava ja periaatteessa ihan ihmisystävälliseen sävyyn laadittu perehdytysaineisto iPhonen ohjelmistokehitykseen täällä. Lataamassani Developer-kansiossa näyttää olevan muunmuassa ohjelma nimeltä Interface Builder, jolla pystyy tekemään ihan oikean iPhone-softan näköisiä dummynäkymiä pelkästään raahailemalla navigointielementtejä paikasta toiseen. Xcode, Applen softaprojektien hallintaohjelma ja lähdekoodieditori näyttäisi ihan järkevältä. Dev Centerissä on ladattavissa hyvin dokumentoituja esimerkkiohjelmia melkein aiheesta kuin aiheesta. Jotenkin punainen lanka ei vaan löydy – kokonaiskuva Cocoa-ohjelmoinnista vilkkuu koko ajan jossakin näkökentän rajoilla mutta katoaa aina kun siihen yrittää kohdistaa katsetta.

Otetaan nyt vaikka ”Hello World” – klassinen ohjelmanpätkä, jonka olemassaolon ainoa tarkoitus on julistaa: ”Hei! Ohjelmointi on helppoa ja kivaa, ala sinäkin harrastamaan ohjelmointia!”.

Kuusnelosella naputeltiin:

10 PRINT "HELLO WORLD!"

C:llä jotain tyyliin:

int main() {printf ("Hello World");}

Ai niin, enpä muuten ole vielä kertaakaan maininnut Cocoan bestistä, siis varsinaista kieltä, jolla iPhone-ympäristössä pääasiassa koodataan. Se on Objective-C, monenlaisilla herkuilla kuorrutettu C++ -kieli.

iPhonen Hello World -applikaatio löytyy valmiina iOS Developer Libraryn Sample Code-osiosta, kätevää! Avataas… AAAARGH! Äkkiseltään laskettuna 22 eri tiedostoa, jotta ruutuun saa kirjoitettua tervehdyksen. Okei, on siinä planeetan kuva taustalla, tekstikenttä, johon saa kirjoittaa itse ja label, jossa valmis tervehdys näytetään. Alkujärkytyksen jälkeen helpottaa hieman – suurin osa tiedostoista näyttääkin olevan melko vakiomuotoisia kuvaketiedostoja, ohjelman speksiluetteloita ja muuta peruskauraa. Varsinainen toiminta näyttäisi olevan näissä ”.h” ja ”.m” -päätteisissä tiedostoissa… siis ”header” ja ”method”, kaiketi.

Kuva 3.0. Hello World -projektikansio avattuna Xcodella

Kuva 3.1. Kirjoittaja tutkimassa Hello World -sovelluksen lähdekoodia

Avaan tutuimmalta kuulostavan tiedoston eli ”main.m”: vesiperä. Siellä on vain muutama rivi koodia, joka näyttää liittyvän jotenkin muistinhallintaan, ja palautusarvona (kenelle? Steve Jobsille?) annetaan UIApplicationMain-niminen olio – eli ohjelma itse, tai ainakin joku haamu siitä? Hämärää, mutta Dev Centerin tutoriaalit vahvistavat: main-metodin muokkaaminen ei ole iPhone-hommissa se juttu. Jätän ymmärtämisen tuonnemmaksi ja yritän nyt vain etsiä sitä hemmetin ”Hello Worldia”.

Siispä seuraavien kandidaattien kimppuun: MyViewController.h ja .m. ”Näkymänhallintaa”, se kuulostaa oikealta. Avaan ensin header-tiedoston, sillä sieltähän saa nopeasti selkeän yleiskuvan, mistä on kysymys, eikös vaan?

Hmmm… skipataan alun lakitekstit. Aluksi importoidaan UIKit – sopii minulle, käyttöliittymä on ihan hyvä juttu. Sitten tehdään @interface-määrittely, jossa on tuttuja asioita (string), loogisen oloisia asioita (UITextField *textField) ja sitten ihan taikasienikamaa (<UITextFieldDelegate>, IBOutlet). Seuraavaksi määritellään kaikki uudestaan, mutta @property -tagien avustuksella, joita muuten täsmennetään vielä mm. termillä ”nonatomic” – epäatominen? Niinpä tietysti… puuh… onneksi lopussa ennen @end-määrittelyä on vielä yksi tutunnäköinen elementti:  – (void)updateString; – sehän on ihan selvästi funktio! Ei kun siis metodi, sori kaikille, olen uusi täällä.

MyViewController.m-tiedostossa pitäisi kaiken järjen mukaan vihdoin päästä moikkaamaan sitä maapalloa. Katsotaas… importoidaan headeri, käy järkeen, sitten tulee toteutusosio eli @implementation. Sitten jotakin @synthesize-tageja joilla viitataan headerista tuttuihin muuttujiin (vai instanssejako nuo nyt sitten olivat). Ei mitään käsitystä mitä tuo @property -> @synthesize tekee, mutta jonkunsortin parivaljakko se kuitenkin lienee.

Sitten se metodi. Mutta täällähän lukee ekana ”- (void) viewDidLoad {”… eihän tällaista määritelty headerissa. No, joka tapauksessa tätä metodia on nimestä päätellen tarkoitus käyttää heti näkymän ladattua – jonkinlainen alustusmetodi siis. Pitääpä katsoa koodia eteenpäin, niin nähdään missä tätä metodia kutsutaan, eikös vaan? (Te oikeat Cocoa-kehittäjät siellä, pidetään se naama ihan vaan peruslukemilla).

Sitten on määritelty se updateString-metodi, helppo nakki: stringiin tallennetaan tekstikentän sisältö ja labeliin sitten stringin sisältö. Sitten joku virtuaalinäppikseen liittyvä säätö ja sitten jotain hyvin mielenkiintoista:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{...

Tämä selvästi liittyy kosketusnäytön tarkkailuun. Metodista löytyi kuitenkin vain kolme riviä koodia, jotka eivät tunnu liittyvän sen enempää touches- kuin event-olioonkaan. Tällä kuulemma piilotetaan virtuaalinäppis, kun käyttäjä koskettaa tekstikentän ulkopuolista näyttöpintaa: [textField resignFirstResponder]; Selvä, uskotaan. Sitten perutaan tekstikentän sisältö näin:  textField.text = self.string; Ihan järkeenkäypää. Sitten on vielä kolmas rivi, jota ei selitetä mitenkään eikä myöskään avaudu millään tasolla. [super touchesBegan:touches withEvent:event]; Esimiehelle tässä varmaan soitellaan, mutta millä asialla, miksi, ja kuka se on – beats me.

No niin, kohta varmaan päästään varsinaiseen pääkoodiin, siis siihen main looppiin, josta kaikkia näitä hienoja metodeja kutsutaan yksi toisensa perään ja sitten aloitetaan alusta. Niinhän? Niinhän?

- (void)dealloc {
  // muistin hallintaa ja instanssien nollaamista
}

… ja sitten:

@end

Muutaman hyödyttömän skrollausnapin pyöräytyksen jälkeen alistun tosiasioiden edessä. Siinä se oli, enempää ei tule. Ei mitään alkupistettä, ainakaan sellaista jota olisin pystynyt hahmottamaan. Ei mitään main loopia, joka pitäisi homman kasassa. Eihän näistä metodeista suurinta osaa edes kutsuttu missään! Eikä missään lukenut, että ”Hello, World!” Jumalauta, tämä alkaa jo olla vähän liikaa…

Tunnelmat alkavat olla sen verran ankeat, että skipataan suosiolla muutama viikko vimmaista dokumenttien plaraamista ja tutoriaalien copy/paste-tyylillä läpikäyntiä ilman ymmärryksen häivääkään, ja siirrytään suoraan nykyhetkeen. Tässä välissä on nimittäin ehtinyt tapahtua pari oivallusta, jotka ovat vihdoin johtaneet jopa jonkinlaiseen näppituntumaan Cocoa-ympäristöstä.

1. Minä mitään main loopia tartte!

iPhone on puhelin. Kyllä, näin on. Se taas tarkoittaa sitä, että sen täytyy pystyä vastaanottamaan puheluita ja viestejä ja hoitamaan monenlaisia muita askareita myös silloin kun MINUN hieno applikaationi pyörii. Ja se taas tarkoittaa, että se turhaan koodista etsimäni Main Event Loop (toki se on olemassa, ei tässä sentään mitään barbaareja olla) on tietenkin iOS:n hallussa. Tämä taas vaatii takatukka-aikakauden koodarilta ison myönnytyksen: minulla ei ole mitään keinoa tai syytäkään päästä muokkaamaan pääloopia itse – en sitä paitsi halua käräjille siitä, että tuleva hittiapplikaationi estää hätänumeroon soittamisen ja johdattaa koulubussillisen luokkaretkeläisiä surman suuhun.

Mistä siis korvaushoito tähän main loopin himoon? Eventeistä, siis tapahtumista. Kun käyttäjä sipaisee näyttöä, iOS rekisteröi tapahtuman. Kun puhelinta heilutellaan, kiihtyvyyssensori lähettää tästä tiedon ja iOS poimii taas uuden tapahtuman. Kun kuulokkeiden kaukosäätimestä lisätään volaa – tapahtuma. Jos muisti alkaa täyttyä – tapahtuma. Ja itselleni vaikeimmin oivallettava juttu oli tämä: sinun hommasi ei ole tehdä metodia joka tarkkailisi näitä tapahtumia. Sen sijaan iOS kutsuu, tiettyä komentoketjua seuraten, näitä varta vasten tapahtumiin reagointia varten laadittuja, vakiomuotoisesti nimettyjä metodeja. Nämä kutsut päätyvät automaattisesti myös sinun ohjelmallesi, ja sinun tehtäväsi onkin – oman ohjelmasi osalta – kirjoittaa näihin tapahtumiin reagointimetodit uusiksi siltä osin kun ohjelmasi toiminta sitä edellyttää. Siksi esimerkiksi tuota touchesBegan-metodia ei ollut määritelty tuolla MyViewController-headerissa – se on jo määritelty paljon syvemmällä Cocoan ytimessä, UIKit-frameworkin UIResponder-luokassa. Tämä tapa uudelleenmääritellä tiettyjä vakiotyyppisiä metodeja on ymmärtääkseni yleisemmälläkin tasolla erittäin oleellinen osa Cocoa-filosofiaa.

Mutta yksi perustarve on vielä tyydyttämättä. Kun pitäisi päästä tekemään jotain jokaisella main loopin kierroksella, vaikkapa laskemaan avaruusaluksen uusi sijainti tai laavalampun uudet värit. Tämä ei ymmärtääkseni ole mahdollista, ainakaan jailbreikkaamatta iPhoneasi. Eventit rientävät kuitenkin apuun tässäkin asiassa. On nimittäin vielä yksi tärkeä tapahtumatyyppi, jota ei ole käsitelty: ajastintapahtumat. Kaikessa yksinkertaisuudessaan, tehdään jotain aina kun iPhonen sisäinen kello on edennyt vaikka 5 millisekuntia. Tähänkin hommaan on Cocoassa omat valmiiksi pedatut metodinsa, ja niitä kannattaa käyttää.

2. Ihan sama mistä se alkaa!

Se vanhan koulun koodaajalle hirmuisen tärkeä ajatus, että mikä tässä ohjelmassa on se ihka ensimmäinen komento, ei ole Cocoa-ohjelmoinnissa kovin oleellinen. Se main.m se kai lopulta on, sitä iOS ymmärtääkseni kutsuu kun applikaation kuvaketta tupsauttaa, ja se tosiaan palauttaa iOSille applikaatioinstanssin, johon koko softa on kääritty. Mutta sillä tiedolla ei ole kummoistakaan merkitystä toimivan iPhone-ohjelman rakentamisessa. Sitä tärkeämpään asemaan nousee tuo aiemmin mainittu metodien uudelleenmäärittely -toimintatapa. Sillä kun nimittäin pääsee käsiksi kaikennäköisiin muihinkin tärkeisiin hetkiin applikaation elämässä, kuten siihen kun ohjelman eka näkymä on latautunut loppuun (jolloin voisi esimerkiksi alustaa muuttujia).

3. Kaikkea ei tarvitse tehdä itse

iPhone-kehittäjä: et ole Rambo! Sinun ei tarvitse lahdata kaikkia pahiksia yksin. Olet pikemminkin Rusty Robottipoika, jonka tärkein tehtävä on päästä Ison Hepun (iOS) kyytiin – kun pidät tiukasti kiinni etkä ala ryttyilemään isommallesi, olet aika lailla turvassa.

Eli käytännöllisemmin: kun alat suunnittelemaan jotain toiminnallisuutta uuteen softaasi, älä turhaan tuhlaa aikaasi miettimällä, miten tämän asian ratkaisisit – katso sen sijaan ensi töiksesi iOS  Frameworks -listauksesta missä määrin ja miten Cocoa on jo ratkaissut asian, ja miten sitä ratkaisua tulisi jalostaa, jotta käsillä oleva ongelma ratkeaisi.

Valtavasti maastoa on vielä kartoittamatta, mutta nyt ainakin kartta on jo oikeinpäin kädessä. Seuraavaan kertaan!

Kuva 3.3. iPhone-sovelluskehittäjä ja iOS yhteiskuvassa

Kuva 3.2. Selvästikään ei iPhone-kehittäjä