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ä.

Sample Code ja Base SDK

Heipä taas! Harjoittelu UIKitin parissa jatkuu, ja kylläpä Apple onkin pedannut pehmoisen pedin standardinmukaisten navigointipohjaisten applikaatioiden tekijälle: kaikki Applen omista appseista tutut käyttistoiminnot – animaatiot, feidaukset ynnämuut mukaanlukien – ovat automaattisesti käytettävissä ilman että tarvitsee kirjoitella riviäkään esimerkiksi animointikoodia. Kirjoittelen aiheesta joskus pidemmin, mutta nyt ajattelin ottaa esiin yhden pulman, jonka ratkaiseminen helpotti omaa opiskeluani taas kummasti.

Kuten mainittu, Applella on Sample Code -arkisto, josta löytyy esimerkkitoteutuksia melkeinpä mistä vaan iOS-applikaation perustoiminnosta. Lähdekoodit eivät kuitenkaan näytä päivittyvän kovin usein, mistä seuraa että lataamani iOS SDK 4.2 antaa lähes jokaisen kohdalla virheilmoituksen ”Base SDK missing”, eikä applikaatiota pääse testaamaan.

Ratkaisu on kuitenkin useimmiten hyvin yksinkertainen. Tässä kuvasarja toimenpiteistä, joilla vanhemmatkin Sample Codet pitäisi saada kääntymään:

Pikainen päivitys…

Tässä on nyt tovi opeteltu suhteellisen matalan tason frameworkeja, joihin framerate-kriittisiä pelejä laadittaessa täytyykin useimmiten turvautua. Peliprojektien välissä ajattelin kuitenkin opetella hieman myös UIKitin hyödyntämistä, joka on ehkä tyypillisintä iPhone-ohjelmointia. Pieni hyötyapplikaatiokin on kehitteillä.

Näistä lisää myöhemmin – tämän postauksen varsinainen tarkoitus on vain esitellä loistava linkkilöytö, joka tuli dokumentaatioita kahlaillessa vastaan.

Itunes U:ssa on nimittäin ladattavissa Stanfordin talven 2010 iPhone-ohjelmoinnin peruskurssin luentovideot kokonaisuudessaan, täysin ilmaiseksi!

Linkki esittelysivuille tässä.

Ekan luennon ensimmäiset parikymmentä minuuttia on vain kurssin käytännön asioiden läpikäymistä, ne voi vaikka skipata, mutta siitä lähtien kyllä täyttä asiaa. Olisinpa törmännyt tähän ekana treenipäivänäni, ei ehkä olisi tarvinnut opetella ihan niin paljon kantapään kautta!

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.

Baby steps, baby steps

Kuva 6.0. Kuten yleisesti tiedetään, aikamatkustaja ei yleensä saa mukaansa irtainta omaisuutta, joten tämä referenssiteos ei selvinnyt vuoteen 2011 asti. Kirjoittaja joutuu siis tyytymään Googleen.

Viimeksi raaputeltiin vähän iPhone-arkkitehtuurin pintaa ja saatiinkin siitä ainakin jonkinlainen alkeellinen kuva. On isot Cocoan frameworkit joihin tukeutua, on oliot ja Model-View-Controller -filosofiat. Jottei homma menisi ihan teoretisoinniksi, nyt pitäisi taas päästä tekemään jotain käytännössä.

Käytin autoilupeliä esimerkkitapauksena jossain aiemmassa kirjoituksessani, mutta kaahailugenre on harjoitteluvaiheessa  aivan liian vaativa ja kaiken lisäksi melkoisen loppuunkaluttu pelimarkkinoilla. Jotakin simppelimpää siis.

Ympyröitä on jo saatu ruutuun, mitäs niistä saisi aikaan? No vaikka – planeettoja. Painovoima on aina muodissa ja sen laskemiseen löytyy kaavat googlaamalla (ensimmäinen ajatukseni oli kaivaa MAOL:n taulukkokirja esiin, kunnes muistin taas että nyt ollaankin 2000-luvulla). Näyttöä koskettamalla voisi luoda planeettoja, jotka sitten vaikuttaisivat toistensa liikeratoihin painovoimalain määräämällä tavalla. Suhteellisen helppo toteutus ja lopputuloksena mahdollisesti jopa ihan viihdyttävä lelusovellus. Hei, et sitten ryövää tätä ideaa ja tienaa sillä miljoonia, senkin lurjus, tai David Fincher tekee sinusta leffan, jonka perusteella kaikki alkavat pitää sinua kusipäänä.

Kuva 6.1. Kirjoittajan ensimmäisen applikaation ansaintalogiikka graafisena esityksenä.

