Making iOS UIKit Simulator for MacOS X

24
Core UIKit drop in replacement for UIKit that runs on OS X …a short story Daniele Margutti Software Architect [email protected] danielemargutti.com

description

Presented on Oct 2013 at Pragma Conference 2013 in Milan, this is an introduction about how I and my team at CreoLabs made a drop-in simulator of iOS's UIKit framework from the scratch, which is running on OS X without using native Apple's simulator. This is part of a big project called CreoApp we are developing at CreoLabs (www.creolabs.com).

Transcript of Making iOS UIKit Simulator for MacOS X

Page 1: Making iOS UIKit Simulator for MacOS X

Core UIKitdrop in replacement for UIKit that runs on OS X

…a short story

Daniele Margutti Software Architect [email protected] danielemargutti.com

Page 2: Making iOS UIKit Simulator for MacOS X

COS’È Una implementazione completa e fedele di UIKit in grado di funzionare su OS X.

PERCHÈ •Apple utilizza già UIKit per il simulatore iOS, ma si tratta di una versione privata.

•Avere il controllo totale dell’ambiente di simulazione ci permette di lavorare al nostro IDE senza vincoli derivanti dall’uso di tecnologie esterne.

A CHE PUNTO SIAMO

Abbiamo una architettura di base completa che ci permette di simulare senza modifiche app iOS

QUALCHE NUMERO...

• 8 mesi di sviluppo • oltre 350 classi • più di 60000 linee di codice • vicini al commit #1000• ...e un infinito numero di imprecazioni

Core UIKit

Page 3: Making iOS UIKit Simulator for MacOS X

iOS

Cocoa Touch UIKit

Media

Core Graphics

OpenGL ES

Core Animation

Core ServicesCore Data

Foundation

Core OS

UIKit su iOS

già disponibile su Mac

già disponibile su Mac

non rilevante

INFRASTRUTTURA PER APP TOUCH BASED

• Application Run Loop • View Controller per gestire i contenuti e la

navigazione • Controlli UI di base (button,label,table etc.) • Gestione e propagazione degli eventi touch/motion • Supporto al multitasking • Supporto al disegno (Core Graphics) • Supporto alle animazioni (Core Animation) • Gestione dell’hardware (fotocamera/sensori etc.) • Local Push Notification

• Remote Push Notification • Stampa • iCloud • CoreData

Page 4: Making iOS UIKit Simulator for MacOS X

Creare un nuovo simulatore

•UIView: CALayer come elemento base•Architettura del simulatore (con supporto per le istanze multiple)

•Propagazione degli eventi MacOS al simulatore

•Adapter: utilizzare oggetti nativi Cocoa all’interno del simulatore

Page 5: Making iOS UIKit Simulator for MacOS X

UIView: la scelta

UIView

NSView

CALayer (da OS X 10.5)

CALayer

Mac OS X

iOS

•Ottenere la stessa architettura di iOS •Avere (quasi) gratis la potenza di Core Animation •Astrarre da OS X l’ambiente di simulazione

CALayer come unità base

Page 6: Making iOS UIKit Simulator for MacOS X

UIView: implementazione

