Making iOS UIKit Simulator for MacOS X
-
Upload
daniele-margutti -
Category
Technology
-
view
45 -
download
2
description
Transcript of 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
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
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
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
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
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
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
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:
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
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
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
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); } !
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] ?
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)
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]; }
Qualche controllo UI rilevante•UIScrollView•UITableView•UINavigationController
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!
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
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)
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
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
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
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
Grazie dell’attenzione ;-)
Daniele Margutti@danielemargutti [email protected]