Eli töihin sitten vaan. Tarvitaan ainakin luokka planeetan ominaisuuksien ja toimintojen kokoamiseen. Planeettojen sijainnit ja keskinäiset vetovoimat pitää päivittää säännöllisesti, siihen tarvitaan Timer Eventiä. Uusia planeettoja pitää päästä luomaan näyttöä koskettamalla, sitä varten täytyy ottaa haltuun Touch Eventeihin reagointimetodit. Ja pitäähän ne planeetat myös piirtää ruutuun; aiemman harjoituksen ympyränpiirtorutiinit saavat kelvata siihen toistaiseksi, hieman tuunattuina toki.

Tietysti oikeassa pelissä on myös jonkinlainen intro, highscore-näkymät, preferences-osio ynnämuut, mutta sille kaikelle on aika sitten myöhemmin – ensin pitää saada perustoiminnot kasaan. Motivoin itseni lupaamalla, että jos saan tämän applikaation julkaisukuntoon, voin sijoittaa 100 taalaa Applen Developer Programin vuosimaksuun, joka tarvitaan jotta omia softia voi testata puhelimessa – tai, sitten joskus, julkaista App Storessa!

Opintojen meditatiivinen osuus

Viimeksi saatiin piirrettyä iPhonen ruutuun, kosketusnäytön tarkkailusta nähtiin jonkinlainen esimerkki jo aiemmin ja samalla päästiin jonkinlaiselle hajulle, miten ajastintapahtumilla (timer event) voi korvata perinteisen main loop -ajattelumallin jatkuvasti toistuvien tilannepäivitysten hoitelussa. Toisin sanoen yksinkertaisen peliapplikaation perusainekset ovat jotakuinkin kasassa.

Jottei kuitenkaan kiivettäisi perse edellä kaakaopensaaseen, pitäisi päästä edes hieman jyvälle iPhone-softan arkkitehtuurista. Järkikin sanoo (Quartz-oppaan kompatessa), ettei softaa voi vaan ruveta kasaamaan drawRect: metodiin – sen tehtävä on päivittää näytön sisältö mahdollisimman nopeasti, eikä nysväillä tilannepäivitysten tai muun yleisen säädön parissa. Olisiko itse asiassa optimaalisinta (tai edes mahdollista) naputella jopa nuo piirtokomennot graphic contextiin ihan jossain muualla ja drawRect: -metodissa vain sitten pyöräyttää läpi valmiiksi laadittu piirtosarja? En tosiaan tiedä, joku kokeneempi voisi vihjata tässä oikeaan suuntaan. No joka tapauksessa, jonkinlainen runko ja jäsentely softalle tarvitaan, vaikka se olisikin vain pikkuruinen viihdeapplikaatio – muuten virtuaaliroskis alkaa ennen pitkää rapsahtelemaan lukukelvottomiksi käyneistä lähdekoodinpätkistä.

Olio-ohjelmoinnista on kirjoitettu tuhat kirjaa ja miljoona nettiopasta, joihin ei minun asiantuntemuksellani ole mitään lisättävää, joten en ala sitä tässä kovin syvällisesti ruotimaan. Koska se kuitenkin on yksi Cocoa-kehityksen kulmakivi, hahmoteltakoon tässä sen periaatteet kuten olen ne ymmärtänyt.

Olio-ohjelmointi ottaa tietyssä mielessä mallia siitä, miten asiat toimivat todellisessa maailmassa. Esimerkiksi autolla on tiettyjä tehtäviä (eteneminen, peruuttaminen), ominaisuuksia (väri, moottorin teho) sekä käyttöliittymä (kaasupoljin, ratti), jotka yhdessä tekevät siitä auton. Esimerkiksi käsite ”Auto” olisi olio-ohjelmoinnissa luokka eli class, ja yksittäinen autoyksilö, vaikka naapurin savuttava Samara olisi luokan ”Auto” instanssi. Siis:

Auto *naapurinSamara;

Auton kuljettaja taas olisi:

Ihminen *naapurinTaisto;

Jotta nämä oliot voisivat tehdä yhdessä jotain hyödylllistä, vaikka kuljettaa Taisto kansalaisopiston huovutuskurssille, ei Taiston tarvitse eikä kuulu tietää autonsa jokaikisen osan toimintaa läpikotaisin (saatika että auton tarvitsisi tietää Taiston aineenvaihdunnan yksityiskohtia). Riittää kun Taisto tietää metodit, joilla autoa ohjataan. Samanniminen ominaisuus kahdella oliolla ei myöskään aiheuta ongelmia, kuten saattaisi käydä proseduraalisessa ohjelmoinnissa globaaleja muuttujia käytettäessä – Taiston ei siis tarvitse pelätä, että hänen painaessaan kaasupoljinta, jolloin auton ominaisuus ”kaasu” vaikka tuplaantuisi, Taiston suoliston epäonnisesti nimetty ominaisuus ”kaasu” kasvaisi samaan tahtiin aiheuttaen vaivautuneisuutta huovutuskurssilla.

