Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Making iOS UIKit Simulator for MacOS X

373 views

Published on

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

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Making iOS UIKit Simulator for MacOS X

  1. 1. Core UIKit drop in replacement for UIKit that runs on OS X …a short story Daniele Margutti Software Architect me@danielemargutti.com danielemargutti.com
  2. 2. 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
  3. 3. UIKit su iOS iOS Cocoa Touch UIKit Media Core Graphics OpenGL ES Core Animation già disponibile su Mac Core Services Core Data Foundation già disponibile su Mac Core OS 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
  4. 4. 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
  5. 5. UIView: la scelta UIView NSView CALayer (da OS X 10.5) CALayer Mac OS X iOS CALayer come unità base •Ottenere la stessa architettura di iOS •Avere (quasi) gratis la potenza di Core Animation •Astrarre da OS X l’ambiente di simulazione
  6. 6. UIView: implementazione Init di un UIView - (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
  7. 7. UIView: implementazione Gestire la gerarchia di UIView 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 ...
  8. 8. UIView UIView: implementazione Disegnare in uno UIView CALayer associato ! -display: richiesta dal sistema (metodi delegate) implementa -displayLayer:? si chiamalo no crea buffer, context -drawInContext: passali a se implementato chiama -drawLayer:inContext:? chiama -drawRect: 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:
  9. 9. 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 -runWithDevice:delegate:options: UIKitLayer (Flipped NSView) eventi cocoa mouse/keyboard/touchpad UIScreen (CALayer) UIWindow (CALayer) UIView (CALayer) (di UIViewController) UIButton 0,0 UIStatusBar (CALayer) UITextField UITableView
  10. 10. Architettura del simulatore Intercettare e tradurre gli eventi UIKitLayer (NSView) mouseDown scrollWheel mouseDragged mouseMoved mouseUp mouseEntered 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 touchesMoved Se è iniziata una gesture viene inviato l’evento gestureTouchesMoved alle gesture registrate per il view corrente. NSLeftMouseUp touchesEnded Se è in corso una gesture viene inviato l’evento gestureTouchesEnded alle gesture registrate per il view corrente. EVENTI PRINCIPALI
  11. 11. Architettura del simulatore Hit 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
  12. 12. Architettura del simulatore Hit 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); } !
  13. 13. Architettura del simulatore Ambiente Multi-UIKit E’ possibile supportare più ambienti di UIKit simultaneamente semplicemente impostando una macro. Un nuovo runtime si occupa di creare: • UIApplication • UIScreen • UIDevice 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) [[oggetto allocWithRuntime:runtime] init… 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] (*) tutti quegli oggetti che usano riferimenti al singleton o all’ambiente grafico. UIKitRuntimePool UIKitRuntime UIApplication UIK UIScreen UIK UIWindow UIK UIViewController UIK UIView UIK UIButton UIK [UIApplication sharedApplication] ?
  14. 14. 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 Struttura dell’adapter UIKitLayer (Flipped NSView) UIAppKitAdapter (UIView) CALayer Adapter NSClipView NS* Native Object aggiunto subview di UIKitLayer (dietro tutti gli oggetti) 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)
  15. 15. Adapter AppKit Funzionamento dell’adapter Evento sopra l’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]; }
  16. 16. Qualche controllo UI rilevante •UIScrollView •UITableView •UINavigationController
  17. 17. 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!
  18. 18. UIScrollView 2/3 bounds per il raster (0,0,100,80) 10,20 110,20 u n d i s e g n o t roppo grande 80 100 Rasterization • 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 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. 0 LOC X= COMPISITED Superview.frame.origin.y; frame.origin un disegno troppo gr 20,15 100 80 bounds 0,0 110 View.frame.origin.x - Superview.frame.origin.x; Y= View.frame.origin.y - 0 140 20 15
  19. 19. UIScrollView 3/3 Ma 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; COMPISITED LOC scrollview bounds {-30,-30,140,110} subview frame {20,15} un disegno troppo gr 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]; } bounds -30,-30 frame.origin 20,15 una porzione del disegno si sposta offscreen (non viene rasterizzata)
  20. 20. 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 NASCOSTI V I S I B I L E 2 AL MOMENTO DELLO SCROLL, LE CELLE CHE 3 1 FINISCONO OFFSCREEN SONO INSERITE IN UNA CACHE PRONTE AD ESSERE RIUTILIZZATE DURANTE LO SCROLL NUOVE RIGHE DOVRANNO ESSERE VISUALIZZATE... SI CERCA NELLA CACHE SE CI SONO CELLE DISPONIBILI CON L’IDENTIFICATORE RICHIESTO DISPONIBILI NON DISPONIBILI SI TOLGONO DALLA CACHE E VENGONO RESE DISPONIBILI SONO ALLOCATE NUOVE ISTANZE
  21. 21. UITableView 2/3 UITableView UITableSection #1 Sec. Header Row #1 Row #2 Row #3 Row #n Sec. Footer Header View ..... UITableSection #1 Sec. Header Row #1 Row #2 Row #3 ..... Row #n Sec. Footer Footer View 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 GEOMETRIA 1 • 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
  22. 22. UITableView 3/3 layoutTableStructure: algoritmo per lo scroll • viene chiamata ad ogni cambio di contentOffset (lo scroll) • deve perciò essere molto performante Row #N-1 Section #1 Row #1 INTERSECA L’AREA VISIBILE DELLA TABLE? UITableSection #N Row #N 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 #2 Row #1 Row #2 tableView: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 UITableView 3 Row #1 • OGNI NUOVA CELLA VIENE INSERITA TRA QUELLE VISIBILI • LA GEOMETRIA CALCOLATA PRIMA CI DA LA SUA POSIZIONE FINALE
  23. 23. UINavigationController UINavigationBar UIScrollView UINavigationController Struttura di base View Controller #4 ! (not loaded) n+2 BLOCCO 1 BLOCCO 2 BLOCCO 3 View Controller #2 ! (current) n View Controller #3 ! (push) n+1 View Controller #1 ! (pop) n-1 View Controller #4 ! (cached) n-2 stack navigazione fuori dalla cache x1 x2 • 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
  24. 24. Grazie dell’attenzione ;-) Daniele Margutti @danielemargutti daniele@creolabs.com

×