- (id) initWithFrame:(CGRect) { if (self = [super init]) {

// other init stuff viewLayer = [[isa layerClass] alloc] init]; viewLayer.delegate = self; viewLayer.needsDisplayOnBoundsChange = YES; viewLayer.layoutManager = [UILayoutManager mainLayoutManager];

// ... again, lots of fancy code self.frame = aFrame;

}

Ogni UIView si porta dietro un CALayer che conterrà la sua

rappresentazione grafica

Permette di ricevere l’evento layoutSublayersOfLayer

che useremo per inviare il layoutSubviews

Non farà altro che impostare il frame di viewLayer

- (void) layoutSublayersOfLayer:(CALayer *) aLayer { [((UIView*)aLayer.delegate) layoutSubviews]

}

Quando il layout manager riceve la richiesta di un update a fronte di modifiche del CALayer

abbiamo la possibilità per il suo UIView di eseguire un update del contenuto.

Init di UILayoutManager associato

Init di un UIView

Page 7: Making iOS UIKit Simulator for MacOS X

UIView: implementazione

UIWindow

UIView (di UIViewController)

UIButton

UITextField

CALayer (di UIWindow)

CALayer (dello UIView di UIViewController)

CALayer (di UIButton)

CALayer (di UITextField)

- (void) addSubview:(UIView *) aSubview { ... [aSubview willlMoveFromWindow: aSubview.window toWindow: self.window]; [aSubview willMoveToSuperView: self]; [aSubview willChangeValueForKey:@”superview”]; [aSubview removeFromSuperview]; // rimuoviamo il subview dal suo precedente parent subview->viewSuperview = self; // teniamo un riferimento weak al nuovo parent view [subviewsArray addObject:self]; // aggiungiamo lo UIView alla nostra gerarchia [viewLayer addSublayer: self]; // aggiungiamo il suo layer alla gerarchia dei CALayer ...

Gestire la gerarchia di UIView

Page 8: Making iOS UIKit Simulator for MacOS X

UIView

UIView: implementazioneDisegnare in uno UIView

CALayer associato

!-display:

richiesta dal sistema

(metodi delegate)

implementa -displayLayer:?

sichiamalo

no

crea buffer, context

-drawInContext:

se implementato chiama -drawLayer:inContext:?

chiama -drawRect:

passali a

La creazione del buffer e del context è dispendiosa. Per evitare che venga fatta anche senza che ci sia reale necessità implementiamo -displayLayer: ma mentiamo al CALayer (usando l’override di -respondsToSelector:) e consentendone la creazione solo se lo UIView implementa realmente -drawRect:

Page 9: Making iOS UIKit Simulator for MacOS X

Architettura del simulatore

UIKitRuntimePool

UIKitRuntime UIKitRuntime UIKitRuntime

UIKitRuntimePool si occupa di allocare e mantenere le varie istanze di UIKit avviate. !Ogni oggetto mantiene un riferimento al proprio runtime.

il run di una istanza richiede il device che si vuole simulare e un run point

1. intercetta gli eventi iOS (mouse, tastiera, trackpad)

2. traduce gli eventi Mac in eventi iOS 3. Li invia lungo il responder chain ome

accade su iOS

UIKitLayer (Flipped NSView)

UIScreen (CALayer)

UIWindow (CALayer)

UIView (CALayer) (di UIViewController)

UIButton

0,0

UIStatusBar (CALayer)

-runWithDevice:delegate:options:

UITextField

UITableView

eventi cocoamouse/keyboard/touchpad

Page 10: Making iOS UIKit Simulator for MacOS X

Architettura del simulatoreIntercettare e tradurre gli eventi

UIKitLayer (NSView)

mouseMovedmouseDragged mouseEnteredmouseDown mouseUpscrollWheel

Viene identificata la posizione dell’evento rispetto allo screen principale e individuato l’eventuale

UIView sotto il touch. Poi a seconda del tipo di evento:

EVENTO COCOA EQUIVALENTE iOS EVENTI SECONDARI

NSLeftMouseDown touchesBegan

• [ALT KEY] viene simulato pinch/rotation: il punto del touch diventa il punto intermedio tra due dita

• [HOLD] viene simulato l’inizio di una gesture (gestureTouchesBegin) per le gesture registrate al view corrente.

NSLeftMouseDragged touchesMovedSe è iniziata una gesture viene inviato l’evento gestureTouchesMoved alle gesture registrate per il view corrente.

NSLeftMouseUp touchesEndedSe è in corso una gesture viene inviato l’evento gestureTouchesEnded alle gesture registrate per il view corrente.

EVENTI PRINCIPALI

Page 11: Making iOS UIKit Simulator for MacOS X

Architettura del simulatoreHit test per UIView

View A

View B

View C

View D

View E

hitTest funziona in maniera ricorsiva scendendo nella gerarchia dei view “toccati” fino a quando un subview risponde all’evento (o si torna al root view). !!Consideriamo il touch nel “View E”. La catena sarà:

• Il touch è dentro A quindi controlliamo B e C

• Il touch non è dentro B, saltiamo

• Il touch è dentro C, verifichiamo D ed E

• Il touch non è dentro D ma è dentro E, ritorniamo E

Page 12: Making iOS UIKit Simulator for MacOS X

Architettura del simulatoreHit test per UIView

View A

View B

View C

View D

View E

- (UIView *) hitTest:(CGPoint) point withEvent:(UIEvent *) event { if (hidden || !userInteractionEnabled || alpha < 0.01f) return nil; // se il view è disabilitato o nascosto lo ignoriamo ! BOOL isInside = CGRectContainsPoint(self.frame, point); for (UIView *subview in [subviewsArray reverseObjectEnumerator]) { // convertiamo le coordinate al parent e richiamiamo hitTest CGPoint localPoint = [self convertPoint:point fromView:self.superview]; if (UIView* hitView = [subview hitTest:localPoint withEvent:event]) return hitView; // se cattura l’evento proseguiamo } // avremo il root view di partenza oppure il subview più interno valido return (isInside ? self : nil); } !

Page 13: Making iOS UIKit Simulator for MacOS X

Architettura del simulatoreAmbiente Multi-UIKit

• UIApplication • UIScreen • UIDevice

E’ possibile supportare più ambienti di UIKit simultaneamente semplicemente impostando una macro. Un nuovo runtime si occupa di creare:

2. Devono essere prodotte delle macro in grado di redirezionare le chiamate dei singleton.

#ifdef UIKitMultipleInstances #define UIApplication ((UIApplication*)self.runtime.application;#else #define UIApplication [UIApplication sharedApplication]

[[oggetto allocWithRuntime:runtime] init…

1.Tutti gli oggetti* creati all’interno dell’app devono essere allocati con un riferimento al loro UIKitRuntime. A cascata i figli ereditano il runtime del padre (ex. UIWindow lo eredita da UIScreen)

(*) tutti quegli oggetti che usano riferimenti al singleton o all’ambiente grafico.

UIKitRuntimePool

UIKitRuntime

UIScreen UIK

UIViewController

UIWindow UIK

UIK

UIView UIK

UIButton UIK

UIApplication UIK

[UIApplication sharedApplication] ?

Page 14: Making iOS UIKit Simulator for MacOS X

Adapter AppKit•Consente di utilizzare oggetti NS* all’interno del simulatore UIKit •Necessario per evitare di implementare i controlli più complessi

• UIWebView contiene un adapter per il WebView di OS X • UITextField e UITextView sono implementati come adapter di NSTextField/NSTextView

UIKitLayer (Flipped NSView)

aggiunto subview di UIKitLayer (dietro tutti gli oggetti)

Struttura dell’adapter

UIAppKitAdapter(UIView)

CALayer Adapter

NSClipView

NS* Native Object

E’ uno UIView contenente l’oggetto Cocoa incapsulato dentro un NSClipView

L’adapter acquisisce il layer di NSClipView aggiungendolo alla propria gerarchia

La NSClipView viene posizionata allo stesso frame dell’adapter ma all’interno di UIKitLayer principale (una NSView)

Page 15: Making iOS UIKit Simulator for MacOS X

Adapter AppKitFunzionamento dell’adapter

Evento sopral’adapter

(es. mouseDown)

UIKitLayer

NSClipView

- (NSView *) hitTest:(NSPoint) aPoint { NSView *hitNSView = [super hitTest:aPoint]; if (!hitNSView) return nil; !CGPoint pointInScreen = ... // convertiamo il point nelle coordinate dello UIScreen // verifichiamo se il touch è avvenuto su un adapter, se si procediamo col focus UIView *touchedUIView = [UIMainScreen touchedViewAt: pointInScreen] if ([touchedView isKindOfClass:[UIAppKitAdapter class]])

return hitNSView; else

return nil; }

UIKitLayer

UIAppKitAdapter- (void) viewWillDraw {

[adapter updateLayer]; }

Page 16: Making iOS UIKit Simulator for MacOS X

Qualche controllo UI rilevante•UIScrollView•UITableView•UINavigationController

Page 17: Making iOS UIKit Simulator for MacOS X

UIScrollView 1/3

• E’ molto simile allo UIView......In verità la gran parte dei metodi utilizza proprietà già esposte dallo UIView!

• Lo scrolling si basa sull’architettura a layer (“rasterizzazione”+”composizione”) con cui iOS/OSX visualizzano elementi sullo schermo

... proprio come accade con i layer che si usano nei programmi di grafica!

Page 18: Making iOS UIKit Simulator for MacOS X

UIScrollView 2/3

u n d i s e g n o t ro p p o g r a n d e

bounds per il raster (0,0,100,80)

80

100

10,20 110,20Rasterization

• Si ha un’area di disegno con bounds origin tipicamente 0,0.

• Di ogni view viene prodotta la sua rappresentazione (il view non conosce il contesto di destinazione)

• Quello che eccede l’area di disegno viene escluso dal raster finale (...non sempre)

• Il risultato è la rappresentazione del view

X=

Composition• I view vengono montati per livelli da quello più in

basso nella gerarchia a quello difronte (frontmost) • La proprietà frame.origin di ogni view indica la sua

posizione nel superview di destinazione.un disegno troppo grframe.origin

20,15

100

80

bounds 0,0

110

View.frame.origin.x -Superview.frame.origin.x;

Y= View.frame.origin.y -Superview.frame.origin.y;

CO

MPI

SIT

ED L

OC

0

0

140

20

15

Page 19: Making iOS UIKit Simulator for MacOS X

UIScrollView 3/3Ma che c’entra tutto questo con UIScrollView?

• Lo scrolling non fa altro che alterare il frame di ogni subview all’interno della scrollview a seconda del movimento delle dita sul touchscreen.

... è un’operazione piuttosto dispendiosa; le scrollview potrebbero avere molte subview

• Fino ad ora abbiamo avuto bounds origin del superview 0,0. Potremmo però modificarlo per ottenere uno spostamento (a livello di rasterizzazione) di tutti i view che la compongono, a costo zero!

X= View.frame.origin.x -Superview.frame.origin.x;

Y= View.frame.origin.y -Superview.frame.origin.y;

CO

MPI

SIT

ED L

OC

un disegno troppo gr

scrollview bounds {-30,-30,140,110}

subview frame {20,15}

20-(-30)=50

15-(-30)=45

• contentOffset lavora proprio in questo modo: !- (void)setContentOffset:(CGPoint)offset { CGRect bounds = [self bounds]; bounds.origin = offset; [self setBounds:bounds];}

frame.origin 20,15

bounds -30,-30

una porzione del disegno si sposta offscreen (non viene rasterizzata)

Page 20: Making iOS UIKit Simulator for MacOS X

UITableView 1/3• E’ tra le classi più usate di iOS (si basa su UIScrollView) • Viene utilizzata spesso per mostrare grandi quantità di dati • ... è quindi necessario che sia performante e con basso impatto sulla memoria

Implementazione Lazy

VENGONO CREATE SOLO LE

CELLE (UIView) VISIBILI

VIS

IBIL

EN

ASC

OST

I

3

1

DURANTE LO SCROLL NUOVE RIGHE

DOVRANNO ESSERE VISUALIZZATE...

2AL MOMENTO DELLO SCROLL, LE CELLE CHE

FINISCONO OFFSCREEN SONO INSERITE IN

UNA CACHE PRONTE AD ESSERE RIUTILIZZATE

SI CERCA NELLA CACHE SE CI SONO

CELLE DISPONIBILI CON

L’IDENTIFICATORE RICHIESTO

SI TOLGONO DALLA CACHE

E VENGONO RESE DISPONIBILI

DISPONIBILI NON DISPONIBILISONO ALLOCATE

NUOVE ISTANZE

Page 21: Making iOS UIKit Simulator for MacOS X

UITableView 2/3

UITableView

UITableSection #1

Sec. Header

Row #1

Row #2

Row #3

Row #n

Sec. Footer

Header View

.....

Footer View

UITableSection #1

Sec. Header

Row #1

Row #2

Row #3

Row #n

Sec. Footer

.....

UITableSection #1

Sec. Header

Row #1

Row #2

Row #3

Row #n

Sec. Footer

.....

PER OGNI SECTION DELLA TABLE VIENE CREATA

UNA CLASSE CHE NE INCAPSULA LA GEOMETRIA1

• frame SEC. HEADER

• frame PER OGNI ROW

• frame SEC. FOOTER

• frame globale del blocco

(i frame sono rispetto alla table)

2 OTTENUTA LA GEOMETRIA DEGLI ELEMENTI

SI RICHIAMA IL METODO CHE SI OCCUPA DI

VISUALIZZARE GLI ELEMENTI E GESTIRE LA

CACHE

- layoutTableStructure

reloadData: calcolo della geometria

Page 22: Making iOS UIKit Simulator for MacOS X

Section #1

Row #1

• viene chiamata ad ogni cambio di contentOffset (lo scroll)

• deve perciò essere molto performante

UITableView 3/3layoutTableStructure: algoritmo per lo scroll

INTERSECA L’AREA

VISIBILE DELLA TABLE?

UITableSection #N

Row #N

Row #N-1

Row #N+1

NULLA. LA CELLA RIMANE VISIBILE.

VIENE RIMOSSA E MESSA IN CACHE

SI

NO

1

2 SI OTTENGONO GLI INDICI DELLE CELLE VISIBILI TRAMITE LA GEOMETRIA.

PER QUELLE NUOVE SI CHIEDE AL DATASOURCE.

Row #3

Row #2

Section #2Row #1

Row #2tableView:cellForRowAtIndexPath:

IL DATA SOURCE PUO’ CHIEDERE UNA VERSIONE IN

CACHE CON

dequeueReusableCellWithIdentifier:

VIENE RESTITUITA UNA VERSIONE IN CACHE O UNA NUOVA ISTANZA SE

NON DISPONIBILE.

2a

2b

UITableView3Row #1

• OGNI NUOVA CELLA VIENE INSERITA TRA QUELLE VISIBILI

• LA GEOMETRIA CALCOLATA PRIMA CI DA LA SUA

POSIZIONE FINALE

Page 23: Making iOS UIKit Simulator for MacOS X

UINavigationController

UIScrollView

UINavigationBar

UINavigationControllerStruttura di base

View Controller #4

!(not loaded)

n+2

View Controller #2

!(current)

n

View Controller #3

!(push) n+1

View Controller #1

!(pop) n-1

View Controller #4

!(cached)

n-2

• Una UIScrollView contiene il view corrente • Uno stack (NSArray) mantiene la “storia” del navigation • Ad ogni push/pop viene mostrato il view ed eventualmente caricato il view

controller precedente e successivo (on demand) • I controller > n+1 possono essere rimossi dalla cache (e deallocati) • I controller < n+1 sono mantenuti per lo stack

fuori dalla cachestack navigazione

BLOCCO 3BLOCCO 2BLOCCO 1

x1 x2

Page 24: Making iOS UIKit Simulator for MacOS X

Grazie dell’attenzione ;-)

Daniele Margutti@danielemargutti [email protected]