Olio on siis hyvin itsenäinen otus, jonka täysipainoinen hyödyntäminen onnistuu vaikket tietäisi siitä mitään muuta kuin sen käyttöliittymässä (@interface) määritellyt julkiset metodit. Tämähän on kieltämättä erittäin kätevää, kun ohjelmointiympäristö on laajuudeltaan nykyaikaisen käyttöjärjestelmän luokkaa. Se tarkoittaa kuitenkin myös sitä, että uuden olion käyttöliittymä täytyy suunnitella hyvin ja toteuttaa erittäin pedantisti, jotta homma toimisi.

Koska olion sisäisiin ominaisuuksiin ei tosiaan muilla ole mitään asiaa, ja toisaalta niitä useimmiten kuitenkin pitää päästä muokkaamaan jotta mitään tapahtuisi, tarvitaan jokaista julkista ominaisuutta varten ”getter”- ja ”setter”-metodit. Aika puuduttavaa naputtelua, eikös? Siinä tuleekin apuun tuo aiemmin ihmettelemäni @property -> @synthesize -parivaljakko. Se luo nämä getter- ja setter-metodit automaattisesti, ja nimeää ne vakiokäytännön mukaisesti, jonka jälkeen niitä voi käyttää aivan normaalisti kuten olisit nakutellut ne koodiin itse. Myös niiden suluissa olevien lisämääritysten merkitykset selvisivät täältä (paitsi se epäatominen, jonka hatarasti ymmärsin liittyvän jotenkin moniajoon ja päällekkäisiin prosesseihin, ”threads”).

No, uuden object classin luominen (vaikkapa sen ”Auto”-classin jotain kaahailupeliä varten) ei ole mitenkään monimutkaista hommaa, kunhan seurailee oppaiden määrittelemää syntaksia. Hämärä alue alkaa omalla kohdallani edelleen siitä, missä vaikkapa nyt tuon Auto-luokan instanssit olisi järkevä luoda, ja esimerkiksi missä tarkkaan ottaen määriteltäisiin se timer action -toiminto, jolla pelin autojen sijainnit ym. ominaisuudet päivitettäisiin.

Yhdenlaisen, joskin vielä melkoisen abstraktin vastauksen tarjoaa oppaissa usein toistuva termi ”MVC” eli ”Model – View – Controller design pattern”. Äärimmilleen yksinkertaistettuna kyse on siitä, että ”Model” – ohjelman perusdata ja sen käsittelyyn tarvittavat metodit (johon tuo Auto-luokkakin kuuluisi), ”View” – eli autojen ja maiseman piirto näytölle ja käyttäjän toimien tarkkailu ja ”Controller”, eli kahden edellämainitun välillä toimiva ”komentokeskus” pidetään visusti erillään. Tämä on selvästi eräänlainen jatke olio-logiikan ”itsenäisyysperiaatteesta”, ja helpottanee erityisesti monimutkaisten ohjelmistojen kurinpitoa ja päivittämistä. Pienten ohjelmien laatimiseen periaate tuntuu kuitenkin edelleen lisäävän ylimääräistä ”byrokratiaa”. Opettelemassa kun tässä kuitenkin ollaan, koitin motivoida itseäni keksimällä periaatteesta jotain käytännön etuja, ja kyllähän niitä hetken mietinnän jälkeen löytyikin.

Ensinnäkin, jos pitäisin varsinaisen näkymän piirron erillään pelin perustoiminnallisuuksista, eli en esimerkiksi kutsuisi näytön piirtotoimintoja suoraan tuolta Auto-luokasta, vaan tämä ”Controller” välittäisi Auto-instanssien piirtämiseen tarvittavat tiedot näkymälle, niin – silloinhan voisin kokeilla näkymässä mitä vain rendaustyylejä koskematta Auto-luokkaan lainkaan. Itse asiassa pystyisin varmaan laatimaan monta rinnakkaista visutyyliä, joita voisi vaihtaa vaikka kesken pelin preferenssejä säätämällä, jolloin vaihdettaisiin vain kutsuttava näkymäinstanssi toiseen. Ihan näppärää näin pienkehittäjänkin näkökulmasta!

Viimeksi piirrettiin ruutuun luomalla UIView:lle oma alaluokka TreeniView ja määrittelemällä sen drawRect -metodi uudestaan. Olisikohan tuon UIViewController -luokan instanssi sitten hyvä paikka alkaa kasaamaan pelin ”komentokeskusta”, jossa pelidatan päivitysmetodeja ja näytön piirto- ja tarkkailumetodeja kutsuttaisiin? Pitääpä perehtyä tarkemmin… siihen asti, öitä!

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ä.