# Alberto De Bortoli > Principal Software Engineer @ Just Eat Takeaway, London. Public Ghost content for AI and LLM tooling. This file includes a bounded export of public pages first, then recent public posts. Append `.md` to any post or page URL to get the content in Markdown (for example, `/example-post.md`). ## Pages ### About me URL: https://albertodebortoli.com/about-me/ Last updated: 2026-04-21T09:46:56.000Z ### Contacts - [LinkedIn](https://www.linkedin.com/in/albertodebortoli/?ref=albertodebortoli.com) - [Bluesky](https://bsky.app/profile/albertodebo.bsky.social?ref=albertodebortoli.com) - [X / Twitter](https://x.com/albertodebo?ref=albertodebortoli.com) - [GitHub](http://github.com/albertodebortoli?ref=albertodebortoli.com) - [stackoverflow](https://stackoverflow.com/users/3010877/albertodebortoli?ref=albertodebortoli.com) ### Short Professional Curriculum Vitae ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2026/04/360.png) I currently work at [Just Eat Takeaway](https://www.justeattakeaway.com/?ref=albertodebortoli.com) in London as Principal Software Engineer / Technical iOS Lead where, since the beginning of 2015, I have been focusing on the consumer iOS apps. I've established teams and development processes, driven large-scale cross-functional initiatives, including company-wide rebrands, app architecture overhauls, and platform unification efforts across multiple markets. I have vastly invested in bringing DevOps to mobile and set the mobile stack to stand the test of time. My work bridges product and technology, aligning engineering execution with business outcomes. Previously at [Beamly](http://beamly.com/?ref=albertodebortoli.com), [Badoo](http://badoo.com/?ref=albertodebortoli.com), [EF Education First](http://ef.com/?ref=albertodebortoli.com) in London focusing on designing apps for education and at H-umus in Italy ([H-Farm](https://www.h-farm.com/en?ref=albertodebortoli.com)) focusing on developing iPad apps for famous fashion brands. I held iOS courses at [Digital Accademia](http://digitalaccademia.com/?ref=albertodebortoli.com). I got my Master Degree in Computer Science at the University of Padua (Italy) with full marks (110/110). You can check out my résumé on [LinkedIn](http://linkedin.com/in/albertodebortoli?ref=albertodebortoli.com), my open source projects on [GitHub](http://github.com/albertodebortoli?ref=albertodebortoli.com), my [talks](https://albertodebortoli.com/talks/) and my [blog](https://albertodebortoli.com/) to have an idea about my works, backgrounds and skills. ### Musical Curriculum Vitae **Alert**: I quit world of music in 2010 and I'm no more available for live or recording sessions. I just keep this section and the following links as archive/memories: - [Live dates](https://albertodebortoli.com/live-dates) - [Photos](https://photos.app.goo.gl/3wMBxAi9WpHXOSCI2?ref=albertodebortoli.com) - [Paper](https://albertodebortoli.com/music-sheets) - [Videos and Discography](https://albertodebortoli.com/videos-discography) ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/12/14477501566_2c152c33a6_o-1.jpg) Born in 1985 in Pordenone (Italy), Alberto had his first introduction to music at the age of eight with the piano. When he was 14 he started playing bass and guitar with some local rock bands, as well as picking up the double bass. He studied with important teachers like Mauro Zavagno, Roberto Pascucci and Alex Stornello at the Lizard Academy in Florence and Padua. He took part in important clinics with the greatest bass players like Stu Hamm, Billy Sheehan, Alain Caron, and Faso. In 2003 Alberto wrote a book “Il Volo del Calabrone ed altri classici per basso” (The Flight of the bumble-bee and others classical transcription for el. bass). In 2005 Alberto composed a personal demo, “Genre?!? What Genre?,” encompasing different kinds of music. In the same year he met Marco Anzovino, a great Italian song-writer, with whom he started collaborating with. He had a great experience playing in Marco Anzovino‘s second album “Canzoni Ad Occhi Chiusi.” In 2007/’08 he held bass class as teacher at G. Verdi school in Fontanafredda (PN). In 2007 he met Matt Cafissi, acclaimed by critics as “the new melodic Italian genius,” and had the honor to play in his second album “All The Little Things.” At the end of 2007 he joined the Centrica line-up releasing their first acclaimed album with Musea Records. In April 2009 he joined the famous Italian north-east Absolute5 cover band, performing with them over 80 concerts per year. In 2010 took part of Marco Anzovino & Gianpiero Perone (Colorado Café, Italia1)’s “Rideremo tra 20 anni” show. Alberto uses the following stuff: **Instruments** - MusicMan StingRay5 - Yamaha TRB 6PII Ovangkol - Fender Marcus Miller Jazz Bass - Fender American Standard Jazz Bass - Fender Precision Bass Fretless /w Di Marzio Model P J - Epiphone Les Paul Custom Flame Top - Marco Anzovino’s Takamine EF341 - ErnieBall Hybrid Slinky strings **Studio & Live** - Gallien Krueger 400-RB III - Gallien Krueger 200W speaker - DBX 160A rack compressor - Korg DTR2000 rack tuner - Digitech JamMan loop station - Volume pedal Bespeco - MOTU Ultralite Mk3 - JTS UHF US-901D Receiver - JTS UHF PT-950Bmi Transmitter - Sennheiser Ear Monitor EW 300 IEM G2 - iMac and MacBook Pro - iPhone 4 and iPad - Logic Express 8 software - Speakers Behringer Truth B2031A ### Live dates URL: https://albertodebortoli.com/live-dates/ Last updated: 2018-02-10T17:41:12.000Z Since May 2010 I decided to take a break from the frenzy of the lives, to better devote to other interests. Actually deciding to quit world of music (Centrica and Absolute5), no more live concerts with me will be performed. ## Live Dates Archive - Saturday 12-Feb-11 Start: 22.00 Presentazione disco ‘Marnit’ Udine (UD) Marnit Calvi - Friday 10-Sep-10 Start: 24.00 VIP Party – Festa anni ’70 *(special guest Corrinne Marchini from “Amici” di Maria De Filippi)* Milano (MI) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Sunday 5-Sep-10 Start: 21.00 I Love Casarsa Casarsa della Delizia (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 3-Jul-10 Start: 21.30 La Balonade Buttrio (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 12-Jun-10 Start: 21.00 Meduno (PN) Teatro Pasolini [Gianpiero Perone e Marco Anzovino con lo spettacolo “Rideremo tra vent’anni” \[info\]](http://www.marcoanzovino.it/?ref=albertodebortoli.com) - Saturday 1-May-10 Start: 21.30 Buttrio in festa Buttrio (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 30-Apr-10 Start: 21.30 Festa dei Fiori Primulacco (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Tuesday 27-Apr-10 Start: 9.30 Teatro Verdi Maniago (PN) [Gianpiero Perone e Marco Anzovino con lo spettacolo “Rideremo tra vent’anni” \[info\]](http://www.marcoanzovino.it/?ref=albertodebortoli.com) - Saturday 24-Apr-10 Start: 22.00 Bar Cooperativa Calalzo (BL) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Tuesday 30-Mar-10 Start: 9.30 Teatro Zancanaro Sacile (PN) [Gianpiero Perone e Marco Anzovino con lo spettacolo “Rideremo tra vent’anni” \[info\]](http://www.marcoanzovino.it/?ref=albertodebortoli.com) - Friday 26-Mar-10 Start: 22.00 Alla Filanda Brazzano di Cormons (GO) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Thursday 25-Mar-10 Start: 9.30 Auditorium Concordia Pordenone (PN) [Gianpiero Perone e Marco Anzovino con lo spettacolo “Rideremo tra vent’anni” \[info\]](http://www.marcoanzovino.it/?ref=albertodebortoli.com) - Tuesday 23-Mar-10 Start: 9.30 Auditorium San Vito al Tagliamento (PN) [Gianpiero Perone e Marco Anzovino con lo spettacolo “Rideremo tra vent’anni” \[info\]](http://www.marcoanzovino.it/?ref=albertodebortoli.com) - Sunday 21-Mar-10 Start: 14.00 Rifugio Tamai Sutrio M.te Zoncolan (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 12-Mar-10 Start: 22.00 Tashica Social Club Villotta di Chions (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Monday 08-Mar-10 Start: 21.00 Una Rotonda sul Verde Festa della donna, Basiliano (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 26-Feb-10 Start: 22.00 Caffè Barocco Revolution Azzano Decimo (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 19-Feb-10 Start: 22.00 Festa Privata Tributo anni ’70 Mestre (VE) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Tuesday 16-Feb-10 Start: 17.00 Music Pub “La Stua” Falcade (BL) [Marco Anzovino con lo spettacolo “Tracce sulla neve” \[info\]](http://www.marcoanzovino.it/?ref=albertodebortoli.com) - Friday 12-Feb-10 Start: 22.30 Tashica Social Club Villotta di Chions (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 06-Feb-10 Start: 22.30 Adiamo Music Bar Jesolo (VE) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 29-Feb-10 Start: 22.00 Tashica Social Club Villotta di Chions (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 23-Jan-10 Start: 22.00 Una Rotonda sul Verde Basiliano (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Monday 17-Jan-10 Start: 13.00 Festa Privata Varmo (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Thursday 31-Dec-09 Start: 24.00 La Fornace Porto Viro (RO) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Wednesday 23-Dec-09 Start: 18.30 Convention Aziendale Treviso (TV) [Absolute5 ACOUSTIC SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 18-Dec-09 Start: 22.00 Tashica Social Club Villotta di Chions (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 5-Dec-09 Start: 23.00 Jamila Bar Bassano del Grappa (VI) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 4-Dec-09 Start: 22.00 El Mordisco Polcenigo (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 27-Nov-09 Start: 22.00 Gran Café Tiziano Pieve di Cadore (BL) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 21-Nov-09 Start: 22.30 Adiamo Music Bar Jesolo (VE) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 31-Oct-09 Start: 22.30 La Casa Matta (Halloween) Biauzzo di Codroipo (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 23-Oct-09 Start: 22.00 El Mordisco Polcenigo (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 9-Oct-09 Start: 22.00 Area Festeggiamenti Muzzana del Turgnano (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 2-Oct-09 Start: 22.00 Corte del Becco Fino Manzano (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Sunday 27-Sep-09 Start: 18.00 Gusti di Frontiera Gorizia (GO) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 26-Sep-09 Start: 21.30 Festa Privata Cassacco (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 25-Sep-09 Start: 21.00 Gusti di Frontiera Gorizia (GO) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 18-Sep-09 Start: 22.00 Area Festeggiamenti Blessaglia di Pramaggiore (VE) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Sunday 30-Aug-09 Start: 18.00 Isola D’Oro Grado (GO) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 29-Aug-09 Start: 22.00 Piazza Duomo San Daniele del Friuli (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 28-Aug-09 Start: 22.00 Festa di Piazza Precenicco (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Thursday 27-Aug-09 Start: 22.00 Sagra della Faraona Cesena di Azzano X (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Sunday 23-Aug-09 Start: 18.00 Isola D’Oro Grado (GO) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Sunday 16-Aug-09 Start: 18.00 Gran Café Tiziano Pieve di Cadore (BL) [Absolute5 ACOUSTIC SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 14-Aug-09 Start: 22.00 Festa di Piazza San Rocco di Cordignano (TV) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 8-Aug-09 Start: 22.00 Festa di Piazza San Giorgio della Richinvelda Aurava (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 7-Aug-09 Start: 22.00 El Mordisco Polcenigo (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Thursday 6-Aug-09 Start: 22.00 Cocobongo Bibione (VE) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Sunday 2-Aug-09 Start: 22.00 Festa dell’Emigrante Bordano (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 1-Aug-09 Start: 23.00 Chies D’Alpago (BL) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Thursday 30-Aug-09 Start: 22.00 Sandy Bar San Michele al Tagliamento (VE) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 24-Jul-09 Start: 22.30 Festa del Noce in Fiore Tambre (BL) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 18-Jul-09 Start: 02.00 Discoteca Sottosopra Diamante (CS) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Sunday 12-Jul-09 Start time: 21.30 Sagra degli gnocchi Azzano Decimo (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 10-Jul-09 Start: 22.00 El Mordisco Polcenigo (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Sunday 5-Jul-09 Start time: 22.00 Cordovado per l’Abruzzo Serata di beneficenza, Cordovado (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 4-Jul-09 Start time: 22.00 Festa di Piazza San Giorgio della Richinvelda Aurava (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 3-Jul-09 Start time: 22.00 Sagra dei Gamberi Orcenico Superiore (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Sunday 28-Jun-09 Start time: 18.00 Isola D’Oro Grado (GO) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 27-Jun-09 Start time: 22.00 Gran Café Tiziano Pieve di Cadore (BL) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 26-Jun-09 Start time: 21.30 Festa promozione Texa Bas Roncade (TV) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 20-Jun-09 Start time: 22.00 Festa di piazza Marano Lagunare (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 13-Jun-09 Start time: 22.00 Festa della birra del rugby Calvisano (BS) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 12-Jun-09 Start time: 22.00 El Mordisco Polcenigo (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Saturday 6-Jun-09 Start time: 22.00 Adiamo Music Bar Jesolo (VE) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 5-Jun-09 Start time: 22.00 La Casa Matta Biauzzo di Codroipo (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Monday 1-Jun-09 Start time: 22.00 Festa privata Fossalta di Portogruaro (VE) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 29-May-09 Start time: 21.00 Torneo di Calcetto Teson Concordia Sagittaria (VE) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday 15-May-09 Start time: 21.30 Festa in Piazza, Area Giova Pravisdomini (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Sunday 17-May-09 Start time: 18.00 Glass Wine Bar Bibione (VE) [Absolute5 ACOUSTIC SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Friday El Mordisco 8-May-09 Start time: 22.00 Polcenigo (PN) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Thursday 30-Apr-09 Start time: 21.30 Festa dei Fiori Primulacco (UD) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Sunday 12-Apr-09 Start: 17.00 Gran Café Tiziano Pieve di Cadore (BL) [Absolute5 ACOUSTIC SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) - Thursday 02-Apr-09 Start: 22.00 Birreria Accademia Fossalta di Portogruaro (VE) [Absolute5 DISCO-POP SHOW \[info\]](http://www.absolutefive.it/?ref=albertodebortoli.com) ### Music Sheets URL: https://albertodebortoli.com/music-sheets/ Last updated: 2026-06-10T12:40:09.000Z ### The flight of the bumblebee and other classics for bass ![The flight of the bumble-bee and other classics for bass](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/book1.png "The flight of the bumble-bee and other classics for bass") *by Alberto De Bortoli* This book is about 5 famous piece of classical music arranged for 6 and 4 string bass. The authors are Rimsky-Korsakov, J.S. Bach, W.A. Mozart, L.V. Beethoven, N. Paganini. Date: august 2003 [Download (PDF)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/Il%5FVolo%5Fdel%5FCalabrone.pdf?ref=albertodebortoli.com) --- ### Aeolian ![Aeolian](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/book2.png "Aeolian") *by Alberto De Bortoli* Armonizations of all the minor scales and their modes with patterns for electric bass. Date: october 2004 [Download (PDF)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/Aeolian.pdf?ref=albertodebortoli.com) --- ### Metodo di base per chitarra moderna rock ![Metodo di base per chitarra moderna rock](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/book3.png "Metodo di base per chitarra moderna rock") *by Alberto De Bortoli* Metodo DI BASE in 15 lezioni per chitarristi alle prime armi, niente di complicato. Scritto nel 2001. Date: august 2001 [Download (PDF)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/Metodo%5Fdi%5Fbase%5Fper%5Fchitarra%5Fmoderna%5Frock.pdf?ref=albertodebortoli.com) --- ### Tears in Heaven (Jeff Berlin) \*transcripted by Alberto De Bortoli date: 12th july 2008\* [Bass Tab (TXT)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/tears%5Fin%5Fheaven.txt?ref=albertodebortoli.com) Ok guys, since 1997 nobody on earth tabbed this out. I asked myself why, and I realized that this song is incredibly complex. This is actually the first attempt on the web to tab this song out. I know that many people were looking for this tab for a long time, now it’s time to try to learn how to play it! Although this is the first version of the tab I think it’s very accurate. Enjoy ! --- ### Trying to forget (Alberto Rigoni) *trascritto da Alberto De Bortoli* [Bass Tab (TXT)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/trying%5Fto%5Fforget.txt?ref=albertodebortoli.com) L’album di esordio del bassista (nonché mio amico) Alberto Rigoni “Something Different” è veramente qualcosa di particolare. Il titolo dice tutto, è qualcosa di diverso dalla solita musica che ci martella i timpani tutti i giorni. Una sua canzone intitolata Trying to Forget è molto interessante per il gioco non banale di armonici tanto da incuriosirmi e cercare di trascrivere il brano. Il sito originale del bassista di Montebelluna è . --- ### Prelude in C [Bass Tab (TXT)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/stu%5Fhamm-prelude%5Fc.txt?ref=albertodebortoli.com) Nell’album “Kings of Sleep” di Stuart Hamm, c’è un interessante arrangiamento di un Preludio in Do maggiore di Johann Sebastian Bach. È possibile scaricare la tablatura completa in formato testo del brano originale. L’originale è stata trovata su www.stuarthamm.net ma mi sono sentito in dovere di darci una correzione generale. --- ### Portrait of Tracy *trascritto da Alberto De Bortoli* [Bass Tab (TXT)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/jaco%5Fpastorius-portrait%5Fof%5Ftracy.txt?ref=albertodebortoli.com) Il pezzo che fece rimanere sbalorditi tutti i bassisti nel 1976 quando lo ascoltarono! Per l’uso degli armonici naturali questo pezzo sembra essere stato scritto per un altro strumento, invece a suonare è proprio un basso. Ovviamente il brano è del “più grande bassista del mondo” Jaco Pastorius. Vi propongo una registrazione da me eseguita con tanto di tablatura. Il tutto è molto simile all’originale. È stato usato un basso adatto all’occasione, ovvero un Fender fretless stile Vintage (come quello che usava Pastorius) con pick up Di Marzio. --- ### Shpalman *trascritto da Alberto De Bortoli* [Bass Tab (TXT)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/eelst-shpalman.txt?ref=albertodebortoli.com) Credo proprio che Il pezzo non abbia bisogno di presentazioni! Il primo singolo, da Cicciput degli Elio EELST, che ci ha fatto sognare per tutta un’estate intera (…) ora per la prima volta (a quanto mi risulta) è stato tablato per basso! Dato che solo pochi eletti osano trascrivere le linee di basso del mitico Faso, provo a fare la mia parte. Shpalman --- ### Prelude from “Suite in stile antico” by Hans Fryba *trascritto e arrangiato da Alberto De Bortoli* [Download (PDF)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/Hans-Fryba.pdf?ref=albertodebortoli.com) Questo difficilissimo pezzo di Hans Fryba è stato scritto per contrabbasso, e siccome avevo in programma un concerto di basso elettrico ho deciso di metterlo in scaletta. Ho speso (troppe) ore a impararlo e tablarlo per renderne più facile la lettura a prima vista su basso a 6 corde. Ve lo propongo come studio (molto impegnativo) per i bassisti più navigati. Il pezzo è molto raro e …niente di speciale a dirla tutta, ma sotto certi punti di vista merita suonarlo. Buon lavoro. --- ### Stu Hamm bass solo *trascritto da Alberto De Bortoli* [Bass Tab (TXT)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/stu%5Fhamm-bass%5Fsolo.txt?ref=albertodebortoli.com) Nell’album con doppio cd di Joe Satriani, Live in San Francisco, Stu Hamm esegue questo bellissimo solo (traccia 6 cd 2) a seguito di Love Thing, facendo un po’ un medley dei suoi pezzi più famosi: Moonlight Sonata, Sexually Active, Country Music… L’assolo è fantastico e di grande effetto sul pubblico. È basato per la maggior parte sul tapping, come insegna giustamente il maestro Hamm. Vi propongo la tablatura e il file midi. --- ### Solar Groove and Opening bass solo *tratti dal video istruttivo “Progressive Bass Concepts”* *trascritti da Alberto De Bortoli* [Opening Bass Solo (TXT)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/john%5Fmyung-opening%5Fbass%5Fsolo.txt?ref=albertodebortoli.com) [Solar Groove (TXT)](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-sheets/john%5Fmyung-solar%5Fgroove.txt?ref=albertodebortoli.com) Il primo (e anche l’unico) video istruttivo di John Rho Myung, il bassista dei Dream Theater, è veramente interessante per il suo tempo. John è progressivo, un virtuoso del basso elettrico, totalmente differente da altri grandi come Patitucci e Pastorius, il suo stile si basa principalmente sulla velocità e ben poco sulle dinamiche o cose simili…insomma, ad ognuno il suo genere… “Solar Groove” è eseguita da Myung in “Progressive Bass Concepts”. È un pezzo che John suona tra una lezione e l’altra nel video. Senza sentirla non saprete esattamente come eseguirla. È un buon esercizio da suonare completamente in tapping e non riuscendo a trovare una tablatura in Internet ho dovuto trascriverla ad orecchio. Attenzione, questi due pezzi sono per basso a 6 corde. ### Videos and Discography URL: https://albertodebortoli.com/videos-discography/ Last updated: 2020-05-23T16:48:43.000Z ### Centrica – Centrica (2008) Musea Records Recorded at New Frontiers Recording Studio ![centrica](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-discography/centrica.png "centrica") 1. Centrica Experience \[8.32\] 2. Secret Vision \[7.37\] 3. DNA part.1 \[6.21\] 4. DNA part.2 \[8.22\] 5. Reality and Illusion \[9.26\] 6. Dulcedo \[3.56\] 7. Eternal Dimension \[8.21\] Andrea Pavanello (keyboards) Giorgio Rovati (guitars) Alberto De Bortoli (bass) Dario Ciccioni (drums) ### Matt Cafissi – All the little things (2008) GC Records ![matt cafissi](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-discography/matt.png "matt cafissi") 1. Draw the line 2. Chained 3. Everything you want 4. One in a million 5. Bring me down 6. Straight to you 7. Inside out 8. Heaven knows tracks 2, 3, 6. ### Marco Anzovino - Canzoni ad occhi chiusi (2005) artesuono records ![canzoni ad occhi chiusi](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-discography/canzoniadocchichiusi.png "canzoni ad occhi chiusi") 1. Già lo so 2. La valigia delle cose che amo 3. Fiore dal mare 4. Navigo piano 5. Splendida 6. Il giro del palazzo 7. Lentamente sul Noncello 8. D.J. 9. Lettera 10. Codici a memoria 11. A volte va così 12. Viaggiando su Marta \[live\] ### Alberto De Bortoli - Personal demo (2005) ![genre](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-discography/genre.png "genre") 1. No Answer 2. Red Baron 3. Bye 18 giugno 4. Il Volo del Calabrone 5. Whatta?!? 6. Autumn Basses 7. Portrait of Patty 8. Route 66 9. Air on the G string 10. No Answer \[radio edit\] ### Jazzy Sound quintet - Drops of jazz (2005) ![dropofjazz](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-discography/dropofjazz.png "dropofjazz") 1. Autumn Leaves 2. Red Baron (instrumental) 3. East of the sun (west of the moon) 4. Route 66 5. So What (instrumental) 6. Loverman 7. Oliloqui Valley (instrumental) 8. Summertime Elisa Aramonte (vocal) Samuele Stefanoni (keyboards) Andrea Manfrin (sax tenore) Stefano Manfrin (sax contralto e soprano) Alberto De Bortoli (bass) Nicola Golisano (drums) ### Crop Circle - L’impatto (2004) ![cropcircle](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-discography/cropcircle.png "cropcircle") 1. Pianeta 2. Anima 3. La retta via 4. Come anni fa 5. Apocalisse 6. Che fine farà 7. Vai con lui 8. L’impatto Bruno Piazzon (keyboards & vocal) Stefano Antoniolli (guitars) Alberto De Bortoli (bass) Christian Di Giovanni (percussions) Marco Del Puppo (drums) ### Alberto De Bortoli - The flight of the bumble-bee and other classics for bass (2003) ![calabrone](https://s3-eu-west-1.amazonaws.com/albertodebortoli-ghost-blog/music-discography/calabrone.png "calabrone") 1. Il Volo del Calabrone (da La Favola dello Zar Saltan, Rimsky-Korsakov) 2. Preludio in Sol Maggiore (dalla Suite n.1 per Violoncello Solo BWV 1007, J.S. Bach) 3. Rondò; Alla Turca (dalla Sonata per pianoforte K 331, W.A. Mozart) 4. Al chiaro di luna (Sonata quasi una fantasia op.27 n.2, Primo Movimento, L. V. Beethoven) 5. Capriccio n.5 (dai 24 capricci op.1, N. Paganini) 6. Il Volo del Calabrone (base piano) 7. Rondò; Alla Turca (base basso1) 8. Rondò; Alla Turca (base basso2) 9. Al chiaro di luna (base basso1) 10. Al chiaro di luna (base basso2) Alberto De Bortoli (basses) ### Talks URL: https://albertodebortoli.com/talks/ Last updated: 2025-06-08T22:22:18.000Z ### Slides - [Scalable Continuous Integration for iOS](https://speakerdeck.com/albertodebortoli/scalable-continuous-integration-for-ios-52fd994c-e0b8-4438-b417-a5300623a8c0?ref=albertodebortoli.com) (SwiftLeeds 2024) - [Scalable Continuous Integration for iOS](https://speakerdeck.com/albertodebortoli/scalable-continuous-integration-for-ios?ref=albertodebortoli.com) (Swift Heroes 2024) - [Scalable Modular iOS Architecture @ Just Eat](https://speakerdeck.com/albertodebortoli/scalable-modular-ios-architecture-at-just-eat?ref=albertodebortoli.com) (Swift Heroes 2020) - [Scalable Modulat iOS Architecture @ Just Eat](https://speakerdeck.com/albertodebortoli/modular-ios-architecture-at-just-eat?ref=albertodebortoli.com) (Mobile London 2020) - [Music Information Retrieval](https://www.slideshare.net/albertodebortoli/music-information-retrieval?ref=albertodebortoli.com) - [Cross platform mobile development](https://www.slideshare.net/slideshow/cross-platform-mobile-development-6663183/6663183?ref=albertodebortoli.com) ### Projects URL: https://albertodebortoli.com/projects/ Last updated: 2026-07-29T20:50:20.000Z [Luca — Tool, Skill & Task Manager | LucaManage tools, agentic skills and tasks your way. On macOS and Linux.![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/favicon-8b3b990b-2f61-4f53-b5bd-d6b954ac92e7.svg)Get Started![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/og-image-3291c261-2876-4a80-8499-d545e056ffb7.png)](https://luca.tools/?ref=albertodebortoli.com) [GitHub - TogglesPlatform/Toggles: Toggles is an elegant and powerful solution to feature flagging for Apple platforms.Toggles is an elegant and powerful solution to feature flagging for Apple platforms. - TogglesPlatform/Toggles![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/favicon-011b4273-376b-4e36-a1bc-487110b80390.svg)GitHubTogglesPlatform![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/Toggles-44e3a100-9906-47d5-9d38-f364446266f7)](https://github.com/TogglesPlatform/Toggles?ref=albertodebortoli.com) [iHarmony App - App StoreDownload iHarmony by Alberto De Bortoli on the App Store. See screenshots, ratings and reviews, user tips and more games like iHarmony.![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/favicon-32-d2b275c6-5971-467d-955c-ef053faf6c59.png)App StoreAlberto De Bortoli![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/1200x630wa-0abf0d49-3f16-4ac9-8c25-23d2ed1d6130.jpg)](https://apps.apple.com/gb/app/iharmony/id292413210?ref=albertodebortoli.com) [GitHub - albertodebortoli/Stateful: A minimalistic, thread-safe, non-boilerplate and super easy to use state machine in Swift.A minimalistic, thread-safe, non-boilerplate and super easy to use state machine in Swift. - albertodebortoli/Stateful![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/favicon-5b759491-3b9d-46e0-b4c8-76ac30ed13fd.svg)GitHubalbertodebortoli![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/Stateful-38b4d8a4-dda6-4e33-a22f-ac7e1eef8eb4)](https://github.com/albertodebortoli/Stateful?ref=albertodebortoli.com) ### Legacy [albertodebortoli - RepositoriesPrincipal Software Engineer @ Just Eat Takeaway. albertodebortoli has 94 repositories available. Follow their code on GitHub.![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/favicon-cf7e7854-32b4-469f-8441-6931fbb0a712.svg)GitHub![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/653112-202e0c89-43dd-43df-bcd4-793477781340)](https://github.com/albertodebortoli?tab=repositories&q=&type=source&language=&sort=&ref=albertodebortoli.com) [GitHub - albertodebortoli/Skopelos: A minimalistic, thread safe, non-boilerplate and super easy to use version of Active Record on Core Data. Simply all you need for doing Core Data. Swift flavour.A minimalistic, thread safe, non-boilerplate and super easy to use version of Active Record on Core Data. Simply all you need for doing Core Data. Swift flavour. - albertodebortoli/Skopelos![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/favicon-d7b2fb8d-5cff-4e90-a84b-e2d39e48b6bd.svg)GitHubalbertodebortoli![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/Skopelos-9ed028d5-e073-4817-9b68-63ffeefb9e30)](https://github.com/albertodebortoli/Skopelos?ref=albertodebortoli.com) [GitHub - albertodebortoli/Promis: The easiest Future and Promises framework in Swift. No magic. No boilerplate.The easiest Future and Promises framework in Swift. No magic. No boilerplate. - albertodebortoli/Promis![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/favicon-0386945e-622d-4f5b-a27e-36e1c6233ce1.svg)GitHubalbertodebortoli![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/Promis-300feb58-d9d1-4486-9cf5-d8a785688504)](https://github.com/albertodebortoli/Promis?ref=albertodebortoli.com) [GitHub - objc-zen/objc-zen-book: Zen and the Art of the Objective-C CraftsmanshipZen and the Art of the Objective-C Craftsmanship. Contribute to objc-zen/objc-zen-book development by creating an account on GitHub.![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/favicon-50dc3735-64a3-423e-9d24-603cc6025eab.svg)GitHubobjc-zen![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/objc-zen-book-7ffcba7d-ed5a-4d3c-b44d-4b2e390f72a6)](https://github.com/objc-zen/objc-zen-book?ref=albertodebortoli.com) ### Mentions URL: https://albertodebortoli.com/mentions/ Last updated: 2026-08-03T08:08:19.000Z | Date | Link | Article / Topic | | ----------- | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 31 Jul 2026 | [iOS Deb Weekly #761](https://main--iosdevweekly.netlify.app/issues/761/?ref=albertodebortoli.com) | [Revisiting the JET iOS Modular Architecture in 2026](https://albertodebortoli.com/2026/07/15/revisiting-the-jet-ios-modular-architecture-in-2026/) | | 21 Jul 2026 | [SwiftLee #323](https://newsletter.avanderlee.com/posts/swiftlee-weekly-issue-323-1?ref=albertodebortoli.com) | [Revisiting the JET iOS Modular Architecture in 2026](https://albertodebortoli.com/2026/07/15/revisiting-the-jet-ios-modular-architecture-in-2026/) | | 22 Apr 2026 | [iOS CI #87](https://www.ioscinewsletter.com/issues/87?ref=albertodebortoli.com) | [Luca: A Decentralized Tool and Skills Manager](https://albertodebortoli.com/2026/04/13/luca-a-decentralized-tool-and-skills-manager-for-the-ai-augmented-developer-workflow) | | 17 Apr 2026 | [iOS Dev Weekly #748](https://iosdevweekly.com/issues/748?ref=albertodebortoli.com) | [Luca: A Decentralized Tool and Skills Manager](https://albertodebortoli.com/2026/04/13/luca-a-decentralized-tool-and-skills-manager-for-the-ai-augmented-developer-workflow/) | | 16 Jan 2026 | [iOS Dev Weekly #739](https://iosdevweekly.com/issues/739?ref=albertodebortoli.com) | [Universal Links At Scale: The Challenges Nobody Talks About](https://albertodebortoli.com/2026/01/15/universal-links-at-scale-the-challenges-nobody-talks-about/) | | 21 Apr 2024 | [iOS CI #40](https://www.ioscinewsletter.com/issues/40?ref=albertodebortoli.com) | [Scalable Continuous Integration for iOS](https://www.youtube.com/watch?v=gy5ZHcDj4tE&ref=albertodebortoli.com)*(Swift Heroes talk mention)* | | 14 Jan 2024 | [iOS CI #33](https://www.ioscinewsletter.com/issues/33?ref=albertodebortoli.com) | [Scalable Continuous Integration for iOS](https://albertodebortoli.com/2024/01/03/scalable-continuous-integration-for-ios) | | 5 Nov 2023 | [iOS CI #28](https://www.ioscinewsletter.com/issues/28?ref=albertodebortoli.com) | [Stellar: The Idea of a Swift Replacement for Fastlane](https://albertodebortoli.com/2023/10/29/the-idea-of-a-fastlane-replacement/) | | 3 Nov 2023 | [iOS Dev Weekly #634](https://iosdevweekly.com/issues/634?ref=albertodebortoli.com) | [The Idea of a Fastlane Replacement](https://albertodebortoli.com/2023/10/29/the-idea-of-a-fastlane-replacement/) | | 13 Aug 2023 | [iOS CI #22](https://www.ioscinewsletter.com/issues/22?ref=albertodebortoli.com) | [CloudWatch Dashboards and Alarms on Mac Instances](https://albertodebortoli.com/2023/08/06/cloudwatch-dashboards-and-alarms-on-mac-instances) | | 16 Jul 2023 | [iOS CI #20](https://www.ioscinewsletter.com/issues/20?ref=albertodebortoli.com) | [Easy Connection to AWS Mac Instances with EC2macConnector](https://albertodebortoli.com/2023/07/05/easy-connection-to-aws-mac-instances-with-ec2macconnector) | | 2 Jul 2023 | [iOS CI #19](https://www.ioscinewsletter.com/issues/19?ref=albertodebortoli.com) | Swift Tooling at Just Eat Takeaway *(mention)* | | 2021 | [Building Mobile Apps at Scale](mobileatscale.com)(book) by Gergely Orosz | Quotation from [Deep Linking at Scale on iOS](https://albertodebortoli.com/2019/04/16/deep-linking-at-scale-on-ios/)in Deeplinks chapter (page 13) | | 19 Apr 2019 | [iOS Dev Weekly #400](https://iosdevweekly.com/issues/400?ref=albertodebortoli.com) | [Deep Linking at Scale on iOS](https://albertodebortoli.com/2019/04/16/deep-linking-at-scale-on-ios/) | | 17 Jul 2015 | [iOS Dev Weekly #207](https://iosdevweekly.com/issues/207?ref=albertodebortoli.com) | [The Journey of Apple Pay at JUST EAT](https://tech.just-eat.com/2015/07/14/the-journey-of-apple-pay-at-just-eat/?ref=albertodebortoli.com) | | 27 Mar 2015 | [iOS Dev Weekly #191](https://iosdevweekly.com/issues/191?ref=albertodebortoli.com) | [Notes on the Developer Portal. For Dummies.](http://albertodebortoli.github.io/blog/2015/03/22/notes-on-the-developer-portal-for-dummies/?ref=albertodebortoli.com) | | 11 Jul 2014 | [iOS Dev Weekly #154](https://iosdevweekly.com/issues/154?ref=albertodebortoli.com) | [Zen and the Art of the Objective-C Craftsmanship](https://github.com/objc-zen/objc-zen-book?ref=albertodebortoli.com) | | 23 May 2014 | [iOS Dev Weekly #147](https://iosdevweekly.com/issues/147?ref=albertodebortoli.com) | [Asynchronous Message Passing with Actors in Objective-C](http://albertodebortoli.github.io/blog/2014/05/20/asynchronous-message-passing-with-actors-in-objective-c/?ref=albertodebortoli.com) | ## Posts ### Revisiting the JET iOS Modular Architecture in 2026 URL: https://albertodebortoli.com/2026/07/15/revisiting-the-jet-ios-modular-architecture-in-2026/ Last updated: 2026-07-22T10:17:23.000Z *Revisiting the Modular iOS Architecture @ Just Eat from 2019 addressing previous limitations and refining the vocabulary* --- **Originally published on the* [**Just Eat Takeaway Engineering Blog*](https://medium.com/justeattakeaway-tech/revisiting-the-just-eat-takeaway-ios-modular-architecture-in-2026-752142331755?ref=albertodebortoli.com)**.* When I published [Modular iOS Architecture @ Just Eat](https://albertodebortoli.com/2019/12/19/modular-ios-architecture-at-just-eat/) in December 2019, modularisation was still a relatively novel concept in iOS development. We had been at it since 2016, driven by specific organisational needs: bringing together country apps onto a single platform and apply Conway's Law rather than the more common motivation of build-time parallelisation. The article described the architecture we had settled on after three years of iteration: a directed acyclic graph of modules, partitioned into three broad groups (domain, core and shared), with strict rules against peer-module dependencies. In the seven years since, a great deal has changed. CocoaPods was fully replaced by Swift Package Manager. The monorepo approach we were tentatively advocating for became the unquestioned standard in the industry. The team grew substantially, the product expanded into new markets, and the number of modules roughly doubled. What didn't change was the fundamental shape of the graph or the core dependency rules governing it. Yet in late 2025, a seemingly small problem — a UI component with nowhere to live in the existing model — surfaced a conceptual ambiguity that had been quietly accumulating for years. Resolving it led to the refresh described in this article. --- ## What the 2019 model got right Before describing what changed, it's worth being precise about what didn't. Several properties of the 2019 architecture proved robust enough to survive intact: - **The graph must be acyclic.** Dependencies flow in one direction, always. No module may depend — directly or transitively — on a module that depends on it. This means respecting the Acyclic Dependencies Principle. - **Feature modules do not depend on other feature modules.** Anything two features need to share is extracted into a lower-level module. This prevents the lateral coupling that tends to develop over time when features are allowed to reach into each other. - **Each module exposes a facade.** Modules should be deep with a narrow, stable public interface. The internal architecture of a module is unconstrained and teams may use MVVM, TCA, or anything else, provided they do so behind the facade. - **Demo apps are first-class citizens.** Every module ships with a standalone demo application. This remains one of the most impactful practices we have: it forces good API design, serves as living documentation, and provides a fast local feedback loop during development. The 2026 refresh doesn't replace the 2019 architecture and it's rather a refinement of the vocabulary and an extension of the rules to cover cases the original model didn't address. --- ## The crack that started the conversation The problem that triggered the rethink was a component called `CountdownTimer`. It is a countdown UI component built on top of our design library and not part of the JET design system ([PIE](https://pie.design/?ref=albertodebortoli.com)), used by both the `Checkout` and `Restaurant` domain modules. Under the 2019 model, every module belonged to one of three groups: *domain* (feature-specific, may change frequently), *shared* (pure utilities with no local dependencies) and *core* (like shared but only imported at app-level). Domain modules could depend on shared modules, but not on other domain modules. Shared and core modules could not depend on anything local. Modules in each group can depend on third-party dependencies. `CountdownTimer` fit neither group cleanly: - It can't be *shared* because it depends on PIE. Under the 2019 definition, shared modules have no dependencies. - It can't live inside `Checkout` or `Restaurant` because the other would need it too, creating a domain → domain dependency — which is forbidden. flowchart TB Checkout --> CountdownTimer Restaurant --> CountdownTimer The model had no concept for a module that has dependencies *and* is shared across domain modules. CountdownTimer was first the crack and it didn't take long for other similar modules to appear highlighting the need for revisiting the design. --- ## A note on the cross-domain proposal A colleague proposed an elegant-looking fix: introduce a third layer called *cross-domain*, sitting between domain and shared. The layering would become: ``` domain → cross-domain → shared Checkout CountdownTimer DateFormatting Restaurant DynamicLayout ErrorUtilities ``` `CountdownTimer` would live in cross-domain: it could depend on shared modules, and domain modules could depend on it. The approach was implemented: layer rules were encoded in a YAML config file, and a new CLI tool was introduced to enforce them. For a moment it looked like the problem was solved. I revisited the proposal shortly after and something felt off. The more I thought about it, the more I realised the cross-domain layer was a positional label rather than a meaningful category. `CountdownTimer` is not "cross-domain" because of anything intrinsic to what it is. It was labelled "cross-domain" purely because of how many modules happened to use it and because it had a dependency on PIE. If a future project used it from only one feature module, would we move it back to domain? The category was defined by the topology rather than the purpose. The same confusion was visible elsewhere. We had long kept a distinction between "Core" modules (`APIClient`, `NavigationEngine`) and "Shared" modules (`DateFormatting`, `ErrorUtilities`). Both sat near the bottom of the graph, but they occupied separate mental buckets with no formal rule distinguishing them. Core and shared were a positional label and not a purposeful one. I believed that the right fix was not a new positional layer but rather a different categorisation model altogether. --- ## The 2026 model: categorise by purpose The refreshed model categorises every component by its *purpose* (the role it plays) rather than by where it happens to sit in the dependency graph. The topology is then a consequence of the rules and not an input to them. There are five categories. Three describe components we own; two describe third-party packages. ### First-party categories - **App** — iOS applications and their extensions: the consumer app, demo apps, notification extensions, widgets, etc. These are the roots of the graph; nothing depends on an App. - **Feature** — what we used to call "domain": an area of the product with UI belonging to a business process. E.g. `Checkout`, `Orders`, `Restaurant`, `Account`. Deep modules implementing a facade pattern; they change frequently and are consumed only by the App. - **Foundation** — modules with a single, clear responsibility: `DateFormatting`, `ErrorUtilities`, `APIClient`, `NavigationEngine`, `Logger`, `CountdownTimer`. The key move here is the unification of what were previously two separate buckets (Core and Shared) into one category. `APIClient` and `DateFormatting` are both Foundation modules *because of their purpose* (a single, clear responsibility), not because of their depth in the graph. It follows naturally that Foundation modules may depend on other Foundation modules. The Foundation category is also where `CountdownTimer` lands. It has a single clear responsibility and depends on PIE, which is itself a Foundation module (a design library with a single clear responsibility). With this new model, no new layer are needed. ### Third-party categories A longstanding gap in the original model was that third-party packages were outside the rules entirely. Nothing prevented a Foundation module from pulling in third-party dependencies, even though we all agreed this was bad practice. The 2026 model closes that gap by giving third-party packages their own categories: - **SDK** — a heavyweight third-party capability that encapsulates an entire domain and should sit behind an injection boundary. `Firebase`, `BranchSDK`, `GoogleTagManager`, `SnowplowTracker`. The rule: only an App may depend on SDKs. Features and Foundations may not. This is the *enforced* form of "place large third-party dependencies high in the graph, behind an abstraction" — a principle that existed in 2019 as guidance and exists in 2026 as a hard constraint. - **Utility** — a small, stable third-party helper safe to depend on widely. `Alamofire`, `Nuke`, `Stateful`, `PhoneNumberKit`. A Utility behaves like a Foundation leaf: App, Feature, and Foundation modules can all depend on one. The SDK/Utility split mirrors the first-party Feature/Foundation split: SDKs are heavyweight capabilities we keep at arm's length; Utilities are low-level building blocks. The distinction is recorded in a package catalogue file (that we use for a variety of operations on the stack) that lists all third-party dependencies, where each entry carries a `category` field: ```json { "name": "Firebase", "category": "sdk" } { "name": "Alamofire", "category": "utility" } { "name": "PIE", "category": "foundation" } ``` First-party remote packages follow the same category rules as local modules. This is the case of PIE which is consumed as a remote first-party package and is categorised as a `Foundation` module. --- ## Two rules The architecture enforces exactly two rules. Everything else follows from them. **Rule 1 — No cyclic dependencies:** The graph must be a DAG. A→B→…→A in any form is rejected, detected via depth-first search. **Rule 2 — The category matrix:** Every dependency edge from component `u` to component `v` must be permitted by: | From ↓ / To → | App | Feature | Foundation | SDK | Utility | | -------------- | --- | ------- | ---------- | --- | ------- | | **App** | ✗ | ✓ | ✓ | ✓ | ✓ | | **Feature** | ✗ | ✗ | ✓ | ✗ | ✓ | | **Foundation** | ✗ | ✗ | ✓ | ✗ | ✓ | Several consequences are worth calling out explicitly: - **Feature → Feature remains forbidden.** This is the unchanged core of the 2019 rule. Anything two features need to share is extracted into a Foundation module or coordinated through an upstream abstraction in the App. - **Foundation → Foundation is now explicitly allowed.** This is the deliberate relaxation. It resolves the Core/Shared ambiguity and removes the need for any special treatment of modules. - **App → SDK is the only entry point for heavyweight third-party SDKs.** A Feature or Foundation depending on `Firebase` or `BranchSDK` will fail validation. The SDK is a dependency of the App, and Feature modules receive its capabilities through an injected abstraction. - **Utility modules are Foundation-equivalent for third-party packages.** A Utility may be depended on by any first-party component, exactly as a Foundation module can. - **SDK and Utility nodes are leaves.** Their outgoing edges are not modelled or enforced since we do not control what third-party components depend on. The category matrix is hardcoded in a validator CLI, not read from a configuration file. This was a deliberate choice: the matrix encodes an architectural law derived from ADP (Acyclic Dependency Principle), SDP (Stable Dependency Principle), and DIP (Dependency Inversion Principle). The following diagram shows a representative slice of our module graph, with edges colour-coded by allowed category pair: flowchart TB subgraph L0["L0"] App end subgraph L1["L1"] APIClient Account Checkout NavigationEngine Firebase SnapshotTesting end subgraph L2["L2"] CountdownTimer DateFormatting Stateful KeychainAccess end subgraph L3["L3"] PIE end App --> Account App --> Checkout App --> APIClient App --> NavigationEngine App --> Firebase Checkout --> CountdownTimer Account --> PIE APIClient --> DateFormatting CountdownTimer --> PIE NavigationEngine --> Stateful Account --> KeychainAccess App --> SnapshotTesting linkStyle 0,1 stroke:blue linkStyle 2,3 stroke:green linkStyle 4 stroke:magenta linkStyle 5,6 stroke:orange linkStyle 7,8 stroke:brown linkStyle 9 stroke:pink linkStyle 10 stroke:teal linkStyle 11 stroke:grey Blue: App → Feature Green: App → Foundation Magenta: App → SDK Grey: App → Utility Orange: Feature → Foundation Teal: Feature → Utility Brown: Foundation → Foundation Pink: Foundation → Utility --- ## Layers are a derived view, not an authored property One of the more significant conceptual shifts in the 2026 model is the distinction between *category* — an authored, stable property — and *layer* — a computed, dynamic one. **Category** is what you write: modules are tagged with the `category` property once, and that tag doesn't change unless the module's purpose changes. It is stable precisely because it describes what something *is*. **Layer** is what the validator computes: each node's longest distance from the set of App roots, calculated as a topological-order pass over the graph. Layer numbers are not written anywhere and therefore layers cannot be broken. The concept doesn't exist in code. As a consequence, the following observations arise: 1. **No two components in the same computed layer can depend on each other.** Same-layer dependencies are impossible by construction as they would contradict the longest-path definition. 2. **Adding a legal dependency re-layers the graph without violating any rule.** Say `App` depends directly on both `AppUpdate` and `DateFormatting`, placing both at L1\. If `AppUpdate` later adds a dependency on `DateFormatting` — a legal Feature → Foundation edge — `DateFormatting` is simply recomputed to L2\. Its longest path is now `App → AppUpdate → DateFormatting`. Nothing is violated; the graph just gets one step deeper. flowchart TB subgraph L0["L0"] App end subgraph L1["L1"] AppUpdate end subgraph L2["L2"] DateFormatting end App --> AppUpdate App --> DateFormatting AppUpdate --> DateFormatting linkStyle 0 stroke:blue linkStyle 1 stroke:green linkStyle 2 stroke:orange This matters in practice because it means the question "which layer is my module at?" has no single, stable answer — it depends on the current state of the graph. The right question is always "which category is my module?" and the layer follows from the rules, automatically. --- ## How it fits together in practice Each module carries a YAML spec declaring its category and its dependencies: ```yaml name: CountdownTimer category: foundation localDependencies: [] remoteDependencies: - name: PIE ``` ```yaml name: Checkout category: feature localDependencies: - name: CountdownTimer path: ../CountdownTimer - name: DateFormatting path: ../DateFormatting - name: ErrorUtilities path: ../ErrorUtilities remoteDependencies: - name: PIE ``` A separate CLI ([PackageGenerator](https://github.com/justeattakeaway/PackageGenerator?ref=albertodebortoli.com)) converts these specs into SPM `Package.swift` manifests. App-level dependencies are declared in `TargetDependencies.json` files which are read by Tuist to generate the Xcode project. Third-party package categories live in the package catalogue (`PackageDependencies.json`). This is the only place where the SDK/Utility distinction is declared, and the validator enforces it with the same rules as local modules. The validator (`ModularArchitectureValidator`) is a Swift CLI tool that pulls all three sources together, builds the full dependency graph, and runs the rules over it: ```bash ModularArchitectureValidator validate \ --modules-folder Modules \ --package-dependencies PackageDependencies.json \ --target-dependencies TargetDependencies.json ``` On a rule violation, it reports every offending edge and exits with a non-zero code: ```bash ERROR: Checkout (feature) → Orders (feature) — feature cannot depend on feature ERROR: APIClient (foundation) → Firebase (sdk) — foundation cannot depend on sdk ``` The validator runs locally during project setup and on every pull request on the CI. A violation blocks the PR. Is also exposes an `emit-graph` command that outputs the full graph in Mermaid or Graphviz DOT format which we find useful for visualising the dependency topology as the codebase evolves. --- ## A note on the diamond shape PIE is a leaf that many modules share. Several features and Foundation modules converge on it: flowchart TB App --> Search App --> Restaurants App --> Orders App --> ... Search --> PIE Restaurants --> PIE Orders --> PIE ... --> PIE This is a diamond shape in the graph, and it is entirely deliberate. "Dependency hell" describes conflicting *versions* of a shared dependency; for source-integrated first-party packages, there are no versions to conflict. A diamond over a stable, widely-shared Foundation module is healthy reuse. We accept that the Dependency Inversion Principle cannot be applied to a design library in any practical sense as it's not feasible to abstract the entire design system behind protocols and that is the right call. PIE is a stable leaf that any Feature or Foundation may depend on. It is also worth noting that Apple has made the build-time cost of wide diamond dependencies substantially smaller in recent Xcode releases. With explicit module builds (`SWIFT_ENABLE_EXPLICIT_MODULES`), Xcode determines the full module graph upfront and begins compiling dependencies as soon as their emit-module phase completes, rather than waiting for the full compilation of each target. The diamond topology that looked concerning from a build-time perspective in 2019 is much less of an issue these days. --- ## Onwards and upwards All in all, the 2026 refresh is narrower in scope than it might appear. The graph looks similar to what it looked like six years ago. The Facade pattern, the no-Feature → Feature rule, the monorepo, the reliance on demo apps remain unchanged. What changed is the vocabulary and the scope of enforcement. "Category" is now the authored, stable label for a component's purpose; "layer" is reserved for the computed depth in the graph, which is a consequence of the rules rather than a property you configure. The Foundation category unified what was previously called "Core" and "Shared", removing an ambiguity that had confused engineers. Third-party packages were brought inside the rules for the first time, giving us a formal way to enforce what was previously only a convention. What this unlocks is small but consequential. `APIClient` depending on `DateFormatting` is now an ordinary Foundation → Foundation edge, not a special case requiring explanation. New shared component like `CountdownTimer` are simply Foundation modules. A team proposing to add `Firebase` as a dependency of a Feature module will get a CI failure, not just a code review comment. Architectures rarely change in dramatic leaps. Most meaningful improvements are renamings, small relaxations, and rules written more precisely than before. We hope this one proves as durable as the last. ### Luca: A Decentralized Tool and Skills Manager for the AI-Augmented Developer Workflow URL: https://albertodebortoli.com/2026/04/13/luca-a-decentralized-tool-and-skills-manager-for-the-ai-augmented-developer-workflow/ Last updated: 2026-07-15T18:50:50.000Z Most developers never question where their CLI tools come from. You run `brew install swiftlint`, it works, and you move on. But in teams of any size, the question "which version of SwiftLint are you running?" surfaces regularly. Someone upgrades globally, a build breaks on CI, and the team gets distracted for hours to figure out what changed. At [Just Eat Takeaway](https://www.justeattakeaway.com/?ref=albertodebortoli.com), a couple of years ago I built an internal tool called ToolManager that solved exactly this problem for our iOS teams. It pinned tool versions per project, downloaded pre-built binaries, and stayed out of the way. It worked well — well enough that I wrote about the concept in [How to Implement a Decentralised CLI Tool Manager](https://albertodebortoli.com/2025/07/13/how-to-implement-a-decentralised-cli-tool-manager/). That article documented the design principles and the reasoning behind a decentralised approach. The experience I built at JET and the feedback from the community convinced me to take the idea further: [**Luca**](https://luca.tools/?ref=albertodebortoli.com) **is a proper open-source project, built from scratch**, expanding the original concept into something broader — including skills management for AI coding agents. ## The problem with centralized tool management Homebrew is excellent at what it does. But it was designed to manage system-wide software, not project-specific developer tools. It installs one version of SwiftLint globally. If project A needs 0.53.0 and project B needs 0.62.0, you are left with some confusing Homebrew gymnastics. [Mint](https://github.com/yonaskolb/Mint?ref=albertodebortoli.com) solves the versioning problem for Swift packages — and it does it well. But it builds from source, which requires a full Swift toolchain and can be slow. It is also limited to Swift packages. In practice, teams use tools written in Go, Rust, Python, Zig, and more. A version manager that only covers one language leaves gaps. Tools like [mise](https://mise.jdx.dev/?ref=albertodebortoli.com) take a broader approach, combining tool management with environment variables, task running, and a plugin system. It is powerful, but that power comes with complexity that not every team needs. > The point is not to replace Homebrew, Mint, or mise. It is to fill a gap they were never designed to fill: **project-local, version-pinned installation of pre-built binaries from any source**, with zero configuration overhead. ## Introducing Luca [Luca](https://luca.tools/?ref=albertodebortoli.com) is a lightweight tool and skills manager for macOS and Linux, written in Swift. It reads a YAML file called a `Lucafile`, downloads pre-built binaries from GitHub Releases or any URL, and symlinks them into `.luca/tools/` inside the project directory. No central registry. No building from source. No global PATH pollution. Read the [**Manifesto**](https://luca.tools/manifesto.html?ref=albertodebortoli.com). Install it with a single command: ```bash curl -fsSL https://luca.tools/install.sh | bash ``` Then create a `Lucafile` in your project: ```yaml --- tools:   - name: FirebaseCLI     version: 14.12.1     url: https://github.com/firebase/firebase-tools/releases/download/v14.12.1/firebase-tools-macos   - name: SwiftLint     binaryPath: SwiftLintBinary.artifactbundle/swiftlint-0.61.0-macos/bin/swiftlint     version: 0.61.0     url: https://github.com/realm/SwiftLint/releases/download/0.61.0/SwiftLintBinary.artifactbundle.zip   - name: Tuist     binaryPath: tuist     version: 4.80.0     url: https://github.com/tuist/tuist/releases/download/4.80.0/tuist.zip ``` The `binaryPath` field handles nested archives — when the binary lives inside a subdirectory of the zip. For direct executables, you can omit it entirely. Optional `checksum` and `algorithm` fields (supporting MD5, SHA1, SHA256, SHA512) let you verify integrity. ## How it works You can install tools directly from GitHub Releases: ``` luca install TogglesPlatform/ToggleGen@1.0.0 ``` or with a Lucafile: ``` luca install ``` Run `luca install` and Luca downloads each tool at `~/.luca/tools/{ToolName}/{versio}/` — a global cache shared across projects — and creates symlinks in `.luca/tools/` inside your project directory. Different projects can pin different version of the same tool without ```bash luca install # Tools are immediately available swiftlint --version   # 0.61.0 tuist --help ``` Two automation features make it practical for daily use. A **shell hook** (sourced from `~/.luca/shell_hook.sh`) automatically prepends `.luca/tools/` to your PATH when you `cd` into a project — and removes it when you leave. A **git post-checkout hook** runs `luca install` automatically after `git checkout` or `git switch`, so tools stay in sync with the branch. Switch to a branch that pins SwiftLint 0.62.0 and it is there without thinking about it. You can also install tools directly from GitHub Releases without a Lucafile: ```bash luca install TogglesPlatform/ToggleGen@1.0.0 ``` ## Skills management: the second dimension Tool management alone would justify Luca's existence, but the developer landscape has shifted. AI coding agents — Claude Code, Cursor, GitHub Copilot, Gemini CLI, Windsurf, and dozens more — now read project-local Markdown files as "skills" or "instructions" that shape their behaviour. The problem is fragmentation. Each agent stores skills in a different directory: `.claude/skills/`, `.cursor/skills/`, `.agents/skills/`, `.windsurf/skills/`, and so on. Luca currently supports more then 4o agents. Installing the same skill for multiple agents means copying files to multiple locations — a tedious and error-prone process that no one should do manually. Luca solves this with the `skills` and `agents` sections of the Lucafile: ```yaml --- tools:   - name: SwiftLint     binaryPath: SwiftLintBinary.artifactbundle/swiftlint-0.61.0-macos/bin/swiftlint     version: 0.61.0     url: https://github.com/realm/SwiftLint/releases/download/0.61.0/SwiftLintBinary.artifactbundle.zip repos:   vercel: vercel-labs/agent-skills agents:   - claude-code   - cursor skills:   - name: swift-testing-expert     repository: AvdLee/Swift-Testing-Agent-Skill   - name: frontend-design     repository: vercel   - name: skill-creator     repository: vercel ``` The `repos` section defines shorthand aliases so you do not repeat full repository paths. Skills are Markdown files with YAML frontmatter, hosted in Git repositories — following the convention established by [Vercel Labs' agent-skills](https://github.com/vercel-labs/agent-skills?ref=albertodebortoli.com). The `agents` list controls which agents receive the skill files; omit it to target all the known agents. You can also install skills directly from the command line: ```bash luca install vercel-labs/agent-skills luca install AvdLee/Swift-Concurrency-Agent-Skill \ --skill swift-concurrency \ --agent claude-code ``` ## CI integration Luca ships a GitHub Action — [setup-luca](https://github.com/marketplace/actions/setup-luca?ref=albertodebortoli.com) — that installs Luca and your project's tools in two lines: ```yaml steps:   - uses: actions/checkout@v4   - uses: LucaTools/setup-luca@v1     with:       spec: Lucafile   - run: swiftlint --version ``` For tool authors, the companion repository [LucaWorkflows](https://github.com/LucaTools/LucaWorkflows?ref=albertodebortoli.com) provides ready-to-use GitHub Actions workflows to build, package, and publish Luca-compatible releases. Push a tag and get a release with macOS universal and Linux binaries — templates are available for Swift, Go, Rust, Python, C#, and Zig. ## Where Luca sits today Luca is a young project and I want to be upfront about that. It works well for the use cases it targets — pre-built binary distribution and AI agent skill management — but it is not trying to replace Homebrew or any general-purpose package manager. The entire ecosystem — [Luca](https://github.com/LucaTools/Luca?ref=albertodebortoli.com), [setup-luca](https://github.com/LucaTools/setup-luca?ref=albertodebortoli.com), [LucaWorkflows](https://github.com/LucaTools/LucaWorkflows?ref=albertodebortoli.com) — is open source under the Apache 2.0 license. Luca is written in Swift 6 with strict concurrency checking and has full test coverage on both macOS and Linux. [Documentation](https://luca.tools/Luca/documentation/lucacli/?ref=albertodebortoli.com) and [tutorials](https://luca.tools/Luca/tutorials/luca/?ref=albertodebortoli.com) are available at [luca.tools](https://luca.tools/?ref=albertodebortoli.com). What started as an internal tool at Just Eat Takeaway — one that proved the concept in production across multiple iOS teams — became a [blog post](https://albertodebortoli.com/2025/07/13/how-to-implement-a-decentralised-cli-tool-manager/) that documented the design principles, and eventually an open-source project that takes those ideas further. At JET, we eventually replaced ToolManager with Luca to leverage its feature set. In the latest release at the time of writing (April 2026), Luca appears solid with various edge cases covered. > I believe the right tool manager is the one that stays out of your way. It pins versions in a file, downloads pre-built binaries, and disappears into the background. If it does its job, you forget it is there — until you switch branches and everything just works. The project lives at [github.com/LucaTools](https://github.com/LucaTools?ref=albertodebortoli.com), with documentation at [luca.tools](https://luca.tools/?ref=albertodebortoli.com). Contributions and feedback are welcome. Find me on [X](https://x.com/albertodebo?ref=albertodebortoli.com) / [Bluesky](https://bsky.app/profile/albertodebo.bsky.social?ref=albertodebortoli.com) / [LinkedIn](https://www.linkedin.com/in/albertodebortoli/?ref=albertodebortoli.com). ### Universal Links At Scale: The Challenges Nobody Talks About URL: https://albertodebortoli.com/2026/01/15/universal-links-at-scale-the-challenges-nobody-talks-about/ Last updated: 2026-01-16T09:04:30.000Z *A deep dive into the practical challenges of implementing, testing, and maintaining Universal Links at scale* --- **Originally published on the* [**Just Eat Takeaway Engineering Blog*](https://medium.com/justeattakeaway-tech/universal-links-at-scale-the-challenges-nobody-talks-about-bab45d557d8b?ref=albertodebortoli.com)**.* Universal Links have been around since iOS 9 (2015), yet the topic remains surprisingly underrated in the iOS community. While most developers understand the basic concept (associating your website with your app so links open directly in the app) the practical challenges of implementing and maintaining Universal Links at scale are rarely discussed. When a user taps a Universal Link, iOS checks if the domain is associated with any installed app. If it is, iOS opens the app directly. If not, it opens the link in the browser. This "universal" behavior makes them superior to custom URL schemes (deep links) for user-facing communications. However, despite their importance, many developers treat AASA files as simple configuration files, overlooking the complex challenges involved in validating, testing, and maintaining them at scale. In 2024, I put a lot of effort into crafting a solid solution for some overlooked challenges surrounding universal links. Every time I refer back to that work, I am impressed by how well it has served the company, which constantly renews my desire to write about it. GenAI has become incredibly helpful with the drafting process, so I finally have no excuse not to share this story! In this post, I'll walk through the real-world challenges I've encountered and the solutions I've developed over the years working with Universal Links across multiple web domains and localized applications. ## The Basics: What Makes Universal Links Work? Universal Links work through a combination of three pieces: 1. **Associated Domains Entitlement** in your app (the `.entitlements` file) 2. **Apple App Site Association (AASA) file** on your website 3. **Proper handling** of incoming links in your app code The AASA file must be served from `/.well-known/apple-app-site-association` over HTTPS, without redirects, and with the correct content type (`application/json`). Here's a simple AASA file: ```json { "applinks": { "details": [ { "appIDs": ["TEAMID.com.example.app"], "components": [ { "/": "/account/login" }, { "/": "/restaurants/*" } ] } ] } } ``` What most tutorials don't tell you is that this is just the beginning. Real-world AASA files are far more complex, and validating them is a challenge in itself. The reality gets complicated when you need to: - Validate that your AASA file respects a schema - Test links before deploying to production - Handle dynamic URL patterns with substitution variables - Ensure Apple's CDN has picked up your latest changes - Parse and match wildcard patterns correctly - Handle encoding and special characters ## Challenge 1: Nobody Validates Against a JSON Schema Here's a dirty secret: **most AASA files in production have never been validated against a schema**. Teams deploy files, hope for the best, and only discover issues when links stop working. **Why It Matters:** An invalid AASA file might be served successfully but fail to associate your app with your website. iOS won't throw errors; Universal Links simply won't work, and you might not notice until users report issues. Online validators like [branch.io/resources/aasa-validator](https://branch.io/resources/aasa-validator/?ref=albertodebortoli.com) and [getuniversal.link](https://getuniversal.link/?ref=albertodebortoli.com) check basic accessibility and JSON parsing, but they don't validate the actual schema. A file can be valid JSON yet completely invalid as an AASA file. ### The Solution: JSON Schema Validation in CI Create a comprehensive JSON Schema that validates the entire AASA structure, including: - Required fields (`applinks`, `details`, `appIDs`, `components`) - Optional fields (`substitutionVariables`, `exclude`, `caseSensitive`, `percentEncoded`) - Proper nesting and data types - Support for other AASA features (`webcredentials`, `appclips`, `activitycontinuation`) Here's a schema that defines the correct structure of an AASA file (just for the `applinks` section) : ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", "properties": { "applinks": { "type": "object", "properties": { "defaults": { "type": "object", "properties": { "caseSensitive": { "type": "boolean" }, "percentEncoded": { "type": "boolean" } } }, "details": { "type": "array", "items": { "type": "object", "properties": { "appIDs": { "type": "array", "items": { "type": "string" } }, "components": { "type": "array", "items": { "type": "object", "properties": { "/": { "type": "string" }, "?": { "type": "object" }, "#": { "type": "string" }, "exclude": { "type": "boolean" }, "caseSensitive": { "type": "boolean" }, "percentEncoded": { "type": "boolean" } }, "required": ["/"] } }, "defaults": { "type": "object", "properties": { "caseSensitive": { "type": "boolean" }, "percentEncoded": { "type": "boolean" } } } } } }, "substitutionVariables": { "type": "object" } }, "required": ["details"] } }, "required": ["applinks"] } ``` Integrating schema validation into your CI pipeline ensures invalid files never reach production. This catches issues like: - Missing required fields - Wrong types (string instead of array) - Typos in property names (which would be silently ignored) - Invalid component structures You might want to consider building a Swift CLI tool with Argument Parser, in which case I would suggest using [JSONSchema.swift](https://github.com/kylef/JSONSchema.swift?ref=albertodebortoli.com). ## Challenge 2: The Apple CDN Layer Here's something that surprises many developers: **iOS doesn't fetch the AASA file directly from your website**. Instead, Apple operates a CDN that caches AASA files from websites. The CDN URL follows this pattern: ``` https://app-site-association.cdn-apple.com/a/v1/ ``` For example, for `just-eat.co.uk`: - **Website**: `https://just-eat.co.uk/.well-known/apple-app-site-association` - **Apple CDN**: `https://app-site-association.cdn-apple.com/a/v1/just-eat.co.uk` **Why It Matters:** This caching happens periodically (every few hours), and there's no guarantee that your latest changes are immediately available. If your website's AASA file differs from what's cached on Apple's CDN, Universal Links may not work as expected. You might deploy a fix, but iOS devices could still be using the old cached version for hours or even days. ### The Solution: CDN Validation To ensure your AASA file has propagated correctly, you need to compare the file on your website with the one on Apple's CDN. This validates that: 1. Your file is publicly accessible and has a valid SSL certificate 2. The file has the correct MIME type (`application/json`) 3. Apple's CDN has successfully cached your latest version Here's a validator that does exactly this: ```swift struct AASAContent: Equatable, Decodable { let appLinks: AppLinks enum CodingKeys: String, CodingKey { case appLinks = "applinks" } // and nested Decodable structs } enum AASAFileLocation { case website case appleCdn func buildURL(with domain: Domain) throws -> URL { switch self { case .website: return URL(string: "https://\(domain)")! .appendingPathComponent(".well-known") .appendingPathComponent("apple-app-site-association") case .appleCdn: return URL(string: "https://app-site-association.cdn-apple.com/a/v1/")! .appendingPathComponent(domain) } } } func validateCDN(for domain: Domain) async throws { let websiteURL = try AASAFileLocation.website.buildURL(with: domain) let appleCdnURL = try AASAFileLocation.appleCdn.buildURL(with: domain) let domainFile: AASAContent = try await downloadFile(url: websiteURL) let appleFile: AASAContent = try await downloadFile(url: appleCdnURL) guard domainFile == appleFile else { throw ValidateCDNError.fileMismatch(domain: domain) } } ``` Running this validation daily in CI ensures you're alerted when CDN synchronization fails or is delayed. A daily automated check can alert you if there's a mismatch, allowing you to investigate and resolve issues before they impact users. This simple check has prevented numerous incidents where teams assumed links were working when they weren't. ### Developer Mode Bypass For development and debugging, iOS offers a bypass. By adding `?mode=developer` to your associated domain: ```xml applinks:just-eat.co.uk?mode=developer ``` Debug builds should use a specific entitlements file where the developer mode is used. Debug builds will fetch the AASA file directly from your domain, bypassing the CDN. This requires enabling "Associated Domains Development" in iOS Settings → Developer. App Store builds always use the CDN and their entitlements file shouldn't mention the developer mode. ## Challenge 3: Regular Expression Parsing and Pattern Matching The AASA file supports powerful pattern matching through wildcards for flexible URL matching. However, these patterns aren't standard regex and use Apple's own pattern syntax that needs to be converted to regular expressions for validation. **The Pattern Syntax:** - `*` matches zero or more characters (converted to `.*` in regex) - `?` matches exactly one character (converted to `.` in regex) - `?*` matches one or more characters (converted to `.+` in regex) - `*?` also matches one or more characters (converted to `.+` in regex) **The Problem:** I couldn't find any online tool or open-source library implementing Apple's matching logic. If you want to validate that specific URLs match your AASA file patterns (for testing or regression prevention), you need to correctly parse and convert these patterns. Online validators like [Branch.io's AASA validator](https://branch.io/resources/aasa-validator/?ref=albertodebortoli.com) don't support this matching logic, they only validate the file structure. ### The Solution: Implementing Apple's Matching Logic I built a custom validator that implements the matching rules. The key insight is that Apple's wildcards map to regular expressions. A naïve conversion (where the order of substitutions is important) would look like this: ```swift extension String { var regEx: String { self // One or more characters .replacingOccurrences(of: "?*", with: ".+") // One or more characters .replacingOccurrences(of: "*?", with: ".+") // Zero or more characters .replacingOccurrences(of: "*", with: ".*") // Exactly one character .replacingOccurrences(of: "?", with: ".") } } ``` Additionally, you need to handle URL components properly. For example, if a pattern specifies only a path (`/restaurants/*`), you should still match URLs that have query parameters or fragments, unless explicitly excluded. This requires careful construction of the regex pattern to account for optional components, which to be completely honest was very tricky to implement by hand at a time when LLMs weren't too helpful. ## Challenge 4: The `substitutionVariables` Problem Apple supports [applinks.substitutionVariables](https://developer.apple.com/documentation/bundleresources/applinks/substitutionvariables-swift.dictionary?ref=albertodebortoli.com) for dynamic URL matching. This feature allows you to define variables that can be used in path, query, and fragment components. Substitution variables are particularly helpful to reduce duplication when dealing with URLs that are localised per language. However, I couldn't find any online validator or open-source tool to support validating links against AASA files that use substitution variables. Here's a real-world example from a multi-language website: ```json { "applinks": { "substitutionVariables": { "menu": ["speisekarte", "menu"], "stamp-cards": ["stempelkarten", "stamp-cards", "cartes-épargne", "stempelkaarten"] }, "details": [{ "appIDs": ["TEAMID.com.example.app"], "components": [ { "/": "/$(lang)/$(menu)/?*" }, { "/": "/", "#": "$(stamp-cards)" }, { "/": "/", "#": "$(order-history)" } ] }] } } ``` The pattern `/$(lang)/$(menu)/?*` should match URLs like: - `/de/speisekarte/restaurant-name` - `/en/menu/restaurant-name` - `/fr/menu/pizza-place` And `/#$(stamp-cards)` should match: - `/#stempelkarten` - `/#stamp-cards` - `/#cartes-épargne` **Why It Matters:** Without proper support for substitution variables, you can't validate that your Universal Links work correctly. You might think a URL should match, but if the substitution isn't handled correctly, it won't. ### The Solution: Substitution Variable Expansion Implement substitution variable expansion before pattern matching. Here is a trimmed down example: 1. **Parse substitution variables** from the AASA file 2. **Replace variable references** (`$(variableName)`) with regex alternatives of their possible values 3. **Handle default variables** like `$(lang)` and `$(region)` which match any two characters 4. **Apply the expanded pattern** to URL matching after converting Apple's pattern syntax to standard regex The key insight is that substitution variables create a disjunction (OR) of possible values: ```swift func replaceWithSubstitutionVariables(_ substitutionVariables: [String: [String]]) -> String { var modifiedString = self let substitutionVariablesWithDefaults = substitutionVariables.merging(defaultSubstitutionVariables) { (current, _) in current } for (key, values) in substitutionVariablesWithDefaults { let pattern = "\\$\\(\(key)\\)" let replacement = "(\(values.joined(separator: "|")))" // Replace $(key) with (value1|value2|value3) modifiedString = modifiedString.replacingOccurrences( of: pattern, with: replacement, options: .regularExpression ) } return modifiedString } private var defaultSubstitutionVariables: [String: [String]] { [ "lang": [".."], "region": [".."] ] } ``` So `/$(lang)/$(menu)/?*` with the variables above becomes: ``` /(..)/((speisekarte|menu))/.+ ``` Note: `$(lang)` and `$(region)` are special default variables Apple provides that match any two characters. **The order of operations matters**: first expand substitution variables, then convert Apple's pattern syntax (`*`, `?`, `?*`) to standard regex. This ensures that wildcards within substitution variable values are handled correctly. ### The Full Matching Pipeline The complete validation process: 1. Parse the AASA file and extract components for the target bundle ID 2. For each component, build a regex pattern by: - Replacing substitution variables with alternations - Converting `*` and `?` to regex equivalents - Handling paths, query parameters, and fragments 3. Match incoming URLs against these patterns 4. Account for the `exclude` flag that explicitly prevents matching The matcher also needs to handle edge cases: - URLs with query parameters not specified in the component (allowed) - URLs with fragments not specified in the component (allowed) - The `exclude: true` flag that creates negative matches - Case sensitivity settings - Percent encoding Here's the core matching logic: ```swift enum AllowPolicy { case allowed case notAllowed } func validateDeepLinking( policy: AllowPolicy, for url: URL, domain: Domain, components: [AASAContent.AppLinks.Detail.Component], substitutionVariables: AASAContent.AppLinks.SubstitutionVariables ) throws { switch policy { case .allowed: for component in components { let regEx = try regEx(for: component, substitutionVariables: substitutionVariables, on: domain) if findMatch(for: url, in: regEx) { if component.exclude != true { return } else { throw ValidateUniversalLinkError.excludedUniversalLink(url: url) } } } throw ValidateUniversalLinkError.unhandledUniversalLink(url: url) case .notAllowed: for component in components { let regEx = try regEx(for: component, substitutionVariables: substitutionVariables, on: domain) if findMatch(for: url, in: regEx) { if component.exclude == true { return } else { throw ValidateUniversalLinkError.incorrectlyHandledUniversalLink(url: url) } } } } } ``` **Important**: Components are evaluated in order, and the first match wins. This means exclusion rules must come before the broader patterns they're excluding from. ## Challenge 5: Testing Before Production One of the trickiest aspects of Universal Links is testing. You can't just deploy to production and hope it works. Testing requires the AASA file to be hosted on a real domain with proper SSL certificates. You can't just test locally or in a simulator without additional setup. **Why It Matters:** Deploying untested AASA changes to production can break Universal Links for all users. Since Apple caches AASA files, fixing issues can take hours or days to propagate. ### The Solution: A Staging Environment with Real Domains Set up a staging environment using AWS infrastructure (or similar): 1. **S3 bucket** to host AASA files 2. **CloudFront distributions** for each staging domain with HTTPS 3. **Route53 records** pointing staging subdomains to CloudFront The staging domains follow a pattern like: ``` lieferando-de.aasa-staging.mobile-team.example.com ``` This mirrors the production domain `lieferando.de` and serves the same AASA file structure. Debug builds include both staging and production domains in its entitlements: ```xml com.apple.developer.associated-domains applinks:lieferando-de.aasa-staging.mobile-team.example.com applinks:lieferando.de applinks:www.lieferando.de ``` Now you can test a link like: ``` https://lieferando-de.aasa-staging.mobile-team.example.com/menu/pizzeria ``` Instead of: ``` https://lieferando.de/menu/pizzeria ``` The staging URL opens your debug/ad-hoc build exactly like the production URL would open your App Store build, but without risking production changes. ### Important Testing Considerations - **TestFlight builds** are production builds and therefore use production entitlements and won't work with staging domains - **Ad-hoc and debug builds** can include staging domains - **Simulator testing** has limitations. Universal Links work best on physical devices - **Developer mode** must be enabled on device for direct AASA fetching (bypassing CDN) - For **non-debug builds**, you'll need to wait for Apple's CDN to cache your staging AASA file ## Challenge 6: Regression Testing and Comprehensive Link Validation Manual testing doesn't scale. Every change to the AASA file needs verification across dozens or hundreds of URLs. And you need to verify both: 1. URLs that **should** open the app (deep-linkable) 2. URLs that **should not** open the app (non-deep-linkable, excluded) 3. Complex URLs with query parameters, fragments, and wildcards **Why It Matters:** Without comprehensive validation, you might: - Miss URLs that should deep link but don't - Accidentally deep link URLs that should open in the browser - Break existing functionality when making changes ### The Solution: Automated Content Validation I maintain JSON files alongside each AASA file listing the expected behavior: ```json { "deep_linkable_urls": [ "https://lieferando.de/", "https://lieferando.de/punkte", "https://lieferando.de/#stempelkarten", "https://lieferando.de/en#stamp-cards", "https://lieferando.de/lieferservice/essen/berlin-10115", "https://lieferando.de/en/delivery/food/berlin-10115" ], "non_deep_linkable_urls": [ "https://lieferando.de/?openOnWeb=true", "https://lieferando.de/anyPath?openOnWeb=true" ] } ``` The validator ensures: - Every URL in `deep_linkable_urls` matches a non-excluded component - Every URL in `non_deep_linkable_urls` either doesn't match or matches an excluded component This runs on CI on every pull request. Changes to the AASA file must include updates to the expected URLs, creating living documentation of what's supported. ### Error Types The validator catches several error conditions: 1. **Unhandled URL**: A URL expected to be deep-linkable doesn't match any component 2. **Excluded URL**: A URL expected to be deep-linkable matches an excluded component 3. **Incorrectly Handled URL**: A URL expected to be non-deep-linkable actually matches a component Each error provides clear diagnostics: ```swift enum ValidateUniversalLinkError: Error { case unhandledUniversalLink(url: URL) case excludedUniversalLink(url: URL) case incorrectlyHandledUniversalLink(url: URL) // ... } ``` ## Challenge 7: Encoding and Special Characters Universal Links often contain special characters, especially for localized content: ``` https://lieferando.at/fr#cartes-épargne ``` The fragment `cartes-épargne` contains an accented character. Here's the critical rule: **Universal Links must NOT be percent-encoded in the AASA file or when shared with users.** ✅ Correct: `https://lieferando.at/fr#cartes-épargne` ❌ Wrong: `https://lieferando.at/fr#cartes-%C3%A9pargne` However, when these URLs are processed by `URLComponents` in Swift, they may get encoded. The validator must handle both forms and compare them correctly: ```swift private func findMatch(for url: URL, in regEx: NSRegularExpression) -> Bool { let searchString = url.absoluteString.removingPercentEncoding! let searchRange = NSRange(location: 0, length: searchString.utf16.count) if let result = regEx.firstMatch(in: searchString, options: [.anchored], range: searchRange) { return result.range.length == searchRange.length } return false } ``` The key is to decode the URL before matching against the regex. ## Putting It All Together: The Complete Validation Pipeline To address all these challenges, I built **AASAValidator**, a Swift command-line tool that provides three main commands: 1. **`validate-schema`**: Validates AASA files against a JSON schema 2. **`validate-cdn`**: Compares your website's AASA file with Apple's CDN version 3. **`validate-universal-links`**: Validates that specific URLs match (or don't match) your AASA file patterns The tool handles: - JSON schema validation - CDN comparison - Regular expression parsing and conversion - Substitution variable expansion - Complex URL matching with query parameters and fragments - Exclusion validation - Percent encoding ### The Full Automation Pipeline #### 1\. PR Validation (CI) - **Schema validation**: Ensure AASA files are structurally correct - **Content validation**: Verify all expected URLs match (or don't match) correctly - **Bundle ID validation**: Ensure the target app's bundle ID is in the AASA #### 2\. Post-Deployment (Staging) - Deploy AASA files to staging environment - Test Universal Links on physical devices with staging builds - Verify end-to-end behavior #### 3\. Post-Deployment (Production) - Deploy AASA files to production - **CDN validation**: Check that Apple's CDN has the latest version - Smoke test with production app #### 4\. Ongoing Monitoring - Daily CDN synchronization checks - Alerting if files fall out of sync ### Example GitHub Actions Integration Here's how you might use the validator in a GitHub Actions workflow: ```yaml strategy: fail-fast: false matrix: domain: - just-eat.co.uk - just-eat.es bundle-id: - com.eatch.mobileapp - com.justeat.JUSTEAT - com.takeaway.lu steps: - name: Validate AASA Schema run: | AASAValidator validate-schema \ --aasa-path ./${{ matrix.domain }}-aasa.json \ --json-schema-path path/to/JSONSchema.json - name: Validate Universal Links run: | AASAValidator validate-universal-links \ --bundle-id ${{ matrix.bundle-id }} \ --domain ${{ matrix.domain }} \ --aasa-path ./${{ matrix.domain }}-aasa.json \ --universal-links-path ./universal-links/${{ matrix.domain }}.json ``` ## Key Takeaways 1. **Validate against a schema**: JSON parsing success doesn't mean your AASA file is valid. Use JSON Schema validation in CI to catch structural errors early. 2. **Don't trust the CDN blindly**: Always verify that Apple's CDN has your latest AASA file. Implement automated daily checks. 3. **Implement custom regex parsing**: No existing tool handles Apple's wildcard pattern syntax. Build your own matcher to validate URL matching. 4. **Test substitution variables properly**: No existing tool handles `applinks.substitutionVariables`. Build your own matcher or use custom validation logic. 5. **Implement proper staging**: Set up real staging domains with HTTPS. Testing Universal Links requires real infrastructure. 6. **Automate regression testing**: Maintain lists of expected URLs and validate them automatically. Manual testing doesn't scale. 7. **Watch your encoding**: Special characters must not be percent-encoded in Universal Links. Handle encoding carefully in your validation logic. 8. **Order matters for exclusions**: Components are evaluated in order. Place exclusion rules before broader patterns. 9. **Plan for multi-language support**: If your website supports multiple languages, your AASA file needs substitution variables to handle localized URLs. ## Conclusion Universal Links are deceptively simple on the surface but require careful attention to detail in practice. The challenges we've discussed (schema validation, CDN synchronization, regex parsing, substitution variables, staging environments, comprehensive link validation, and encoding) are often ignored but are essential for a robust implementation. By building tooling that addresses these challenges and integrating validation into your development workflow, you can ensure that Universal Links work reliably for your users. The investment in proper validation and testing pays off by preventing production issues and giving you confidence when making changes to your AASA files. Consider implementing similar validation for your own Universal Links setup and your future self will thank you. ### How to Implement a Decentralised CLI Tool Manager URL: https://albertodebortoli.com/2025/07/13/how-to-implement-a-decentralised-cli-tool-manager/ Last updated: 2025-09-30T21:40:39.000Z SPONSORED Based on this article, I've published Luca! A lightweight decentralised tool manager for macOS to manage project-specific tool environments. 👶 Check it out: [luca.tools](https://luca.tools/?ref=albertodebortoli.com) ## Overview It's common for iOS teams to rely on various CLI tools such as SwiftLint, Tuist, and Fastlane. These tools are often installed in different ways. The most common way is to use [Homebrew](https://brew.sh/?ref=albertodebortoli.com), which is known to lack version pinning and, as Pedro [puts it](https://tuist.dev/blog/2023/12/15/rtx-default/?ref=albertodebortoli.com): > Homebrew is not able to install and activate multiple versions of the same tool I also fundamentally dislike the `tap` system for installing dependencies from third-party repositories. Although I don't have concrete data, I feel that most development teams profoundly dislike Homebrew when used beyond the simple installation of individual tools from the command line and the [brew taps](https://docs.brew.sh/Taps?ref=albertodebortoli.com) system is cumbersome and bizarre enough to often discourage developers from using it. Alternatives to manage sets of CLI tools that got traction in the past couple of years are [Mint](https://github.com/yonaskolb/Mint?ref=albertodebortoli.com) and [Mise](https://mise.jdx.dev/lang/swift.html?ref=albertodebortoli.com). As Pedro again says in [his article about Mise](https://tuist.dev/blog/2025/02/04/mise?ref=albertodebortoli.com): > The first and most core feature of Mise is the ability to install and activate [dev tools](https://mise.jdx.dev/dev-tools/?ref=albertodebortoli.com). Note that we say "activate" because, unlike Homebrew, Mise differentiates between installing a tool and making a specific version of it available. While beyond the scope of this article, I recommend a great article about [installing Swift executables from source with Mise](https://swifttoolkit.dev/posts/mise-swift?ref=albertodebortoli.com) by [Natan Rolnik](https://x.com/natanrolnik?ref=albertodebortoli.com). In this article I describe a CLI tool manager very similar to what I've implemented for my team. I'll simply call it "ToolManager". The tool is designed to: 1. Support installing any external CLI tool distributed in zip archives 2. Support activating specific versions per project 3. Be decentralised (requiring no registry) I believe the decentralisation is an interesting aspect and makes the tool reusable in any development environment. Also, differently from the design of mise and mint, ToolManager doesn't build from source and rather relies on pre-built executables. In the age of GenAI, it's more important than ever to develop critical thinking and learn how to solve problems. For this reason, I won't show the implementation of ToolManager, as it's more important to understand how it's meant to work. The code you'll see in this article supports the overarching design, not the nitty-gritty details of how ToolManager's commands are implemented. If, by the end of the article, you understand how the system should work and are interested in implementing it (perhaps using GenAI), you should be able to convert the design to code fairly easily—hopefully, without losing the [joy of coding](https://annievella.com/posts/the-software-engineering-identity-crisis/?ref=albertodebortoli.com). I myself am considering implementing ToolManager as an open source project later, as I believe it might be very helpful to many teams, just as its incarnation was (and continues to be) for the platform team at JET. There doesn't seem to be an existing tool with the design described in this article. A different title could have reasonably placed this article in "The easiest X" "series" ([1](https://albertodebortoli.com/2023/03/26/toggles/), [2](https://albertodebortoli.com/2018/02/12/the-easiest-promises-in-swift/), [3](https://albertodebortoli.com/2018/12/16/the-easiest-state-machine-in-swift/), [4](https://albertodebortoli.com/2016/08/05/the-easiest-core-data/)), if I may say so. ## Design The point here is to learn what implementing a tool manager entails. I'll therefore describe the MVP of ToolManager, leaving out details that would make the design too straightforward to implement. The tool itself is a CLI and it's reasonably implemented in Swift using [ArgumentParser](https://github.com/apple/swift-argument-parser?ref=albertodebortoli.com) like all modern Swift CLI tools are. In its simplest form, ToolManager exposes 3 commands: - `install`: - download and installs the tools defined in a spec file (`Toolfile.yml`) at `~/.toolManager/tools` optionally validating the checksum - creates symlinks to the installed versions at `$(PWD)/.toolManager/active` - `uninstall`: - clears the entire or partial content of `~/.toolManager/tools` - clears the content of `$(PWD)/.toolManager/active` - `version`: - returns the version of the tool The `install` commands allows to specify the location of the spec file using the `--spec` flag, which defaults to `Toolfile.yml` in the current directory. The installation of ToolManager should be done in the most raw way, i.e. via a remote script. It'd be quite laughable to rely on Brew, wouldn't it? This practice is commonly used by a variety of tools, for example originally by Tuist (before the introduction of Mise) and... you guessed it... by Brew. We'll see below a basic script to achieve so that you could host on something lik AWS S3 with the desired public permissions. The installation command would be: ```bash curl -Ls 'https://my-bucket.s3.eu-west-1.amazonaws.com/install_toolmanager.sh' | bash ``` The version of ToolManager must be defined in the `.toolmanager-version` file in order for the installation script of the repo to work: ```bash echo "1.2.0" > .toolmanager-version ``` ToolManager manages versions of CLI tools but it's not in the business of managing its own versions. Back in the day, Tuist used to use [tuistenv](https://tuist.dev/blog/2023/12/15/rtx-default?ref=albertodebortoli.com) to solve this problem. I simply avoid it and have single version of ToolManager available at `/usr/local/bin/` that the installation script overrides with the version defined for the project. The `version` command is used by the script to decide if a download is needed. > There will be only one version of ToolManager in the system at a given time, and that's absolutely OK. At this point, it's time to show an example of installation script: ```bash #!/bin/bash set -euo pipefail # Fail fast if essential commands are missing. command -v curl >/dev/null || { echo "curl not found, please install it."; exit 1; } command -v unzip >/dev/null || { echo "unzip not found, please install it."; exit 1; } readonly EXEC_NAME="ToolManager" readonly INSTALL_DIR="/usr/local/bin" readonly EXEC_PATH="$INSTALL_DIR/$EXEC_NAME" readonly HOOK_DIR="$HOME/.toolManager" readonly REQUIRED_VERSION=$(cat .toolmanager-version) # Exit if the version file is missing or empty. if [[ -z "$REQUIRED_VERSION" ]]; then echo "Error: .toolmanager-version not found or is empty." >&2 exit 1 fi # Exit if the tool is already installed and up to date. if [[ -f "$EXEC_PATH" ]] && [[ "$($EXEC_PATH version)" == "$REQUIRED_VERSION" ]]; then echo "$EXEC_NAME version $REQUIRED_VERSION is already installed." exit 0 fi # Determine OS and the corresponding zip filename. case "$(uname -s)" in Darwin) ZIP_FILENAME="$EXEC_NAME-macOS.zip" ;; Linux) ZIP_FILENAME="$EXEC_NAME-Linux.zip" ;; *) echo "Unsupported OS: $(uname -s)" >&2; exit 1 ;; esac # Download and install in a temporary directory. TMP_DIR=$(mktemp -d) trap 'rm -rf "$TMP_DIR"' EXIT # Ensure cleanup on script exit. echo "Downloading $EXEC_NAME ($REQUIRED_VERSION)..." DOWNLOAD_URL="https://github.com/MyOrg/$EXEC_NAME/releases/download/$REQUIRED_VERSION/$ZIP_FILENAME" curl -LSsf --output "$TMP_DIR/$ZIP_FILENAME" "$DOWNLOAD_URL" unzip -o -qq "$TMP_DIR/$ZIP_FILENAME" -d "$TMP_DIR" # Use sudo only when the install directory is not writable. SUDO_CMD="" if [[ ! -w "$INSTALL_DIR" ]]; then SUDO_CMD="sudo" fi echo "Installing $EXEC_NAME to $INSTALL_DIR..." $SUDO_CMD mkdir -p "$INSTALL_DIR" $SUDO_CMD mv "$TMP_DIR/$EXEC_NAME" "$EXEC_PATH" $SUDO_CMD chmod +x "$EXEC_PATH" # Download and source the shell hook to complete installation. echo "Installing shell hook..." mkdir -p "$HOOK_DIR" curl -LSsf --output "$HOOK_DIR/shell_hook.sh" "https://my-bucket.s3.eu-west-1.amazonaws.com/shell_hook.sh" # shellcheck source=/dev/null source "$HOOK_DIR/shell_hook.sh" echo "Installation complete." ``` You might have noticed that: - the required version of ToolManager (defined in `.toolmanager-version`) is downloaded from the release from the corresponding GitHub repository if missing locally. The ToolManager repo should have a GHA workflow in place to build, archive and upload the version. - a `shell_hook` script is downloaded and run to insert the following line in the shell profile: `[[ -s "$HOME/.toolManager/shell_hook.sh" ]] && source "$HOME/.toolManager/shell_hook.sh"`. This allows switching location in the terminal and loading the active tools for the current project. Showing an example of `shell_hook.sh` is in order: ```bash #!/bin/bash # Overrides 'cd' to update PATH when entering a directory with a local tool setup. # Add the project-specific bin directory to PATH if it exists. update_tool_path() { local tool_bin_dir="$PWD/.toolManager/active" if [[ -d "$tool_bin_dir" ]]; then export PATH="$tool_bin_dir:$PATH" fi } # Redefine 'cd' to trigger the path update after changing directories. cd() { builtin cd "$@" || return update_tool_path } # --- Installation Logic --- # The following function only runs when this script is sourced by an installer. install_hook() { local rc_file case "${SHELL##*/}" in bash) rc_file="$HOME/.bashrc" ;; zsh) rc_file="$HOME/.zshrc" ;; *) echo "Unsupported shell for hook installation: $SHELL" >&2 return 1 ;; esac # The line to add to the shell's startup file. local hook_line="[[ -s \"$HOME/.toolManager/shell_hook.sh\" ]] && source \"$HOME/.toolManager/shell_hook.sh\"" # Add the hook if it's not already present. if ! grep -Fxq "$hook_line" "$rc_file" &>/dev/null; then printf "\n%s\n" "$hook_line" >> "$rc_file" echo "Shell hook installed in $rc_file. Restart your shell to apply changes." fi } # This check ensures 'install_hook' only runs when sourced, not when executed. if [[ "${BASH_SOURCE[0]}" != "$0" ]]; then install_hook fi ``` Now that we have a working installation of ToolManager, let define our `Toolfile.yml` in our project folder: ```yaml --- tools: - name: PackageGenerator binaryPath: PackageGenerator version: 3.3.0 zipUrl: https://github.com/justeattakeaway/PackageGenerator/releases/download/3.3.0/PackageGenerator-macOS.zip - name: SwiftLint binaryPath: swiftlint version: 0.57.0 zipUrl: https://github.com/realm/SwiftLint/releases/download/0.58.2/portable_swiftlint.zip - name: ToggleGen binaryPath: ToggleGen version: 1.0.0 zipUrl: https://github.com/TogglesPlatform/ToggleGen/releases/download/1.0.0/ToggleGen-macOS-universal-binary.zip - name: Tuist binaryPath: tuist version: 4.48.0 zipUrl: https://github.com/tuist/tuist/releases/download/4.54.3/tuist.zip - name: Sourcery binaryPath: bin/sourcery version: 2.2.5 zipUrl: https://github.com/krzysztofzablocki/Sourcery/releases/download/2.2.5/sourcery-2.2.5.zip ``` The `install` command of ToolManager loads the Toolfile at the root of the repo and for each defined dependency, performs the following: - checks if the version of the dependency already exists on the machine - if it doesn’t exist, downloads it, unzips it, and places the binary at `~/.toolManager/tools/` (e.g. `~/.toolManager/tools/PackageGenerator/3.3.0/PackageGenerator`) - creates a symlink to the binary in the project directory from `.toolManager/active` (e.g. `.toolManager/active/PackageGenerator`) After running `ToolManager install` (or `ToolManager install --spec=Toolfile.yml`), ToolManager should produce the following structure ```bash ~ tree ~/.toolManager/tools -L 2 ├── PackageGenerator │   └── 3.3.0 ├── Sourcery │   └── 2.2.5 ├── SwiftLint │   └── 0.57.0 ├── ToggleGen │   └── 1.0.0 └── Tuist    └── 4.48.0 ``` and from the project folder ```bash ls -la .toolManager/active PackageGenerator -> /Users/alberto/.toolManager/tools/PackageGenerator/3.3.0/PackageGenerator Sourcery -> /Users/alberto/.toolManager/tools/Sourcery/2.2.5/Sourcery SwiftLint -> /Users/alberto/.toolManager/tools/SwiftLint/0.57.0/SwiftLint ToggleGen -> /Users/alberto/.toolManager/tools/ToggleGen/1.0.0/ToggleGen Tuist -> /Users/alberto/.toolManager/tools/Tuist/4.48.0/Tuist ``` Bumping the versions of some tools in the Toolfile, for example SwiftLint and Tuist, and re-running the install command, should result in the following: ```bash ~ tree ~/.toolManager/tools -L 2 ├── PackageGenerator │   └── 3.3.0 ├── Sourcery │   └── 2.2.5 ├── SwiftLint │   ├── 0.57.0 │   └── 0.58.2 ├── ToggleGen │   └── 1.0.0 └── Tuist    ├── 4.48.0    └── 4.54.3 ``` ```bash ls -la .toolManager/active PackageGenerator -> /Users/alberto/.toolManager/tools/PackageGenerator/3.3.0/PackageGenerator Sourcery -> /Users/alberto/.toolManager/tools/Sourcery/2.2.5/Sourcery SwiftLint -> /Users/alberto/.toolManager/tools/SwiftLint/0.58.2/SwiftLint ToggleGen -> /Users/alberto/.toolManager/tools/ToggleGen/1.0.0/ToggleGen Tuist -> /Users/alberto/.toolManager/tools/Tuist/4.54.3/Tuist ``` ### CI Setup On CI, the setup is quite simple. It involves 2 steps: - install ToolManager - install the tools The commands can be wrapped in GitHub composite actions: ```yaml name: Install ToolManager runs: using: composite steps: - name: Install ToolManager shell: bash run: curl -Ls 'https://my-bucket.s3.eu-west-1.amazonaws.com/install_toolmanager.sh' | bash ``` ```yaml name: Install tools inputs: spec: description: The name of the ToolManager spec file required: false default: Toolfile.yml runs: using: composite steps: - name: Install tools shell: bash run: | ToolManager install --spec=${{ inputs.spec }} echo "$PWD/.toolManager/active" >> $GITHUB_PATH ``` simply used in workflows: ```yaml - name: Install ToolManager uses: ./.github/actions/install-toolmanager - name: Install tools uses: ./.github/actions/install-tools with: spec: Toolfile.yml ``` ### CLI tools conformance ToolManager can install tools that are made available in zip files, without the need of implementing any particular spec. Depending on the CLI tool, the executable can be at the root of the zip archive or in a subfolder. Sourcery for example places the executable in the bin folder. ``` - name: Sourcery binaryPath: bin/sourcery version: 2.2.5 zipUrl: https://github.com/krzysztofzablocki/Sourcery/releases/download/2.2.5/sourcery-2.2.5.zip ``` GitHub releases are great to host releases as zip files and that's all we need. Ideally, one should decorate the repositories with appropriate release workflows. Following is a simple example that builds a macOS binary. It could be extended to also create a Linux binary. ``` name: Publish Release on: push: tags: - '*' env: CLI_NAME: my-awesome-cli-tool permissions: contents: write jobs: build-and-archive: name: Build and Archive macOS Binary runs-on: macos-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: '16.4' - name: Build universal binary run: swift build -c release --arch arm64 --arch x86_64 - name: Archive the binary run: | cd .build/apple/Products/Release/ zip -r "${{ env.CLI_NAME }}-macOS.zip" "${{ env.CLI_NAME }}" - name: Upload artifact for release uses: actions/upload-artifact@v4 with: name: cli-artifact path: .build/apple/Products/Release/${{ env.CLI_NAME }}-macOS.zip create-release: name: Create GitHub Release needs: [build-and-archive] runs-on: ubuntu-latest steps: - name: Download CLI artifact uses: actions/download-artifact@v4 with: name: cli-artifact - name: Create Release and Upload Asset uses: softprops/action-gh-release@v2 with: files: "${{ env.CLI_NAME }}-macOS.zip" ``` ### A note on version pinning Dependency management systems tend to use a lock file (like `Package.resolved` in Swift Package manager, `Podfile.lock` in the old days of CocoaPods, `yarn.lock`/`package-lock.json` in JavaScript, etc.). The benefits of using a lock file are mainly 2: 1. **Reproducibility** It locks the exact versions (including transitive dependencies) so that every team member, CI server, or production environment installs the same versions. 2. **Faster installs** Dependency managers can skip version resolution if a lock file is present, using it directly to fetch the exact versions, improving speed. We can remove the need for lock files if we pin the versions in the spec (the file defining the tools). If version range operators like the CocoaPods' optimistic operator `~>` and the SPM's `.upToNextMajor` and similar one didn't exist, usages of lock files would lose its utility. While useful, lock files are generally annoying and can create that odd feeling of seeing unexpected updates in pull requests made by others. ToolManager doesn't use a lock file; **instead**, it requires teams to pin their tools' versions, which I strongly believe is a good practice. This approach comes at the cost of teams having to keep an eye out for patch releases and not leaving updates to the machine, which risks pulling in dependencies that don't respect Semantic Versioning ([SemVer](https://semver.org/?ref=albertodebortoli.com)). ### Support for different architectures This design allows to support different architectures. Some CI workflows might only need a Linux runner to reduce the burden on precious macOS instances. Both macOS and Linux can be supported with individual Toolfile that can be specified when running the `install` command. ```bash # on macOS ToolManager install --spec=Toolfile_macOS # on Linux ToolManager install --spec=Toolfile_Linux ``` ## Conclusion The design described in this article powers the solution implemented at JET and has served our teams successfully since October 2023\. JET has always preferred to implement in-house solutions where possible and sensible, and I can say that moving away from Homebrew was a blessing. With this design, the work usually done by a package manager and a central spec repository is shifted to individual components that are only required to publish releases in zip archives, ideally via a release workflow. By decentralising and requiring version pinning, we made ToolManager a simple yet powerful system for managing the installation of CLI tools. ### How to setup a Swift Package Registry in Artifactory URL: https://albertodebortoli.com/2025/06/06/how-to-setup-a-swift-package-registry-in-artifactory/ Last updated: 2025-10-13T22:30:10.000Z ## Introduction It's very difficult to have GenAI not hallucinate when in comes to Swift Package Registry. No surprise there: the feature is definitely niche, has not been vastly adopted and there's a lack of examples online. As Dave [put it](https://iosdevweekly.com/issues/696/?ref=albertodebortoli.com), *Swift Package Registries had an even rockier start compared to SPM.* I've recently implemented a Swift Package Registry on Artifactory for my team and I thought of summarising my experience here since it's still fresh in my head. While some details are left out, the happy path should be covered. I hope with this article to help you all indirectly by providing more material to the LLMs overlords. ## Problem The main problem that led us to look into Swift Package Registry is due to SPM [deep-cloning](https://github.com/swiftlang/swift-package-manager/blob/7b98bedc91ac8b6581ed6785990ee62c97cde363/Sources/SourceControl/GitRepository.swift?ref=albertodebortoli.com#L32) entire Git repositories for each dependency, which became time-consuming. Our CI jobs took a few minutes just to pull all the Swift packages. For dependencies with very large repositories, such as [SendbirdUIKit](https://github.com/sendbird/sendbird-uikit-ios?ref=albertodebortoli.com) (which is more than 2GB), one could rely on pre-compiled XCFrameworks as a workaround. Airbnb provides a workaround via the [SPM-specific repo](https://github.com/airbnb/lottie-spm?ref=albertodebortoli.com) for Lottie. A Swift Registry allows to serve dependencies as zip artifacts containing only the required revision, avoiding the deep clone of the git repositories. ## What is a Swift Package Registry? A Swift Package Registry is a server that stores and vends Swift packages by implementing [SE-0292](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0292-package-registry-service.md?ref=albertodebortoli.com) and the corresponding [specification](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/Registry.md?ref=albertodebortoli.com). Instead of relying on Git repositories to source our dependencies, we can use a registry to download them as versioned archives (zip files). [swift-package-manager/Documentation/PackageRegistry/PackageRegistryUsage.md at main · swiftlang/swift-package-managerThe Package Manager for the Swift Programming Language - swiftlang/swift-package-manager![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/pinned-octocat-093da3e6fa40.svg)GitHubswiftlang![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/swift-package-manager)](https://github.com/swiftlang/swift-package-manager/blob/main/Documentation/PackageRegistry/PackageRegistryUsage.md?ref=albertodebortoli.com) The primary advantages of using a Swift Package Registry are: - **Reduced CI/CD Pipeline Times:** by fetching lightweight zip archives from the registry rather than cloning the entire repositories from GitHub. - **Improved Developer Machine Performance:** the same time savings on CI are reflected on the developers' machines during dependency resolution. - **Availability:** by hosting a registry, teams are no longer dependent on the availability of external source control systems like GitHub, but rather on internal ones (for example, self-hosted Artifactory). - **Security**: injecting vulnerabilities in popular open-source projects is known as a [supply chain attack](https://en.wikipedia.org/wiki/Supply%5Fchain%5Fattack?ref=albertodebortoli.com) and has become increasingly popular in recent years. A registry allows to adopt a process to trust the sources published on it. ## Platforms Apple has accepted the Swift Registry specification and implemented support to interact with registries within SPM but has left the implementation of actual registries to third-party platforms. Apple is not in the business of providing a Swift Registry. The main platform having adopted Swift Registries is Artifactory. [Artifactory, Your Swift Package RepositoryJFrog now offers the first and only Swift binary package repository, enabling developers to use JFrog Artifactory for resolving Swift dependencies instead of enterprise source control (Git) systems.![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/Jfrog16-1.png)JFroggiannit![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/1200x628_BTN.png)](https://jfrog.com/blog/artifactory-your-swift-package-repository/?ref=albertodebortoli.com) although AWS CodeArtifact, Cloudsmith and Tuist provide support too: [New – Add Your Swift Packages to AWS CodeArtifact | Amazon Web ServicesStarting today, Swift developers who write code for Apple platforms (iOS, iPadOS, macOS, tvOS, watchOS, or visionOS) or for Swift applications running on the server side can use AWS CodeArtifact to securely store and retrieve their package dependencies. CodeArtifact integrates with standard developer tools such as Xcode, xcodebuild, and the Swift Package Manager (the swift \[…\]![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/touch-icon-ipad-144-smile-1.png)Amazon Web ServicesSébastien Stormacq![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/2023-09-12_00-03-46-1260x397-1.png)](https://aws.amazon.com/blogs/aws/new-add-your-swift-packages-to-aws-codeartifact/?ref=albertodebortoli.com) [Private, secure, hosted Swift registryCloudsmith offers secure, private Swift registries as a service, with cloud native performance. Book a demo today.![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/favicon.svg)Cloudsmith![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/opengraph.png)](https://cloudsmith.com/product/formats/swift?ref=albertodebortoli.com) [Announcing Tuist RegistryWe’re thrilled to announce the launch of the Tuist Registry – a new feature that optimizes the resolution of Swift packages in your projects.![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/icon/favicon-1.ico)TuistMarek Fořt![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/thumbnail/og.jpg)](https://tuist.dev/blog/2025/01/22/announcing-tuist-registry?ref=albertodebortoli.com) The benefits are usually appealing to teams with large apps, hence it's reasonable to believe that only big companies have looked into adopting a registry successfully. ## Artifactory Setup Let's assume a JFrog Artifactory to host our Swift Package Registry exists at [https://packages.acme.com](https://artifacts.takeaway.com/?ref=albertodebortoli.com). Artifactory support local, remote, and virtual repositories but a realistic setup consists of only local and virtual repositories. ![](https://speedmedia.jfrog.com/08612fe1-9391-4cf3-ac1a-6dd49c36b276/media.jfrog.com/wp-content/uploads/2022/06/03190625/Artifactory_Swift_Flow.png) Source: [Artifactory](https://jfrog.com/blog/artifactory-your-swift-package-repository/?ref=albertodebortoli.com) Local Repositories are meant to be used for publishing dependencies from CI pipelines. Virtual Repositories are instead meant to be used for resolving (pulling) dependencies on both CI and the developers' machines. Remote repositories are not really relevant in a typical Swift Registry setup. Following the documentation at [https://jfrog.com/help/r/jfrog-artifactory-documentation/set-up-a-swift-registry](https://jfrog.com/help/r/jfrog-artifactory-documentation/set-up-a-swift-registry?ref=albertodebortoli.com), let's create 2 repositories with the following names: - local repository: `swift-local` - virtual repository: `swift-virtual` ## Local Setup To pull dependencies from the Swift Package Registry, we need to configure the local environment. ### 1\. Set the Registry URL First, we need to inform SPM about the existence of the registry. We can do this on a per-project basis or globally for the user account. From a package's root directory, run the following command. This will create a `.swiftpm/configuration/registries.json` file within your project folder. ```bash swift package-registry set "https://packages.acme.com/artifactory/api/swift/swift-virtual" ``` The resulting `registries.json` file will look like this: ```json { "authentication": {}, "registries": { "[default]": { "supportsAvailability": false, "url": "https://packages.acme.com/artifactory/api/swift/swift-virtual" } }, "version": 1 } ``` To set the registry for all your projects, use the `--global` flag. ```bash swift package-registry set --global "https://packages.acme.com/artifactory/api/swift/swift-virtual" ``` This will create the configuration file at `~/.swiftpm/configuration/registries.json`. Xcode projects don't support project-level registries nor (in my experience) support scopes other than the default one (i.e. avoid using the `--scope` flag). ### 2\. Authentication To pull packages, authenticating with Artifactory is usually required. It's reasonable though that your company allows all artifacts from Artifactory to be read without authentication as long as one is connected to the company VPN. In cases where authentication is required, SPM uses a `.netrc` file in the home directory to find credentials for remote servers. This file is a standard way to handle login information for various network protocols. Using a token generated from the Artifactory dashboard, the line to add to the `.netrc` file would be: ``` machine packages.acme.com login password ``` Alternatively, it's possible to log in using the `swift package-registry login` command. This command securely stores your token in the system's keychain. ``` swift package-registry login "https://packages.acme.com/artifactory/api/swift/swift-virtual" \ --token # or swift package-registry login "https://packages.acme.com/artifactory/api/swift/swift-virtual" \ --username \ --password ``` ## CI/CD Setup On CI, the setup is slightly different as the goals are: - to *resolve* dependencies in CI/CD jobs - to *publish* new package versions in CD jobs for both internal and external dependencies The steps described for the local setup are valid for the resolution on CI too. The interesting part here is how publishing is done. I will assume the usage of GitHub Actions. ### 1\. Retrieving the Artifactory Token The JFrog CLI can be used via the [setup-jfrog-cli](https://github.com/jfrog/setup-jfrog-cli?ref=albertodebortoli.com) action to authenticate using the most appropriate method. You might want to wrap the action in a custom composable one exporting the token as the output of a step: ``` TOKEN=$(jf config export) echo "::add-mask::$TOKEN" echo "artifactory-token=$TOKEN">> "$GITHUB_OUTPUT" ``` ### 2\. Logging into the Registry The CI job must log in to the local repository (`swift-local`) to gain push permissions. The token retrieved in the previous step is used for this purpose. ```bash swift package-registry login \ "https://packages.acme.com/artifactory/api/swift/swift-local" \ --token ${{ steps.get-token.outputs.artifactory-token }} ``` ### 3\. Publishing Packages Swift Registry requires archives created with the `swift package archive-source` command from the dependency folder. E.g. ```bash swift package archive-source -o "Alamofire-5.10.2.zip" ``` We could avoid creating the archive and instead download it directly from GitHub releases. ```bash curl -L -o Alamofire-5.10.1.zip \ https://github.com/Alamofire/Alamofire/archive/refs/tags/5.10.1.zip ``` Uploading the archive can then be done by using the JFrog CLI that needs customization via the [setup-jfrog-cli](https://github.com/jfrog/setup-jfrog-cli?ref=albertodebortoli.com) action. If going down this route, the upload command would be: ```bash jf rt upload Alamofire-5.10.1.zip \ https://packages.acme.com/artifactory/api/swift/swift-local/acme/Alamofire/Alamofire-5.10.1.zip ``` There is a [specific structure](https://jfrog.com/help/r/jfrog-artifactory-documentation/swift-repository-structure?ref=albertodebortoli.com) to respect: ``` ///-.zip ``` which is the last part of the above URL: ``` swift-local/acme/Alamofire/Alamofire-5.10.1.zip ``` Too bad that using the steps above causes a downstream problem with SPM not being able to resolve the dependencies in the registry. I tried extensively and couldn't find the reason why SPM wouldn't be happy with how the packages were published. I might have missed something but eventually I necessarily had to switch to use the `publish` command. Using the `swift package-registry publish` command instead, doesn't present this issue hence it's the solution adopted in this workflow. ```bash swift package-registry publish acme.Alamofire 5.10.1 \ --url https://packages.acme.com/artifactory/api/swift/swift-local \ --scratch-directory $(mktemp -d) ``` To verify the upload and indexing succeeded, check that the uploaded `*.zip` artifact is available and that the `.swift` exists (indication that the indexing has occurred). If the specific structure is not respected, the `.swift` folder wouldn't be generated. ## Consuming Packages from the Registry ### Packages The easiest and only documented way to consume a package from a registry is via a Package. In the `Package.swift` file, declare dependencies using the `.package(id:from:)` syntax to declare a registry-based dependency. The `id` is a combination of the scope and the package name. ```swift ... dependencies: [ .package(id: "acme.Alamofire", from: "5.10.1"), ], targets: [ .target( name: "MyApp", dependencies: [ .product(name: "Alamofire", package: "acme.Alamofire"), ] ), ... ] ) ``` Run `swift package resolve` or simply build the Package in Xcode to pull the dependencies. You might bump into transitive dependencies (i.e. dependencies listed in the `Package.swift` files of the packages published on the registry) pointing to GitHub. In this case, it'd be great to instruct SPM to use the corresponding versions on the registry. The `--replace-scm-with-registry` flag is designed to work for the entire dependency graph, including transitive dependencies. The cornerstone of associating a registry-hosted package with its GitHub origin is the `package-metadata.json` file. This file allows to provide essential metadata about the packages at the time of publishing (the `--metadata-path` flag of the `publish` command defaults to `package-metadata.json`). Crucially, it includes a field to specify the source control repository URLs. When `swift package resolve --replace-scm-with-registry` is executed, SPM queries the configured registry. The registry then uses the information from the `package-metadata.json` to map the package identity to its corresponding GitHub URL, enabling a smooth and transparent resolution process. The metadata file must conform to the [JSON schema](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0391-package-registry-publish.md?ref=albertodebortoli.com#package-release-metadata-standards) defined in [SE-0391](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0391-package-registry-publish.md?ref=albertodebortoli.com). It is recommended to include all URL variations (e.g., SSH, HTTPS) for the same repository. E.g. ```json { "repositoryURLs": [ "https://github.com/Alamofire/Alamofire", "https://github.com/Alamofire/Alamofire.git", "git@github.com:Alamofire/Alamofire.git" ] } ``` Printing the dependencies should confirm the source of the dependencies: ```bash swift package show-dependencies --replace-scm-with-registry ``` When loading a package with Xcode, the flag can be enabled via an environment variable in the scheme ``` IDEPackageDependencySCMToRegistryTransformation=useRegistryIdentityAndSources ``` Too bad that for packages, the schemes won't load until SPM completes the resolution hence running the following from the terminal would address the issue: ``` defaults write com.apple.dt.Xcode IDEPackageDependencySCMToRegistryTransformation useRegistryIdentityAndSources ``` that can be unset with: ``` defaults delete com.apple.dt.Xcode IDEPackageDependencySCMToRegistryTransformation ``` ### Xcode It's likely that you'll want to use the registry from Xcode projects for direct dependencies. If using the [Tuist registry](https://tuist.dev/blog/2025/01/22/announcing-tuist-registry?ref=albertodebortoli.com), it seems you would be able to leverage a Package Collection to add dependencies from the registry from the Xcode UI. Note that until Xcode 26 Beta 1, it's not possible to add registry dependencies directly in the Xcode UI, but if you use Tuist to generate your project (as you should), you can use the `Package.registry` (introduced with [https://github.com/tuist/tuist/pull/7225](https://github.com/tuist/tuist/pull/7225?ref=albertodebortoli.com)). E.g. ```swift let project = Project( ... packages: [ .registry( identifier: "acme.Alamofire", requirement: .exact(Version(stringLiteral: "5.10.1")) ) ], ... ) ``` If not using Tuist, you'd have to rely on setting `IDEPackageDependencySCMToRegistryTransformation` either as an environment variable in the scheme or globally via the terminal. You can also use `xcodebuild` to resolve dependencies using the correct flag: ``` xcodebuild \ -resolvePackageDependencies \ -packageDependencySCMToRegistryTransformation useRegistryIdentityAndSources ``` ## Conclusions We’ve found that using an in-house Swift registry drastically reduces dependency resolution time and size on disk by downloading only the required revision instead of the entire, potentially large, Git repository. This improvement benefits both CI pipelines and developers’ local environments. Additionally, registries help mitigate the risk of supply chain attacks. As of this writing, Swift registries are not widely adopted, which is reflected in the limited number of platforms that support them. It also shows [various](https://forums.swift.org/t/package-registry-support-in-xcode/73626?ref=albertodebortoli.com) [bugs](https://forums.swift.org/t/internal-error-uncaught-exception-when-building-swiftpm-project/76237?ref=albertodebortoli.com) I myself bumped into when using particular configurations. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2025/06/image.png) source: [https://forums.swift.org/t/package-registry-support-in-xcode/73626/19](https://forums.swift.org/t/package-registry-support-in-xcode/73626/19?ref=albertodebortoli.com) It's unclear whether adoption will grow and uncertain if Apple will ever address the issues reported by the community, but when a functioning setup is put in place, registries offer an efficient and secure alternative to using XCFrameworks in production builds and reduce both memory and time footprints. ### Scalable Continuous Integration for iOS URL: https://albertodebortoli.com/2024/01/03/scalable-continuous-integration-for-ios/ Last updated: 2024-01-04T07:37:10.000Z *Originally published on the* [*Just Eat Takeaway Engineering Blog*](https://medium.com/justeattakeaway-tech/scalable-continuous-integration-for-ios-15ff33435992?ref=albertodebortoli.com)*.* How Just Eat Takeaway.com leverage AWS, Packer, Terraform and GitHub Actions to manage a CI stack of macOS runners. ### Problem At Just Eat Takeaway.com (JET), our journey through continuous integration (CI) reflects a landscape of innovation and adaptation. Historically, JET’s multiple iOS teams operated independently, each employing their distinct CI solutions. The original Just Eat iOS and Android teams had pioneered an in-house CI solution anchored in Jenkins. This setup, detailed in our 2021 [article](https://medium.com/justeattakeaway-tech/the-continuous-integration-system-used-by-the-mobile-teams-28ba057ef628?ref=albertodebortoli.com), served as the backbone of our CI practices up until 2020\. It was during this period that the iOS team initiated a pivotal migration: moving from in-house Mac Pros and Mac Minis to AWS EC2 macOS instances. Fast forward to 2023, a significant transition occurred within our Continuous Delivery Engineering (CDE) Platform Engineering team. The decision to adopt GitHub Actions company-wide has marked the end of our reliance on Jenkins while other teams are in the process of migrating away from solutions such as CircleCI and GitLab CI. This transition represented a fundamental shift in our CI philosophy. By moving away from Jenkins, we eliminated the need to maintain an instance for the Jenkins server and the complexities of managing how agents connected to it. Our focus then shifted to transforming our Jenkins pipelines into GitHub Actions workflows. This transformation extended beyond mere tool adoption. Our primary goal was to ensure that our macOS instances were not only scalable but also configured in code. We therefore enhanced our global CI practices and set standards across the entire company. ### Desired state of CI As we embarked on our journey to refine and elevate our CI process, we envisioned a state-of-the-art CI system. Our goals were ambitious yet clear, focusing on scalability, automation, and efficiency. At the time of implementing the system, no other player in the industry seemed to have implemented the complete solution we envisioned. Below is a summary of our desired CI state: - **Instance setup in code**: One primary objective was to enable the definition of the setup of the instances entirely in code. This includes specifying macOS version, Xcode version, Ruby version, and other crucial configurations. For this purpose, the HashiCorp tool Packer, emerged once again as an ideal solution, offering the flexibility and precision we required. - **IaC (Infrastructure as Code) for macOS instances**: To define the infrastructure of our fleet of macOS instances, we leaned towards Terraform, another HashiCorp tool. Terraform provided us with the capability to not only deploy but also to scale and migrate our infrastructure seamlessly, crucially maintaining its state. - **Auto and Manual Scaling**: We wanted the ability to dynamically create CI runners based on demand, ensuring that resources were optimally utilized and available when needed. To optimize resource utilization, especially during off-peak hours, we desired an autoscaling feature. Scaling down our CI runners on weekends when developer activity is minimal was critical to be cost-effective. - **Automated Connection to GitHub Actions**: We aimed for the instances to automatically connect to GitHub Actions as runners upon deployment. This automation was crucial in eliminating manual interventions via SSH or VNC. - **Multi-Team Use**: Our vision included CI runners that could be easily used by multiple teams across different time zones. This would not only maximize the utility of our infrastructure but also encourage reuse and standardization. - **Centralized Management via GitHub Actions**: To further streamline our CI processes, we intended to run all tasks through GitHub Actions workflows. This approach would allow the teams to self-serve and alleviate the need for developers to use Docker or maintain local environments. Getting to the desired state was a journey that presented multiple challenges and constant adjustments to make sure we could migrate smoothly to a new system. ### **Instance setup in code** We implemented the desired configuration [with Packer](https://aws.amazon.com/blogs/compute/building-amazon-machine-images-amis-for-ec2-mac-instances-with-packer/?ref=albertodebortoli.com) leveraging a number of [Shell Provisioners](https://developer.hashicorp.com/packer/docs/provisioners/shell?ref=albertodebortoli.com) and variables to configure the instance. Here are some of the configuration steps: - Set user password (to allow remote desktop access) - Resize the partition to use all the space available on the EBS volume - Start the Apple Remote Desktop agent and enable remote desktop access - Update Brew & Install Brew packages - Install CloudWatch agent - Install rbenv/Ruby/bundler - Install Xcode versions - Install GitHub Actions actions-runner - Copy scripts to connect to GitHub Actions as a runner - Copy daemon to start the GitHub Actions self-hosted runner [as a service](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service?platform=mac&ref=albertodebortoli.com) - Set [macos-init](https://github.com/aws/ec2-macos-init?ref=albertodebortoli.com) modules to perform provisioning of the first launch While the steps above are naturally configuration steps to perform when creating the AMI, the macos-init modules include steps to perform once the instance becomes available. The `create_ami` workflow accepts inputs that are eventually passed to Packer to generate the AMI. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2024/01/1_UTehZkRYTyeGW-xHO2uGEw.webp) ```bash packer build \ --var ami_name_prefix=${{ env.AMI_NAME_PREFIX }} \ --var region=${{ env.REGION }} \ --var subnet_id=${{ env.SUBNET_ID }} \ --var vpc_id=${{ env.VPC_ID }} \ --var root_volume_size_gb=${{ env.ROOT_VOLUME_SIZE_GB }} \ --var macos_version=${{ inputs.macos-version}} \ --var ruby_version=${{ inputs.ruby-version }} \ --var xcode_versions='${{ steps.parse-xcode-versions.outputs.list }}' \ --var gha_version=${{ inputs.gha-version}} \ bare-metal-runner.pkr.hcl ``` Different teams often use different versions of software, like Xcode. To accommodate this, we permit multiple versions to be installed on the same instance. The choice of which version to use is then determined within the GitHub Actions workflows. The seamless generation of AMIs has proven to be a significant enabler. For example, when Xcode 15.1 was released, we executed this workflow the same evening. In just over two hours, we had an AMI ready to deploy all the runners (it usually takes 70–100 minutes for a macOS AMI with 400GB of EBS volume to become ready after creation). This efficiency enabled our teams to use the new Xcode version just a few hours after its release. ### **IaC** (Infrastructure as Code) **for macOS instances** Initially, we used distinct Terraform modules for each instance to facilitate the deployment and decommissioning of each one. Given the high cost of EC2 Mac instances, we managed this process with caution, carefully balancing host usage while also being mindful of the 24-hour minimum allocation time. We ultimately ended up using Terraform to define a single infrastructure (i.e. a single Terraform module) defining resources such as: - `aws_key_pair`, `aws_instance`, `aws_ami` - `aws_security_group`, `aws_security_group_rule` - `aws_secretsmanager_secret` - `aws_vpc`, `aws_subnet` - `aws_cloudwatch_metric_alarm` - `aws_sns_topic`, `aws_sns_topic_subscription` - `aws_iam_role`, `aws_iam_policy`, `aws_iam_role_policy_attachment`, `aws_iam_instance_profile` A crucial part was to use `count` in `aws_instance`, setting the value of a variable passed in from `deploy_infra` workflow. Terraform performs the necessary scaling upon changing the value. We have implemented a workflow to perform Terraform `apply` and `destroy` commands for the infrastructure. Only the AMI and the number of instances are required as inputs. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2024/01/1_DBUpUssi6rj8DqpIL6VXkA.webp) ```bash terraform ${{ inputs.command }} \ --var ami_name=${{ inputs.ami-name }} \ --var fleet_size=${{ inputs.fleet-size }} \ --auto-approve ``` Using the name of the AMI instead of the ID allows us to use the most recent one that was generated, useful in case of name clashes. ```terraform variable "ami_name" { type = string } variable "fleet_size" { type = number } data "aws_ami" "bare_metal_gha_runner" { most_recent = true filter { name = "name" values = ["${var.ami_name}"] } ... } resource "aws_instance" "bare_metal" { count = var.fleet_size ami = data.aws_ami.bare_metal_gha_runner.id instance_type = "mac2.metal" tenancy = "host" key_name = aws_key_pair.bare_metal.key_name ... } ``` Instead of maintaining multiple CI instances with varying software configurations, we concluded that it’s simpler and more efficient to have a single, standardised setup. While teams still have the option to create and deploy their unique setups, a smaller, unified system allows for easier support by a single global configuration. ### **Auto and Manual Scaling** The `deploy_infra` workflow allows us to scale on demand but it doesn’t release the underlying dedicated hosts which are the resources that are ultimately billed. The autoscaling solution provided by AWS is great for VMs but gets sensibly more complex when actioned on dedicated hosts. [Auto Scaling groups](https://docs.aws.amazon.com/autoscaling/ec2/userguide/auto-scaling-groups.html?ref=albertodebortoli.com) on macOS instances would require a [Custom Managed License](https://docs.aws.amazon.com/license-manager/latest/userguide/create-license-configuration.html?ref=albertodebortoli.com), a [Host Resource Group](https://docs.aws.amazon.com/license-manager/latest/userguide/host-resource-groups.html?ref=albertodebortoli.com) and, of course, a [Launch Template](https://docs.aws.amazon.com/autoscaling/ec2/userguide/launch-templates.html?ref=albertodebortoli.com). Using only AWS services appears to be a lot of work to pull things together and the result wouldn’t allow for automatic release of the dedicated hosts. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2024/01/1_RF0qy26XVnZldt3SpFkc8w.webp) AirBnb mention in their [Flexible Continuous Integration for iOS](https://medium.com/airbnb-engineering/flexible-continuous-integration-for-ios-4ab33ea4072f?ref=albertodebortoli.com) article that an internal scaling service was implemented: > An internal scaling service manages the desired capacity of each environment’s Auto Scaling group. Some articles explain how to set up Auto Scaling groups for mac instances (see [1](https://aws.amazon.com/blogs/compute/implementing-autoscaling-for-ec2-mac-instances/?ref=albertodebortoli.com) and [2](https://devdosvid.blog/2021/10/24/auto-scaling-group-for-your-macos-ec2-instances-fleet/?ref=albertodebortoli.com)) but after careful consideration, we agreed that implementing a simple scaling service via GitHub Actions (GHA) was the easiest and most maintainable solution. We implemented 2 GHA workflows to fully automate the weekend autoscaling: - Upscaling workflow to `n`, triggered at a specific time at the beginning of the working week - Downscaling workflow to `1`, triggered at a specific time at the beginning of the weekend We retain the capability for manual scaling, which is essential for situations where we need to scale down, such as on bank holidays, or scale up, like on release cut days, when activity typically exceeds the usual levels. Additionally, we have implemented a workflow that runs multiple times a day and tries to release all available hosts without an instance attached. This step lifts us from the burden of having to remember to release the hosts. Dedicated hosts take up to 110 minutes to move from the Pending to the Available state due to the [scrubbing workflow](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-mac-instances.html?ref=albertodebortoli.com#mac-instance-stop) performed by AWS. Manual scaling can be executed between the times the autoscaling workflows are triggered and they must be resilient to unexpected statuses of the infrastructure, which thankfully Terraform takes care of. Both down and upscaling are covered in the following flowchart: ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2024/01/1_uT28oUGw_PZWKCs7_IUlag.webp) The autoscaling values are defined as configuration variables in the repo settings: ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2024/01/1_In73VTcwiWA7iMoaTEClKA.webp) It usually takes \~8 minutes for an EC2 mac2.metal instance to become reachable after creation, meaning that we can redeploy the entire infrastructure very quickly. ### **Automated Connection to GitHub Actions** We provide some user data when deploying the instances. ```terraform resource "aws_instance" "bare_metal" { ami = data.aws_ami.bare_metal_gha_runner.id count = var.fleet_size ... user_data = <", "github_pat_secret_manager_arn": ${data.aws_secretsmanager_secret_version.ghe_pat.arn}, "github_url": "", "runner_group": "CI-MobileTeams", "runner_name": "bare-metal-runner-${count.index + 1}" } EOF ``` The user data is stored in a specific folder by [macos-init](https://github.com/aws/ec2-macos-init?ref=albertodebortoli.com) and we implement a module to copy the content to `~/actions-runner-config.json`. ``` ### Group 10 ### [[Module]] Name = "Create actions-runner-config.json from userdata" PriorityGroup = 10 RunPerInstance = true FatalOnError = false [Module.Command] Cmd = ["/bin/zsh", "-c", 'instanceId="$(curl http://169.254.169.254/latest/meta-data/instance-id)"; if [[ ! -z $instanceId ]]; then cp /usr/local/aws/ec2-macos-init/instances/$instanceId/userdata ~/actions-runner-config.json; fi'] RunAsUser = "ec2-user" ``` which is in turn used by the `configure_runner.sh` script to configure the GitHub Actions runner. ```bash #!/bin/bash GITHUB_ENTERPRISE=$(cat $HOME/actions-runner-config.json | jq -r .github_enterprise) GITHUB_PAT_SECRET_MANAGER_ARN=$(cat $HOME/actions-runner-config.json | jq -r .github_pat_secret_manager_arn) GITHUB_PAT=$(aws secretsmanager get-secret-value --secret-id $GITHUB_PAT_SECRET_MANAGER_ARN | jq -r .SecretString) GITHUB_URL=$(cat $HOME/actions-runner-config.json | jq -r .github_url) RUNNER_GROUP=$(cat $HOME/actions-runner-config.json | jq -r .runner_group) RUNNER_NAME=$(cat $HOME/actions-runner-config.json | jq -r .runner_name) RUNNER_JOIN_TOKEN=` curl -L \ -X POST \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GITHUB_PAT"\ $GITHUB_URL/api/v3/enterprises/$GITHUB_ENTERPRISE/actions/runners/registration-token | jq -r '.token'` MACOS_VERSION=`sw_vers -productVersion` XCODE_VERSIONS=`find /Applications -type d -name "Xcode-*" -maxdepth 1 \ -exec basename {} \; \ | tr '\n' ',' \ | sed 's/,$/\n/' \ | sed 's/.app//g'` $HOME/actions-runner/config.sh \ --unattended \ --url $GITHUB_URL/enterprises/$GITHUB_ENTERPRISE \ --token $RUNNER_JOIN_TOKEN \ --runnergroup $RUNNER_GROUP \ --labels ec2,bare-metal,$RUNNER_NAME,macOS-$MACOS_VERSION,$XCODE_VERSIONS \ --name $RUNNER_NAME \ --replace ``` The above script is run by a macos-init module. ``` ### Group 11 ### [[Module]] Name = "Configure the GHA runner" PriorityGroup = 11 RunPerInstance = true FatalOnError = false [Module.Command] Cmd = ["/bin/zsh", "-c", "/Users/ec2-user/configure_runner.sh"] RunAsUser = "ec2-user" ``` The [GitHub documentation](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/configuring-the-self-hosted-runner-application-as-a-service?platform=mac&ref=albertodebortoli.com#customizing-the-self-hosted-runner-service-1) states that it’s possible to create a customized service starting from a provided template. It took some research and various attempts to find the right configuration that allows the connection without having to log in in the UI (over VNC) which would represent a blocker for a complete automation of the deployment. We believe that the single person who managed to get this right is [Sébastien Stormacq](https://github.com/sebsto?ref=albertodebortoli.com) who provided the [correct solution](https://github.com/actions/runner/issues/1056?ref=albertodebortoli.com#issuecomment-1237426462). The connection to GHA can be completed with 2 more modules that install the runner as a service and load the custom daemon. ``` ### Group 12 ### [[Module]] Name = "Run the self-hosted runner application as a service" PriorityGroup = 12 RunPerInstance = true FatalOnError = false [Module.Command] Cmd = ["/bin/zsh", "-c", "cd /Users/ec2-user/actions-runner && ./svc.sh install"] RunAsUser = "ec2-user" ### Group 13 ### [[Module]] Name = "Launch actions runner daemon" PriorityGroup = 13 RunPerInstance = true FatalOnError = false [Module.Command] Cmd = ["sudo", "/bin/launchctl", "load", "/Library/LaunchDaemons/com.justeattakeaway.actions-runner-service.plist"] RunAsUser = "ec2-user" ``` Using a daemon instead of an agent (see [Creating Launch Daemons and Agents](https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html?ref=albertodebortoli.com)), doesn’t require us to set up any auto-login which on macOS is a bit of a tricky procedure and is best avoided also for security reasons. The following is the content of the daemon for completeness. ```xml Label com.justeattakeaway.actions-runner-service ProgramArguments /Users/ec2-user/actions-runner/runsvc.sh UserName ec2-user GroupName staff WorkingDirectory /Users/ec2-user/actions-runner RunAtLoad StandardOutPath /Users/ec2-user/Library/Logs/com.justeattakeaway.actions-runner-service/stdout.log StandardErrorPath /Users/ec2-user/Library/Logs/com.justeattakeaway.actions-runner-service/stderr.log EnvironmentVariables ACTIONS_RUNNER_SVC 1 ProcessType Interactive SessionCreate ``` Not long after the deployment, all the steps above are executed and we can appreciate the runners appearing as connected. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2024/01/1_SRzNVowEas-7_g5J1nxzTw.webp) ### Multi-Team Use We start the downscaling at 11:59 PM on Fridays and start the upscaling at 6:00 AM on Mondays. These times have been chosen in a way that guarantees a level of service to teams in the UK, the Netherlands (GMT+1) and Canada (Winnipeg is on GMT-6) accounting for BST (British Summer Time) and DST (Daylight Saving Time) too. Times are defined in UTC in the GHA workflow triggers and the local time of the runner is not taken into account. Since the instances are used to build multiple projects and tools owned by different teams, one problem we faced was that instances could get compromised if workflows included unsafe steps (e.g. modifications to global configurations). GitHub Actions has a documentation page about [Hardening self-hosted runners](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions?ref=albertodebortoli.com#hardening-for-self-hosted-runners) specifically stating: > Self-hosted runners for GitHub do not have guarantees around running in ephemeral clean virtual machines, and can be persistently compromised by untrusted code in a workflow. We try to combat such potential problems by educating people on how to craft workflows and rely on the quick redeployment of the stack should the instances break. We also run [scripts before and after each job](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/running-scripts-before-or-after-a-job?ref=albertodebortoli.com) to ensure that instances can be reused as much as possible. This includes actions like deleting the simulators’ content, derived data, caches and archives. ### Centralized Management via GitHub Actions The macOS runners stack is defined in a dedicated `macOS-runners` repository. We implemented GHA workflows to cover the use cases that allow teams to self-serve: - create macOS AMI - deploy CI - downscale for the weekend\* - upscale for the working week\* - release unused hosts\* *\* run without inputs and on a scheduled trigger* The runners running the jobs in this repo are small t2.micro Linux instances and come with the [AWSCLI](https://aws.amazon.com/cli/?ref=albertodebortoli.com) installed. An IAM instance role with the correct policies is used to make sure that `aws ec2` commands `allocate-hosts`, `describe-hosts` and `release-hosts` could execute and we used `jq` to parse the API responses. ### A note on VM runners In this article, we discussed how we’ve used bare metal instances as runners. We have spent a considerable amount of time investigating how we could leverage the [Virtualization framework](https://developer.apple.com/documentation/virtualization?ref=albertodebortoli.com) provided by Apple to create virtual machines via [Tart](https://tart.run/?ref=albertodebortoli.com). If you’ve grasped the complexity of crafting a CI system of runners on bare metal instances, you can understand that introducing VMs makes the setup sensibly more convoluted which would be best discussed in a separate article. While a setup with Tart VMs has been implemented, we realised that it’s not performant enough to be put to use. Using VMs, the number of runners would double but we preferred to have native performance as the slowdown is over 40% compared to bare metal. Moreover, when it comes to running heavy UI test suites like ours, tests became too flaky. Testing the VMs, we also realised that the standard values of Throughput and IOPS on the EBS volume didn’t seem to be enough and caused disk congestion resulting in an unacceptable slowdown in performance. Here is a quick summary of the setup and the challenges we have faced. - Virtual runners require 2 images: one for the VMs (tart) and one for the host (AMI). - We use Packer to create VM images (Vanilla, Base, IDE, Tools) with the software required based on the [templates](https://github.com/cirruslabs/macos-image-templates?ref=albertodebortoli.com) provided by Tart and we store the OCI-compliant images on ECR. We create these images on CI with dedicated workflows similar to the one described earlier for bare metal but, in this case, macOS runners (instead of Linux) are required as publishing to ECR is done with tart which runs on macOS. Extra policies are required on the instance role to allow the runner to push to ECR (using `temporary_iam_instance_profile_policy_document` in Packer’s [Amazon EBS](https://developer.hashicorp.com/packer/integrations/hashicorp/amazon/latest/components/builder/ebs?ref=albertodebortoli.com)). - Apple set a limit to the number of VMs that can be run on an instance to 2, which would allow to double the size of the fleet of runners. Creating AMIs hosting 2 VMs is done with Packer and steps include cloning the image from ECR and configuring macos-init modules to run daemons to run the VMs via Tart. - Deploying a virtual CI infrastructure is identical to what has already been described for bare metal. - Connecting to and interfacing with the VMs happens from within the host. Opening SSH and especially VNC sessions from within the bare metal instances can be very confusing. - The version of macOS on the host and the one on the VMs could differ. The version used on the host must be provided with an AMI by AWS, while the version used on the VMs is provided by Apple in IPSW files (see [ipsw.me](https://ipsw.me/?ref=albertodebortoli.com)). - The GHA runners run on the VMs meaning that the host won’t require Xcode installed nor any other software used by the workflows. - VMs don’t allow for provisioning meaning we have to share configurations with the VMs via shared folders on the host with the `— dir` flag which causes extra setup complexity. - VMs can’t easily run the GHA runner as a service. The `svc` script requires the runner to be configured first, an operation that cannot be done during the provisioning of the host. We therefore need to implement an agent ourselves to configure and connect the runner in a single script. - To have UI access (a-la VNC) to the VMs, it’s first required to stop the VMs and then run them without the `--no-graphics` flag. At the time of writing, copy-pasting won’t work even if using the `--vnc` or `--vnc-experimental` flags. - [Tartelet](https://github.com/shapehq/tartelet?ref=albertodebortoli.com) is a macOS app on top of Tart that allows to manage multiple GitHub Actions runners in ephemeral environments on a single host machine. We didn’t consider it to avoid relying on too much third-party software and because it [doesn’t have yet](https://github.com/shapehq/tartelet/issues/27?ref=albertodebortoli.com) GitHub Enterprise support. - Worth noting that the Tart team worked on an orchestration solution named [Orchard](https://github.com/cirruslabs/orchard?ref=albertodebortoli.com) that seems to be in its initial stage. ### Conclusion In 2023 we have revamped and globalised our approach to CI. We have migrated from Jenkins to GitHub Actions as the CI/CD solution of choice for the whole group and have profoundly optimised and improved our pipelines introducing a greater level of job parallelisation. We have implemented a brand new scalable setup for bare metal macOS runners leveraging the HashiCorp tools Packer and Terraform. We have also implemented a setup based on Tart virtual machines. We have increased the size of our iOS team over the past few years, now including more than 40 developers, and still managed to be successful with only 5 bare metal instances on average, which is a clear statement of how performant and optimised our setup is. We have extended the capabilities of our Internal Developer Platform with a globalised approach to provide macOS runners; we feel that this setup will stand the test of time and serve well various teams across JET for years to come. ### The idea of a Fastlane replacement URL: https://albertodebortoli.com/2023/10/29/the-idea-of-a-fastlane-replacement/ Last updated: 2023-11-19T15:25:48.000Z ## Prelude [Fastlane](https://github.com/fastlane/fastlane?ref=albertodebortoli.com) is widely used by iOS teams all around the world. It became the standard de facto to automate common tasks such as building apps, running tests, and uploading builds to App Store Connect. Fastlane has been recently [moved](https://github.com/MobileNativeFoundation/discussions/discussions/194?ref=albertodebortoli.com#discussioncomment-7406759) under the [Mobile Native Foundation](https://mobilenativefoundation.org/?ref=albertodebortoli.com) which is amazing as Google wasn't actively maintaining the project. At Just Eat Takeaway, we have implemented an extensive number of custom lanes to perform domain-specific tasks and used them from our CI. The major problem with Fastlane is that it's written in Ruby. When it was born, using Ruby was a sound choice but iOS developers are not necessarily familiar with such language which represents a barrier to contributing and writing lanes. While [Fastlane.swift](https://docs.fastlane.tools/getting-started/ios/fastlane-swift/?ref=albertodebortoli.com), a version of Fastlane in Swift, has been in beta for years, it's not a rewrite in Swift but rather a "solution on top" meaning that developers and CI systems still have to rely on Ruby, install related software ([rbenv](https://github.com/rbenv/rbenv?ref=albertodebortoli.com) or [rvm](https://rvm.io/?ref=albertodebortoli.com)) and most likely maintain a Gemfile. The average iOS dev knows well that Ruby environments are a pain to deal with and have caused an infinite number of headaches. In recent years, Apple has introduced technologies that would enable a replacement of Fastlane using Swift: - [Swift Package Manager](https://www.swift.org/package-manager/?ref=albertodebortoli.com) (SPM) - [Swift Argument Parser](https://github.com/apple/swift-argument-parser?ref=albertodebortoli.com) (SAP) Being myself a big fan of CLI tools written in Swift, I soon started maturing the idea of a Fastlane rewrite in Swift in early 2022\. I circulated the idea with friends and colleagues for months and the sentiment was clear: it's time for a fresh simil-Fastlane tool written in Swift. ## Journey Towards the end of 2022, I was determined to start this project. I teamed up with 2 iOS devs (not working at Just Eat Takeaway) and we started working on a design. I was keen on calling this project "Swiftlane" but the preference seemed to be for the name "Interstellar" which was eventually shortened into "Stellar". Fastlane has the concept of [Actions](https://docs.fastlane.tools/actions/?ref=albertodebortoli.com) and I instinctively thought that in Swift-land, they could take the form of SPM packages. This would make Stellar a modular system with pluggable components. For example, consider the [Scan](https://docs.fastlane.tools/actions/scan/?ref=albertodebortoli.com) action in Fastlane. It could be a package that solely solves the same problem around testing. My goal was not to implement the plethora of existing Fastlane actions but rather to create a system that allows plugging in any package building on macOS. A sound design of such system was crucial. The Stellar ecosystem I had in mind was composed of 4 parts: ### Actions Actions are the basic building blocks of the ecosystem. They are packages that define a library product. An action can do anything, from taking care of build tasks to integrating with GitHub. Actions are independent packages that have no knowledge of the Stellar system, which treats them as pluggable components to create higher abstractions. Ideally, actions should expose an executable product (the CLI tool) using SAP calling into the action code. This is not required by Stellar but it’s advisable as a best practice. Official Actions would be hosted in the Stellar organisation on GitHub. Custom Actions could be created using Stellar. ### Tasks Tasks are specific to a project and implemented by the project developers. They are SAP `ParsableCommand` or `AsyncParsableCommand` which use actions to construct complex logic specific to the needs of the project. ### Executor Executor is a command line tool in the form of a package generated by Stellar. It’s the entry point to the user-defined tasks. Invoking tasks on the Executor is like invoking lanes in Fastlane. Both developers and CI would interface with the Executor (masked as Stellar) to perform all operations. E.g. ```swift stellar setup_environment --developer-mode stellar run_unit_tests module=OrderHistory stellar setup_demo_app module=OrderHistory stellar run_ui_tests module=OrderHistory device="iPhone 15 Pro" ``` ### Stellar CLI Stellar CLI is a command line tool that takes care of the heavy lifting of dealing with the Executor and the Tasks. It allows the integration of Stellar in a project and it should expose the following main commands: - `init`: initialise the project by creating an Exectutor package in the `.stellar` folder - `build`: builds the Executor generating a binary that is shared with the team members and used by CI - `create-action`: scaffolding to create a new action in the form of a package - `create-task`: scaffolding to create a new task in the form of a package - `edit`: opens the Executor package for editing, similar to `tuist edit` This design was presented to a restricted group of devs at Just Eat Takeaway and it didn't take long to get an agreement on it. It was clear that once Stellar was completed, we would have integrated it in the codebase. ## Wider design I believe that a combination of CLI tools can create complex, templateable and customizable stacks to support the creation and growth of iOS codebases. Based on the experience developed at JET working on a large modular project with lots of packages, helper tools and optimised CI pipelines, I wanted Stellar to be eventually part of a set of tools taking the name “Stellar Tools” that could enable the creation and the management of large codebases. Something like the following: - [Tuist](https://tuist.io/?ref=albertodebortoli.com): generates projects and workspaces programmatically - [PackageGenerator](https://github.com/justeattakeaway/PackageGenerator?ref=albertodebortoli.com): generates packages using a DSL - Stacker: creates a modular iOS project based on a DSL - Stellar: automate tasks - Workflows: generates GitHub Actions workflows that use Stellar From my old notes: ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2023/10/image-1.png) ## Current state After a few months of development within this team (made of devs not working at Just Eat Takeaway), I realised things were not moving in the direction I desired and I decided it was not beneficial to continue the collaboration with the team. We stopped working on Stellar mainly due to different levels of commitment from each of us and focus on the wrong tasks signalling a lack of project management from my end. For example, a considerable amount of time and effort went into the implementation of a version management system (vastly inspired by the one used in [Tuist](https://tuist.io/?ref=albertodebortoli.com)) that was not part of the scope of the Stellar project. The experience left me bitter and demotivated, learning that sometimes projects are best started alone. We made the repo public on GitHub aware that it was far from being production-ready but in my opinion, it's no doubt a nice, inspiring, MVP. [GitHub - StellarTools/StellarContribute to StellarTools/Stellar development by creating an account on GitHub.![](https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg)GitHubStellarTools![](https://opengraph.githubassets.com/d16992a8bc857f411032868e40dadc952a75d0f54a48d0cec40d55592aa027e9/StellarTools/Stellar)](https://github.com/StellarTools/Stellar?ref=albertodebortoli.com) [GitHub - StellarTools/ActionDSLContribute to StellarTools/ActionDSL development by creating an account on GitHub.![](https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg)GitHubStellarTools![](https://opengraph.githubassets.com/2f7bf81f51f52e27ad08afc02b784f0d1d3f6c2e403a0e91c724d09967a9eb40/StellarTools/ActionDSL)](https://github.com/StellarTools/ActionDSL/tree/main?ref=albertodebortoli.com) The intent was then to progress on my own or with my colleagues at JET. As things evolved in 2023, we embarked on big projects that continued to evolve the platform such as a massive migration to GitHub Actions. To this day, we still plan to remove Fastlane as our vision is to rely on external dependencies as little as possible but there is no plan to use Stellar as-is. I suspect that, for the infrastructure team at JET, things will evolve in a way that sees more CLI tools being implemented and more GitHub actions using them. ## ### CloudWatch dashboards and alarms on Mac instances URL: https://albertodebortoli.com/2023/08/06/cloudwatch-dashboards-and-alarms-on-mac-instances/ Last updated: 2025-07-14T23:11:03.000Z [CloudWatch](https://aws.amazon.com/cloudwatch/?ref=albertodebortoli.com) is great for observing and monitoring resources and applications on AWS, on premises, and on other clouds. While it's trivial to have the agent running on Linux, it's a bit more involved for mac instances (which are commonly used as CI workers). The support [was](https://aws.amazon.com/about-aws/whats-new/2021/01/amazon-cloudwatch-agent-now-supports-macos-on-amazon-ec2-mac-instances/?ref=albertodebortoli.com)[ announced](https://aws.amazon.com/about-aws/whats-new/2021/01/amazon-cloudwatch-agent-now-supports-macos-on-amazon-ec2-mac-instances/?ref=albertodebortoli.com) in January 2021 for mac1.metal (Intel/x86\_64) and I bumped into some challenges on mac2.metal (M1/ARM64) that the team at AWS helped me solve (see [this issue](https://github.com/aws/amazon-cloudwatch-agent/issues/798?ref=albertodebortoli.com) on the GitHub repo). I couldn't find other articles nor precise documentation from AWS which is why I'm writing this article to walk you through a common CloudWatch setup. The given code samples are for the HashiCorp tools [Packer](https://www.packer.io/?ref=albertodebortoli.com) and [Terraform](https://www.terraform.io/?ref=albertodebortoli.com) and focus on mac2.metal instances. I'll cover the following steps: - install the CloudWatch agent on mac2.metal instances - configure the CloudWatch agent - create a CloudWatch dashboard - setup CloudWatch alarms - setup IAM permissions ### Install the CloudWatch agent The CloudWatch agent can be installed by downloading the `pkg` file listed on [this page](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/download-cloudwatch-agent-commandline.html?ref=albertodebortoli.com) and running the installer. You probably want to bake the agent into your AMI, so here is the Packer code for mac2.metal (ARM): ``` # Install wget via brew provisioner "shell" { inline = [ "source ~/.zshrc", "brew install wget" ] } # Install CloudWatch agent provisioner "shell" { inline = [ "source ~/.zshrc", "wget https://s3.amazonaws.com/amazoncloudwatch-agent/darwin/arm64/latest/amazon-cloudwatch-agent.pkg", "sudo installer -pkg ./amazon-cloudwatch-agent.pkg -target /" ] } ``` For the agent to work, you'll need `collectd` ([https://collectd.org/](https://collectd.org/?ref=albertodebortoli.com)) to be installed on the machine, which is usually done via brew. Brew installs it at `/opt/homebrew/sbin/`. This is also a step you want to perform when creating your AMI. ``` # Install collectd via brew provisioner "shell" { inline = [ "source ~/.zshrc", "brew install collectd" ] } ``` ### Configure the CloudWatch agent In order to run, the agent needs a configuration which can be created using the wizard. [This page](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/create-cloudwatch-agent-configuration-file-wizard.html?ref=albertodebortoli.com) defines the metric sets that are available. Running the wizard with the command below will allow you to generate a basic json configuration which you can [](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html?ref=albertodebortoli.com)[modify](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-Configuration-File-Details.html?ref=albertodebortoli.com) later. ``` sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard ``` The following is a working configuration for Mac instances so you can skip the process. ``` { "agent": { "metrics_collection_interval": 60, "run_as_user": "root" }, "metrics": { "aggregation_dimensions": [ [ "InstanceId" ] ], "append_dimensions": { "AutoScalingGroupName": "${aws:AutoScalingGroupName}", "ImageId": "${aws:ImageId}", "InstanceId": "${aws:InstanceId}", "InstanceType": "${aws:InstanceType}" }, "metrics_collected": { "collectd": { "collectd_typesdb": [ "/opt/homebrew/opt/collectd/share/collectd/types.db" ], "metrics_aggregation_interval": 60 }, "cpu": { "measurement": [ "cpu_usage_idle", "cpu_usage_iowait", "cpu_usage_user", "cpu_usage_system" ], "metrics_collection_interval": 60, "resources": [ "*" ], "totalcpu": false }, "disk": { "measurement": [ "used_percent", "inodes_free" ], "metrics_collection_interval": 60, "resources": [ "*" ] }, "diskio": { "measurement": [ "io_time", "write_bytes", "read_bytes", "writes", "reads" ], "metrics_collection_interval": 60, "resources": [ "*" ] }, "mem": { "measurement": [ "mem_used_percent" ], "metrics_collection_interval": 60 }, "netstat": { "measurement": [ "tcp_established", "tcp_time_wait" ], "metrics_collection_interval": 60 }, "statsd": { "metrics_aggregation_interval": 60, "metrics_collection_interval": 10, "service_address": ":8125" }, "swap": { "measurement": [ "swap_used_percent" ], "metrics_collection_interval": 60 } } } } ``` I have enhanced the output of the wizard with some reasonable metrics to collect. The configuration created by the wizard is almost working but it's lacking a fundamental config to make it work out-of-the-box: the `collectd_typesdb` value. Linux and Mac differ when it comes to the location of `collectd` and `types.db`, and the agent defaults to the Linux path even if it was built for Mac, causing the following error when trying to run the agent: ``` ======== Error Log ======== 2023-07-23T04:57:28Z E! [telegraf] Error running agent: Error loading config file /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml: error parsing socket_listener, open /usr/share/collectd/types.db: no such file or directory ``` Moreover, the `/usr/share/` folder is not writable unless you disable SIP (System Integrity Protection) which cannot be done on EC2 mac instances nor is something you want to do for security reasons. The final configuration is something you want to save in [System Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html?ref=albertodebortoli.com) using the [ssm\_parameter](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm%5Fparameter?ref=albertodebortoli.com) resource in Terraform: ``` resource "aws_ssm_parameter" "cw_agent_config_darwin" { name = "/cloudwatch-agent/config/darwin" description = "CloudWatch agent config for mac instances" type = "String" value = file("./cw-agent-config-darwin.json") } ``` and use it when running the agent in a provisioning step: ``` resource "null_resource" "run_cloudwatch_agent" { depends_on = [ aws_instance.mac_instance ] connection { type = "ssh" agent = false host = aws_instance.mac_instance.private_ip user = "ec2-user" private_key = tls_private_key.mac_instance.private_key_pem timeout = "30m" } # Run CloudWatch agent provisioner "remote-exec" { inline = [ "sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c ssm:${aws_ssm_parameter.cw_agent_config_darwin.name}" ] } } ``` ### Create a CloudWatch dashboard Once the instances are deployed and running, they will send events to CloudWatch and we can create a dashboard to visualise them. You can create a dashboard manually in the console and once you are happy with it, you can just copy the source code, store it in a file and feed it to Terraform. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2023/08/image.png) Here is mine that could probably work for you too if you tag your instances with the `Type` set to `macOS`: ``` { "widgets": [ { "height": 15, "width": 24, "y": 0, "x": 0, "type": "explorer", "properties": { "metrics": [ { "metricName": "cpu_usage_user", "resourceType": "AWS::EC2::Instance", "stat": "Average" }, { "metricName": "cpu_usage_system", "resourceType": "AWS::EC2::Instance", "stat": "Average" }, { "metricName": "disk_used_percent", "resourceType": "AWS::EC2::Instance", "stat": "Average" }, { "metricName": "diskio_read_bytes", "resourceType": "AWS::EC2::Instance", "stat": "Average" }, { "metricName": "diskio_write_bytes", "resourceType": "AWS::EC2::Instance", "stat": "Average" } ], "aggregateBy": { "key": "", "func": "" }, "labels": [ { "key": "Type", "value": "macOS" } ], "widgetOptions": { "legend": { "position": "bottom" }, "view": "timeSeries", "stacked": false, "rowsPerPage": 50, "widgetsPerRow": 1 }, "period": 60, "splitBy": "", "region": "eu-west-1" } } ] } ``` You can then use the [cloudwatch\_dashboard](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch%5Fdashboard?ref=albertodebortoli.com) resource in Terraform: ``` resource "aws_cloudwatch_dashboard" "mac_instances" { dashboard_name = "mac-instances" dashboard_body = file("./cw-dashboard-mac-instances.json") } ``` It will show something like this: ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2023/08/Screenshot-2023-08-06-at-13.43.09.png) ### Setup CloudWatch alarms Once the dashboard is up, you should set up alarms so that you are notified of any anomalies, rather than actively monitoring the dashboard for them. What works for me is having alarms triggered via email when the used disk space is going above a certain level (say 80%). We can use the [cloudwatch\_metric\_alarm](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch%5Fmetric%5Falarm?ref=albertodebortoli.com) resource. ``` resource "aws_cloudwatch_metric_alarm" "disk_usage" { alarm_name = "mac-${aws_instance.mac_instance.id}-disk-usage" comparison_operator = "GreaterThanThreshold" evaluation_periods = 30 metric_name = "disk_used_percent" namespace = "CWAgent" period = 120 statistic = "Average" threshold = 80 alarm_actions = [aws_sns_topic.disk_usage.arn] dimensions = { InstanceId = aws_instance.mac_instance.id } } ``` We can then create an SNS topic and subscribe all interested parties to it. This will allow us to broadcast to all subscribers when the alarm is triggered. For this, we can use the [sns\_topic](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns%5Ftopic?ref=albertodebortoli.com) and [sns\_topic\_subscription](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns%5Ftopic%5Fsubscription?ref=albertodebortoli.com) resources. ``` resource "aws_sns_topic" "disk_usage" { name = "CW_Alarm_disk_usage_mac_${aws_instance.mac_instance.id}" } resource "aws_sns_topic_subscription" "disk_usage" { for_each = toset(var.alarm_subscriber_emails) topic_arn = aws_sns_topic.disk_usage.arn protocol = "email" endpoint = each.value } variable "alarm_subscriber_emails" { type = list(string) } ``` If you are deploying your infrastructure via [GitHub Actions](https://github.com/features/actions?ref=albertodebortoli.com), you can set your subscribers as a workflow input or as an environment variable. Here is how you should pass a list of strings via a variable in Terraform: ``` name: Deploy Mac instance env: ALARM_SUBSCRIBERS: '["user1@example.com","user2@example.com"]' AMI: ... jobs: deploy: ... steps: - name: Terraform apply run: | terraform apply \ --var ami=${{ env.AMI }} \ --var alarm_subscriber_emails='${{ env.ALARM_SUBSCRIBERS }}' \ --auto-approve ``` ### Setup IAM permissions The instance that performs the deployment requires permissions for CloudWatch, System Manager, and SNS. The following is a policy that is enough to perform both `terraform apply` and `terraform destroy`. Please consider restricting to specific resources. ``` { "Version": "2012-10-17", "Statement": [ { "Sid": "CloudWatchDashboardsPermissions", "Effect": "Allow", "Action": [ "cloudwatch:DeleteDashboards", "cloudwatch:GetDashboard", "cloudwatch:ListDashboards", "cloudwatch:PutDashboard" ], "Resource": "*" }, { "Sid": "CloudWatchAlertsPermissions", "Effect": "Allow", "Action": [ "cloudwatch:DescribeAlarms", "cloudwatch:DescribeAlarmsForMetric", "cloudwatch:DescribeAlarmHistory", "cloudwatch:DeleteAlarms", "cloudwatch:DisableAlarmActions", "cloudwatch:EnableAlarmActions", "cloudwatch:ListTagsForResource", "cloudwatch:PutMetricAlarm", "cloudwatch:PutCompositeAlarm", "cloudwatch:SetAlarmState" ], "Resource": "*" }, { "Sid": "SystemsManagerPermissions", "Effect": "Allow", "Action": [ "ssm:GetParameter", "ssm:GetParameters", "ssm:ListTagsForResource", "ssm:DeleteParameter", "ssm:DescribeParameters", "ssm:PutParameter" ], "Resource": "*" }, { "Sid": "SNSPermissions", "Effect": "Allow", "Action": [ "sns:CreateTopic", "sns:DeleteTopic", "sns:GetTopicAttributes", "sns:GetSubscriptionAttributes", "sns:ListSubscriptions", "sns:ListSubscriptionsByTopic", "sns:ListTopics", "sns:SetSubscriptionAttributes", "sns:SetTopicAttributes", "sns:Subscribe", "sns:Unsubscribe" ], "Resource": "*" } ] } ``` On the other hand, to send logs to CloudWatch, the Mac instances require permissions given by the `CloudWatchAgentServerPolicy`: ``` resource "aws_iam_role_policy_attachment" "mac_instance_iam_role_cw_policy_attachment" { role = aws_iam_role.mac_instance_iam_role.name policy_arn = "arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy" } ``` ### Conclusion You have now defined your CloudWatch dashboard and alarms using "Infrastructure as Code" via Packer and Terraform. I've covered the common use case of instances running out of space on disk which is useful to catch before CI starts becoming unresponsive slowing your team down. ### Easy connection to AWS Mac instances with EC2macConnector URL: https://albertodebortoli.com/2023/07/05/easy-connection-to-aws-mac-instances-with-ec2macconnector/ Last updated: 2023-08-06T13:00:35.000Z ## Overview Amazon Web Services (AWS) provides [EC2 Mac instances](https://aws.amazon.com/ec2/instance-types/mac/?ref=albertodebortoli.com) commonly used as CI workers. Configuring them can be either a manual or an automated process, depending on the DevOps and Platform Engineering experience in your company. No matter what process you adopt, it is sometimes useful to log into the instances to investigate problems. [EC2macConnector](https://github.com/albertodebortoli/EC2macConnector?ref=albertodebortoli.com) is a CLI tool written in Swift that simplifies the process of connecting over SSH and VNC for DevOps engineers, removing the need of updating private keys and maintaining the list of IPs that change across deployment cycles. ## Connecting to EC2 Mac instances without EC2macConnector [AWS documentation](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-mac-instances.html?ref=albertodebortoli.com) describes the steps needed to allow connecting via VNC: 1. Start the Apple Remote Desktop agent and enable remote desktop access on the instance 2. Set the password for the `ec2-user` user on the instance to allow connecting over VNC 3. Start an SSH session 4. Connect over VNC Assuming steps 1 and 2 and done, steps 3 and 4 are usually manual and repetitive: the private keys and IPs usually change across deployments which could happen frequently, even daily. Here is how to start an SSH session in the terminal binding a port locally: ```bash ssh ec2-user@ \ -L :localhost:5900 \ -i \ ``` To connect over VNC you can type the following in Finder → Go → Connect to Server (⌘ + K) and click Connect: ```bash vnc://ec2-user@localhost: ``` or you could create a `.vncloc` file with the following content and simply open it: ```xml "> URL vnc://ec2-user@localhost: ``` If you are a system administrator, you might consider [EC2 Instance Connect](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Connect-using-EC2-Instance-Connect.html?ref=albertodebortoli.com), but sadly, in my experience, it's not a working option for EC2 Mac instances even though I couldn't find evidence confirming or denying this statement. Administrators could also consider using [Apple Remote Desktop](https://apps.apple.com/gb/app/apple-remote-desktop/id409907375?mt=12&ref=albertodebortoli.com) which comes with a price tag of $/£79.99. ## Connecting to EC2 Mac instances with EC2macConnector EC2macConnector is a simple and free tool that works in 2 steps: - the `configure` command fetches the private keys and the IP addresses of the running EC2 Mac instances in a given region, and creates files using the said information to connect over SSH and VNC: ```bash ec2macConnector configure \ --region \ --secrets-prefix ``` Read below or the [README](https://github.com/albertodebortoli/EC2macConnector?ref=albertodebortoli.com) for more information on the secrets prefix value. - the `connect` command connects to the instances via SSH or VNC. ```bash ec2macConnector connect --region ``` ```bash ec2macConnector connect --region --vnc ``` 💡 Connecting over VNC requires an SSH session to be established first. As with any tool written using [ArgumentParser](https://github.com/apple/swift-argument-parser?ref=albertodebortoli.com), use the `--help` flag to get more information. ## Requirements There are some requirements to respect for the tool to work: ### Permissions EC2macConnector requires AWS credentials either set as environment variables (`AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`) or configured in `~/.aws/credentials` via the [AWS CLI](https://aws.amazon.com/cli/?ref=albertodebortoli.com). Environment variables take precedence. The user must be granted the following permissions: - `ec2:DescribeInstances` - `secretsmanager:ListSecrets` - `secretsmanager:GetSecretValue` ### EC2 instances The EC2 Mac instances must have the `EC2macConnector:FleetIndex` tag set to the index of the instance in the fleet. Indexes should start at 1\. Instances that don't have the said tag will be ignored. ### Secrets and key pairs formats EC2macConnector assumes that the private key for each instance key pair is stored in SecretsManagers. The name of the key pair could and should be different from the secret ID. For example, the instance key pair should include an incremental number also part of the corresponding secret ID. Consider that the number of Mac instances in an AWS account is limited and it's convenient to refer to them using an index starting at 1\. It's good practice for the secret ID to also include a nonce as secrets with the same name cannot be recreated before the deletion period has elapsed, allowing frequent provisioning-decommissioning cycles. For the above reasons, EC2macConnector assumes the following formats are used: - instance key pairs: `_` e.g. `mac_instance_key_pair_5` - secret IDs: `__` e.g. `private_key_mac_metal_5_dx9Wna73B` ## EC2macConnector Under the hood The `configure` command: - downloads the private keys in the `~/.ssh` folder - creates scripts to connect over SSH in `~/.ec2macConnector//scripts` - creates vncloc files to connect over VNC in `~/.ec2macConnector//vnclocs` ```bash ➜ .ec2macConnector tree ~/.ssh /Users/alberto/.ssh ├── mac_metal_1_i-08e4ffd8e9xxxxxxx ├── mac_metal_2_i-07bfff1f52xxxxxxx ├── mac_metal_3_i-020d680610xxxxxxx ├── mac_metal_4_i-08516ac980xxxxxxx ├── mac_metal_5_i-032bedaabexxxxxxx ├── config ├── known_hosts └── ... ``` The `connect` command: - runs the scripts (opens new shells in Terminal and connects to the instances over SSH) - opens the vncloc files ```bash ➜ .ec2macConnector tree ~/.ec2macConnector /Users/alberto/.ec2macConnector └── us-east-1 ├── scripts │   ├── connect_1.sh │   ├── connect_2.sh │   ├── connect_3.sh │   ├── connect_4.sh │   └── connect_5.sh └── vnclocs ├── connect_1.vncloc ├── connect_2.vncloc ├── connect_3.vncloc ├── connect_4.vncloc └── connect_5.vncloc ``` ### Toggles: the easiest feature flagging in Swift URL: https://albertodebortoli.com/2023/03/26/toggles/ Last updated: 2023-07-05T14:30:07.000Z I previously wrote about [JustTweak](https://github.com/justeat/JustTweak?ref=albertodebortoli.com) [here](https://albertodebortoli.com/2019/11/26/a-smart-feature-flagging-system-for-ios/). It's the feature flagging mechanism we've been using at Just Eat Takeaway.com to power the iOS consumer apps since 2017\. It's proved to be very stable and powerful and it has evolved over time. Friends have heard me promoting it vehemently and some have integrated it with success and appreciation. I don't think I've promoted it in the community enough (it definitely deserved more) but marketing has never been my thing. Anyway, JustTweak grew old and some changes were debatable and not to my taste. I have then decided to use the knowledge of years of working on the feature flagging matter to give this project a new life by rewriting it from scratch as a personal project. And here it is: [Toggles](https://github.com/TogglesPlatform/Toggles?ref=albertodebortoli.com). > I never tweeted about this side project of mine 😜 > It's like JustTweak (feature flagging), but sensibly better. [https://t.co/bdGWuUyQEU](https://t.co/bdGWuUyQEU?ref=albertodebortoli.com) [#Swift](https://twitter.com/hashtag/Swift?src=hash&ref%5Fsrc=twsrc%5Etfw&ref=albertodebortoli.com) [#iOS](https://twitter.com/hashtag/iOS?src=hash&ref%5Fsrc=twsrc%5Etfw&ref=albertodebortoli.com) [#macOS](https://twitter.com/hashtag/macOS?src=hash&ref%5Fsrc=twsrc%5Etfw&ref=albertodebortoli.com) > > — Alberto De Bortoli (@albertodebo) [March 23, 2023](https://twitter.com/albertodebo/status/1638851577468588032?ref%5Fsrc=twsrc%5Etfw&ref=albertodebortoli.com) Think of JustTweak, but better. A lot better. Frankly, I couldn't have written it better. Here are the main highlights: - brand new code, obsessively optimized and kept as short and simple as possible - extreme performances - fully tested - fully documented - performant UI debug view in SwiftUI - standard providers provided - demo app provided - ability to listen for value changes (using Combine) - simpler APIs - ToggleGen CLI, to allow code generation - ToggleCipher CLI, to allow encoding/decoding of secrets - JustTweakMigrator CLI, to allow a smooth transition from JustTweak ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2023/03/DemoApp_iPad.png) Read all about it on the [repo's README](https://github.com/TogglesPlatform/Toggles?ref=albertodebortoli.com#readme) and on the [DocC page](https://togglesplatform.github.io/Toggles/documentation/toggles/?ref=albertodebortoli.com). It's on Swift Package Index too. [Toggles – Swift Package IndexToggles by TogglesPlatform on the Swift Package Index – Toggles is an elegant and powerful solution to feature flagging for Apple platforms.![](https://swiftpackageindex.com/images/logo-small.png)Learn more![](https://swiftpackageindex.com/images/logo.png)](https://swiftpackageindex.com/TogglesPlatform/Toggles?ref=albertodebortoli.com) There are plans (or at least the desire!) to write a backend with [Andrea Scuderi](https://twitter.com/andreascuderi13?ref=albertodebortoli.com). That'd be really nice! > [@albertodebo](https://twitter.com/albertodebo?ref%5Fsrc=twsrc%5Etfw&ref=albertodebortoli.com) This wasn't planned! It looks like we need to build the backend for [#Toggles](https://twitter.com/hashtag/Toggles?src=hash&ref%5Fsrc=twsrc%5Etfw&ref=albertodebortoli.com) with [#Breeze](https://twitter.com/hashtag/Breeze?src=hash&ref%5Fsrc=twsrc%5Etfw&ref=albertodebortoli.com)! [pic.twitter.com/OxNovRl70L](https://t.co/OxNovRl70L?ref=albertodebortoli.com) > > — andreascuderi (@andreascuderi13) [March 26, 2023](https://twitter.com/andreascuderi13/status/1640105067381506049?ref%5Fsrc=twsrc%5Etfw&ref=albertodebortoli.com) ### The Continuous Integration system used by the mobile teams URL: https://albertodebortoli.com/2021/07/23/the-continuous-integration-system-used-by-the-mobile-teams/ Last updated: 2021-07-23T09:23:57.000Z **Originally published on the [Just Eat Takeaway Engineering Blog](https://medium.com/takeaway-tech/the-continuous-integration-system-used-by-the-mobile-teams-28ba057ef628?ref=albertodebortoli.com).* ## Overview In this article, we’ll discuss the way our mobile teams have evolved the Continuous Integration (CI) stack over the recent years. We don’t have DevOps engineers in our team and, until recently, we had adopted a singular approach in which CI belongs to the whole team and everyone should be able to maintain it. This has proven to be difficult and extremely time-consuming. The Just Eat side of our newly merged entity has a dedicated team providing continuous integration and deployment tools to their teams but they are heavily backend-centric and there has been little interest in implementing solutions tailored for mobile teams. As is often the case in tech companies, there is a missing link between mobile and DevOps teams. The iOS team is the author and first consumer of the solution described but, as you can see, we have ported the same stack to Android as well. We will mainly focus on the iOS implementation in this article, with references to Android as appropriate. ### 2016–2020 Historically speaking, the iOS UK app was running on [Bitrise](https://www.bitrise.io/customer-stories/just-eat?ref=albertodebortoli.com) because it was decided not to invest time in implementing a CI solution, while the Bristol team was using a Jenkins version installed by a different team. This required manual configuration with custom scripts and it had custom in-house hardware. These are two quite different approaches indeed and, at this stage, things were not great but somehow good enough. It’s fair to say we were still young on the DevOps front. When we merged the teams, it was clear that we wanted to unify the CI solution and the obvious choice for a company of our size was to not use a third-party service, bringing us to invest more and more in Jenkins. Only one team member had good knowledge of Jenkins but the rest of the team showed little interest in learning how to configure and maintain it, causing the stack to eventually become a dumping ground of poorly configured jobs. It was during this time that we introduced Fastlane (making the common tasks portable), migrated the UK app from Bitrise to Jenkins, started running the UI tests on Pull Requests, and other small yet sensible improvements. ### 2020–2021 Starting in mid-2020 the iOS team has significantly revamped its CI stack and given it new life. The main goals we wanted to achieve (and did by early 2021) were: - Revisit the pipelines - Clear Jenkins configuration and deployment strategy - Make use of [AWS Mac instances](https://aws.amazon.com/ec2/instance-types/mac/?ref=albertodebortoli.com) - Reduce the pool size of our mac hardware - Share our knowledge across teams better Since the start of the pandemic, we have implemented the pipelines in code (bidding farewell to custom bash scripts), we moved to a monorepo which was a massive step ahead and began using SonarQube even more aggressively. We added Slack reporting and [PR Assigner](https://github.com/justeat/PRAssigner?ref=albertodebortoli.com), an internal tool implemented by [Andrea Antonioni](https://twitter.com/%5Faantonioni?ref=albertodebortoli.com). We also automated the common release tasks such as cutting and completing a release and uploading the dSYMS to Firebase. We surely invested a lot in optimizing various aspects such as running the UI tests in parallel, making use of shallow repo cloning, We also moved to not checking in the pods within the repo. This, eventually, allowed us to reduce the number of agents for easier infrastructure maintenance. Automating the infrastructure deployment of Jenkins was a fundamental shift compared to the previous setup and we have introduced AWS Mac instances replacing part of the fleet of our in-house hardware. ## CI system setup Let’s take a look at our stack. Before we start, we’d like to thank [Isham Araia](https://twitter.com/isham%5Faraia?ref=albertodebortoli.com) for having provided a proof of concept for the configuration and deployment of Jenkins. He talked about it at [https://ish-ar.io/jenkins-at-scale/](https://ish-ar.io/jenkins-at-scale/?ref=albertodebortoli.com) and it represented a fundamental starting point, saving us several days of researching. ### Triggering flow ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2021/06/0_nShqCMN_dUGfxKpv.png) Starting from the left, we have our repositories (plural, as some shared dependencies don’t live in the monorepo). The repositories contain the pipelines in the form of Jenkinsfiles and they call into Fastlane lanes. Pretty much every action is a lane, from running the tests to archiving for the App Store to creating the release branches. Changes are raised through pull requests that trigger Jenkins. There are other ways to trigger Jenkins: by user interaction (for things such as completing a release or archiving and uploading the app to App Store Connect) and cron triggers (for things such as building the nightly build, running the tests on the develop branch every 12 hours, or uploading the [PACT](https://pact.io/?ref=albertodebortoli.com) contract to the broker). Once Jenkins has received the information, it will then schedule the jobs to one of the agents in our pool, which is now made up of 5 agents, 2 in the cloud and 3 in-house mac pros. ### Reporting flow Now that we’ve talked about the first part of the flow, let’s talk about the flow of information coming back at us. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2021/06/0_ZQEEEnodojpGtC50.png) Every PR triggers [PR Assigner](https://github.com/justeat/PRAssigner?ref=albertodebortoli.com), a tool that works out a list of reviewers to assign to pull requests and notifies engineers via dedicated Slack channels. The pipelines post on Slack, providing info about all the jobs that are being executed so we can read the history without having to log into Jenkins. We have in place the standard notification flow from Jenkins to GitHub to set the status checks and Jenkins also notifies SonarQube to verify that any change meets the quality standards (namely code coverage percentage and coding rules). We also have a smart lambda named SonarQubeStatusProcessor that reports to GitHub, written by Alan Nichols. This is due to a current limitation of SonarQube, which only allows reporting the status of one SQ project to one GitHub repo. Since we have a monorepo structure we had to come up with this neat customization to report the SQ status for all the modules that have changed as part of the PR. ## Configuration Let’s see what the new interesting parts of Jenkins are. Other than Jenkins itself and several plugins, it’s important to point out [JCasC](https://github.com/jenkinsci/configuration-as-code-plugin?ref=albertodebortoli.com) and [Job DSL](https://github.com/jenkinsci/job-dsl-plugin?ref=albertodebortoli.com). ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2021/06/1_DPeIIS4s9GBotWrXJQ0RBw.png) JCasC stands for Jenkins Configuration as Code, and it allows you to configure Jenkins via a yaml file. The point here is that nobody should ever touch the Jenkins settings directly from the configuration page, in the same way, one ideally shouldn’t apply configuration changes manually in any dashboard. The CasC file is where we define the Slack integration, the user roles, SSO configuration, the number of agents and so on. We could also define the jobs in CasC but we go a step further than that. We use the Job DSL plugin that allows you to configure the jobs in groovy and in much more detail. One job we configure in the CasC file though is the seed job. This is a simple freestyle job that will go pick the jobs defined with Job DSL and create them in Jenkins. ## Deployment Let’s now discuss how we can get a configured Jenkins instance on EC2\. In other words, how do we deploy Jenkins? We use a combination of tools that are bread and butter for DevOps people. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2021/06/0_CAnhTU1F_7EskQ7T.png) The commands on the left spawn a Docker container that calls into the tools on the right. We start with Packer which allows us to create the AMI (Amazon Machine Image) together with Ansible, allowing us to configure an environment easily (much more easily than Chef or Puppet). Running the `create-image` command the script will: 1\. Create a temporary EC2 instance 2\. Connect to the instance and execute an ansible playbook Our playbook encompasses a number of steps, here’s a summary: - install the Jenkins version for the given Linux distribution - install Nginx - copy the SSL cert over - configure nginx w/ SSL termination and reverse proxy - install the plugins for Jenkins Once the playbook is executed, Packer will export an AMI in EC2 with all of this in it and destroy the instance that was used. With the AMI ready, we can now proceed to deploy our Jenkins. For the actual deployment, we use Terraform which allows us to define our infrastructure in code. The deploy command runs Terraform under the hood to set up the infrastructure, here’s a summary of the task: - create an IAM Role + IAM Policy - configure security groups - create the VPC and subnet to use with a specific CIDER block and the subnet - create any private key pair to connect over SSH - deploy the instance using a static private IP (it has to be static otherwise the A record in Route53 would break) - copy the JCasC configuration file over so that when Jenkins starts it picks that up to configure itself The destroy command runs a “terraform destroy” and destroys everything that was created with the deploy command. Deploy and destroy balance each other out. Now that we have Jenkins up and running, we need to give it some credentials so our pipelines are able to work properly. A neat way of doing this is by having the secrets (SSH keys, Firebase tokens, App Store Connect API Key and so forth) in AWS Secrets Manager which is based on KMS and use a Jenkins plugin to allow Jenkins to access them. It’s important to note that developers don’t have to install Packer, Ansible, Terraform or even the AWS CLI locally because the commands run a Docker container that does the real work with all the tools installed. As a result, the only thing one should have installed is really Docker. ## CI agents Enough said about Jenkins, it’s time to talk about the agents.As you probably already know, in order to run tests, compile and archive iOS apps we need Xcode, which is only available on macOS, so Linux or Windows instances are not going to cut it. We experimented with the recently introduced AWS Mac instances and they are great, ready out-of-the-box with minimal configuration on our end. What we were hoping to get to with this recent work was the ability to leverage the Jenkins Cloud agents. That would have been awesome because it would have allowed us to: - let Jenkins manage the agent instances - scale the agent pool according to the load on CI Sadly we couldn't go that far. Limitations are: - the bootstrapping of a mac1.metal takes around 15 minutes - reusing the dedicated host after having stopped an instance can take up to 3 hours — during that time we just pay for a host that is not usable > When you stop or terminate a Mac instance, Amazon EC2 performs a scrubbing workflow on the underlying Dedicated Host to erase the internal SSD, to clear the persistent NVRAM variables, and if needed, to update the bridgeOS software on the underlying Mac mini. > This ensures that Mac instances provide the same security and data privacy as other EC2 Nitro instances. It also enables you to run the latest macOS AMIs without manually updating the bridgeOS software. During the scrubbing workflow, the Dedicated Host temporarily enters the pending state. If the bridgeOS software does not need to be updated, the scrubbing workflow takes up to 50 minutes to complete. If the bridgeOS software needs to be updated, the scrubbing workflow can take up to 3 hours to complete. Source: [https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-mac-instances.html](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-mac-instances.html?ref=albertodebortoli.com) In other words: scaling mac instances is not an option and leaving the instances up 24/7 seems to be the easiest option. This is especially valid if your team is distributed and jobs could potentially run over the weekend as well, saving you the hassle of implementing downscaling ahead of the weekend. There are some pricing and instance allocation considerations to make. Note that On-Demand Mac1 Dedicated Hosts have a minimum host allocation and billing duration of 24 hours. > “You can purchase Savings Plans to lower your spend on Dedicated Hosts. Savings Plans is a flexible pricing model that provides savings of up to 72% on your AWS compute usage. This pricing model offers lower prices on Amazon EC2 instances usage, regardless of instance family, size, OS, tenancy or AWS Region.” Source: [https://aws.amazon.com/ec2/dedicated-hosts/pricing/](https://aws.amazon.com/ec2/dedicated-hosts/pricing/?ref=albertodebortoli.com) The On-Demand rate is **$1.207** per hour. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2021/06/0_NkSjKzAiR5va1BnX.png) I’d like to stress that no CI solution comes for free. I’ve often heard developers indicating that Travis and similar products are cheaper. The truth is that the comparison is not even remotely reasonable: virtual boxes are incredibly slow compared to native Apple hardware and take ridiculous bootstrapping times. Even the smallest projects suffer terribly. One might ask if it’s at least possible to use the same configuration process we used for the Jenkins instance (with Packer and Ansible) but sadly we hit additional limitations: - Apple requires 2FA for downloading Xcode via [xcode-version](https://github.com/xcpretty/xcode-install?ref=albertodebortoli.com) - Apple requires 2FA for signing into Xcode The above pretty much causes the configuration flow to fall apart making it impossible to configure an instance via Ansible. ### Cloud agents for Android It was a different story for Android, in which we could easily configure the agent instance with Ansible and therefore leverage the Cloud configuration to allow automatic agent provisioning. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2021/06/0_t6i18L8XEJjw1-Z-.png) This configuration is defined via CasC as everything else. To better control EC2 usage and costs, a few settings come in handy: - minimum number of instances (up at all times) - minimum number of spare instances (created to accommodate future jobs) - instance cap: the maximum number of instances that can be provisioned at the same time - idle termination time: how long agents should be kept alive after they have completed the job All of the above allow for proper scaling and a lot less maintenance compared to the iOS setup. A simple setup with 0 instances up at all times allows saving costs overnight and given that in our case the bootstrapping takes only 2 minutes, we can rely on the idle time setting. ## Conclusions Setting up an in-house CI is never a straightforward process and it requires several weeks of dedicated work. After years of waiting, Apple has announced [Xcode Cloud](https://developer.apple.com/documentation/Xcode/Xcode-Cloud?ref=albertodebortoli.com) which we believe will drastically change the landscape of continuous integration on iOS. The solution will most likely cause havoc for companies such as Bitrise and CircleCI and it’s reasonable to assume the pricing will be competitive compared to AWS, maybe running on custom hardware that only Apple is able to produce. A shift this big will take time to occur, so we foresee our solution to stay in use for quite some time. We hope to have inspired you on how a possible setup for mobile teams could be and informed you on what are the pros & cons of using EC2 mac instances. ### iOS Monorepo & CI Pipelines URL: https://albertodebortoli.com/2021/06/16/ios-monorepo-ci-pipelines/ Last updated: 2024-10-17T17:01:38.000Z *Originally published on the* [*Just Eat Takeaway Engineering Blog*](https://medium.com/takeaway-tech/ios-monorepo-ci-pipelines-99a3b69240a9?ref=albertodebortoli.com)*.* We have presented our modular iOS architecture in a previous [article](https://albertodebortoli.com/2019/12/19/modular-ios-architecture-at-just-eat/) and I gave a [talk](https://www.youtube.com/watch?v=QzM3lsFewN4&ref=albertodebortoli.com) at Swift Heroes 2020 about it. In this article, we’ll analyse the challenges we faced to have the modular architecture integrated with our CI pipelines and the reasoning behind migrating to a monorepo. ## The Problem Having several modules in separate repositories brings forward 2 main problems: 1. Each module is versioned independently from the consuming app 2. Each change involves at least 2 pull requests: 1 for the module and 1 for the integration in the app While the above was acceptable in a world where we had 2 different codebases, it soon became unnecessarily convoluted after we migrated to a new, global codebase. New module versions are implemented with the ultimate goal of being adopted by the only global codebase in use, making us realise we could simplify the change process. The monorepo approach has been discussed at length by the community for a few years now. Many talking points have come out of these conversations, even leading to an interesting story as told by Uber. In short, it entails putting all the code owned by the team in a single repository, precisely solving the 2 problems stated above. ## Monorepo structure The main advantage of a monorepo is a streamlined PR process that doesn’t require us to raise multiple PRs, de facto reducing the number of pull requests to one. It also simplifies the versioning, allowing module and app code (ultimately shipped together) to be aligned using the same versioning. The first step towards a monorepo was to move the content of the repositories of the modules to the main app repo (we’ll call it “monorepo” from now on). Since we rely on [CocoaPods](https://cocoapods.org/?ref=albertodebortoli.com), the modules would be consumed as[ development pods](https://guides.cocoapods.org/making/making-a-cocoapod.html?ref=albertodebortoli.com#development). Here’s a brief summary of the steps used to migrate a module to the monorepo: - Inform the relevant teams about the upcoming migration - Make sure there are no open PRs in the module repo - Make the repository read-only and archive it - Copy the module to the Modules folder of the monorepo (it’s possible to[ merge 2 repositories to keep the history](https://saintgimp.org/2013/01/22/merging-two-git-repositories-into-one-repository-without-losing-file-history/?ref=albertodebortoli.com) but we felt we wanted to keep the process simple, the old history is still available in the old repo anyway) - Delete the module `.git` folder (or it would cause a git submodule) - Remove `Gemfile` and `Gemfile.lock` `fastlane` folder, `.gitignore` file, `sonar-project.properties`, `.swiftlint.yml` so to use those in the monorepo - Update the monorepo’s `CODEOWNERS` file with the module codeowners - Remove the `.github` folder - Modify the app `Podfile` to point to the module as a dev pod and install it - Make sure all the modules’ demo apps in the monorepo refer to the new module as a dev pod (if they depend on it at all). The same applies to the module under migration. - Delete the CI jobs related to the module - Leave the podspecs in the private Specs repo (might be needed to build old versions of the app) The above assumes that CI is configured in a way that preserves the same integration steps upon a module change. We’ll discuss them later in this article. Not all the modules could be migrated to the monorepo, due to the fact the second-level dependencies need to live in separate repositories in order to be referenced in the podspec of a development pod. If not done correctly, CocoaPods will not be able to install them. We considered moving these dependencies to the monorepo whilst maintaining separate versioning, however, the main problem with this approach is that the version tags might conflict with the ones of the app. Even though CocoaPods supports tags that don’t respect [semantic versioning](https://semver.org/?ref=albertodebortoli.com) (for example prepending the tag with the name of the module), violating it just didn’t feel right. EDIT: we’ve learned that it’s possible to move such dependencies to the monorepo. This is done not by defining `:path=>` in the podspecs but instead by doing so in the Podfile of the main app, which is all Cocoapods needs to work out the location of the dependency on disk. ### Swift Package Manager considerations We investigated the possibility of migrating from CocoaPods to Apple’s [Swift Package Manager](https://swift.org/package-manager/?ref=albertodebortoli.com). Unfortunately, when it comes to handling the equivalent of development pods, Swift Package Manager really falls down for us. It turns out that Swift Package Manager only supports[ one package per repo](https://stackoverflow.com/a/50095567?ref=albertodebortoli.com), which is frustrating because the process of working with editable packages is surprisingly powerful and transparent. ## Version pinning rules While development pods don’t need to be versioned, other modules still need to. This is either because of their open-source nature or because they are second-level dependencies (referenced in other modules’ podspecs). Here’s a revised overview of the current modular architecture in 2021. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2021/06/The-Just-Eat-iOS-Stack---Holistic-Design-2021.png) We categorised our pods to better clarify what rules should apply when it comes to version pinning both in the `Podfile`s and in the `podspec`s. ### Open-Source pods Our open-source repositories on [github.com/justeat](https://github.com/justeat?ref=albertodebortoli.com) are only used by the app. - *Examples:* JustTweak, AutomationTools, Shock - *Pinning in other modules’ podspec:* **NOT APPLICABLE** open-source pods don’t appear in any podspec, those that do are called ‘open-source shared’ - *Pinning in other modules’ Podfile (demo apps):* **PIN** (e.g. AutomationTools in Orders demo app’s Podfile) - *Pinning in main app’s Podfile:* **PIN** (e.g. AutomationTools) ### Open-Source shared pods The Just Eat pods we put open-source on [github.com/justeat](https://github.com/justeat?ref=albertodebortoli.com) and are used by modules and apps. - *Examples:* JustTrack, JustLog, ScrollingStackViewController, ErrorUtilities - *Pinning in other modules’ podspec:* **PIN** w/ optimistic operator (e.g. JustTrack in Orders) - *Pinning in other modules’ Podfile (demo apps):* **PIN** (e.g. JustTrack in Orders demo app’s Podfile) - *Pinning in main app’s Podfile:* **DON’T LIST** latest compatible version is picked by CocoaPods (e.g. JustTrack). **LIST & PIN** if the pod is explicitly used in the app too, so we don’t magically inherit it. ### Internal Domain pods Domain modules (yellow). - *Examples:* Orders, SERP, etc. - *Pinning in other modules’ podspec:* **NOT APPLICABLE** domain pods don’t appear in other pods’ podspecs (domain modules don’t depend on other domain modules) - *Pinning in other modules’ Podfile (demo apps):* **PIN** only if the pod is used in the app code, rarely the case (e.g. Account in Orders demo app’s Podfile) - *Pinning in main app’s Podfile:* **PIN** (e.g. Orders) ### Internal Core pods Core modules (blue) minus those open-source. - *Examples:* APIClient, AssetProvider - *Pinning in other modules’ podspec:* **NOT APPLICABLE** core pods don’t appear in other pods’ podspecs (core modules are only used in the app(s)) - *Pinning in other modules’ Podfile (demo apps):* **PIN** only if pod is used in the app code (e.g. APIClient in Orders demo app’s Podfile) - *Pinning in main app’s Podfile:* **PIN** (e.g. NavigationEngine) ### Internal shared pods Shared modules (green) minus those open-source. - *Examples:* JustUI, JustAnalytics - *Pinning in other modules’ podspec:* **DON’T PIN** (e.g. JustUI in Orders podspec) - *Pinning in other modules’ Podfile (demo apps):* **PIN** (e.g. JustUI in Orders demo app’s Podfile) - *Pinning in main app’s Podfile:* **PIN** (e.g. JustUI) ### External shared pods Any non-Just Eat pod used by any internal or open-source pod. - *Examples:* Usabilla, SDWebImage - *Pinning in other modules’ podspec:* **PIN** (e.g. Usabilla in Orders) - *Pinning in other modules’ Podfile (demo apps):* **DON’T LIST** because the version is forced by the podspec. **LIST & PIN** if the pod is explicitly used in the app too, so we don’t magically inherit it. Pinning is irrelevant but good practice. - *Pinning in main app’s Podfile:* **DON’T LIST** because the version is forced by the podspec(s). **LIST & PIN** if the pod is explicitly used in the app too, so we don’t magically inherit it. Pinning is irrelevant but good practice. ### External pods Any non-Just Eat pod used by the app only. - *Examples:* Instabug, GoogleAnalytics - *Pinning in other modules’ podspec:* **NOT APPLICABLE** external pods don’t appear in any podspec, those that do are called ‘external shared’ - *Pinning in other modules’ Podfile (demo apps):* **PIN** only if the pod is used in the app code, rarely the case (e.g. Promis) - *Pinning in main app’s Podfile:* **PIN** (e.g. Adjust) Pinning is a good solution because it guarantees that we always build the same software regardless of new released versions of dependencies. It’s also true that pinning every dependency all the time makes the dependency graph hard to keep updated. This is why we decided to allow some flexibility in some cases. Following is some more reasoning. ### Open-source For “open-source shared” pods, we are optimistic enough (pun intended) to tolerate the usage of the optimistic operator `~>` in podspecs of other pods (i.e Orders using JustTrack) so that when a new patch version is released, the consuming pod gets it for free upon running `pod update`. We have control over our code and, by respecting semantic versioning, we guarantee the consuming pod to always build. In case of new minor or major versions, we would have to update the podspecs of the consuming pods, which is appropriate. Also, we do need to list any “open-source shared” pod in the main app’s Podfile only if directly used by the app code. ### External We don’t have control over the “external” and “external shared” pods, therefore we always pin the version in the appropriate place. New patch versions might not respect semantic versioning for real and we don’t want to pull in new code unintentionally. As a rule of thumb, we prefer injecting external pods instead of creating a dependency in the podspec. ### Internal Internal shared pods could change frequently (not as much as domain modules). For this reason, we’ve decided to relax a constraint we had and not to pin the version in the podspec. This might cause the consuming pod to break when a new version of an “internal shared” pod is released and we run `pod update`. This is a compromise we can tolerate. The alternative would be to pin the version causing too much work to update the podspec of the domain modules. ## Continuous Integration changes With modules in separate repositories, the CI was quite simply replicating the same steps for each module: - install pods - run unit tests - run UI tests - generated code coverage - submit code coverage to SonarQube Moving the modules to the monorepo meant creating smart CI pipelines that would run the same steps upon modules’ changes. If a pull request is to change only app code, there is no need to run any step for the modules, just the usual steps for the app: ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2021/06/0_iE4VY6rYjdrFzu5K.png) If instead, a pull request applies changes to one or more modules, we want the pipeline to first run the steps for the modules, and then the steps for the app: ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2021/06/0_JEC2Pxzbispwi531.png) Even if there are no changes in the app code, module changes could likely impact the app behaviour, so it’s important to always run the app tests. We have achieved the above setup through constructing our Jenkins pipelines dynamically. The solution should scale when new modules are added to the monorepo and for this reason, it’s important that all modules: - respect the same project setup (generated by CocoaPods w/ the `pod lib create` command) - use the same naming conventions for the test schemes (`UnitTests`/`ContractTests`/`UITests`) - make use of Apple Test Plans - are in the same location ( `./Modules/` folder). Following is an excerpt of the code that constructs the modules’ stages from the Jenkinsfile used for pull request jobs. ```Groovy scripts = load "./Jenkins/scripts/scripts.groovy" def modifiedModules = scripts.modifiedModulesFromReferenceBranch(env.CHANGE_TARGET) def modulesThatNeedUpdating = scripts.modulesThatNeedUpdating(env.CHANGE_TARGET) def modulesToRun = (modulesThatNeedUpdating + modifiedModules).unique() sh "echo \"List of modules modified on this branch: ${modifiedModules}\"" sh "echo \"List of modules that need updating: ${modulesThatNeedUpdating}\"" sh "echo \"Pipeline will run the following modules: ${modulesToRun}\"" for (int i = 0; i < modulesToRun.size(); ++i) { def moduleName = modulesToRun[i] stage('Run pod install') { sh "bundle exec fastlane pod_install module:${moduleName}" } def schemes = scripts.testSchemesForModule(moduleName) schemes.each { scheme -> switch (scheme) { case "UnitTests": stage("${moduleName} Unit Tests") { sh "bundle exec fastlane module_unittests \ module_name:${moduleName} \ device:'${env.IPHONE_DEVICE}'" } stage("Generate ${moduleName} code coverage") { sh "bundle exec fastlane generate_sonarqube_coverage_xml" } stage("Submit ${moduleName} code coverage to SonarQube") { sh "bundle exec fastlane sonar_scanner_pull_request \ component_type:'module' \ source_branch:${env.BRANCH_NAME} \ target_branch:${env.CHANGE_TARGET} \ pull_id:${env.CHANGE_ID} \ project_key:'ios-${moduleName}' \ project_name:'iOS ${moduleName}' \ sources_path:'./Modules/${moduleName}/${moduleName}'" } break; case "ContractTests": stage('Install pact mock service') { sh "bundle exec fastlane install_pact_mock_service" } stage("${moduleName} Contract Tests") { sh "bundle exec fastlane module_contracttests \ module_name:${moduleName} \ device:'${env.IPHONE_DEVICE}'" } break; case "UITests": stage("${moduleName} UI Tests") { sh "bundle exec fastlane module_uitests \ module_name:${moduleName} \ number_of_simulators:${env.NUMBER_OF_SIMULATORS} \ device:'${env.IPHONE_DEVICE}'" } break; default: break; } } } ``` and here are the helper functions to make it all work: ```Groovy def modifiedModulesFromReferenceBranch(String referenceBranch) { def script = "git diff --name-only remotes/origin/${referenceBranch}" def filesChanged = sh script: script, returnStdout: true Set modulesChanged = [] filesChanged.tokenize("\n").each { def components = it.split('/') if (components.size() > 1 && components[0] == 'Modules') { def module = components[1] modulesChanged.add(module) } } return modulesChanged } def modulesThatNeedUpdating(String referenceBranch) { def modifiedModules = modifiedModulesFromReferenceBranch(referenceBranch) def allModules = allMonorepoModules() def modulesThatNeedUpdating = [] for (module in allModules) { def podfileLockPath = "Modules/${module}/Example/Podfile.lock" def dependencies = podfileDependencies(podfileLockPath) def dependenciesIntersection = dependencies.intersect(modifiedModules) as TreeSet Boolean moduleNeedsUpdating = (dependenciesIntersection.size() > 0) if (moduleNeedsUpdating == true && modifiedModules.contains(module) == false) { modulesThatNeedUpdating.add(module) } } return modulesThatNeedUpdating } def podfileDependencies(String podfileLockPath) { def dependencies = [] def fileContent = readFile(file: podfileLockPath) fileContent.tokenize("\n").each { line -> def lineComponents = line.split('\\(') if (lineComponents.length > 1) { def dependencyLineSubComponents = lineComponents[0].split('-') if (dependencyLineSubComponents.length > 1) { def moduleName = dependencyLineSubComponents[1].trim() dependencies.add(moduleName) } } } return dependencies } def allMonorepoModules() { def modulesList = sh script: "ls Modules", returnStdout: true return modulesList.tokenize("\n").collect { it.trim() } } def testSchemesForModule(String moduleName) { def script = "xcodebuild -project ./Modules/${moduleName}/Example/${moduleName}.xcodeproj -list" def projectEntitites = sh script: script, returnStdout: true def schemesPart = projectEntitites.split('Schemes:')[1] def schemesPartLines = schemesPart.split(/\n/) def trimmedLined = schemesPartLines.collect { it.trim() } def filteredLines = trimmedLined.findAll { !it.allWhitespace } def allowedSchemes = ['UnitTests', 'ContractTests', 'UITests'] def testSchemes = filteredLines.findAll { allowedSchemes.contains(it) } return testSchemes } ``` You might have noticed the `modulesThatNeedUpdating` method in the code above. Each module comes with a demo app using the dependencies listed in its Podfile and it’s possible that other monorepo modules are listed there as development pods. This not only means that we have to run the steps for the main app, but also the steps for every module consuming modules that show changes. For example, the Orders demo app uses APIClient, meaning that pull requests with changes in APIClient will generate pipelines including the Orders steps. ## Pipeline parallelization Something we initially thought was sensible to consider is the parallelisation of the pipelines across different nodes. We use parallelisation for the release pipelines and learned that, while it seems to be a fundamental requirement at first, it soon became apparent not to be so desirable nor truly fundamental for the pull requests pipeline. We’ll discuss our CI setup in a separate article, but suffice to say that we have aggressively optimized it and managed to reduce the agent pool from 10 to 5, still maintaining a good level of service. Parallelisation sensibly complicates the Jenkinsfiles and their maintainability, spreads the cost of checking out the repository across nodes and makes the logs harder to read. The main benefit would come from running the app UI tests on different nodes. In the WWDC[ session 413](https://developer.apple.com/videos/play/wwdc2019/413/?ref=albertodebortoli.com), Apple recommends generating the `.xctestrun` file using the build-for-testing option in xcodebuild and distribute it across the other nodes. Since our app is quite large, such file is also large and transferring it has its costs, both in time and bandwidth usage. All things considered, we decided to keep the majority of our pipelines serial. EDIT: In 2022 we have parallelised our PR pipeline in 4 branches: - Validation steps (linting, Fastlane lanes tests, etc.) - App unit tests - App UI tests (short enough that there's no need to share `.xctestrun` across nodes) - Modified modules unit tests - Modified modules UI tests ## Conclusions We have used the setup described in this article since mid-2020 and we are very satisfied with it. We discussed the pipeline used for the pull requests which is the most relevant one when it comes to embracing a monorepo structure. We have a few more pipelines for various use cases, such as verifying changes in release branches, keeping the code coverage metrics up-to-date with jobs running of triggers, archiving the app for internal usage and for App Store. We hope to have given you some useful insights on how to structure a monorepo and its CI pipelines, especially if you have a structure similar to ours. ### The algorithm powering iHarmony URL: https://albertodebortoli.com/2020/05/24/the-algorithm-powering-iharmony/ Last updated: 2025-09-08T20:41:16.000Z ## Problem I wrote the first version of [iHarmony](https://apps.apple.com/gb/app/iharmony/id292413210?ref=albertodebortoli.com) in 2008\. It was the very first iOS app I gave birth to, combining my passion for music and programming. I remember buying an iPhone and my first Mac with the precise purpose of jumping on the apps train at a time when it wasn't clear if the apps were there to stay or were just a temporary hype. But I did it, dropped my beloved Ubuntu to join a whole new galaxy. iHarmony was also one of the first 2000 apps on the App Store. Up until the recent rewrite, iHarmony was powered by a manually crafted database containing scales, chords, and harmonization I inputted. What-a-shame! ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2020/05/shame.png) I guess it made sense, I wanted to learn iOS and not to focus on implementing some core logic independent from the platform. Clearly a much better and less error-prone way to go would be to implement an algorithm to generate all the entries based on some DSL/spec. It took me almost 12 years to decide to tackle the problem and I've recently realized that writing the algorithm I wanted was [harder than I thought](https://twitter.com/albertodebo/status/1258123943573180425?s=20&ref=albertodebortoli.com). Also thought was a good idea give SwiftUI a try since the UI of iHarmony is extremely simple but... [nope](https://twitter.com/albertodebo/status/1254096544468553728?s=20&ref=albertodebortoli.com). Since [someone](https://twitter.com/SteveBarnegren/status/1258391221397053441?ref=albertodebortoli.com) on the Internet expressed interest 😉, I wrote this article to explain how I solved the problem of modeling music theory concepts in a way that allows the generation of any sort of scales, chords, and harmonization. I only show the code needed to get a grasp of the overall structure. I know there are other solutions ready to be used on GitHub but, while I don't particularly like any of them, the point of rewriting iHarmony from scratch was to challenge myself, not to reuse code someone else wrote. Surprisingly to me, getting to the solution described here took me 3 rewrites and 2 weeks. ## Solution The first fundamental building blocks to model are surely the musical notes, which are made up of a natural note and an accidental. ```swift enum NaturalNote: String { case C, D, E, F, G, A, B } enum Accidental: String { case flatFlatFlat = "bbb" case flatFlat = "bb" case flat = "b" case natural = "" case sharp = "#" case sharpSharp = "##" case sharpSharpSharp = "###" func applyAccidental(_ accidental: Accidental) throws -> Accidental {...} } struct Note: Hashable, Equatable { let naturalNote: NaturalNote let accidental: Accidental ... static let Dff = Note(naturalNote: .D, accidental: .flatFlat) static let Df = Note(naturalNote: .D, accidental: .flat) static let D = Note(naturalNote: .D, accidental: .natural) static let Ds = Note(naturalNote: .D, accidental: .sharp) static let Dss = Note(naturalNote: .D, accidental: .sharpSharp) ... func noteByApplyingAccidental(_ accidental: Accidental) throws -> Note {...} } ``` Combinations of notes make up scales and chords and they are... many. What's fixed instead in music theory, and therefore can be hard-coded, are the keys (both major and minor) such as: - C major: C, D, E, F, G, A, B - A minor: A, B, C, D, E, F, G - D major: D, E, F#, G, A, B, C# We'll get back to the keys later, but we can surely implement the note sequence for each musical key. ```swift typealias NoteSequence = [Note] extension NoteSequence { static let C = [Note.C, Note.D, Note.E, Note.F, Note.G, Note.A, Note.B] static let A_min = [Note.A, Note.B, Note.C, Note.D, Note.E, Note.F, Note.G] static let G = [Note.G, Note.A, Note.B, Note.C, Note.D, Note.E, Note.Fs] static let E_min = [Note.E, Note.Fs, Note.G, Note.A, Note.B, Note.C, Note.D] ... } ``` Next stop: intervals. They are a bit more interesting as not every degree has the same types. Let's split into 2 sets: 1. *2nd*, *3rd*, *6th* and *7th* degrees can be *minor*, *major*, *diminished* and *augmented* 2. *1st* (and *8th*), *4th* and *5th* degrees can be *perfect*, *diminished* and *augmented*. We need to use different kinds of "diminished" and "augmented" for the 2 sets as later on we'll have to calculate the accidentals needed to turn an interval into another. Some examples: - to get from *2nd augmented* to *2nd diminished*, we need a *triple flat* accidental (e.g. in C major scale, from D♯ to D♭♭ there are 3 semitones) - to get from *5th augmented* to *5th diminished*, we need a *double flat* accidental (e.g. in C major scale, from G♯ to G♭there are 2 semitones) We proceed to hard-code the allowed intervals in music, leaving out the invalid ones (e.g. `Interval(degree: ._2, type: .augmented)`) ```swift enum Degree: Int, CaseIterable { case _1, _2, _3, _4, _5, _6, _7, _8 } enum IntervalType: Int, RawRepresentable { case perfect case minor case major case diminished case augmented case minorMajorDiminished case minorMajorAugmented } struct Interval: Hashable, Equatable { let degree: Degree let type: IntervalType static let _1dim = Interval(degree: ._1, type: .diminished) static let _1 = Interval(degree: ._1, type: .perfect) static let _1aug = Interval(degree: ._1, type: .augmented) static let _2dim = Interval(degree: ._2, type: .minorMajorDiminished) static let _2min = Interval(degree: ._2, type: .minor) static let _2maj = Interval(degree: ._2, type: .major) static let _2aug = Interval(degree: ._2, type: .minorMajorAugmented) ... static let _4dim = Interval(degree: ._4, type: .diminished) static let _4 = Interval(degree: ._4, type: .perfect) static let _4aug = Interval(degree: ._4, type: .augmented) ... static let _7dim = Interval(degree: ._7, type: .minorMajorDiminished) static let _7min = Interval(degree: ._7, type: .minor) static let _7maj = Interval(degree: ._7, type: .major) static let _7aug = Interval(degree: ._7, type: .minorMajorAugmented) } ``` Now it's time to model the keys (we touched on them above already). What's important is to define the intervals for all of them (major and minor ones). ```swift enum Key { // natural case C, A_min // sharp case G, E_min case D, B_min case A, Fs_min case E, Cs_min case B, Gs_min case Fs, Ds_min case Cs, As_min // flat case F, D_min case Bf, G_min case Ef, C_min case Af, F_min case Df, Bf_min case Gf, Ef_min case Cf, Af_min ... enum KeyType { case naturalMajor case naturalMinor case flatMajor case flatMinor case sharpMajor case sharpMinor } var type: KeyType { switch self { case .C: return .naturalMajor case .A_min: return .naturalMinor case .G, .D, .A, .E, .B, .Fs, .Cs: return .sharpMajor case .E_min, .B_min, .Fs_min, .Cs_min, .Gs_min, .Ds_min, .As_min: return .sharpMinor case .F, .Bf, .Ef, .Af, .Df, .Gf, .Cf: return .flatMajor case .D_min, .G_min, .C_min, .F_min, .Bf_min, .Ef_min, .Af_min: return .flatMinor } } var intervals: [Interval] { switch type { case .naturalMajor, .flatMajor, .sharpMajor: return [ ._1, ._2maj, ._3maj, ._4, ._5, ._6maj, ._7maj ] case .naturalMinor, .flatMinor, .sharpMinor: return [ ._1, ._2maj, ._3min, ._4, ._5, ._6min, ._7min ] } } var notes: NoteSequence { switch self { case .C: return .C case .A_min: return .A_min ... } } ``` At this point we have all the fundamental building blocks and we can proceed with the implementation of the algorithm. The idea is to have a function that given - a key - a root interval - a list of intervals it works out the list of notes. In terms of inputs, it seems the above is all we need to correctly work out scales, chords, and - by extension - also harmonizations. Mind that the root interval doesn't have to be part of the list of intervals, that is simply the interval to start from based on the given key. Giving a note as a starting point is not good enough since some scales simply don't exist for some notes (e.g. G♯ major scale doesn't exist in the major key, and G♭minor scale doesn't exist in any minor key). Before progressing to the implementation, please consider the following unit tests that should make sense to you: ```swift func test_noteSequence_C_1() { let key: Key = .C let noteSequence = try! engine.noteSequence(customKey: key.associatedCustomKey, intervals: [._1, ._2maj, ._3maj, ._4, ._5, ._6maj, ._7maj]) let expectedValue: NoteSequence = [.C, .D, .E, .F, .G, .A, .B] XCTAssertEqual(noteSequence, expectedValue) } func test_noteSequence_withRoot_C_3maj_majorScaleIntervals() { let key = Key.C let noteSequence = try! engine.noteSequence(customKey: key.associatedCustomKey, rootInterval: ._3maj, intervals: [._1, ._2maj, ._3maj, ._4, ._5, ._6maj, ._7maj]) let expectedValue: NoteSequence = [.E, .Fs, .Gs, .A, .B, .Cs, .Ds] XCTAssertEqual(noteSequence, expectedValue) } func test_noteSequence_withRoot_Gsmin_3maj_alteredScaleIntervals() { let key = Key.Gs_min let noteSequence = try! engine.noteSequence(customKey: key.associatedCustomKey, rootInterval: ._3maj, intervals: [._1aug, ._2maj, ._3dim, ._4dim, ._5aug, ._6dim, ._7dim]) let expectedValue: NoteSequence = [.Bs, .Cs, .Df, .Ef, .Fss, .Gf, .Af] XCTAssertEqual(noteSequence, expectedValue) } ``` and here is the implementation. Let's consider a simple case, so it's easier to follow: - key = C major - root interval = 3maj - interval = major scale interval (1, 2maj, 3min, 4, 5, 6maj, 7min) if you music theory allowed you to understand the above unit tests, you would expect the output to be: E, F♯, G, A, B, C♯, D (which is a Dorian scale). Steps: 1. we start by shifting the notes of the C key to position the 3rd degree (based on the 3maj) as the first element of the array, getting the note sequence E, F, G, A, B, C, D; 2. here's the first interesting bit: we then get the list of intervals by calculating the number of semitones from the root to any other note in the sequence and working out the corresponding `Interval`: *1\_perfect, 2\_minor, 3\_minor, 4\_perfect, 5\_perfect, 6\_minor, 7\_minor;* 3. we now have all we need to create a `CustomKey` which is pretty much a `Key` (with notes and intervals) but instead of being an enum with pre-defined values, is a struct; 4. here's the second tricky part: return the notes by mapping the input intervals. Applying to each note in the custom key the accidental needed to match the desired interval. In our case, the only 2 intervals to 'adjust' are the 2nd and the 6th intervals, both minor in the custom key but major in the list of intervals. So we have to apply a sharp accidental to 'correct' them. 👀 I've used force unwraps in these examples for simplicity, the code might already look complex by itself. ```swift class CoreEngine { func noteSequence(customKey: CustomKey, rootInterval: Interval = ._1, intervals: [Interval]) throws -> NoteSequence { // 1. let noteSequence = customKey.shiftedNotes(by: rootInterval.degree) let firstNoteInShiftedSequence = noteSequence.first! // 2. let adjustedIntervals = try noteSequence.enumerated().map { try interval(from: firstNoteInShiftedSequence, to: $1, targetDegree: Degree(rawValue: $0)!) } // 3. let customKey = CustomKey(notes: noteSequence, intervals: adjustedIntervals) // 4. return try intervals.map { let referenceInterval = customKey.firstIntervalWithDegree($0.degree)! let note = customKey.notes[$0.degree.rawValue] let accidental = try referenceInterval.type.accidental(to: $0.type) return try note.noteByApplyingAccidental(accidental) } } } ``` It's worth showing the implementation of the methods used above: ```swift private func numberOfSemitones(from sourceNote: Note, to targetNote: Note) -> Int { let notesGroupedBySameTone: [[Note]] = [ [.C, .Bs, .Dff], [.Cs, .Df, .Bss], [.D, .Eff, .Css], [.Ds, .Ef, .Fff], [.E, .Dss, .Ff], [.F, .Es, .Gff], [.Fs, .Ess, .Gf], [.G, .Fss, .Aff], [.Gs, .Af], [.A, .Gss, .Bff], [.As, .Bf, .Cff], [.B, .Cf, .Ass] ] let startIndex = notesGroupedBySameTone.firstIndex { $0.contains(sourceNote)}! let endIndex = notesGroupedBySameTone.firstIndex { $0.contains(targetNote)}! return endIndex >= startIndex ? endIndex - startIndex : (notesGroupedBySameTone.count - startIndex) + endIndex } private func interval(from sourceNote: Note, to targetNote: Note, targetDegree: Degree) throws -> Interval { let semitones = numberOfSemitones(from: sourceNote, to: targetNote) let targetType: IntervalType = try { switch targetDegree { case ._1, ._8: return .perfect ... case ._4: switch semitones { case 4: return .diminished case 5: return .perfect case 6: return .augmented default: throw CustomError.invalidConfiguration ... case ._7: switch semitones { case 9: return .minorMajorDiminished case 10: return .minor case 11: return .major case 0: return .minorMajorAugmented default: throw CustomError.invalidConfiguration } } }() return Interval(degree: targetDegree, type: targetType) } ``` the `Note`'s `noteByApplyingAccidental` method: ```swift func noteByApplyingAccidental(_ accidental: Accidental) throws -> Note { let newAccidental = try self.accidental.apply(accidental) return Note(naturalNote: naturalNote, accidental: newAccidental) } ``` and the `Accidental`'s `apply` method: ```swift func apply(_ accidental: Accidental) throws -> Accidental { switch (self, accidental) { ... case (.flat, .flatFlatFlat): throw CustomError.invalidApplicationOfAccidental case (.flat, .flatFlat): return .flatFlatFlat case (.flat, .flat): return .flatFlat case (.flat, .natural): return .flat case (.flat, .sharp): return .natural case (.flat, .sharpSharp): return .sharp case (.flat, .sharpSharpSharp): return .sharpSharp case (.natural, .flatFlatFlat): return .flatFlatFlat case (.natural, .flatFlat): return .flatFlat case (.natural, .flat): return .flat case (.natural, .natural): return .natural case (.natural, .sharp): return .sharp case (.natural, .sharpSharp): return .sharpSharp case (.natural, .sharpSharpSharp): return .sharpSharpSharp ... } ``` With the above engine ready (and 💯﹪ unit tested!), we can now proceed to use it to work out what we ultimately need (scales, chords, and harmonizations). ``` extension CoreEngine { func scale(note: Note, scaleIdentifier: Identifier) throws -> NoteSequence {...} func chord(note: Note, chordIdentifier: Identifier) throws -> NoteSequence {...} func harmonization(key: Key, harmonizationIdentifier: Identifier) throws -> NoteSequence {...} func chordSignatures(note: Note, scaleHarmonizationIdentifier: Identifier) throws -> [ChordSignature] {...} func harmonizations(note: Note, scaleHarmonizationIdentifier: Identifier) throws -> [NoteSequence] {...} } ``` ## Conclusions There's more to it but with this post I only wanted to outline the overall idea. The default database is available on GitHub at [albertodebortoli/iHarmonyDB](https://github.com/albertodebortoli/iHarmonyDB?ref=albertodebortoli.com). The format used is JSON and the community can now easily suggest additions. Here is how the definition of a scale looks: ```swift "scale_dorian": { "group": "group_scales_majorModes", "isMode": true, "degreeRelativeToMain": 2, "inclination": "minor", "intervals": [ "1", "2maj", "3min", "4", "5", "6maj", "7min" ] } ``` and a chord: ```swift "chord_diminished": { "group": "group_chords_diminished", "abbreviation": "dim", "intervals": [ "1", "3min", "5dim" ] } ``` and a harmonization: ```swift "scaleHarmonization_harmonicMajorScale4Tones": { "group": "group_harmonization_harmonic_major", "inclination": "major", "harmonizations": [ "harmonization_1_major7plus", "harmonization_2maj_minor7dim5", "harmonization_3maj_minor7", "harmonization_4_minor7plus", "harmonization_5_major7", "harmonization_6min_major7plus5sharp", "harmonization_7maj_diminished7" ] } ``` Have to say, I'm pretty satisfied with how extensible this turned out to be. Thanks for reading 🎶 ### The iOS internationalization basics I keep forgetting URL: https://albertodebortoli.com/2020/01/06/the-ios-internationalization-basics-i-keep-forgetting/ Last updated: 2020-01-11T18:20:24.000Z In this article, I try to summarize the bare minimum one needs to know to add internationalization support to an iOS app. > Localizations, locales, timezones, date and currency formatting... it's shocking how easy is to forget how they work and how to use them correctly. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/12/3kj59j.jpg) After years more than 10 years into iOS development, I decided to write down a few notes on the matter, with the hope that they will come handy again in the future, hopefully not only to me. ## TL;DR From Apple docs: [Date](https://developer.apple.com/documentation/foundation/date?ref=albertodebortoli.com): a specific point in time, independent of any calendar or time zone; [TimeZone](https://developer.apple.com/documentation/foundation/timezone?ref=albertodebortoli.com): information about standard time conventions associated with a specific geopolitical region; [Locale](https://developer.apple.com/documentation/foundation/locale?ref=albertodebortoli.com): information about linguistic, cultural, and technological conventions for use in formatting data for presentation. Rule of thumb: - All DateFormatters should use the locale and the timezone of the device; - All NumberFormatter, in particular those with `numberStyle` set to `.currency` (for the sake of this article) should use a specific locale so that prices are not shown in the wrong currency. ## General notes on formatters Let's start by stating the obvious. Since iOS 10, Foundation (finally) provides `[ISO8601DateFormatter](https://developer.apple.com/documentation/foundation/iso8601dateformatter?ref=albertodebortoli.com)`, which, alongside with `[DateFormatter](https://developer.apple.com/documentation/foundation/DateFormatter?ref=albertodebortoli.com)` and `[NumberFormatter](https://developer.apple.com/documentation/foundation/NumberFormatter?ref=albertodebortoli.com)`, inherits from `[Formatter](https://developer.apple.com/documentation/foundation/Formatter?ref=albertodebortoli.com)`. | Formatter | locale property | timeZone property | | -------------------- | --------------- | ----------------- | | ISO8601DateFormatter | ❌ | ✅ | | DateFormatter | ✅ | ✅ | | NumberFormatter | ✅ | ❌ | In an app that only consumes data from an API, the main purpose of `ISO8601DateFormatter` is to convert strings to dates (`String` \-> `Date`) more than the inverse. `DateFormatter` is then used to format dates (`Date` \-> `String`) to ultimately show the values in the UI. `NumberFormatter` instead, converts numbers (prices in the vast majority of the cases) to strings (`NSNumber`/`Decimal` \-> `String`). ## Formatting dates 🕗 🕝 🕟 It seems the following 4 are amongst the most common ISO 8601 formats, including the optional UTC offset. - A: `2019-10-02T16:53:42` - B: `2019-10-02T16:53:42Z` - C: `2019-10-02T16:53:42-02:00` - D: `2019-10-02T16:53:42.974Z` In this article I'll stick to these formats. > The 'Z' at the end of an ISO8601 date indicates that it is in UTC, not a local time zone. ### Locales Converting strings to dates (`String` \-> `Date`) is done using `ISO8601DateFormatter` objects set up with various `formatOptions`. Once we have a `Date` object, we can deal with the formatting for the presentation. Here, the locale is important and things can get a bit tricky. Locales have nothing to do with timezones, locales are for applying a format using a language/region. > Locale identifiers are in the form of `_` (e.g. `en_GB`). We should use the user's locale when formatting dates (`Date` \-> `String`). Consider a British user moving to Italy, the apps should keep showing a UI localized in English, and the same applies to the dates that should be formatted using the `en_GB` locale. Using the `it_IT` locale would show "2 ott 2019, 17:53" instead of the correct "2 Oct 2019 at 17:53". `Locale.current`, shows the locale set (overridden) in the iOS simulator and setting the language and regions in the scheme's options comes handy for debugging. Some might think that it's acceptable to use `Locale.preferredLanguages.first` and create a Locale from it with `let preferredLanguageLocale = Locale(identifier: Locale.preferredLanguages.first!)` and set it on the formatters. I think that doing so is not great since we would display dates using the Italian format but we won't necessarily be using the Italian language for the other UI elements as the app might not have the IT localization, causing an inconsistent experience. In short: don't use `preferredLanguages`, best to use `Locale.current`. Apple strongly suggests using `en_US_POSIX` pretty much everywhere ([1](https://developer.apple.com/library/archive/qa/qa1480/%5Findex.html?ref=albertodebortoli.com), [2](https://developer.apple.com/documentation/foundation/dateformatter?ref=albertodebortoli.com)). From Apple docs: > \[...\] if you're working with fixed-format dates, you should first set the locale of the date formatter to something appropriate for your fixed format. In most cases the best locale to choose is "en\_US\_POSIX", a locale that's specifically designed to yield US English results regardless of both user and system preferences. "en\_US\_POSIX" is also invariant in time (if the US, at some point in the future, changes the way it formats dates, "en\_US" will change to reflect the new behaviour, but "en\_US\_POSIX" will not), and between machines ("en\_US\_POSIX" works the same on iOS as it does on OS X, and as it it does on other platforms). > > Once you've set "en\_US\_POSIX" as the locale of the date formatter, you can then set the date format string and the date formatter will behave consistently for all users. I couldn't find a really *valid* reason for doing so and quite frankly using the device locale seems more appropriate for converting dates to strings. Here is the string representation for the same date using different locales: - `en_US_POSIX`: May 2, 2019 at 3:53 PM - `en_GB`: 2 May 2019 at 15:53 - `it_IT`: 2 mag 2019, 15:53 The above should be enough to show that `en_US_POSIX` is not what we want to use in this case, but it has more to do with maintaining a standard for communication across machines. From this [article](https://www.maddysoft.com/articles/dates.html?ref=albertodebortoli.com): > *"\[...\] Unless you specifically need month and/or weekday names to appear in the user's language, you should always use the special locale of `en_US_POSIX`. This will ensure your fixed format is actually fully honored and no user settings override your format. This also ensures month and weekday names appear in English. Without using this special locale, you may get 24-hour format even if you specify 12-hour (or visa-versa). And dates sent to a server almost always need to be in English."* ### Timezones Stating the obvious one more time: > Greenwich Mean Time (GMT) is a time zone while Coordinated Universal Time (UTC) is a time standard. There is no time difference between them. Timezones are fundamental to show the correct date/time in the final text shown to the user. The timezone value is taken from macOS and the iOS simulator inherits it, meaning that printing `TimeZone.current`, shows the timezone set in the macOS preferences (e.g. Europe/Berlin). ### Show me some code Note that in the following example, we use GMT (Greenwich Mean Time) and CET (Central European Time), which is GMT+1\. Mind that it's best to reuse formatters since the creation is [expensive](http://www.chibicode.org/?p=41&ref=albertodebortoli.com). ```swift class CustomDateFormatter { private let dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .medium dateFormatter.timeStyle = .short return dateFormatter }() private let locale: Locale private let timeZone: TimeZone init(locale: Locale = .current, timeZone: TimeZone = .current) { self.locale = locale self.timeZone = timeZone } func string(from date: Date) -> String { dateFormatter.locale = locale dateFormatter.timeZone = timeZone return dateFormatter.string(from: date) } } ``` ```swift let stringA = "2019-11-02T16:53:42" let stringB = "2019-11-02T16:53:42Z" let stringC = "2019-11-02T16:53:42-02:00" let stringD = "2019-11-02T16:53:42.974Z" // The ISO8601DateFormatter's extension (redacted) // internally uses multiple formatters, each one set up with different // options (.withInternetDateTime, .withFractionalSeconds, withFullDate, .withTime, .withColonSeparatorInTime) // to be able to parse all the formats. // timeZone property is set to GMT. let dateA = ISO8601DateFormatter.date(from: stringA)! let dateB = ISO8601DateFormatter.date(from: stringB)! let dateC = ISO8601DateFormatter.date(from: stringC)! let dateD = ISO8601DateFormatter.date(from: stringD)! var dateFormatter = CustomDateFormatter(locale: Locale(identifier: "en_GB"), timeZone: TimeZone(identifier: "GMT")!) dateFormatter.string(from: dateA) // 2 Nov 2019 at 16:53 dateFormatter.string(from: dateB) // 2 Nov 2019 at 16:53 dateFormatter.string(from: dateC) // 2 Nov 2019 at 18:53 dateFormatter.string(from: dateD) // 2 Nov 2019 at 16:53 dateFormatter = CustomDateFormatter(locale: Locale(identifier: "it_IT"), timeZone: TimeZone(identifier: "CET")!) dateFormatter.string(from: dateA) // 2 nov 2019, 17:53 dateFormatter.string(from: dateB) // 2 nov 2019, 17:53 dateFormatter.string(from: dateC) // 2 nov 2019, 19:53 dateFormatter.string(from: dateD) // 2 nov 2019, 17:53 ``` Using the CET timezone also for `ISO8601DateFormatter`, the final string produced for `dateA` would respectively be "15:53" when formatted with GMT and "16:53" when formatted with CET. As long as the string passed to `ISO8601DateFormatter` is in UTC, it's irrelevant to set the timezone on the formatter. Apple [suggests](https://developer.apple.com/documentation/foundation/dateformatter?ref=albertodebortoli.com) to set the timeZone property to UTC with `TimeZone(secondsFromGMT: 0)`, but this is irrelevant if the string representing the date already includes the timezone. If your server returns a string representing a date that is not in UTC, it's probably because of one of the following 2 reasons: 1. it's not meant to be in UTC (questionable design decision indeed) and therefore the timezone of the device should be used instead; 2. the backend developers implemented it wrong and they should add the 'Z 'at the end of the string if what they intended is to have the date in UTC. In short: > All DateFormatters should have timezone and locale set to `.current` and avoid handling non-UTC string if possible. ## Formatting currencies € $ ¥ £ The currency symbol and the formatting of a number should be defined via a `Locale`, and they shouldn't be set/changed on the NumberFormatter. Don't use the user's locale (`Locale.current`) because it could be set to a region not supported by the app. Let's consider the example of a user's locale to be `en_US`, and the app to be available only for the Italian market. We must set a locale `Locale(identifier: "it_IT")` on the formatter, so that: - prices will be shown only in Euro (not American Dollar) - the format used will be the one of the country language (for Italy, "12,34 €", not any other variation such as "€12.34") ```swift class CurrencyFormatter { private let locale: Locale init(locale: Locale = .current) { self.locale = locale } func string(from decimal: Decimal, overriddenCurrencySymbol: String? = nil) -> String { let formatter = NumberFormatter() formatter.numberStyle = .currency if let currencySymbol = overriddenCurrencySymbol { // no point in doing this on a NumberFormatter ❌ formatter.currencySymbol = currencySymbol } formatter.locale = locale return formatter.string(from: decimal as NSNumber)! } } ``` ```swift let itCurrencyFormatter = CurrencyFormatter(locale: Locale(identifier: "it_IT")) let usCurrencyFormatter = CurrencyFormatter(locale: Locale(identifier: "en_US")) let price1 = itCurrencyFormatter.string(from: 12.34) // "12,34 €" ✅ let price2 = usCurrencyFormatter.string(from: 12.34) // "$12.34" ✅ let price3 = itCurrencyFormatter.string(from: 12.34, overriddenCurrencySymbol: "₿") // "12,34 ₿" ❌ let price4 = usCurrencyFormatter.string(from: 12.34, overriddenCurrencySymbol: "₿") // "₿ 12.34" ❌ ``` In short: > All NumberFormatters should have the locale set to the one of the country targeted and no `currencySymbol` property overridden (it's inherited from the locale). ## Languages 🇬🇧 🇮🇹 🇳🇱 Stating the obvious one more time, but there are very rare occasions that justify forcing the language in the app: ```swift func setLanguage(_ language: String) { let userDefaults = UserDefaults.standard userDefaults.set([language], forKey: "AppleLanguages") } ``` The above circumvents the Apple localization mechanism and needs an app restart, so don't do it and localize the app by the book: - add localizations in Project -> Localizations; - create a `Localizable.strings` file and tap the localize button in the inspector; - always use `NSLocalizedString()` in code. Let's consider this content of `Localizable.strings (English)`: ``` "kHello" = "Hello"; "kFormatting" = "Some formatting 1. %@ 2. %d."; ``` and this for another language (e.g. Italian) `Localizable.strings (Italian)`: ``` "kHello" = "Ciao"; "kFormatting" = "Esempio di formattazione 1) %@ 2) %d."; ``` ### Simple localization Here's the trivial example: ```swift let localizedString = NSLocalizedString("kHello", comment: "") ``` If `Locale.current.languageCode` is `it`, the value would be '*Ciao*', and '*Hello*' otherwise. ### Formatted localization For formatted strings, use the following: ```swift let stringWithFormats = NSLocalizedString("kFormatting", comment: "") String.localizedStringWithFormat(stringWithFormats, "some value", 3) ``` As before, if `Locale.current.languageCode` is `it`, value would be '*Esempio di formattazione 1) some value 2) 3.*', and '*Some formatting 1) some value 2) 3.*' otherwise. ### Plurals localization For plurals, create a `Localizable.stringsdict` file and tap the localize button in the inspector. `Localizable.strings` and `Localizable.stringsdict` are independent, so there are no cross-references (something that often tricked me). Here is a sample content: ```xml kPlurality NSStringLocalizedFormatKey Interpolated string: %@, interpolated number: %d, interpolated variable: %#@COUNT@. COUNT NSStringFormatSpecTypeKey NSStringPluralRuleType NSStringFormatValueTypeKey d zero nothing one %d object two few many other %d objects ``` `Localizable.stringsdict` undergo the same localization mechanism of its companion `Localizable.strings`. It's mandatory to only implement 'other', but an honest minimum includes 'zero', 'one', and 'other'. Given the above content, the following code should be self-explanatory: ```swift let localizedHello = NSLocalizedString("kHello", comment: "") // from Localizable.strings let stringWithPlurals = NSLocalizedString("kPlurality", comment: "") // from Localizable.stringsdict String.localizedStringWithFormat(stringWithPlurals, localizedHello, 42, 1) ``` With the `en` language, the value would be '*Interpolated string: Hello, interpolated number: 42, interpolated variable: 1 object.*'. --- Use the scheme's option to run with a specific Application Language (it will change the current locale language and therefore also the output of the DateFormatters). ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/12/Screenshot-2019-12-26-at-22.22.31.png) If the language we've set or the device language are not supported by the app, the system falls back to `en`. ## References - [https://en.wikipedia.org/wiki/ISO\_8601](https://en.wikipedia.org/wiki/ISO%5F8601?ref=albertodebortoli.com) - [https://nsdateformatter.com/](https://nsdateformatter.com/?ref=albertodebortoli.com) - [https://foragoodstrftime.com/](https://foragoodstrftime.com/?ref=albertodebortoli.com) - [https://epochconverter.com/](https://epochconverter.com/?ref=albertodebortoli.com) So... that's all folks. 🌍 ### Modular iOS Architecture @ Just Eat URL: https://albertodebortoli.com/2019/12/19/modular-ios-architecture-at-just-eat/ Last updated: 2024-01-03T22:29:21.000Z > The journey we took to restructure our mobile apps towards a modular architecture. *Originally published on the* [*Just Eat Engineering Blog*](https://tech.just-eat.com/2019/12/18/modular-ios-architecture-just-eat/?ref=albertodebortoli.com)*.* ## Overview Modular mobile architectures have been a hot topic over the past 2 years, counting a plethora of articles and conference talks. Almost every big company promoted and discussed modularization publicly as a way to scale big projects. At Just Eat, we jumped on the modular architecture train probably before it was mainstream and, as we'll discuss in this article, the root motivation was quite peculiar in the industry. Over the years (2016-2019), we've completely revamped our iOS products from the ground up and learned a lot during this exciting and challenging journey. There is so much to say about the way we structured our iOS stack that it would probably deserve a series of articles, some of which have previously been posted. Here we summarize the high-level iOS architecture we crafted, covering the main aspects in a way concise enough for the reader to get a grasp of them and hopefully learn some valuable tips. ## Modular Architecture Lots of information can be found online on modular architectures. In short: > A modular architecture is a software design technique that emphasizes separating the functionality of a program into independent, interchangeable modules, such that each one contains everything necessary to execute only one aspect of the desired functionality. Note that modular design applies to the code you own. A project with several third-party dependencies but no sensible separation for the code written by your team is not considered modular. A modular design is more about the principle rather than the specific technology. One could achieve it in a variety of ways and with different tools. Here are some key points and examples that should inform the decision of the ifs and the hows of implementing modularization: ### Business reasons - The company requires that parts of the codebase are reused and shared across projects, products, and teams; - The company requires multiple products to be unified into a single one. ### Tech reasons - The codebase has grown to a state where things become harder and harder to maintain and to iterate over; - Development is slowed down due to multiple developers working on the same monolithic codebase; - Besides reusing code, you need to port functionalities across projects/products. ### Multiple teams - The company structured teams following strategic models (e.g. Spotify model) and functional teams only work on a subset of the final product; - Ownership of small independent modules distributed across teams enables faster iterations; - The much smaller cognitive overhead of working on a smaller part of the whole product can vastly simplify the overall development. ### Pre-existing knowledge - Members of the team might already be familiar with specific solutions (Carthage, CocoaPods, Swift Package Manager, manual frameworks setup within Xcode). In the case of a specific familiarity with a system, it's recommended to start with it since all solutions come with pros and cons and there's not a clear winner at the time of writing. Modularizing code (if done sensibly) is almost always a good thing: it enforces separation of concerns, keeps complexity under control, allows faster development, etc. It has to be said that it's not necessarily what one needs for small projects and its benefits become tangible only after a certain complexity threshold is crossed. ## Journey to a new architecture In 2014, Just Eat was a completely different environment from today and back then the business decided to split the tech department into separate departments: one for UK and one for the other countries. While this was done with the best intentions to allow faster evolution in the main market (UK), it quickly created a hard division between teams, services, and people. In less than 6 months, the UK and International APIs and consumer clients deeply diverged introducing country-specific logic and behaviors. By mid-2016 the intent of "merging back" into a single global platform was internally announced and at that time it almost felt like a company acquisition. This is when we learned the importance of *integrating people before technology.* The teams didn’t know each other very well and became reasonably territorial on their codebase. It didn’t help that the teams span multiple cities. It's understandable that getting to an agreement on how going back to a single, global, and unified platform took months. The options we considered spanned from rewriting the product from scratch to picking one of the two existing ones and make it global. A complete rewrite would have eventually turned out to be a big-bang release with the risk of regressions being too high; not something sensible or safe to pursue. Picking one codebase over the other would have necessarily let down one of the two teams and caused the re-implementation of some missing features present in the other codebase. At that time, the UK project was in a better shape and new features were developed for the UK market first. The international project was a bit behind due to the extra complexity of supporting multiple countries and features being too market-specific. During that time, the company was also undergoing massive growth and with multiple functional teams having been created internally, there was an increasing need to move towards modularization. Therefore, we decided to gradually and strategically modularize parts of the mobile products and onboard them onto the other codebase in a controlled and safe way. In doing so, we took the opportunity to deeply refactor and, in the vast majority of the cases, rewrite parts in their entirety enabling new designs, better tests, higher code coverage, and - holistically - a fully Swift codebase. We knew that the best way to refactor and clean up the code was by following a bottom-up approach. We started with the foundations to solve small and well-defined problems - such as logging, tracking, theming - enabling the team to learn to *think modular*. We later moved to isolating big chunks of code into functional modules to be able to onboard them into the companion codebase and ship them on a phased rollout. We soon realized we needed a solid engine to handle run-time configurations and remote feature flagging to allow switching ON and OFF features as well as entire modules. As discussed in a previous [article](https://albertodebortoli.com/2019/11/26/a-smart-feature-flagging-system-for-ios/), we developed [JustTweak](https://github.com/justeat/JustTweak?ref=albertodebortoli.com) to achieve this goal. At the end of the journey, the UK and the International projects would look very similar, sharing a number of customizable modules, and differing only in the orchestration layer in the apps. The Just Eat iOS apps are far bigger and more complex than they might look at first glance. Generically speaking, merging different codebases takes orders of magnitude longer than separating them, and for us, it was a process that took over 3 years, being possible thanks to unparalleled efforts of engineers brought to work together. Over this time, the whole team learned a lot, from the basics of developing code in isolation to how to scale a complex system. ## Holistic Design 🤘 The following diagram outlines the modular architecture in its entirety as it is at the time of writing this article (December 2019). We can appreciate a fair number of modules clustered by type and the different consumer apps. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/12/The-Just-Eat-iOS-Stack---Holistic-Design--2.svg) Modular iOS architecture - holistic design Whenever possible, we took the opportunity to abstract some modules having them in a state that allows open-sourcing the code. All of our open-source modules are licensed under Apache 2 and can be found at [github.com/justeat](https://github.com/justeat?ref=albertodebortoli.com). ### Apps Due to the history of Just Eat described above, we build different apps - per country - per brand - from different codebases All the modularization work we did bottom-up brought us to a place where the apps differ only in the layer orchestrating the modules. With all the consumer-facing features been moved to the domain modules, there is very little code left in the apps. ### Domain Modules Domain modules contain features specific to an area of the product. As the diagram above shows, the sum of all those parts makes up the Just Eat apps. These modules are constantly modified and improved by our teams and updating the consumer apps to use newer versions is an explicit action. We don't particularly care about backward compatibility here since we are the sole consumers and it's common to break the public interface quite often if necessary. It might seem at first that domain modules should depend on some Core modules (e.g. APIClient) but doing so would complicate the dependency tree as we'll discuss further in the "Dependency Management" section of this article. Instead, we inject core modules' services, simply making them conformant to protocols defined in the domain module. In this way, we maintain a good abstraction and avoid tangling the dependency graph. ### Core & Shared modules The Core and Shared modules represent the foundations of our stack, things like: - custom UI framework - theming engine - logging, tracking, and analytics libraries - test utilities - client for all the Just Eat APIs - feature flagging and experimentation engine and so forth. These modules - which are sometimes also made open-source - should not change frequently due to their nature. Here backward compatibility is important and we deprecate old APIs when introducing new ones. Both apps and domain modules can have shared modules as dependencies, while core modules can only be used by the apps. Updating the backbone of a system requires the propagation of the changes up in the stack (with its maintenance costs) and for this reason, we try to keep the number of shared modules very limited. ## Structure of a module As we touched on in previous articles, one of our fundamental principles is "*always strive to find solutions to problems that are scalable and hide complexity as much as possible*". We are almost obsessed with making things as simple as they can be. When building a module, our root *principle* is: > Every module should be well tested, maintainable, readable, easily pluggable, and reasonably documented. The order of the adjectives implies some sort of priority. First of all, the code must be unit *tested*, and in the case of domain modules, UI tests are required too. Without reasonable code coverage, no code is shipped to production. This is the first step to code *maintainability*, where maintainable code is intended as "code that is easy to modify or extend". *Readability* is down to reasonable design, naming convention, coding standards, formatting, and all that jazz. Every module exposes a Facade that is very succinct, usually no more than 200 lines long. This entry point is what makes a module easily *pluggable*. In our module blueprint, the bare minimum is a combination of a facade class, injected dependencies, and one or more configuration objects driving the behavior of the module (leveraging the underlying feature flagging system powered by JustTweak discussed in a [previous article](https://albertodebortoli.com/2019/11/26/a-smart-feature-flagging-system-for-ios/)). The facade should be all a developer needs to know in order to consume a module without having to look at implementation details. Just to give you an idea, here is an excerpt from the generated public interface of the *Account* module (not including the protocols): ```swift public typealias PasswordManagementService = ForgottenPasswordServiceProtocol & ResetPasswordServiceProtocol public typealias AuthenticationService = LoginServiceProtocol & SignUpServiceProtocol & PasswordManagementService & RecaptchaServiceProtocol public typealias UserAccountService = AccountInfoServiceProtocol & ChangePasswordServiceProtocol & ForgottenPasswordServiceProtocol & AccountCreditServiceProtocol public class AccountModule { public init(settings: Settings, authenticationService: AuthenticationService, userAccountService: UserAccountService, socialLoginServices: [SocialLoginService], userInfoProvider: UserInfoProvider) public func startLogin(on viewController: UIViewController) -> FlowCoordinator public func startResetPassword(on viewController: UIViewController, token: Token) -> FlowCoordinator public func startAccountInfo(on navigationController: UINavigationController) -> FlowCoordinator public func startAccountCredit(on navigationController: UINavigationController) -> FlowCoordinator public func loginUsingSharedWebCredentials(handler: @escaping (LoginResult) -> Void) } ``` Domain module public interface example (Account module) We believe code should be self-descriptive and we tend to put comments only on code that *really* deserves some explanation, very much embracing John Ousterhout's approach described in [A Philosophy of Software Design](https://www.goodreads.com/en/book/show/39996759-a-philosophy-of-software-design?ref=albertodebortoli.com). *Documentation* is mainly relegated to the README file and we treat every module as if it was an open-source project: the first thing consumers would look at is the README file, and so we make it as descriptive as possible. ### Overall design We generate all our modules using CocoaPods via `$ pod lib create` which creates the project with a standard template generating the Podfile, podspec, and demo app in a breeze. The podspec could specify additional dependencies (both third-party and Core modules) that the demo app's Podfile could specify core modules dependencies alongside the module itself which is treated as a development pod as per standard setup. The backbone of the module, which is the framework itself, encompasses both business logic and UI meaning that both source and asset files are part of it. In this way, the demo apps are very much lightweight and only showcase module features that are implemented in the framework. The following diagram should summarize it all. ![Design of a module with Podfile and podspec examples](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/12/The-Just-Eat-iOS-Stack---Module-Design-6.svg) Design of a module with Podfile and podspec examples ### Demo Apps Every module comes with a demo app we give particular care to. Demo apps are treated as first-class citizens and the stakeholders are both engineers and product managers. They massively help to showcase the module features - especially those under development - vastly simplify collaboration across Engineering, Product, and Design, and force a good mock-based test-first approach. Following is a SpringBoard page showing our demo apps, very useful to individually showcase all the functionalities implemented over time, some of which might not surface in the final product to all users. Some features are behind experiments, some still in development, while others might have been retired but still present in the modules. Every demo app has a main menu to: - access the features - force a specific language - toggle configuration flags via JustTweak - customize mock data We show the example of the *Account* module demo app on the right. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/12/asd.png) Domain modules demo apps ### Internal design It's worth noting that our root principle mentioned above does not include any reference to the internal architecture of a module and this is intentional. It's common for iOS teams in the industry to debate on which architecture to adopt across the entire codebase but the truth is that such debate aims to find an answer to a non-existing problem. With an increasing number of modules and engineers, it's fundamentally impossible to align on a single paradigm shared and agreed upon by everyone. Betting on a single architectural design would ultimately let down some engineers who would complain down the road that a different design would have played out better. We decided to stick with the following rule of thumb: > Developers are free to use the architectural design they feel would work better for a given problem. This approach brought us to have a variety of different designs - spanning from simple old-school MVC, to a more evolved VIPER - and we constantly learn from each other's code. What's important at the end of the day is that techniques such as inversion of control, dependency injection, and more generally the SOLID principles, are used appropriately to embrace our root principle. ## Dependency Management We rely heavily on CocoaPods since we adopted it in the early days as it felt like the best and most mature choice at the time we started modularizing our codebase. We think this still holds at the time of writing this article but we can envision a shift to SPM (Swift Package Manager) in 1-2 years time. With a growing number of modules, comes the responsibility of managing the dependencies between them. No panacea can cure [dependency hell](https://en.wikipedia.org/wiki/Dependency%5Fhell?ref=albertodebortoli.com), but one should adopt some tricks to keep the complexity of the stack under reasonable control. Here's a summary of what worked for us: - Always respect [semantic versioning](https://semver.org/?ref=albertodebortoli.com); - Keep the dependency graph as shallow as possible. From our apps to the leaves of the graph there are no more than 2 levels; - Use a minimal amount of shared dependencies. Be aware that every extra level with shared modules brings in higher complexity; - Reduce the number of third-party libraries to the bare minimum. Code that's not written and owned by your team is not under your control; - Never make modules within a group (domain, core, shared) depend on other modules of the same group; - Automate the publishing of new versions. When a pull request gets merged into the master branch, it must also contain a version change in the podspec. Our continuous integration system will automatically validate the podspec, publish it to our private spec repository, and in just a matter of minutes the new version becomes available; - Fix the version for dependencies in the Podfile. Whether it is a consumer app or a demo app, we want both our modules and third-party libraries not to be updated unintentionally. It's acceptable to use the [optimistic operator](https://guides.cocoapods.org/using/the-podfile.html?ref=albertodebortoli.com#specifying-pod-versions) for third-party libraries to allow automatic updates of new patch versions; - Fix the version for third-party libraries in the modules' podspec. This guarantees that modules' behavior won't change in the event of changes in external libraries. Failing to do so would allow defining different versions in the app's Podfile, potentially causing the module to not function correctly or even to not compile; - Do not fix the version for shared modules in the modules' podspec. In this way, we let the apps define the version in the Podfile, which is particularly useful for modules that change often, avoiding the hassle of updating the version of the shared modules in every podspec referencing it. If a new version of a shared module is not backward compatible with the module consuming it, the failure would be reported by the continuous integration system as soon as a new pull request gets raised. ### A note on the Monorepo approach When it comes to dependency management it would be unfair not to mention the opinable [monorepo](https://en.wikipedia.org/wiki/Monorepo?ref=albertodebortoli.com) approach. Monorepos have been discussed quite a lot by the community to pose a remedy to dependency management (de facto ignoring it), some engineers praise them, others are quite contrary. Facebook, Google, and Uber are just some of the big companies known to have adopted this technique, but in hindsight, it's still unclear if it was the best decision for them. In our opinion, monorepos can sometimes be a good choice. For example, in our case, a great benefit a monorepo would give us is the ability to prepare a single pull request for both implementing a code change in a module and integrating it into the apps. This will have an even greater impact when all the Just Eat consumer apps are globalized into a single codebase. ## Onwards and upwards Modularizing the iOS product has been a long journey and the learnings were immense. All in all, it took more than 3 years, from May 2016 to October 2019, always balancing tech and product improvements. Our natural next step is unifying the apps into a single global project, migrating the international countries over to the UK project to ultimately reach the utopian state of having a single global app. All the modules have been implemented in a fairly abstract way and following a white labeling approach, allowing us to extend support to new countries and onboard acquired companies in the easiest possible way. ### Lessons learned from handling JWT on mobile URL: https://albertodebortoli.com/2019/12/04/recommendations-on-handling-jwt-on-mobile/ Last updated: 2019-12-04T17:21:58.000Z > Implementing Authorization on mobile can be tricky. Here are some recommendations to avoid common issues. *Originally published on the [Just Eat Engineering Blog](https://tech.just-eat.com/2019/12/04/lessons-learned-from-handling-jwt-on-mobile/?ref=albertodebortoli.com).* ## Overview Modern mobile apps are more complicated than they used to be back in the early days and developers have to face a variety of interesting problems. While we've put in our two cents on some of them in previous articles, this one is about authorization and what we have learned by handling JWT on mobile at Just Eat. When it comes to authorization, it's standard practice to rely on [OAuth 2.0](https://oauth.net/2/?ref=albertodebortoli.com) and the companion JWT (JSON Web Token). We found this important topic was rarely discussed online while much attention was given to new proposed implementations of network stacks, maybe using recent language features or frameworks such as Combine. We'll illustrate the problems we faced at Just Eat for JWT parsing, usage, and (most importantly) refreshing. You should be able to learn a few things on how to make your app more stable by reducing the chance of unauthorized requests allowing your users to virtually always stay logged in. ## What is JWT JWT stands for JSON Web Token and is an open industry standard used to represent claims transferred between two parties. A signed JWT is known as a JWS (JSON Web Signature). In fact, a JWT has either to be JWS or JWE (JSON Web Encryption). [RFC 7515](https://tools.ietf.org/html/rfc7515?ref=albertodebortoli.com), [RFC 7516](https://tools.ietf.org/html/rfc7516?ref=albertodebortoli.com), and [RFC 7519](https://tools.ietf.org/html/rfc7519?ref=albertodebortoli.com) describe the various fields and claims in detail. What is relevant for mobile developers is the following: - JWT is composed of 3 parts dot-separated: *Header*, *Payload*, *Signature*. - The Payload is the only relevant part. The Header identifies which algorithm is used to generate the signature. There are reasons for [not verifying the signature client-side](https://stackoverflow.com/a/46133159/3010877?ref=albertodebortoli.com) making the Signature part irrelevant too. - JWT has an expiration date. Expired tokens should be renewed/refreshed. - JWT can contain any number of extra information specific to your service. - It's common practice to store JWTs in the app keychain. Here is a valid and very short token example, courtesy of [jwt.io/](https://jwt.io/?ref=albertodebortoli.com) which we recommend using to easily decode tokens for debugging purposes. It shows 3 fragments (base64 encoded) concatenated with a dot. ``` eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1Nzc3NTA0MDB9.7hgBhNK_ZpiteB3GtLh07KJ486Vfe3WAdS-XoDksJCQ ``` The only field relevant to this document is `exp` (Expiration Time), part of Payload (the second fragment). This claim identifies the time after which the JWT must not be accepted. In order to accept a JWT, it's required that the current date/time must be before the expiration time listed in the `exp` claim. It's accepted practice for implementers to consider for some small leeway, usually no more than a few minutes, to account for clock skew. N.B. Some API calls might demand the user is logged in (user-authenticated calls), and others don't (non-user-authenticated calls). JWT can be used in both cases, marking a distinction between Client JWT and User JWT we will refer to later on. ## The token refresh problem By far the most significant problem we had in the past was the renewal of the token. This seems to be something taken for granted by the mobile community, but in reality, we found it to be quite a fragile part of the authentication flow. If not done right, it can easily cause your *customers to end up being logged out*, with the consequent frustration we all have experienced as app users. The Just Eat app makes multiple API calls at startup: it fetches the order history to check for in-flight orders, fetches the most up-to-date consumer details, etc. If the token is expired when the user runs the app, a nasty race condition could cause the same refresh token to be used twice, causing the server to respond with a 401 and subsequently logging the user out on the app. This can also happen during normal execution when multiple API calls are performed very close to each other and the token expires prior to those. It gets trickier if the client and the server clocks are sensibly off sync: while the client might believe to be in possession of a valid token, it has already expired. The following diagram should clarify the scenario. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/11/diagram.svg) ### Common misbehavior I couldn't find a company (regardless of size) or indie developer who had implemented a reasonable token refresh mechanism. The common approach seems to be: to refresh the token whenever an API call fails with 401 Unauthorized. This is not only causing an extra call that could be avoided by locally checking if the token has expired, but it also opens the door for the race condition illustrated above. ## Avoid race conditions when refreshing the token 🚦 We'll explain the solution with some technical details and code snippets but what what's more important is that the reader understands the root problem we are solving and why it should be given the proper attention. The more we thought about it, we more we convinced ourselves that the best way to shield ourselves from race conditions is by using threading primitives when scheduling async requests to fetch a valid token. This means that all the calls would be regulated via a filter that would hold off subsequent calls to fire until a valid token is retrieved, either from local storage or, if a refresh is needed, from the remote OAuth server. We'll show examples for iOS, so we've chosen dispatch queues and semaphores (using [GCD](https://developer.apple.com/documentation/DISPATCH?ref=albertodebortoli.com)); fancier and more abstract ways of implementing the solution might exist - in particular by leveraging modern FRP techniques - but ultimately the same primitives are used. For simplicity, let's assume that only user-authenticated API requests need to provide a JWT, commonly put in the `Authorization` header: > *Authorization: Bearer * --- The code below implements the "Get valid JWT" box from the following flowchart. The logic within this section is the one that must be implemented in mutual exclusion, in our solution, by using the combination of a serial queue and a semaphore. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/11/Perform-Request-flow.svg) Here is just the minimum amount of code (Swift) needed to explain the solution. ```swift typealias Token = String typealias AuthorizationValue = String struct UserAuthenticationInfo { let bearerToken: Token // the JWT let refreshToken: Token let expiryDate: Date // computed on creation from 'exp' claim var isValid: Bool { return expiryDate.compare(Date()) == .orderedDescending } } protocol TokenRefreshing { func refreshAccessToken(_ refreshToken: Token, completion: @escaping (Result) -> Void) } protocol AuthenticationInfoStorage { var userAuthenticationInfo: UserAuthenticationInfo? func persistUserAuthenticationInfo(_ authenticationInfo: UserAuthenticationInfo?) func wipeUserAuthenticationInfo() } ``` ```swift class AuthorizationValueProvider { private let authenticationInfoStore: AuthenticationInfoStorage private let tokenRefreshAPI: TokenRefreshing private let queue = DispatchQueue(label: <#label#>, qos: .userInteractive) private let semaphore = DispatchSemaphore(value: 1) init(tokenRefreshAPI: TokenRefreshing, authenticationInfoStore: AuthenticationInfoStorage) { self.tokenRefreshAPI = tokenRefreshAPI self.authenticationInfoStore = authenticationInfoStore } func getValidUserAuthorization(completion: @escaping (Result) -> Void) { queue.async { self.getValidUserAuthorizationInMutualExclusion(completion: completion) } } } ``` Before performing any user-authenticated request, the network client asks an `AuthorizationValueProvider` instance to provide a valid user Authorization value (the JWT). It does so via the async method `getValidUserAuthorization` which uses a serial queue to handle the requests. The chunky part is the `getValidUserAuthorizationInMutualExclusion`. ```swift private func getValidUserAuthorizationInMutualExclusion(completion: @escaping (Result) -> Void) { semaphore.wait() guard let authenticationInfo = authenticationInfoStore.userAuthenticationInfo else { semaphore.signal() let error = // forge an error for 'missing authorization' completion(.failure(error)) return } if authenticationInfo.isValid { semaphore.signal() completion(.success(authenticationInfo.bearerToken)) return } tokenRefreshAPI.refreshAccessToken(authenticationInfo.refreshToken) { result in switch result { case .success(let authenticationInfo): self.authenticationInfoStore.persistUserAuthenticationInfo(authenticationInfo) self.semaphore.signal() completion(.success(authenticationInfo.bearerToken)) case .failure(let error) where error.isClientError: self.authenticationInfoStore.wipeUserAuthenticationInfo() self.semaphore.signal() completion(.failure(error)) case .failure(let error): self.semaphore.signal() completion(.failure(error)) } } } ``` The method could fire off an async call to refresh the token, and this makes the usage of the semaphore crucial. Without it, the next request to `AuthorizationValueProvider` would be popped from the queue and executed before the remote refresh completes. The semaphore is initialised with a value of 1, meaning that only one thread can access the critical section at a given time. We make sure to call `wait` at the beginning of the execution and to call `signal` only when we have a result and therefore ready to leave the critical section. If the token found in the local store is still valid, we simply return it, otherwise, it's time to request a new one. In the latter case, if all goes well, we persist the token locally and allow the next request to access the method, in the case of an error, we should be careful and wipe the token only if the error is a legit client error (2xx range). This includes also the usage of a refresh token that is not valid anymore, which could happen, for instance, if the user resets the password on another platform/device. It's critical to **not** delete the token from the local store in the case of any other error, such as 5xx or the common Foundation's `NSURLErrorNotConnectedToInternet` (-1009), or else the user would unexpectedly be logged out. It's also important to note that the same `AuthorizationValueProvider` instance must be used by all the calls: using different ones would mean using different queues making the entire solution ineffective. It seemed clear that the network client we developed in-house had to embrace JWT refresh logic at its core so that all the API calls, even new ones that will be added in the future would make use of the same authentication flow. ## General recommendations Here are a couple more (minor) suggestions we thought are worth sharing since they might save you implementation time or influence the design of your solution. ### Correctly parse the Payload Another problem - even though quite trivial and that doesn't seem to be discussed much - is the parsing of the JWT, that can fail in some cases. In our case, this was related to the base64 encoding function and "adjusting" the base64 payload to be parsed correctly. In some implementations of base64, the padding character is not needed for decoding, since the number of missing bytes can be calculated but in Foundation's implementation it is mandatory. This caused us some head-scratching and this [StackOverflow answer](https://stackoverflow.com/a/36366421/3010877?ref=albertodebortoli.com) helped us. The solution is - more officially - stated in [RFC 7515 - Appendix C](https://tools.ietf.org/html/rfc7515?ref=albertodebortoli.com#appendix-C) and here is the corresponding Swift code: ```swift func base64String(_ input: String) -> String { var base64 = input .replacingOccurrences(of: "-", with: "+") .replacingOccurrences(of: "_", with: "/") switch base64.count % 4 { case 2: base64 = base64.appending("==") case 3: base64 = base64.appending("=") default: break } return base64 } ``` The majority of the developers rely on external libraries to ease the parsing of the token, but as we often do, we have implemented our solution from scratch, without relying on a third-party library. Nonetheless, we feel [JSONWebToken](https://github.com/kylef/JSONWebToken.swift?ref=albertodebortoli.com) by [Kyle Fuller](https://twitter.com/kylefuller?ref=albertodebortoli.com) is a very good one and it seems to implement JWT faithfully to the RFC, clearly including the necessary [base64 decode function](https://github.com/kylef/JSONWebToken.swift/blob/master/Sources/JWT/Base64.swift?ref=albertodebortoli.com). ### Handle multiple JWT for multiple app states As previously stated, when using JWT as an authentication method for non-user- authenticated calls, we need to cater for at least 3 states, shown in the following enum: ```swift enum AuthenticationStatus { case notAuthenticated case clientAuthenticated case userAuthenticated } ``` On a fresh install, we can expect to be in the `.notAuthenticated` state, but as soon as the first API call is ready to be performed, a valid Client JWT has to be fetched and stored locally (at this stage, other authentication mechanisms are used, most likely Basic Auth), moving to the `.clientAuthenticated` state. Once the user completes the login or signup procedure, a User JWT is retrieved and stored locally (but separately to the Client JWT), entering the `.userAuthenticated`, so that in the case of a logout we are left with a (hopefully still valid) Client JWT. In this scenario, almost all transitions are possible: ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/11/authorization_states.svg) A couple of recommendations here: - if the user is logged in is important to use the User JWT also for the non-user-authenticated calls as the server may personalise the response (e.g. the list of restaurants in the Just Eat app) - store both Client and User JWT, so that if the user logs out, the app is left with the Client JWT ready to be used to perform non-user-authenticated requests, saving an unnecessary call to fetch a new token ## Conclusion In this article, we've shared some learnings from handling JWT on mobile that are not commonly discussed within the community. As a good practice, it's always best to hide complexity and implementation details. Baking the refresh logic described above within your API client is a great way to avoid developers having to deal with complex logic to provide authorization, and enables all the API calls to undergo the same authentication mechanism. Consumers of an API client, should not have the ability to gather the JWT as it’s not their concern to use it or to fiddle with it. We hope this article helps to raise awareness on how to better handle the usage of JWT on mobile applications, in particular making sure we always do our best to avoid accidental logouts to provide a better user experience. ### A Smart Feature Flagging System for iOS URL: https://albertodebortoli.com/2019/11/26/a-smart-feature-flagging-system-for-ios/ Last updated: 2019-12-04T17:21:05.000Z > How the iOS team at Just Eat built a scalable open-source solution to handle local and remote flags. *Originally published on the [Just Eat Engineering Blog](https://tech.just-eat.com/2019/11/26/a-smart-feature-flagging-system-for-ios/?ref=albertodebortoli.com).* ## Overview At [Just Eat](https://www.just-eat.com/?ref=albertodebortoli.com) we have experimentation at our heart, and it is very much dependent on feature flagging/toggling. If we may be so bold, here's an analogy: feature flagging is to experimentation as machine learning is to AI, you cannot have the second without the first one. We've developed an in-house component, named JustTweak, to handle feature flags and experiments on iOS without the hassle. We open-sourced JustTweak on [github.com](https://github.com/justeat/JustTweak?ref=albertodebortoli.com) in 2017 and we have been evolving it ever since; in particular, with support for major experimentation platforms such as [Optimizely](https://optimizely.com/?ref=albertodebortoli.com) and [Firebase Remote Config](https://firebase.google.com/docs/remote-config/?gclid=EAIaIQobChMI8fursI%5F25QIVVODtCh2p1A8zEAAYASAAEgI3tvD%5FBwE&ref=albertodebortoli.com). JustTweak has been instrumental in evolving the consumer [Just Eat app](https://apps.apple.com/gb/app/just-eat-food-delivery/id566347057?ref=albertodebortoli.com) in a fast and controlled manner, as well as to support a large number of integrations and migrations happening under the hood. In this article, we describe the feature flagging architecture and engine, with code samples and integration suggestions. ## What is feature flagging Feature flagging, in its original form, is a software development technique that provides an alternative to maintaining multiple source-code branches, so that a feature can be tested even before it is completed and ready for release. Feature flags are used in code to show/hide or enable/disable specific features at runtime. The technique also allows developers to release a version of a product that has unfinished features, that can be hidden from the user. Feature toggles also allow shorter software integration cycles and small incremental versions of software to be delivered without the cost of constant branching and merging - needless to say, this is crucial to have on iOS due to the App Store review process not allowing continuous delivery. A boolean flag in code is used to drive what code branch will run, but the concept can easily be extended to non-boolean flags, making them more of configuration flags that drive behavior. As an example, at Just Eat we have been gradually rewriting the whole application over time, swapping and customizing entire modules via configuration flags, allowing gradual switches from old to new features in a way transparent to the user. Throughout this article, the term 'tweaks' is used to refer to feature/configuration flags. A tweak can have a value of different raw types, namely `Bool`, `String`, `Int`, `Float`, and `Double`. Boolean tweaks can be used to drive features, like so: ```swift let isFeatureXEnabled: Bool = ... if isFeatureXEnabled { // show feature X } else { // don't show feature X } ``` Other types of tweaks are instead useful to customise a given feature. Here is an example of configuring the environment using tweaks: ```swift let publicApiHost: String = ... let publicApiPort: Int? = ... let endpoint = Endpoint(scheme: "https", host: publicApiHost, port: publicApiPort, path: "/restaurant/:id/menu") // perform a request using the above endpoint object ``` ## Problem The crucial part to get right is how and from where the flag values (`isFeatureXEnabled`, `publicApiHost`, and `publicApiPort` in the examples above) are fetched. Every major feature flagging/experimentation platform in the market provides its own way to fetch the values, and sometimes the APIs to do so significantly differ (e.g. [Firebase Remote Config](https://firebase.google.com/docs/remote-config/use-config-ios?ref=albertodebortoli.com) Vs [Optimizely](https://docs.developers.optimizely.com/full-stack/docs/example-usage?ref=albertodebortoli.com)). Aware of the fact that it’s increasingly difficult to build any kind of non-trivial app without leveraging external dependencies, it's important to bear in mind that external dependencies pose a great threat to the long term stability and viability of any application. Following are some issues related to third-party experimentation solutions: - third-party SDKs are not under your control - using third-party SDKs in a modular architected app would easily cause [dependency hell](https://en.wikipedia.org/wiki/Dependency%5Fhell?ref=albertodebortoli.com) - third-party SDKs are easily abused and various areas of your code will become entangled with them - your company might decide to move to a different solution in the future and such switch comes with costs - depending on the adopted solution, you might end up tying your app more and more to the platform-specific features that don't find correspondence elsewhere - it is very hard to support multiple feature flag providers For the above reasons, it is best to hide third-party SDKs behind some sort of a layer and to implement an orchestration mechanism to allow fetching of flag values from different providers. We'll describe how we've achieved this in JustTweak. ## A note on the approach When designing software solutions, a clear trait was identified over time in the iOS team, which boils down to the kind of mindset and principle been used: Always strive to find solutions to problems that are scalable and hide complexity as much as possible. One word you would often hear if you were to work in the iOS team is '[Facade](https://en.wikipedia.org/wiki/Facade%5Fpattern?ref=albertodebortoli.com)', which is a design pattern that serves as a front-facing interface masking more complex underlying or structural code. Facades are all over the place in our code: we try to keep components' interfaces as simple as possible so that other engineers could utilize them with minimal effort without necessarily knowing the implementation details. Furthermore, the more succinct an interface is, the rarer the possibility of misusages would be. We have some open source components embracing this approach, such as [JustPersist](https://github.com/justeat/JustPersist?ref=albertodebortoli.com), [JustLog](https://github.com/justeat/JustLog?ref=albertodebortoli.com), and [JustTrack](https://github.com/justeat/JustTrack?ref=albertodebortoli.com). [JustTweak](https://github.com/justeat/JustTweak?ref=albertodebortoli.com) makes no exception and the code to integrate it successfully in a project is minimal. Sticking to the above principle, the idea behind JustTweak is to have a single entry point to gather flag values, hiding the implementation details regarding which source the flag values are gathered from. ## JustTweak to the rescue JustTweak provides a simple facade interface interacting with multiple configurations that are queried respecting a certain priority. Configurations wrap specific sources of tweaks, that are then used to drive decisions or configurations in the client code. You can find JustTweak on [CocoaPods](https://github.com/CocoaPods/CocoaPods?ref=albertodebortoli.com) and it's on version 5.0.0 at the time of writing. We plan to add support for [Carthage](https://github.com/Carthage/Carthage?ref=albertodebortoli.com) and [Swift Package Manager](https://swift.org/package-manager/?ref=albertodebortoli.com) in the future. A demo app is also available for you to try it out. With JustTweak you can achieve the following: - use a JSON local configuration providing default tweak values - use a number of remote configuration providers, such as Firebase and Optmizely, to run A/B tests and feature flagging - enable, disable, and customize features locally at runtime - provide a dedicated UI for customization (this comes particularly handy for features that are under development to showcase the progress to stakeholders) Here is a screenshot of the `TweakViewController` taken from the demo app. Tweak values changed via this screen are immediately available to your code at runtime. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/11/demo_app_view_controller.png) ## Stack setup The facade class previously mentioned is represented by the `TweakManager`. There should only be a single instance of the manager, ideally configured at startup, passed around via dependency injection, and kept alive for the whole lifespan of the app. Following is an example of the kind of stack implemented as a `static let`. ``` static let tweakManager: TweakManager = { // mutable configuration (to override tweaks from other configurations) let userDefaultsConfiguration = UserDefaultsConfiguration(userDefaults: .standard) // remote configurations (optional) let optimizelyConfiguration = OptimizelyConfiguration() let firebaseConfiguration = FirebaseConfiguration() // local JSON configuration (default tweaks) let jsonFileURL = Bundle.main.url(forResource: "Tweaks", withExtension: "json")! let localConfiguration = LocalConfiguration(jsonURL: jsonFileURL) // priority is defined by the order in the configurations array // (from highest to lowest) let configurations: [Configuration] = [userDefaultsConfiguration, optimizelyConfiguration, firebaseConfiguration, localConfiguration] return TweakManager(configurations: configurations) }() ``` ``` JustTweak comes with three configurations out-of-the-box: - `UserDefaultsConfiguration` which is mutable and uses `UserDefaults` as a key/value store - `LocalConfiguration` which is read-only and uses a JSON configuration file that is meant to be the default configuration - `EphemeralConfiguration` which is simply an instance of `NSMutableDictionary` Besides, JustTweak defines `Configuration` and `MutableConfiguration` protocols you can implement to create your own configurations to fit your needs. In the example project, you can find a few example configurations which you can use as a starting point. You can have any source of flags via wrapping it in a concrete implementation of the above protocols. Since the protocol methods are synchronous, you'll have to make sure that the underlying source has been initialised as soon as possible at startup. All the experimentation platforms provide mechanisms to do so, for example [here](https://docs.developers.optimizely.com/full-stack/docs/initialize-sdk-swift?ref=albertodebortoli.com#section-use-synchronous-or-asynchronous-initialization) is how Optimizely does it. The order of the objects in the `configurations` array defines the configurations' priority. The `MutableConfiguration` with the highest priority, such as `UserDefaultsConfiguration` in the example above, will be used to reflect the changes made in the UI (`TweakViewController`). The `LocalConfiguration` should have the lowest priority as it provides the default values from a local JSON file. It's also the one used by the `TweakViewController` to populate the UI. When fetching a tweak, the engine will inspect the chain of configurations in order and pick the tweak from the first configuration having it. The following diagram outlines a possible setup where values present in Optimizely override others in the subsequent configurations. Eventually, if no override is found, the local configuration would return the default tweak baked in the app. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/11/JustTweak_stack.png) Structuring the stack this way brings various advantages: - the same engine is used to customise the app for development, production, and test runs - consumers only interface with the facade and can ignore the implementation details - new code put behind flags can be shipped with confidence since we rely on a tested engine - ability to remotely override tweaks de facto allowing to greatly customise the app without the need for a new release `TweakManager` gets populated with the tweaks listed in the JSON file used as backing store of the `LocalConfiguration` instance. It is therefore important to list every supported tweak in there so that development builds of the app can allow tweaking the values. Here is an excerpt from the file used in the `TweakViewController` screenshot above. ```json { "ui_customization": { "display_red_view": { "Title": "Display Red View", "Description": "shows a red view in the main view controller", "Group": "UI Customization", "Value": false }, ... "red_view_alpha_component": { "Title": "Red View Alpha Component", "Description": "defines the alpha level of the red view", "Group": "UI Customization", "Value": 1.0 }, "label_text": { "Title": "Label Text", "Description": "the title of the main label", "Group": "UI Customization", "Value": "Test value" } }, "general": { "greet_on_app_did_become_active": { "Title": "Greet on app launch", "Description": "shows an alert on applicationDidBecomeActive", "Group": "General", "Value": false }, ... } } ``` ### Testing considerations We've seen that the described architecture allows customization via configurations. We've shown in the above diagram that JustTweak can come handy when used in conjunction with our [AutomationTools](https://github.com/justeat/AutomationTools?ref=albertodebortoli.com) framework too, which is open-source. An Ephemeral configuration would define the app environment at run-time greatly simplifying the implementation of UI tests, which is well-known to be a tedious activity. ## Usage The two main features of JustTweak can be accessed from the `TweakManager`. - Checking if a feature is enabled ```swift // check for a feature to be enabled let isFeatureXEnabled = tweakManager.isFeatureEnabled("feature_X") if isFeatureXEnabled { // show feature X } else { // hide feature X } ``` - Getting and setting the value of a flag for a given feature/variable. JustTweak will return the value from the configuration with the highest priority that provides it, or nil if none of the configurations have that feature/variable. ```swift // check for a tweak value let tweak = tweakManager.tweakWith(feature: <#feature_key#>, variable: <#variable_key#>") if let tweak = tweak { // tweak was found in some configuration, use tweak.value } else { // tweak was not found in any configuration } ``` The `Configuration` and `MutableConfiguration` protocols define the following methods: ```swift func tweakWith(feature: String, variable: String) -> Tweak? func set(_ value: TweakValue, feature: String, variable: String) func deleteValue(feature: String, variable: String) ``` You might wonder why is there a distinction between feature and variable. The reason is that we want to support the Optimizely [lingo](https://docs.developers.optimizely.com/full-stack/docs/define-feature-variables?ref=albertodebortoli.com) for features and related variables and therefore the design of JustTweak has to necessarily reflect that. Other experimentation platforms (such as Firebase) have a single parameter key, but we had to harmonise for the most flexible platform we support. ## Property Wrappers With [SE-0258](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md?ref=albertodebortoli.com), Swift 5.1 introduces Property Wrappers. If you haven't read about them, we suggest you watch the WWDC 2019 "[Modern Swift API Design](https://developer.apple.com/videos/play/wwdc2019/415/?ref=albertodebortoli.com) talk where Property Wrappers are explained starting at 23:11. In short, a property wrapper is a generic data structure that encapsulates read/write access to a property while adding some extra behavior to augment its semantics. Common examples are `@AtomicWrite` and `@UserDefault` but more creative usages are up for grabs and we couldn't help but think of how handy it would be to have property wrappers for feature flags, and so we [implemented them](https://github.com/justeat/JustTweak/blob/master/JustTweak/Classes/Utilities/PropertyWrapper.swift?ref=albertodebortoli.com). `@TweakProperty` and `@OptionalTweakProperty` are available to mark properties representing feature flags. Here are a couple of examples, making the code so much nicer than before. ```swift @TweakProperty(fallbackValue: <#default_value#>, feature: <#feature_key#>, variable: <#variable_key#>, tweakManager: tweakManager) var isFeatureXEnabled: Bool @TweakProperty(fallbackValue: <#default_value#>, feature: <#feature_key#>, variable: <#variable_key#>, tweakManager: tweakManager) var publicApiHost: String @OptionalTweakProperty(fallbackValue: <#default_value_or_nil#>, feature: <#feature_key#>, variable: <#variable_key#>, tweakManager: tweakManager) var publicApiPort: Int? ``` Mind that by using these property wrappers, a static instance of `TweakManager` must be available. ## Update a configuration at runtime JustTweak comes with a ViewController that allows the user to edit the tweaks while running the app. That is achieved by using the `MutableConfiguration` with the highest priority from the configurations array. This is de facto a debug menu, useful for development and internal builds but not to include in release builds. ```swift #if DEBUG func presentTweakViewController() { let tweakViewController = TweakViewController(style: .grouped, tweakManager: tweakManager) // either present it modally or push it on a UINavigationController } #endif ``` Additionally, when a value is modified in any `MutableConfiguration`, a notification is fired to give the clients the opportunity to react and reflect changes in the UI. ```swift override func viewDidLoad() { super.viewDidLoad() NotificationCenter.defaultCenter().addObserver(self, selector: #selector(updateUI), name: TweakConfigurationDidChangeNotification, object: nil) } @objc func updateUI() { // update the UI accordingly } ``` ## A note on modular architecture It's reasonable to assume that any non-trivial application approaching 2020 is composed of a number of modules and our Just Eat iOS app surely is too. With more than 30 modules developed in-house, it's crucial to find a way to inject flags into the modules but also to avoid every module to depend on an external library such as JustTweak. One way to achieve this would be: - define one or more protocols in the module with the set of properties desired - structure the modules to allow dependency injection of objects conforming to the above protocol - implement logic in the module to consume the injected objects For instance, you could have a class wrapping the manager like so: ```swift protocol ModuleASettings { var isFeatureXEnabled: Bool { get } } protocol ModuleBSettings { var publicApiHost: String { get } var publicApiPort: Int? { get } } ``` ```swift import JustTweak public class AppConfiguration: ModuleASettings, ModuleBSettings { static let tweakManager: TweakManager = { ... } @TweakProperty(...) var isFeatureXEnabled: Bool @TweakProperty(...) var publicApiHost: String @OptionalTweakProperty(...) var publicApiPort: Int? } ``` ## Future evolution With recent versions of Swift and especially with 5.1, developers have a large set of powerful new tools, such as generics, associated types, opaque types, type erasure, etc. With [Combine](https://developer.apple.com/documentation/combine?ref=albertodebortoli.com) and [SwiftUI](https://developer.apple.com/documentation/swiftui?ref=albertodebortoli.com) entering the scene, developers are also starting adopting new paradigms to write code. Sensible paths to evolve JustTweak could be to have the `Tweak` object be generic on `TweakValue` have `TweakManager` be an [ObservableObject](https://developer.apple.com/documentation/combine/observableobject?ref=albertodebortoli.com) which will enable publishing of events via Combine, and use [@EnvironmentObject](https://developer.apple.com/documentation/swiftui/environmentobject?ref=albertodebortoli.com) to ease the dependency injection in the SwiftUI view hierarchy. While such changes will need time to be introduced since our contribution to JustTweak is in-line with the evolution of the Just Eat app (and therefore a gradual adoption of SwiftUI), we can't wait to see them implemented. If you desire to contribute, we are more than happy to receive [pull requests](https://github.com/justeat/JustTweak/pulls?ref=albertodebortoli.com). ## Conclusion In this article, we illustrated how JustTweak can be of great help in adding flexible support to feature flagging. Integrations with external providers/experimentation platforms such as Optimizely, allow remote override of flags without the need of building a new version of the app, while the UI provided by the framework allows local overrides in development builds. We've shown how to integrate JustTweak in a project, how to setup a reasonable stack with a number of configurations and we’ve given you some guidance on how to leverage it when writing UI tests. We believe JustTweak to be a great tool with no similar open source alternatives nor proprietary ones and we hope developers will adopt it more and more. ### Deep Linking at Scale on iOS URL: https://albertodebortoli.com/2019/04/16/deep-linking-at-scale-on-ios/ Last updated: 2019-12-04T17:21:30.000Z > How the iOS team at Just Eat built a scalable architecture to support navigation and deep linking. *Originally published on the [Just Eat Engineering Blog](https://tech.just-eat.com/2019/04/16/deep-linking-at-scale-on-ios/?ref=albertodebortoli.com).* In this article, we propose an architecture to implement a scalable solution to Deep Linking on iOS using an underlying Flow Controller-based architecture, all powered by a state machine and the [Futures & Promises](https://en.wikipedia.org/wiki/Futures%5Fand%5Fpromises?ref=albertodebortoli.com) paradigm to keep the code more readable. At Just Eat, we use a dedicated component named **NavigationEngine** that is domain-specific to the Just Eat apps and their use cases. A demo project named NavigationEngineDemo that includes the NavigationEngine architecture (stripped out of many details not necessary to showcase the solution) is available on [GitHub](https://github.com/justeat/NavigationEngineDemo?ref=albertodebortoli.com). ## Overview Deep linking is one of the most underestimated problems to solve on mobile. A naïve explanation would say that given some sort of input, mobile apps can load a specific screen, but it only has practical meaning when combined with [Universal Links](https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.html?ref=albertodebortoli.com) on iOS and [App Links](https://developer.android.com/training/app-links?ref=albertodebortoli.com) on Android. In such cases, the input is a URL that would load a web page on the companion website. Let's use an example from Just Eat: opening the URL [https://www.just-eat.co.uk/area/ec4m-london](https://www.just-eat.co.uk/area/ec4m-london?ref=albertodebortoli.com) on a web browser would load the list of restaurants in the UK London area for the postcode EC4M. Deep linking to the mobile apps using the same URL should give a similar experience to the user. In reality, the problem is more complex than what it seems at first glance; non-tech people - and sometimes even developers - find it hard to grasp. Loading a web page in a browser is fundamentally different from implementing dedicated logic on mobile to show a UIViewController (iOS) or Activity (Android) to the user and populate it with information that will most likely be gathered from an API call. The logic to perform deep linking starts with parsing the URL, understanding the intent, constructing the user journey, performing the navigation to the target screen passing the info all the way down, and ultimately loading any required data asynchronously from a remote API. On top of all this, it also has to consider the state of the app: the user might have previously left the app in a particular state and dedicated logic would be needed to deep link from the existing to the target screen. A scenario to consider is when the user is not logged in and therefore some sections of the app may not be available. Deep linking can actually be triggered from a variety of sources: - Safari web browser - any app that allows tapping on a link (iMessage, Notes, etc.) - any app that explicitly tries to open the app using [custom URL schemes](https://developer.apple.com/documentation/uikit/core%5Fapp/allowing%5Fapps%5Fand%5Fwebsites%5Fto%5Flink%5Fto%5Fyour%5Fcontent/defining%5Fa%5Fcustom%5Furl%5Fscheme%5Ffor%5Fyour%5Fapp?ref=albertodebortoli.com) - the app itself (to perform jumps between sections) - TodayExtension - Shortcut items ([Home Screen Quick Actions](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/Adopting3DTouchOniPhone/?ref=albertodebortoli.com)) - Spotlight items It should be evident that implementing a comprehensive and scalable solution that fully addresses deep linking is far from being trivial. It shouldn't be an after-thought but rather be baked into the app architecture from the initial app design. It should also be quite glaring what the main problem that needs to be solved first is: **the app Navigation**. Navigation itself is not a problem with a single solution (if it was, the solution would be provided by Apple/Google and developers would simply stick to it). A number of solutions were proposed over the years trying to make it simpler and generic to some degree - [Router](https://github.com/freshOS/Router?ref=albertodebortoli.com), [Compass](https://github.com/hyperoslo/Compass?ref=albertodebortoli.com), [XCoordinator](https://github.com/quickbirdstudios/XCoordinator?ref=albertodebortoli.com) to name just a few open-source components. I proposed the concept of Flow Controllers in my article [Flow Controllers on iOS for a better navigation control](https://albertodebortoli.com/2014/09/03/flow-controllers-on-ios-for-a-better-navigation-control/) back in 2014 when the community had already (I believe) started shifting towards similar approaches. Articles such as [Improve your iOS Architecture with FlowControllers](http://merowing.info/2016/01/improve-your-ios-architecture-with-flowcontrollers/?ref=albertodebortoli.com) (by *Krzysztof Zabłocki*), [A Better MVC, Part 2: Fixing Encapsulation](https://davedelong.com/blog/2017/11/06/a-better-mvc-part-2-fixing-encapsulation/?ref=albertodebortoli.com) (by *Dave DeLong*), [Flow Coordinators in iOS](https://medium.com/@dkw5877/flow-coordinators-333ed64f3dd?ref=albertodebortoli.com) (by *Dennis Walsh*), and even as recently as 2019, [Navigation with Flow Controllers](https://mecid.github.io/2019/02/20/navigation-with-flow-controllers/?ref=albertodebortoli.com) (by *Majid Jabrayilov*) was published. To me, all the proposals share one main common denominator: flow controllers/coordinator and their API are necessarily *domain-specific*. Consider the following methods taken from one of the articles mentioned above referring to specific use cases: ``` func showLoginViewController() { ... } func showSignupViewController() { ... } func showPasswordViewController() { ... } ``` With the support of colleagues and friends, I tried proposing a generic and abstract solution but ultimately hit a wall. Attempts were proposed using enums to list the supported transitions (as [XCoordinator](https://github.com/quickbirdstudios/XCoordinator?ref=albertodebortoli.com) shows in its [README](https://github.com/quickbirdstudios/XCoordinator?ref=albertodebortoli.com#%EF%B8%8Fgetting-started) for instance) or relying on meta-programming dark magic in Objective-C (which is definitely the sign of a terrible design), neither of which satisfied me in terms of reusability and abstraction. I ultimately realized that it's perfectly normal for such problem to be domain-specific and that we don't necessarily have to find abstract solutions to all problems. ## Terminology For clarity on some of the terminology used in this article. - **Deep Linking**: the ability to reach specific screens (via a flow) in the app either via a Deep Link or a Universal Link. - **Deep Link**: URI with custom scheme (e.g. `just-eat://just-eat.co.uk/login`, `just-eat-dk://just-eat.co.uk/settings`) containing the information to perform deep linking in the app. When it comes to deep links, the host is irrelevant but it's good to keep it as part of the URL since it makes it easier to construct the URL using `URLComponents` and it keeps things more 'standard'. - **Universal Link**: URI with http/https scheme (e.g. `https://just-eat.co.uk/login`) containing the information to perform deep linking in the app. - **Intent**: the abstract intent of reaching a specific area of the app. E.g. `goToOrderDetails(OrderId)`. - **State machine transition**: transitions in the state machine allow navigating to a specific area in the app (state) from another one. If the app is in a state where the deep linking to a specific screen should not be allowed, the underlying state machine should not have the corresponding transition. ## Solution NavigationEngine is the iOS module (pod) used by the teams at Just Eat, that holds the isolated logic for navigation and deep linking. As mentioned above, the magic sauce includes the usage of: - [**FlowControllers**](https://albertodebortoli.com/2014/09/03/flow-controllers-on-ios-for-a-better-navigation-control/) to handle the transitions between ViewControllers in a clear and pre-defined way. - [**Stateful**](https://github.com/albertodebortoli/Stateful?ref=albertodebortoli.com) state machines to allow transitions according to the current application state. More information on FSM (Finite State Machine) [here](https://brilliant.org/wiki/finite-state-machines/?ref=albertodebortoli.com) and on the library at [The easiest State Machine in Swift](https://albertodebortoli.com/2018/12/16/the-easiest-state-machine-in-swift/). - [**Promis**](https://github.com/albertodebortoli/Promis?ref=albertodebortoli.com) to keep the code readable using Futures & Promises to help avoiding the [Pyramid of doom](https://en.wikipedia.org/wiki/Pyramid%5Fof%5Fdoom%5F%28programming%29?ref=albertodebortoli.com). Sticking to such a paradigm is also a key aspect for the whole design since every API in the stack is async. More info on the library at [The easiest Promises in Swift](https://albertodebortoli.com/2018/02/12/the-easiest-promises-in-swift/). - a pretty heavy amount of 🧠 NavigationEngine maintains separation of concerns between *URL Parsing*, *Navigation*, and *Deep Linking*. Readers can inspect the code in the [NavigationEngineDemo](https://github.com/justeat/NavigationEngineDemo?ref=albertodebortoli.com) project that also includes unit tests with virtually 100% code coverage. Following is an overview of the class diagram of the entire architecture stack. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/04/class_diagram_demo-2.png) Architecture class diagram While the navigation is powered by a FlowController-based architecture, the deep linking logic is powered by `NavigationIntentHandler` and `NavigationTransitioner` (on top of the navigation stack). Note the single entry point named `DeepLinkingFacade` exposes the following API to cover the various input/sources we mentioned earlier: ``` public func handleURL(_ url: URL) -> Future public func openDeepLink(_ deepLink: DeepLink) -> Future public func openShortcutItem(_ item: UIApplicationShortcutItem) -> Future public func openSpotlightItem(_ userActivity: NSUserActivityProtocol) -> Future ``` Here are the sequence diagrams for each one. Refer to the demo project to inspect the code. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/04/handleURL-5.svg) ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/04/openDeepLink-3.svg) ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/04/openShortcutItem-4.svg) ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/04/openSpotlightItem-4.svg) ## Navigation As mentioned earlier, the important concept to grasp is that there is simply no single solution to Navigation. I've noticed that such a topic quickly raises discussions and each engineer has different, sometimes strong opinions. It's more important to agree on a working solution that satisfies the given requirements rather than forcing personal preferences. Our NavigationEngine relies on the following navigation rules (based on Flow Controllers): - *FlowControllers* wire up the domain-specific logic for the navigation - *ViewControllers* don't allocate *FlowControllers* - Only *FlowControllers*, *AppDelegate* and similar top-level objects can allocate *ViewControllers* - *FlowControllers* are owned (retained) by the creators - *FlowControllers* can have children *FlowControllers* and create a parent-child chain and can, therefore, be in a 1-to-many relationship - *FlowControllers* in parent-child relationships communicate via delegation - *ViewControllers* have weak references to *FlowControllers* - *ViewControllers* are in a 1-to-1 relationship with *FlowControllers* - All the *FlowController* domain-specific API must be future-based with `Future` as return type - Deep linking navigation should occur with no more than one animation (i.e. for long journeys, only the last step should be animated) - Deep linking navigation that pops a stack should occur without animation In the demo project, there are a number of `*FlowControllerProtocols`, each corresponding to a different section/domain of the hosting app. Examples such as `RestaurantsFlowControllerProtocol` and `OrdersFlowControllerProtocol` are taken from the Just Eat app and each one has domain specific APIs, e.g: ``` func goToSearchAnimated(postcode: Postcode?, cuisine: Cuisine?, animated: Bool) -> Future func goToOrder(orderId: OrderId, animated: Bool) -> Future func goToRestaurant(restaurantId: RestaurantId) -> Future func goToCheckout(animated: Bool) -> Future ``` Note that each one: - accepts the `animated` parameter - returns `Future` so that flow sequence can be combined Flow controllers should be combined sensibly to represent the app UI structure. In the case of Just Eat we have a `RootFlowController` as the root-level flow controller orchestrating the children. A `FlowControllerProvider`, used by the `NavigationTransitioner`, is instead the single entry point to access the entire tree of flow controllers. `NavigationTransitioner` provides an API such as: ``` func goToLogin(animated: Bool) -> Future func goFromHomeToSearch(postcode: Postcode?, cuisine: Cuisine?, animated: Bool) -> Future ``` This is responsible to keep the underlying state machine and what the app actually shows in sync. Note the `goFromHomeToSearch` method being verbose on purpose; it takes care of the specific transition from a given state (home). One level up in the stack, `NavigationIntentHandler` is responsible for combining the actions available from the `NavigationTransitioner` starting from a given `NavigationIntent` and creating a complete deep linking journey. It also takes into account the current state of the app. For example, showing the history of the orders should be allowed only if the user is logged in, but it would also be advisable to prompt the user to log in in case he/she is not, and then resume the original action. Allowing so provides a superior user experience rather than simply aborting the flow (it's what websites achieve by using the referring URL). Here is the implementation of the `.goToOrderHistory` intent in the `NavigationIntentHandler`: ``` case .goToOrderHistory: switch userStatusProvider.userStatus { case .loggedIn: return navigationTransitioner.goToRoot(animated: false).thenWithResult { _ -> Future in self.navigationTransitioner.goToOrderHistory(animated: true) } case .loggedOut: return navigationTransitioner.requestUserToLogin().then { future in switch future.state { case .result: return self.handleIntent(intent) // go recursive default: return Future.futureWithResolution(of: future) } } } ``` Since in the design we make the entire API future-based, we can potentially interrupt the deep linking flow to prompt the user for details or simply gather missing information from a remote API. This is crucial and allows us to construct complex flows. By design, all journeys start by resetting the state of the app by calling `goToRoot`. This vastly reduces the number of possible transitions to take care of as we will describe in more detail in the next section dedicated to the underlying state machine. ## State Machine As you might have realized by now, the proposed architecture makes use of an underlying [Finite State Machine](https://brilliant.org/wiki/finite-state-machines/?ref=albertodebortoli.com) to keep track of the state of the app during a deep linking journey. Here is a simplified version of the state machine configurations used in the Just Eat iOS apps. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/04/state_machine_just_eat-5.png) In the picture, the red arrows are transitions that are available for logged in users only, the blue ones are for logged out users only, while the black ones can always be performed. Note that every state should allow going back to the `.allPoppedToRoot` state so that, regardless of what the current state of the app is, we can always reset the state and perform a deep linking action starting afresh. This drastically simplifies the graph, avoiding unnecessary transitions such as the one shown in the next picture. ![](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2019/04/state_machine_sample-3.png) Notice that intents (`NavigationIntent`) are different from transitions (`NavigationEngine.StateMachine.EventType`). An *intent* contains the information to perform a deep linking journey, while the *event type* is the transition from one FSM state to another (or the same). `NavigationTransitioner` is the class that performs the transitions and applies the companion navigation changes. A navigation step is performed only if the corresponding transition is allowed and completed successfully. If a transition is not allowed, the flow is interrupted, reporting an error in the future. You can showcase a failure in the demo app by trying to follow the *Login Universal Link* (`https://just-eat.co.uk/login`) after having faked the login when following the *Order History Universal Link* (`https://just-eat.co.uk/orders`). ![test_deep_linking_failure](https://albertodebortoli.ghost.io/content/images/2019/04/test_deep_linking_failure.png) ## Usage **NavigationEngineDemo** includes the whole stack that readers can use in client projects. Here are the steps for a generic integration of the code. Add the *NavigationEngine* stack (`NavigationEngineDemo/NavigationEngine` folder) to the client project. This can be done by either creating a dedicated pod as we do at Just Eat or by directly including the code. Include `Promis` and `Stateful` as dependencies in your Podfile (assuming the usage of Cocoapods). Modify according to your needs, implement classes for all the `*FlowControllerProtocols`, and connect them to the *ViewControllers* of the client. This step can be quite tedious depending on the status of your app and we suggest trying to mimic what has been done in the demo app. Add `CFBundleTypeRole` and `CFBundleURLSchemes` to the main target `Info.plist` file to support Deep Links. E.g. ``` CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLSchemes je-internal justeat just-eat just-eat-uk ``` - Add the applinks (in the Capabilities -> Associated Domains section of the main target) you'd like to support. This will allow iOS to register the app for Universal Links on the given domains looking for the `apple-app-site-association` file at the root of those domains once the app is installed. E.g. ![associated_domains-1](https://albertodebortoli.ghost.io/content/images/2019/04/associated_domains-1.png) Implement concrete classes for `DeepLinkingSettingsProtocol` and `UserStatusProviding` according to your needs. Again, see the examples in the demo project. The `internalDeepLinkSchemes` property in `DeepLinkSettingsProtocol` should contain the same values previously added to `CFBundleURLSchemes`, while the `universalLinkHosts` should contain the same `applinks:` values defined in Capabilities -> Associated Domains. Setup the `NavigationEngine` stack in the AppDelegate's `applicationDidFinishLaunching`. To some degree, it should be something similar to the following: ``` var window: UIWindow? var rootFlowController: RootFlowController! var deepLinkingFacade: DeepLinkingFacade! var userStatusProvider = UserStatusProvider() let deepLinkingSettings = DeepLinkingSettings() func applicationDidFinishLaunching(_ application: UIApplication) { // Init UI Stack let window = UIWindow(frame: UIScreen.main.bounds) let tabBarController = TabBarController.instantiate() // Root Flow Controller rootFlowController = RootFlowController(with: tabBarController) tabBarController.flowController = rootFlowController // Deep Linking core let flowControllerProvider = FlowControllerProvider(rootFlowController: rootFlowController) deepLinkingFacade = DeepLinkingFacade(flowControllerProvider: flowControllerProvider, navigationTransitionerDataSource: self, settings: deepLinkingSettings, userStatusProvider: userStatusProvider) // Complete UI Stack window.rootViewController = tabBarController window.makeKeyAndVisible() self.window = window } ``` - Modify `NavigationTransitionerDataSource` according to your needs and implement its methods. You might want to have a separate component and not using the AppDelegate. ``` extension AppDelegate: NavigationTransitionerDataSource { func navigationTransitionerDidRequestUserToLogin() -> Future { <#async logic#> } ... } ``` - Implement the entry points for handling incoming URLs/inputs in the *AppDelegate*: ``` func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { // from internal deep links & TodayExtension deepLinkingFacade.openDeeplink(url).finally { future in <#...#> } return true } func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { switch userActivity.activityType { // from Safari case NSUserActivityTypeBrowsingWeb: if let webpageURL = userActivity.webpageURL { self.deepLinkingFacade.handleURL(webpageURL).finally { future in <#...#> } return true } return false // from Spotlight case CSSearchableItemActionType: self.deepLinkingFacade.openSpotlightItem(userActivity).finally { future in let originalInput = userActivity.userInfo![CSSearchableItemActivityIdentifier] as! String <#...#> } return true default: return false } } func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { // from shortcut items (Home Screen Quick Actions) deepLinkingFacade.openShortcutItem(shortcutItem).finally { future in let originalInput = shortcutItem.type <#...#> completionHandler(future.hasResult()) } } ``` N.B. Since a number of tasks are usually performed at startup (both from cold and warm starts), it's suggested to schedule them using operation queues. The deep linking task should be one of the last tasks in the queue to make sure that dependencies are previously set up. Here is the great [Advanced NSOperations](https://developer.apple.com/videos/play/wwdc2015/226/?ref=albertodebortoli.com) talk by [*Dave DeLong* ](https://twitter.com/davedelong?ref=albertodebortoli.com) from WWDC15. - The `UniversalLinkConverter` class should be modified to match the paths in the `apple-app-site-association`, which should be reachable at the root of the website (the associated domain). It should be noted that if the app is opened instead of the browser, it would be because the Universal Link can be handled; and redirecting the user back to the web would be a fundamental mistake that should be solved by correctly defining the supported paths in the `apple-app-site-association` file. To perform internal app navigation via deep linking, the DeeplinkFactory class should be used to create `DeepLink` objects that can be fed into either `handleURL(_ url: URL)` or `openDeepLink(_ deepLink: DeepLink)`. ### In-app testing The module exposes a `DeepLinkingTesterViewController` that can be used to easily test deep linking within an app. ![deeplinking-tester-2](https://albertodebortoli.ghost.io/content/images/2019/04/deeplinking-tester-2.png) Simply define a JSON file containing the Universal Links and Deep Links to test: ``` { "universal_links": [ "https://just-eat.co.uk/", "https://just-eat.co.uk/home", "https://just-eat.co.uk/login", ... ], "deep_links": [ "JUSTEAT://irrelev.ant/home", "justeat://irrelev.ant/login", "just-eat://irrelev.ant/resetPassword?resetToken=xyz", ... ] } ``` Then feed it to the view controller as shown below. Alternatively, use a storyboard reference as shown in the demo app. ``` let deepLinkingTesterViewController = DeepLinkingTesterViewController.instantiate() deepLinkingTesterViewController.delegate = self let path = Bundle.main.path(forResource: "deeplinking_test_list", ofType: "json")! deepLinkingTesterViewController.loadTestLinks(atPath: path) ``` and implement the DeepLinkingTesterViewControllerDelegate ``` extension AppDelegate: DeepLinkingTesterViewControllerDelegate { func deepLinkingTesterViewController(_ deepLinkingTesterViewController: DeepLinkingTesterViewController, didSelect url: URL) { self.deepLinkingFacade.handleURL(universalLink).finally { future in self.handleFuture(future, originalInput: universalLink.absoluteString) } } } ``` ## Conclusion The solution proposed in this article has proven to be highly scalable and customizable. We shipped it in the Just Eat iOS apps in March 2019 and our teams are gradually increasing the number of Universal Links supported as you can see from our [apple-app-site-association](https://www.just-eat.co.uk/apple-app-site-association?ref=albertodebortoli.com). Before implementing and adopting *NavigationEngine*, supporting new kinds of links was a real hassle. Thanks to this architecture, it is now easy for each team in the company to support new deep link journeys. The declarative approach in defining the API, states, transitions, and intents forces a single way to extend the code which enables a coherent approach throughout the codebase. ### Principal Manifesto URL: https://albertodebortoli.com/2019/04/02/principal-manifesto/ Last updated: 2025-06-08T22:27:17.000Z > *Edit: in 2020, Will Larson published Staff Engineer, the first book that properly reasons about Staff+ roles. I cannot recommend the book, the articles, and the podcast enough. You can find all about them at* [staffeng.com](https://staffeng.com/?ref=albertodebortoli.com). To extend the tech career ladder, a number of roles have been introduced in the tech community over the past years. Depending on the company and related [role and level fragmentation](https://www.levels.fyi/?ref=albertodebortoli.com), they might go by different labels such as *"Principal Engineer"*, *"VP Engineer"*, *"Distinguished Engineer"*, *"Staff Engineer"*, *"Fellow"*, *"Architect"* or sometimes, in small environments, simply *"Tech Lead"*. Such roles are intended to allow progression and recognition for the company's most senior staff giving individuals the ability to grow above a senior level which is becoming more and more mainstream these days and while whose responsibilities may vary, it is too often labeled as expert programmer with a solid tech background and decent experience on the CV. Senior engineers might progress their career towards a managerial path (people management) or prefer to stay focused on the tech side of things (the above roles). I have seen people being very good in the roles mentioned above as natural technical leaders: such individuals were great engineers on their own, well respected by the engineers around them, worked well within teams, understood how software should be designed, built and shipped, and often had a decent sense for making the right kinds of product tradeoffs. Such engineers were willing to do enough project management and people development to keep the team/project humming along. Engineers in these roles usually remain pretty connected with the details of the project and successfully manage/influence from within rather than from outside the project. As a Principal Engineer, I sometimes found describing my role and my responsibilities being too vague at times, with tasks dipping into various topics, especially depending on the need of the company at a particular point in time. For such reason, I list here the expectations and responsibilities of the *Principal role* in the way I've experienced it in my career so far. The document, therefore, represents the *Principal Manifesto* I stand by. Principal Engineers should be expected to: 1. Define tech vision 1-2 years ahead, plan accordingly with the teams and promote it across the business. 2. Define and gradually execute a tech roadmap that aligns with the business requirements and demands and have Tech/Delivery/Product managers supporting it. 3. Persuade teams to do the right thing and align on processes whenever possible. Work with them whenever necessary in a collaborative way and with an eye on the status of the progress across the team. 4. Be able to manage expectations with stakeholders and not over-promise. 5. Help to establish a consensus between the engineering group on architecture, patterns and practices. 6. Understand how software should be designed at both high and low-level, built and shipped, considering maintainability and costs. 7. Understand the company tech platform as a whole. 8. Introduce evolutive, safe, and sound changes both from a technical point of view and a tooling & standards point of view. 9. Collaborate across teams when necessary to move projects forward and smooth the edges when a project is dragging. 10. Identify small course corrections to avoid long term debt or failing projects. 11. Call out good work and highlight poor work to line managers. 12. Be identified as professionals ‘to go to’ and be a respected authority on specific projects/areas/technologies by other engineers but also by management. 13. Being trusted in deciding on what to work on and what projects demands dedicated attention. 14. Be able to solve complex problems both small and large and manage projects of all sizes in isolation and in a team. 15. Be able to make sensible and rational decisions (both technical and not). 16. Successfully own investigations of issues impacting the platform and discuss with the relevant teams to aim for a resolution. 17. Coach, inspire and share knowledge with other engineers. 18. Be great at leading by example by being a top contributor to the platform delivering top standards of quality at all stages of the software development life cycle. 19. Promote the company externally cultivating white papers, blogs posts, and public talks. 20. Be a respected authority in the community: open-source contributor, writing blogs posts, books and other material, public talks. 21. Have a proven and recognized history of success. Similar articles: - [On being a Principal Engineer](https://blog.dbsmasher.com/2019/01/28/on-being-a-principal-engineer.html?ref=albertodebortoli.com) - [What does a Principal Developer do](https://anotherchris.net/opinion/what-does-a-principal-developer-do/?ref=albertodebortoli.com) ### The easiest State Machine in Swift URL: https://albertodebortoli.com/2018/12/16/the-easiest-state-machine-in-swift/ Last updated: 2019-04-12T19:24:10.000Z Here's another article of the serie "The easiest ". Previous ones on Core Data ([The easiest Core Data](https://albertodebortoli.com/2016/08/05/the-easiest-core-data/)) and Future and Promises ([The easiest promises in Swift](https://albertodebortoli.com/2018/02/12/the-easiest-promises-in-swift/)). It was a cold Sunday afternoon when I decided to bring to Swift my [ADBStateMachine](https://github.com/albertodebortoli/ADBStateMachine?ref=albertodebortoli.com) implemented in Objective-C right about 5 years ago. Actually, it was... today, 16/12/2018. # Stateful 🦜 Here it is [Stateful](https://github.com/albertodebortoli/Stateful?ref=albertodebortoli.com), a minimalistic, thread-safe, non-boilerplate and super easy to use state machine. Stateful is available through [CocoaPods](https://cocoapods.org/?ref=albertodebortoli.com) and under the MIT license. ## What is a state machine I'm pretty much assuming the reader knows what a [state machine](https://en.wikipedia.org/wiki/Finite-state%5Fmachine?ref=albertodebortoli.com) is. It's is a mathematical model of computation, an abstract concept whereby the machine can have different states, but at a given time fulfills only one of them. ![state-machine-example](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/12/state-machine-example.png) - State machines have an initial state - Other states can be reached via transitions from previous states - Transitions are performed in reaction to events ## Example Let's see how to user Stateful with a concrete example. First, you should define the events and statuses you need. From the image above: ```swift enum EventType { case click case success case failure case retry } enum StateType { case idle case fetching case error } ``` Create a state machine with the initial state (you might want to retain it in a property) ```swift let stateMachine = StateMachine(initialState: StateType.idle) ``` `StateMachine` will use the main queue to execute the transition pre and post blocks but you can optionally provide a custom one. ```swift let dispatchQueue = DispatchQueue(label: "com.albertodebortoli.someSerialCallbackQueue") let stateMachine = StateMachine(initialState: StateType.idle, callbackQueue: dispatchQueue) ``` Create transitions and add them to the state machine (the state machine will automatically recognize the new states). ```swift let t1 = Transition(with: .click, fromState: .idle, toState: .fetching) let t2 = Transition(with: .success, from: .fetching, to: .idle) let t3 = Transition(with: .failure, fromState: .fetching, toState: .error, preBlock: { print("Going to move from \(StateType.fetching) to \(StateType.error)!") }, postBlock: { print("Just moved from \(StateType.fetching) to \(StateType.error)!") }) let t4 = Transition(with: .retry, fromState: .error, toState: .fetching) stateMachine.add(transition: t1) stateMachine.add(transition: t2) stateMachine.add(transition: t3) stateMachine.add(transition: t4) ``` Process events like so ```swift stateMachine.process(event: .start) stateMachine.process(event: .pause, callback: { result in switch result { case .success: print("Event 'pause' was processed") case .failure: print("Event 'pause' cannot currently be processed.") } }) ``` ### Logging You can optionally enable logging to print extra state change information on the console ```swift stateMachine.enableLogging = true ``` Example: ``` [Stateful 🦜]: Processing event 'start' from 'idle' [Stateful 🦜]: Processed pre condition for event 'start' from 'idle' to 'started' [Stateful 🦜]: Processed state change from 'idle' to 'started' [Stateful 🦜]: Processed post condition for event 'start' from 'idle' to 'started' [Stateful 🦜]: Processing event 'stop' from 'started' [Stateful 🦜]: Processed pre condition for event 'stop' from 'started' to 'idle' [Stateful 🦜]: Processed state change from 'started' to 'idle' [Stateful 🦜]: Processed post condition for event 'stop' from 'started' to 'idle' ``` # Conclusions State machines come handy even to these days when the world seems to have moved to a more stateless way of doing things. Whenever you need to explicitly surface statuses, transition to a different state as a reaction of an event, and possibly leter recover state, a finite state machine might come handy. Good examples that come to mind are: - manage overall application state - ease deeplinking implementation - ease UI testing 🦜🦜🦜 Special thanks to [Matteo Comisso](https://github.com/mcomisso?ref=albertodebortoli.com) for contributing to Stateful adding [support for Generics](https://github.com/albertodebortoli/Stateful/pull/1?ref=albertodebortoli.com). ### The template for View Controller unit testing URL: https://albertodebortoli.com/2018/03/12/easy-view-controller-unit-testing/ Last updated: 2018-03-12T21:03:57.000Z Hot topics like this one, testing view controllers, often come back from time to time and get some updates. It probably all started with [Testing View Controllers](https://www.objc.io/issues/1-view-controllers/testing-view-controllers/?ref=albertodebortoli.com) by Daniel Eggert back in 2013\. Now quite out-dated as in Objective-C and showing examples of mocking using OCMock[\[1\]](#fn1). Also, using mocking frameworks tells me that D.I. could have been better used in those examples. > If you are using mocking frameworks, it means that you haven’t done D.I. correctly. > > — Alberto De Bortoli (@albertodebo) [February 11, 2018](https://twitter.com/albertodebo/status/962810904822902784?ref%5Fsrc=twsrc%5Etfw&ref=albertodebortoli.com) An article less focused on the code and more on how convincing people of the benefit of unit testing view controllers is [The Powerful Hidden Benefit of Testing View Controllers](https://qualitycoding.org/testing-view-controllers/?ref=albertodebortoli.com) by Jon Reid. The final paragraph is even titled *'How to convince your team lead'* (which makes me think a little as it should definitely be the other way around). Grown-ups should be long past that point, if you still have to spend time 'convincing' your team lead that testing is a non optional thing if you need to produce stable software, well... you have a much bigger problem than testing view controllers. > Remember that the only code your tests need to cover is any code you require to work. > > — Graham Lee (@iwasleeg) [June 8, 2015](https://twitter.com/iwasleeg/status/608048449314025472?ref%5Fsrc=twsrc%5Etfw&ref=albertodebortoli.com) A more recent article I recommend reading is Clean Swift's 'Testing View Controllers' part [1](https://clean-swift.com/testing-view-controller-part-1/?ref=albertodebortoli.com) and [2](https://clean-swift.com/testing-view-controller-part-2?ref=albertodebortoli.com). Also good is the iterative solution outlined by [NatashaTheRobot](https://twitter.com/NatashaTheRobot?ref=albertodebortoli.com) in [The One Weird Trick For Testing View Controllers in Swift](https://www.natashatherobot.com/ios-testing-view-controllers-swift/?ref=albertodebortoli.com). The approach described in my article is very much in-line with the content of the two articles linked above and proposes a further refinement also in light of the recently introduced Cocoapods' test specs. This is the approach that we use in the modules we build at Just Eat. ## The foundation Enough talking, let's get to the code. Here is the helper class to be used in test suites. ```swift import XCTest import UIKit class TopLevelUIUtilities { private var rootWindow: UIWindow! func setupTopLevelUI(withViewController viewController: T) { rootWindow = UIWindow(frame: UIScreen.main.bounds) rootWindow.isHidden = false rootWindow.rootViewController = viewController _ = viewController.view viewController.viewWillAppear(false) viewController.viewDidAppear(false) } func tearDownTopLevelUI() { guard let rootWindow = rootWindow as? UIWindow, let rootViewController = rootWindow.rootViewController as? T else { XCTFail("tearDownTopLevelUI() was called without setupTopLevelUI() being called first") return } rootViewController.viewWillDisappear(false) rootViewController.viewDidDisappear(false) rootWindow.rootViewController = nil rootWindow.isHidden = true self.rootWindow = nil } } ``` The above code helps you to setup the necessary environment to load a view controller: a window. It also triggers the loading of the view controller's view and takes care of calling the life-cycle methods, de facto mimicking the presentation of the view controller on screen. It's always good practice to test software in isolation, meaning that there very few reasons to set the host application in the test target, which should be set like so: ![no_host_application_target_setting](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/03/no_host_application_target_setting.png) I'm arguing that in the examples in the article by NatashaTheRobot mentioned above, there are references to the shared application `UIApplication.sharedApplication().keyWindow!.rootViewController = ...` which clearly exists only when tests run with a host application. Using `TopLevelUIUtilities`, which creates a temporary window, we safely avoid the reference to the `UIApplication` singleton. ### A note on Cocoapods' test specs If you are developing a pod, Cocoapods 1.4.0 lets you make use of test specs. You can read more about them at [here](http://blog.cocoapods.org/CocoaPods-1.3.0/?ref=albertodebortoli.com) and [here](http://blog.cocoapods.org/CocoaPods-1.4.0/?ref=albertodebortoli.com)). Long story short, here is what you can now put in the podspec to generate a target in the Pods project for the tests, meaning that you don't have to manually add tests to the main project anymore. ```ruby s.name = 'MyLibrary' ... s.test_spec 'Tests' do |test_spec| test_spec.source_files = 'MyLibrary-Tests/**/*.swift' end ``` You should then add the following to the demo project's Podfile: ```swift target 'MyLibrary_Example' do platform :ios, '10.0' pod 'MyLibrary', :path => '../', :testspecs => ['Tests'] end ``` If you want to be a bad boy and depend on the hosting app, you can still set `test_spec.requires_app_host = true` but as we've seen, with the stack proposed in this article you shouldn't need access to root level objects such as the `AppDelegate`. Not relying on a host app allows to run unit tests without having to install the app on the simulator, which is also great for improving execution time. ## Concrete usage Here is a realistic use case example for one of your view controller test suites. In this case, the view controller instance is created from a storyboard (as you know, variations may apply, depending on the structure of the storyboard). ```swift private var rootViewController: MyViewController! private var topLevelUIUtilities: TopLevelUIUtilities! override func setUp() { super.setUp() let storyboard = UIStoryboard(name: "MyViewController", bundle: MyBundle) let myViewController = storyboard.instantiateInitialViewController() as! MyViewController myViewController.someProperty = stubProperty rootViewController = myViewController topLevelUIUtilities = TopLevelUIUtilities() topLevelUIUtilities.setupTopLevelUI(withViewController: rootViewController) } override func tearDown() { rootViewController = nil topLevelUIUtilities.tearDownTopLevelUI() topLevelUIUtilities = nil super.tearDown() } ``` That's all you need for the setup of the test suite! Obviously, you need to have your code structured appropriately with dependencies injected because... that's how good software is made 👀. I am very much assuming the understanding and application of the dependency injection concept from the reader, allowing the surfacing of light view controllers[\[2\]](#fn2). Just for the sake of completeness, here is a very basic but decent example that you could use as a template to cover most of the logic left in the view controller. It includes a UI component (`UITableView`) displaying data fetched from a service. > After all, what is a view controller if not the glue code between business logic and UI? Close your eyes and force yourself to abstract even further than the architectural design pattern you like. Think higher. MVC, MVP, MVVM, VIPER they are all the same thing. Shots fired. ```swift func test_loadResults_success() { let expectation = XCTestExpectation(description: #function) let numberOfResults = 3 rootViewController.service = StubService(fetch_numberOfResultsForCompletion: numberOfResults) XCTAssertEqual(rootViewController.tableView.numberOfRows(inSection: 0), 0) rootViewController.triggerFetch() // the stubService should fake the async behaviour, making a dispatch async/asyncAfter needed DispatchQueue.main.async { XCTAssertEqual(rootViewController.tableView.numberOfRows(inSection: 0), numberOfResults) expectation.fulfill() } wait(for: [expectation], timeout: someSensibleTimout) } ``` Many other examples can be found in [Unit-Testing a ViewController](https://priteshrnandgaonkar.github.io/Unit-Testing-a-feature/?ref=albertodebortoli.com) by Pritesh Nandgaonkar should you need further help in writing good unit tests. Happy testing! *Special thanks to Alan Nichols for reviewing* 😊 --- 1. I believe the humanity came to realize over the past decade that [OCMock](https://github.com/erikdoe/ocmock?ref=albertodebortoli.com) should be avoided. Pure Swift doesn't really allow reflection out-of-the-box, which is a good thing and it deserves a whole article to discuss why. [↩︎](#fnref1) 2. [Lighter View Controllers](https://www.objc.io/issues/1-view-controllers/lighter-view-controllers/?ref=albertodebortoli.com) by Chris Eidhof from 2013 is still a valid article after all these years. Good principles never go out of fashion. I always thought that once software engineers understand the fundamentals (such as SOLID principals and generally good design), there is no way for them to create a Massive View Controller, it would simply be against nature for good developers. [↩︎](#fnref2) ### The easiest Promises in Swift URL: https://albertodebortoli.com/2018/02/12/the-easiest-promises-in-swift/ Last updated: 2019-04-12T19:24:04.000Z Here's another article of the serie "The easiest ". Previous one on Core Data here -> [The easiest Core Data](https://albertodebortoli.com/2016/08/05/the-easiest-core-data/). Swift 5 will most likely include async/await, which will be a revolution for handling concurrency at language level. See Chris Lattner's proposal [here](https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782?ref=albertodebortoli.com). In the meantime future/promises are a great abstraction that allow developers to better deal with concurrency and its derived problems. So, here it is, let me introduce the easiest Future and Promises framework in Swift. No magic. No boilerplate. It's called Promis and it's available on [GitHub](https://github.com/albertodebortoli/Promis?ref=albertodebortoli.com). ## Overview Promis takes inspiration from the Objective-C version of [JustPromises](https://github.com/justeat/JustPromises?ref=albertodebortoli.com) developed by the iOS Team of [Just Eat](https://www.just-eat.com/?ref=albertodebortoli.com) which is really concise and minimalistic, while other libraries are more weighty. I implemented this library with the main goal of replacing some legacy Objective-C code we had at Just Eat. For this reason, the new code had to be robust, nicely written and production ready. Surely I started from the existing implementation of JustPromises, kept the code minimalistic and added the following: - conversion to Swift 4 - usage of generics to allow great type inference that wasn't possible in Objective-C - overall refactoring for fresh and modern code - remove the unnecessary and misleading concept of Progress causing bad patterns to emerge You can read about the theory behind Future and Promises on [Wikipedia](https://en.wikipedia.org/wiki/Futures%5Fand%5Fpromises?ref=albertodebortoli.com), here are the main things you should know to get started. - Promises represent the promise that a task will be fulfilled in the future while the future holds the state of such resolution. - Futures, when created are in the unresolved state and can be resolved with one of 3 states: with a result, an error, or being cancelled. - Futures can be chained, allowing to avoid the [pyramid of doom](https://twitter.com/piscis168/status/641237956070666240?ref=albertodebortoli.com) problem, clean up asynchronous code paths and simplify error handling. Promis brags about being/having: - Fully unit-tested and documented 💯 - Thread-safe 🚦 - Clean interface 👼 - Support for chaining ⛓ - Support for cancellation 🙅‍♂️ - Queue-based block execution if needed 🚆 - Result type provided via generics 🚀 - Keeping the magic to the minimum, leaving the code in a readable state without going off of a tangent with fancy and unnecessary design decisions ಠ\_ಠ ## Alternatives Other open-source solutions exist such as: - [FutureKit](https://github.com/FutureKit/FutureKit?ref=albertodebortoli.com) - [PromiseKit](https://github.com/mxcl/PromiseKit?ref=albertodebortoli.com) - [JustPromises](https://github.com/justeat/JustPromises?ref=albertodebortoli.com) - [Promises](https://github.com/google/promises?ref=albertodebortoli.com) ## Usage The following example should outline the main benefits of using futures via chaining. ```swift let request = URLRequest(url: URL(string: "http://example.com")!) // starts by hitting an API to download data getData(request: request).thenWithResult { data in // continue by parsing the retrieved data parse(data: data) }.thenWithResult { parsedData in // continue by mapping the parsed data map(data: parsedData) }.onError { error in // executed only in case an error occurred in the chain print("error: " + String(describing: error)) }.finally(queue: .main) { future in // always executed, no matter the state of the previous future or how the chain did perform switch future.state { case .result(let value): print(String(describing: value)) case .error(let err): print(String(describing: err)) case .cancelled: print("future is in a cancelled state") case .unresolved: print("this really cannot be if any chaining block is executed") } } ``` The functions used in the example have the following signatures: ```swift func getData(request: URLRequest) -> Future func parse(data: Data) -> Future<[Dictionary]> func map(data: [Dictionary]) -> Future<[FooBar]> ``` Promises and Futures are parametrized leveraging the power of the generics, meaning that Swift can infer the type of the result compile type. This was a considerable limitation in the Objective-C world and we can now prevent lots of issues at build time thanks to the static typing nature of the language. The state of the future is an enum defined as follows: ```swift enum FutureState { case unresolved case result(ResultType) case error(Error) case cancelled } ``` Promises are created and resolved like so: ```swift let promise = Promise() promise.setResult(value) // or promise.setError(error) // or promise.cancel() ``` Continuation methods used for chaining are the following: ```swift func then(queue: DispatchQueue? = nil, task: @escaping (Future) -> Future) -> Future func thenWithResult(queue: DispatchQueue? = nil, continuation: @escaping (ResultType) -> Future) -> Future { func onError(queue: DispatchQueue? = nil, continuation: @escaping (Error) -> Void) -> Future { func finally(queue: DispatchQueue? = nil, block: @escaping (Future) -> Void) ``` All the functions can accept an optional `DispatchQueue` used to perform the continuation blocks. ### Best practices Functions wrapping async tasks should follow the below pattern: ```swift func wrappedAsyncTask() -> Future { let promise = Promise() someAsyncOperation() { data, error in // resolve the promise according to how the async operations did go switch (data, error) { case (let data?, _): promise.setResult(data) case (nil, let error?): promise.setError(error) // etc. } } return promise.future } ``` You could chain an `onError` continuation before returning the future to allow in-line error handling, which I find to be a very handy pattern. ```swift // ... return promise.future.onError {error in // handle/log error } ``` ### Pitfalls When using `then` or `thenWithResult`, the following should be taken in consideration. ```swift ...}.thenWithResult { data -> Future in /** If a block is not trivial, Swift cannot infer the type of the closure and gives the error 'Unable to infer complex closure return type; add explicit type to disambiguate' so you'll have to add `-> Future to the block signature You can make the closure complex just by adding any extra statement (like a print). All the more reason to structure your code as done in the first given example :) */ print("complex closure") return parse(data: data) } ``` Please check the [GettingStarted playground](https://github.com/albertodebortoli/Promis/tree/master/Example/GettingStarted.playground?ref=albertodebortoli.com) in the demo app to see the complete implementation of the above examples. Installations via Cocoapods and Carthage are available. Happy chaining! 🎉 ### The recipe for Singletons removal URL: https://albertodebortoli.com/2017/03/15/the-recipe-for-singletons-removal/ Last updated: 2018-02-10T16:19:06.000Z ![singleton](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/singleton.png) We all went through it, am I right? You join a new company, you jump on the new codebase, you find lots of singletons, get used to them, become friend with them, but after some time you realize it's time to terminate the friendship. For a greater good. And for better testing, of course. And for proper dependency injection, of course. And for your own sanity, of course. After years in this field you should well know that singletons are bad, if you don't... well... argh, I have bad news for you 🙃, [but](http://stackoverflow.com/questions/137975/what-is-so-bad-about-singletons?ref=albertodebortoli.com) [also](https://blogs.msdn.microsoft.com/scottdensmore/2004/05/25/why-singletons-are-evil/?ref=albertodebortoli.com) [a](http://wiki.c2.com/?SingletonsAreEvil&ref=albertodebortoli.com) [few](http://rcardin.github.io/design/programming/2015/07/03/the-good-the-bad-and-the-singleton.html?ref=albertodebortoli.com) [articles](http://softwareengineering.stackexchange.com/questions/40373/so-singletons-are-bad-then-what?ref=albertodebortoli.com), [yes](http://www.kyleclegg.com/blog/9272013why-singletons-are-bad?ref=albertodebortoli.com). So here is a simple recipe to follow in order to have your app/code free from singletons. To some of you, this could all seem very obvious, and you might also know that I don't write about obvious stuff, but tonight I'm tipsy and I had confirmation that most people are still unsure on how to get around such situations. ## The recipe 1. Make a list of the singletons in the app and the dependencies between them. 2. Starting from the most high-level singleton (the one using the others more), apply 3-14 for each one of them. 3. Create a property in every class that reference the singleton at least once 4. Substitute all the references to the singleton with the reference to the property 5. Create a lazy getter returning the singleton or assign the singleton to the property where more appropriate (in the `viewDidLoad` for ViewControllers or generally speaking the init methods) 6. About the usages of singletons in static methods: refactor the code and turn those methods into instance methods as it surely is crap (this will allow dependency injection) 7. About the usages of singletons in categories: - if you own the class, publicly expose the property in the class - if we class comes from a framework (e.g. `UIViewController`) refactor the code by removing the category (it was probably bad code since the beginning) 8. About the usages of singletons in class methods: - you are simply doing it wrong, reconsider the design and remove the class methods 9. Build the app and make sure it still behaves as expected 10. Make sure the (unit|automation) tests are still green 11. For every class using the singleton, modify it so that: - if the class is a ViewController, we prefer going for dependency setting rather that dependency injection as the view controller might be accessed via a segue (`segue.destinationViewController`). In this case, assert that the dependency is set in the `viewDidLoad` - otherwise, modify the designated initializer having it accepting a dependency via injection as per standard dependency injection. Keep the init chain correct ([Obj-C](https://github.com/objc-zen/objc-zen-book?ref=albertodebortoli.com#designated-and-secondary-initializers) & [Swift](https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift%5FProgramming%5FLanguage/Initialization.html?ref=albertodebortoli.com)) 12. Modify the caller chain back up until the root object (most likely the AppDelegate) instantiates an instance of the original singleton and pass it down the chain 13. Build the app and make sure it still behaves as expected 14. Make sure the unit tests are still green ## Show me the way Really? Do I have to? Ok, let's do something quick. Code is in Objective-C because singletons can be more easily found in legacy codebases... it is well-known that nobody writes singletons in Swift these days! Shots fired. 🎭 So here we have 2 singletons, one referencing the other. ↭ ```objective-c @implementation MySingleton static MySingleton *sharedInstance = nil; #pragma mark - Singleton + (instancetype)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } - (void)performCrazyWork { [[MyOtherSingleton sharedInstance] evenCrazierWork]; } @end ``` ```objective-c @implementation MyOtherSingleton static MyOtherSingleton *sharedInstance = nil; + (instancetype)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; } - (void)evenCrazierWork { // I said, even crazier } @end ``` since `MySingleton` is the most-high level one, we start by addressing it. Here is a service class using it: ```objective-c @interface SomeService () @end @implementation SomeService - (void)method1 { [[MySingleton sharedInstance] performCrazyWork]; // ... } - (void)method2 { // ... [[MySingleton sharedInstance] performCrazyWork]; } @end ``` let's put it in a property ```objective-c @interface SomeService () @property (nonatomic, strong) MySingleton *someDude; @end @implementation SomeService - (instancetype)init { self = [super init]; if (self) { _someDude = [MySingleton sharedInstance]; } return self; } - (void)method1 { [self.someDude performCrazyWork]; // ... } - (void)method2 { // ... [self.someDude performCrazyWork]; } @end ``` and then, one step further, use depedency injection ```objective-c - (instancetype)initWithSomeDude:(MySingleton *)dude { self = [super init]; if (self) { _someDude = dude; } return self; } ``` iterate and modify the callers up the chain. Move to the next singleton in the list, in the example `MyOtherSingleton`: ```objective-c @interface MySingleton () @property (nonatomic, strong) MyOtherSingleton *otherDude; @end @implementation MySingleton - (instancetype)init { self = [super init]; if (self) { _otherDude = [MyOtherSingleton sharedInstance]; } return self; } - (void)performCrazyWork { [self.otherDude evenCrazierWork]; } @end ``` You should have now taken the gist of it. Extra points: in the end, put the dependencies behind protocols so that the unit testing would be easier. Or whatever. Kill those shared instances! 👾👾👾🚀 ### How to abstract your persistence layer on iOS with JustPersist URL: https://albertodebortoli.com/2017/03/02/how-to-abstract-your-persistence-layer-and-migrate-to-another-one-on-ios-with-justpersist/ Last updated: 2019-12-24T23:23:35.000Z **The original post is published on the Just Eat tech blog at this [URL](http://tech.just-eat.com/2017/03/02/how-to-abstract-your-persistence-layer-and-migrate-to-another-one-on-ios-with-justpersist/?ref=albertodebortoli.com).** ![just_persist_banner](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/just_persist_banner.png) In this blog post we introduce a solution to deal with data persistence. We developed it for the Just Eat iOS app and we call it JustPersist. It’s available open source on Github at . JustPersist aims to be the easiest and safest way to do persistence on iOS with Core Data support out of the box. It also allows you to migrate to any new persistence framework with minimal effort. I highly suggest to read my previous article [The Easiest Core Data](https://albertodebortoli.com/blog/2016/08/05/the-easiest-core-data/) as the underlying concepts are explained there. The main author behind JustPersist and its design is [Keith Moon](http://twitter.com/keefmoon?ref=albertodebortoli.com). Major kudos to Keith for the excellent execution in Swift! # Overview At Just Eat, we persist a variety of data in the iOS app. In 2014 we decided to use [MagicalRecord](https://github.com/magicalpanda/MagicalRecord?ref=albertodebortoli.com) as a wrapper on top of Core Data but over time the numerous [problems](https://github.com/magicalpanda/MagicalRecord/issues?ref=albertodebortoli.com) and fundamental thread-safety issues, arose. In 2017, MagicalRecord is not supported anymore and new solutions look more appealing. We decided to adopt [Skopelos](http://github.com/albertodebortoli/Skopelos?ref=albertodebortoli.com): a much younger and lightweight Core Data stack, with a simpler design, developed by [Alberto De Bortoli](http://twitter.com/albertodebo?ref=albertodebortoli.com), one of our engineers. The design of the persistence layer interface gets inspiration from Skopelos as well, and we invite the reader to take a look at [its documentation](https://github.com/albertodebortoli/Skopelos/blob/master/README.md?ref=albertodebortoli.com). The main problem in adopting a new persistence solution is migrating to it. It is rarely easy, especially if the legacy codebase doesn't hide the adopted framework (in our case MagicalRecord) but rather spread it around in view controllers, managers, helper classes, categories and sometimes views. Ultimately, in the case of Core Data, there is a single persistent store and this is enough to make impossible to move access across "one at a time". There can only be one active persistence solution at a time. We believe this is a very common problem, especially in the mobile world. We created JustPersist for this precise reason and to ease the migration process. At the end of the day, JustPersist is two things: - A persistence layer with a clear and simple interface to do transactional readings and writings (Skopelos-style) - A solution to migrate from one persistence layer to another with (we believe) the minimum possible effort JustPersist aims to be the easiest and safest way for persistence on iOS. It supports Core Data out of the box and can be extended to transparently support other frameworks. Since moving from MagicalRecord to Skopelos, we provide available wrappers for these two frameworks. The tone of JustPersist is very much Core Data-oriented but it enables you to migrate to any other persistence framework if a custom data store (wrapper) is implemented (in-memory, key-value store, even [Realm](https://realm.io/?ref=albertodebortoli.com) if you are brave enough). JustPersist is available through [CocoaPods](http://cocoapods.org/?ref=albertodebortoli.com). To install it, simply add the following line to your Podfile: ```ruby pod "JustPersist/Skopelos" # or pod "JustPersist/MagicalRecord" ``` Using only `pod JustPersist` will add the core pod with no subspecs and you'll have to implement your own wrapper to use the it. If you intend to extend JustPersist to support other frameworks, we suggest creating a subspec. # Usage of the persistence layer To perform operation you need a data store, which you can setup like this (or see [related paragraph](#common-way-of-setting-up-a-data-store) paragraph): ```swift let dataStore = SkopelosDataStore(sqliteStack: ) // or let dataStore = MagicalRecordDataStore() ``` Before using the data store for the first time, you must call `setup()` on it, and possibly `tearDown()` when you are completely done with it. We suggest setting up the stack at app startup time, in the `applicationDidFinishLaunchingWithOptions` method in the AppDelegate and to tear it down at the end of the life cycle of your entire app, when resetting the state of the app (if you provide support to do so) or in the `tearDown` method of your unit tests suite. To hide the underlying persistence framework used, JustPersist provides things that conform to `DataStoreItem` and `MutableDataStoreItem`, rather than the CoreData specific `NSManagedObject`. These protocols provide access to properties using `objectForKey` and `setObject:forKey:` methods. In the case of Core Data, JustPersist provides an extension to `NSManagedObject` to make it conforming to `MutableDataStoreItem`. ## Readings and writings The separation between readings and writings is the foundation of JustPersist. Reading are always synchronous by design: ```swift dataStore.read { (accessor) in ... } ``` While writings can be both synchronous or asynchronous: ```swift dataStore.writeSync { (accessor) in ... } dataStore.writeAsync { (accessor) in ... } ``` The accessor provided by the blocks can be a read one (`DataStoreReadAccessor`) or a read/write one (`DataStoreReadWriteAccessor`). Read accessors allow you to do read operations such as: ```swift func items(forRequest request: DataStoreRequest) -> [DataStoreItem] func firstItem(forRequest request: DataStoreRequest) -> DataStoreItem? func countItems(forRequest request: DataStoreRequest) -> Int ``` While the read/write ones allow you to perform a complete set of CRUD operations: ```swift func mutableItems(forRequest request: DataStoreRequest) -> [MutableDataStoreItem] func firstMutableItem(forRequest request: DataStoreRequest) -> MutableDataStoreItem? func createItem(ofMutableType itemType: MutableDataStoreItem.Type) -> MutableDataStoreItem? func insert(_ item: MutableDataStoreItem) -> Bool func delete(item: MutableDataStoreItem) -> Bool func deleteAllItems(ofMutableType itemType: MutableDataStoreItem.Type) -> Bool func mutableVersion(ofItem item: DataStoreItem) -> MutableDataStoreItem? ``` To perform an operation you might need a `DataStoreRequest` which can be customized with itemType, an NSPredicate, an array of NSSortDescriptor, offset and limit. Think of it as the corresponding Core Data's `NSFetchRequest`. Here are some complete examples: ```swift dataStore.read { (accessor) in let request = DataStoreRequest(itemType: Restaurant.self) let count = accessor.countItems(forRequest: request) } dataStore.read { (accessor) in let request = DataStoreRequest(itemType: Restaurant.self) request.setFilter(whereAttribute: "name", equalsValue: ) guard let restaurant = accessor.firstItem(forRequest: request) as? Restaurant else { return } ... } dataStore.writeSync { (accessor) in let restaurant = accessor.createItem(ofMutableType: Restaurant.self) as! Restaurant restaurant.name = ... let wasDeleted = accessor.delete(item: restaurant) } ``` In write blocks there is no need to make any call to a save method. Since it would be the obvious thing to do at the end of a transactional block, JustPersist does it for you. Read blocks are not meant to modify the store and you wouldn't even have the API available to do so (unless `DataStoreItem` objects are casted to `NSManagedObject` in the case of CoreData to allow the setting of properties), therefore a save will not be performed under the hood. ## Common way of setting up a data store We recommend to use dependency injection to pass the data store around but sometimes it might be hard. If you wish to access your data store via a singleton, here is how your app could create a shared instance for the DataStoreClient (e.g. `DataStoreClient.swift`) using Skopelos. ```swift @objc class DataStoreClient: NSObject { static let shared: DataStore = { return DataStoreClient.sqliteStack() }() static let inMemoryShared: DataStore = { return DataStoreClient.inMemoryStack() }() class func sqliteStack() -> DataStore { let modelURL = Bundle.main.url(forResource: "", withExtension: "momd")! // want to crash if schema is missing return SkopelosDataStore(sqliteStack: modelURL, securityApplicationGroupIdentifier: ) { error in print("Core Data error reported via SkopelosDataStore (sqliteStack): \(error.localizedDescription)") } } class func inMemoryStack() -> DataStore { let modelURL = Bundle.main.url(forResource: "", withExtension: "momd")! // want to crash if schema is missing return SkopelosDataStore(inMemoryStack: modelURL) { error in print("Core Data error reported via SkopelosDataStore (inMemoryStack): \(error.localizedDescription)"") } } ``` For unit tests, you might want to use the `inMemoryShared` for better performance. ## Child data store A child data store is useful in situations where you might have the need to rollback all the changes performed in a specific section of the app or in a part of the user journey. Think of it as a scratch/disposable context in the [Core Data stack](http://martiancraft.com/blog/2015/03/core-data-stack/?ref=albertodebortoli.com) by Marcus Zarra. At Just Eat we use a child data store for the addition of complex products to the basket. The user might make many updates to the product and it is easier to perform the final save operation when the user confirms the addition rather than dealing with multiple CRUD operations on the main data store. A child data store behaves just like a normal data store, with the only exception that, to save the changes back to the main data store, developers must explicitly merge the data stores. Here is a complete example: ```swift let childDataStore = dataStore.makeChildDataStore() childDataStore.setup() ... dataStore.merge(childDataStore) childDataStore.tearDown() ``` ## Thread-safety notes Read and sync write blocks are always performed on the main thread, no matter which thread calls them. Async write blocks are always performed on a background thread. Sync writings return only when the changes are persisted (in the case of Core Data, usually to the `NSManagedObjectContext` with main concurrency type). Async writings return immediately and leave the job of saving to the source of truth to JustPersist (whether it be the context or a persistent store). They are eventual consistent, meaning that the next reading could potentially not have the data available. Forcing a transactional programming model for readings and writings helps developers to avoid thread-safety issues which in Core Data can be caught setting the `-com.apple.CoreData.ConcurrencyDebug 1` flag in your scheme (which we recommend enabling). # How to migrate to a different persistence layer Examples in this sections are in Objective-C as 1\. they deal with the legacy code for the nature of the example and 2\. to show that JustPersist works just fine with Objective-C too. Here we'll outline the steps we made to migrate away from MagicalRecord to Skopelos using JustPersist. We believe that a lot of apps still use MagicalRecord, so this may apply to your case too. If your need is to move from and to other 2 frameworks, you need to implement the corresponding data stores to wrap them. You should start by implementing your `DataStoreClient` (you could follow the steps in the [related paragraph](#common-way-of-setting-up-a-data-store)) and allocating the data store for the current persistence layer used by your app in the `sqliteStack` method and possibly in the `inMemoryStack` one too. In our case, since we want to move away from MagicalRecord, the data store used would be `MagicalRecordDataStore`. Standard CRUD interactions with MagicalRecord are like so: ```objective-c NSManagedObjectContext *mainContext = [NSManagedObjectContext MR_defaultContext]; NSManagedObjectContext *childContext = [NSManagedObjectContext MR_contextWithParent:mainContext]; // writing (Create) [childContext performBlockAndWait:^{ Restaurant *restaurant = [Restaurant MR_createEntityInContext:localContext]; [childContext MR_saveToPersistentStoreAndWait]; }]; // reading (Read) [childContext performBlockAndWait:^{ Restaurant *restaurant = [Restaurant MR_findFirstInContext:childContext]; }]; // writing (Update) [childContext performBlockAndWait:^{ Restaurant *restaurant = [Restaurant MR_findFirstInContext:childContext]; restaurant.name = [childContext MR_saveToPersistentStoreAndWait]; }]; // writing (Delete) [childContext performBlockAndWait:^{ [Restaurant MR_truncateAllInContext:localContext]; [childContext MR_saveToPersistentStoreAndWait]; }]; ``` All of them should be converted one by one to JustPersist: ```objective-c DataStore *dataStore = [DataStoreClient shared]; // writing (Create) [dataStore writeSync:^(id accessor) { Restaurant *restaurant = (Restaurant *)[accessor createItemOfMutableType:Restaurant.class]; }]; // reading (Read) [dataStore read:^(id accessor) { JEDataStoreRequest *request = [[JEDataStoreRequest alloc] initWithItemType:Restaurant.class]; Restaurant *restaurant = (Restaurant *)[accessor firstItemForRequest:request]; }]; // writing (Update) [dataStore writeSync:^(id accessor) { JEDataStoreRequest *request = [[JEDataStoreRequest alloc] initWithItemType:Restaurant.class]; Restaurant *restaurant = (Restaurant *)[accessor firstItemForRequest:request]; restaurant.name = }]; // writing (Delete) [dataStore writeSync:^(id accessor) { [accessor deleteAllItemsOfMutableType:Restaurant.class]; }]; ``` You should make sure you don't perform any UI work within the blocks even if the `read` and `writeSync` ones are executed on the main thread. Actually, you should aim for doing only the necessary work related to interact with the persistence layer, which often might be copying values out of objects to have them accessible outside the block (in Objective-C via the `__block` keyword). Developers should not hold references to model objects to pass them around threads (transactional blocks help ensure such rule). By having moved all the direct interactions from MagicalRecord to JustPersist, you should be now able to remove all the various `@import MagicalRecord` and `#import ` from the entire codebase. Once At this point, your `DataStoreClient` can be modified to allocate the target data store in the `sqliteStack` and `inMemoryStack` methods. In our case, the `SkopelosDataStore`. # Conclusion JustPersist aims to be the easiest and safest way to do persistence on iOS. It supports Core Data out of the box and can be extended to transparently support other frameworks. You can use JustPersist to migrate from one persistence layer to another with minimal effort. Since we moved from MagicalRecord to Skopelos, we provide available wrappers for these two frameworks. At its core, JustPersist is a persistence layer with a clear and simple interface to do transactional readings and writings, taking inspirations from Skopelos where readings and writings are separated by design. We hope this library will ease the process of setting up a persistence stack, avoiding the common headache of Core Data and potential threading pitfalls. ### A better local and remote logging on iOS with JustLog URL: https://albertodebortoli.com/2017/01/18/a-better-local-and-remote-logging-on-ios-with-justlog/ Last updated: 2018-02-10T15:23:02.000Z **The original post is published on the Just Eat tech blog at this [URL](http://tech.just-eat.com/2017/01/18/a-better-local-and-remote-logging-on-ios-with-justlog/?ref=albertodebortoli.com).** ![just_log_banner](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/just_log_banner.png) In this blog post we introduce the solution for local and remote logging we developed for the Just Eat iOS app. It’s named JustLog and it’s available open source on Github at [https://github.com/justeat/JustLog](https://github.com/justeat/JustLog?ref=albertodebortoli.com). # Overview At Just Eat, logging and monitoring are fundamental parts of our job as engineers. Whether you are a back-end engineer or a front-end one, you'll often find yourself in the situation where understanding how your software behaves in production is important, if not critical. The ELK stack for real-time logging has gained great adoption over recent years, mainly in the back-end world where multiple microservices often interact with each other. In the mobile world, the common approach to investigating issues is gathering logs from devices or trying to reproduce the issue by following a sequence of reported steps. Mobile developers are mostly familiar with tools such as [Google Analytics](https://analytics.google.com/analytics/web/?ref=albertodebortoli.com) or [Fabric.io](https://fabric.io/?ref=albertodebortoli.com) but they are *tracking* systems, not fully fledged logging solutions. We believe tracking is different in nature from logging and that mobile apps should take advantage of ELK too in order to take their monitoring and analysis to another level. Remote logging the right set of information could provide valuable information that would be difficult to gather otherwise, unveil unexpected behaviours and bugs, and even if the data was properly anonymized, identify the sequences of actions of singular users. JustLog takes logging on iOS to the next level. It supports console, file and remote Logstash logging via TCP socket out of the box. You can also setup JustLog to use [logz.io](http://logz.io/?ref=albertodebortoli.com) with no effort. JustLog relies on [CocoaAsyncSocket](https://github.com/robbiehanson/CocoaAsyncSocket?ref=albertodebortoli.com) and [SwiftyBeaver](https://github.com/SwiftyBeaver/SwiftyBeaver?ref=albertodebortoli.com), exposes a simple swifty API but it also plays just fine with Objective-C. JustLog sets the focus on remote logging, but fully covers the basic needs of local console and file logging. # Usage JustLog, is available through [CocoaPods](http://cocoapods.org/?ref=albertodebortoli.com). To install it, simply add the following line to your Podfile: ```ruby pod "JustLog" ``` Import it into your files like so: ```swift // swift import JustLog // objective-c @import JustLog; ``` This logging system strongly relies on [SwiftyBeaver](https://github.com/SwiftyBeaver/SwiftyBeaver?ref=albertodebortoli.com). We decided to adopt SwiftyBeaver due to the following reasons: - good and extensible design - ability to upload logs to the cloud - macOS app to analyze logs A log can be of one of 5 different types, to be used according to the specific need. A reasonable adopted convention on mobile could be the following: - 📣 **verbose**: Use to trace the code, trying to find one part of a function specifically, sort of debugging with extensive information. - 📝 **debug**: Information that is helpful to developers to diagnose an issue. - ℹ️ **info**: Generally useful information to log (service start/stop, configuration assumptions, etc). Info to always have available but usually don't care about under normal circumstances. Out-of-the-box config level. - ⚠️ **warning**: Anything that can potentially cause application oddities but an automatic recovery is possible (such as retrying an operation, missing data, etc.) - ☠️ **error**: Any error which is fatal to the operation, but not the service or application (can't open a required file, missing data, etc.). These errors will force user intervention. These are usually reserved for failed API calls, missing services, etc. When using JustLog, the only object to interact with is the shared instance of the `Logger` class, which supports 3 destinations: - sync writing to Console (custom destination) - sync writing to File (custom destination) - async sending logs to [Logstash](https://www.elastic.co/products/logstash?ref=albertodebortoli.com) (usually part of an [ELK](https://www.elastic.co/webinars/introduction-elk-stack?ref=albertodebortoli.com) stack) Following is a code sample to configure and setup the Logger. It should be done at app startup time, in the `applicationDidFinishLaunchingWithOptions` method in the AppDelegate. ```swift let logger = Logger.shared // file destination logger.logFilename = "justeat-demo.log" // logstash destination logger.logstashHost = "my.logstash.endpoint.com" logger.logstashPort = 3515 logger.logstashTimeout = 5 logger.logLogstashSocketActivity = true // default info logger.defaultUserInfo = ["app": "my iOS App", "environment": "production", "tenant": "UK", "sessionID": someSessionID] logger.setup() ``` The `defaultUserInfo` dictionary contains a set of basic information to add to every log. The Logger class exposes 5 functions for the different types of logs. The only required parameter is the message, optional error and userInfo can be provided. Here are some examples of sending logs to JustLog: ```swift Logger.shared.verbose("not so important") Logger.shared.debug("something to debug") Logger.shared.info("a nice information", userInfo: ["some key": "some extra info"]) Logger.shared.warning("oh no, that won’t be good", userInfo: ["some key": "some extra info"]) Logger.shared.error("ouch, an error did occur!", error: someError, userInfo: ["some key": "some extra info"]) ``` It plays nicely with Objective-C too: ```objective-c [Logger.shared debug_objc:@"some message"]; [Logger.shared info_objc:@"some message" userInfo:someUserInfo]; [Logger.shared error_objc:@"some message" error:someError]; [Logger.shared error_objc:@"some message" error:someError userInfo:someUserInfo]; ``` The message is the only required argument for each log type, while userInfo and error are optional. The Logger unifies the information from `message`, `error`, `error.userInfo`, `userInfo`, `defaultUserInfo` and call-site info/metadata in a single dictionary with the following schema form of type \[String : Any\] (we call this 'aggregated form'). E.g. in JSON representation: ```json { "message": ..., "userInfo": { "NSLocalizedDescription": ..., "error_domain": ..., "some key": ..., ... }, "metadata": { "file": ..., "function": ..., "line": ..., ... } } ``` All destinations (console, file, logstash) are enabled by default but they can be disabled at configuration time like so: ```swift logger.enableConsoleLogging = false logger.enableFileLogging = false logger.enableLogstashLogging = false ``` The above 5 logs are treated and showed differently on the each destination: ## Console The console prints only the message. ![console](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/console.png) ## File On file we store all the log info in the 'aggregated form'. ```json 2016-12-24 12:31:02.734 📣 VERBOSE: {"metadata":{"file":"ViewController.swift","app_version":"1.0 (1)","version":"10.1","function":"verbose()","device":"x86_64","line":"15"},"userInfo":{"environment":"production","app":"my iOS App","log_type":"verbose","tenant":"UK"},"message":"not so important"} 2016-12-24 12:31:36.777 📝 DEBUG: {"metadata":{"file":"ViewController.swift","app_version":"1.0 (1)","version":"10.1","function":"debug()","device":"x86_64","line":"19"},"userInfo":{"environment":"production","app":"my iOS App","log_type":"debug","tenant":"UK"},"message":"something to debug"} 2016-12-24 12:31:37.368 ℹ️ INFO: {"metadata":{"file":"ViewController.swift","app_version":"1.0 (1)","version":"10.1","function":"info()","device":"x86_64","line":"23"},"userInfo":{"environment":"production","app":"my iOS App","log_type":"info","tenant":"UK","some key":"some extra info"},"message":"a nice information"} 2016-12-24 12:31:37.884 ⚠️ WARNING: {"metadata":{"file":"ViewController.swift","app_version":"1.0 (1)","version":"10.1","function":"warning()","device":"x86_64","line":"27"},"userInfo":{"environment":"production","app":"my iOS App","log_type":"warning","tenant":"UK","some key":"some extra info"},"message":"oh no, that won’t be good"} 2016-12-24 12:31:38.475 ☠️ ERROR: {"metadata":{"file":"ViewController.swift","app_version":"1.0 (1)","version":"10.1","function":"error()","device":"x86_64","line":"47"},"userInfo":{"error_code":1234,"environment":"production","error_domain":"com.just-eat.test","log_type":"error","some key":"some extra info","NSLocalizedDescription":"description","NSLocalizedRecoverySuggestion":"recovery suggestion","app":"my iOS App","tenant":"UK","NSLocalizedFailureReason":"error value"},"message":"ouch, an error did occur!"} ``` ## Logstash Before sending a log to Logstash, the 'aggregated form' is flattened to a simpler \`\[String : Any\] dictionary, easily understood by Logstash and handy to be displayed on Kibana. E.g. in JSON representation: ```json { "message": "ouch, an error did occur!", "environment": "production", "log_type": "error", "version": "10.1", "app": "iOS UK app", "tenant": "UK", "app_version": "1.0 (1)", "device": "x86_64", "file": "ViewController.swift", "function": "error()", "line": "47", "error_domain": "com.just-eat.test", "error_code": "1234", "NSLocalizedDescription": "description", "NSLocalizedFailureReason": "error value", "NSLocalizedRecoverySuggestion": "recovery suggestion" } ``` Which would be shown in Kibana as follows: ![kibana](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/kibana.png) ## A note on Logstash destination The logstash destination is configured via properties exposed by the Logger. E.g.: ```swift let logger = Logger.shared logger.logstashHost = "my.logstash.endpoint.com" logger.logstashPort = 3515 logger.logstashTimeout = 5 logger.logLogstashSocketActivity = true ``` When the `logLogstashSocketActivity` is set to true, socket activity is printed to the console: ![socket_activity](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/socket_activity.png) This destination is the only asynchronous destination that comes with JustLog. This means that logs to Logstash are batched and sent at some point in future when the timer fires. The `logstashTimeout` property can be set to the number of seconds for the dispatch. In some cases, it might be important to dispatch the logs immediately after an event occurs like so: ```swift Logger.shared.forceSend() ``` or, more generally, in the `applicationDidEnterBackground` and `applicationWillTerminate` methods in the AppDelegate like so: ```swift func applicationDidEnterBackground(_ application: UIApplication) { forceSendLogs(application) } func applicationWillTerminate(_ application: UIApplication) { forceSendLogs(application) } private func forceSendLogs(_ application: UIApplication) { var identifier: UIBackgroundTaskIdentifier = 0 identifier = application.beginBackgroundTask(expirationHandler: { application.endBackgroundTask(identifier) identifier = UIBackgroundTaskInvalid }) Logger.shared.forceSend { completionHandler in application.endBackgroundTask(identifier) identifier = UIBackgroundTaskInvalid } } ``` ## Sending logs to logz.io JustLog supports sending logs to [logz.io](http://logz.io/?ref=albertodebortoli.com). At the time of writing, logz.io uses the following host and port (please refer to the official [documentation](https://app.logz.io/?ref=albertodebortoli.com#/dashboard/data-sources/TLSSSL-TCP)): ```swift logger.logstashHost = "listener.logz.io" logger.logstashPort = 5052 ``` When configuring the Logger (before calling `setup()`), simply set the token like so: ```swift logger.logzioToken = ``` # Conclusion JustLog aims to be an easy-to-use working solution with minimal setup. It covers the most basic logging needs (console and file logging) via the great foundations given by SwiftBeaver, but also provides an advanced remote logging solution for Logstash (which is usually paired with Elasticsearch and Kibana in an [ELK](https://www.elastic.co/webinars/introduction-elk-stack?ref=albertodebortoli.com) stack). JustLog integrates with [logz.io](http://logz.io/?ref=albertodebortoli.com), one of the most widely used ELK SaaS, placing itself as the only solution in the market (at the time of writing) to leverage such stack on iOS. We hope this library will ease the process of setting up the logging for your team and help you find solutions to the issues you didn't know you had. ### The easiest Core Data URL: https://albertodebortoli.com/2016/08/05/the-easiest-core-data/ Last updated: 2026-05-09T19:12:17.000Z Over the past months I spent a lot of time on Core Data, I had to deal with a project with a lot of legacy code, Core Data horros and multithreading violations. Core Data is hard, at times it can be frustrating and confusing. For this reasons, I decided to come up with a refined solution for a super simple design. The aim was to write a minimalistic, thread-safe, non-boilerplate and super easy to use version of Active Record on Core Data, that is actually all you need for doing Core Data in the 95% of the cases. The iterations were a few and I reconsidered my solution multiple times until I finally got where I wanted. So... here it is. Let me introduce [Skiathos](https://github.com/albertodebortoli/Skiathos?ref=albertodebortoli.com) and [Skopelos](https://github.com/albertodebortoli/Skopelos?ref=albertodebortoli.com). Skiathos is the Objective-C version, while Skopelos is the Swifty one. They are available as CocoaPods. The names come from 2 islands in Greece where I spent my 2016 summer holidays and found the inspiration to refine the final versions. ## General notes This component aims to have an extremely easy interface to introduce Core Data into your app with almost zero effort. The design introduced here involves a few main components: - CoreDataStack - AppStateReactor - DALService (Data Access Layer) ### CoreDataStack If you have experience with Core Data, you might know that creating a stack is an annoying process full of pitfalls. This component is responsible for the creation of the stack (in terms of chain of managed object contexts) using the design described [here](http://martiancraft.com/blog/2015/03/core-data-stack/?ref=albertodebortoli.com) by Marcus Zarra. ![coredatastack](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/coredatastack.png) An important difference from Magical Record, or other third-party libraries, is that the savings always go in one direction, from slaves down (or up?) to the persistent store. Other components allow you to create slaves that have the private context as parent and this causes the main context not to be updated or to be updated via notifications to merge the context. The main context should be the source of truth and it is tied the UI: having a much simpler approach helps to create a system easier to reason about. ### AppStateReactor You should ignore this one. It sits in the CoreDataStack and takes care of saving the in-flight changes back to disk if the app goes to background, loses focus or is about to be terminated. It's a silent friend who takes care of us. ### DALService (Data Access Layer) / (Skiathos/Skopelos) If you have experience with Core Data, you might also know that most of the operations are repetitive and that we usually call `performBlock:`/`performBlockAndWait:` on a context providing a block that eventually will call `save:` on that context as last statement. Databases are all about readings and writings and for this reason our APIs are in the form of `read:` and `write:`: 2 protocols providing a CQRS (Command and Query Responsibility Segregation) approach. Read blocks will be executed on the main context (as it's considered to be the single source of truth). Write blocks are executed on a slave context which is saved synchronously at the end; changes are eventually saved asynchronously back to the persistent store without blocking the main thread. The method `write:completion:` calls the completion handler when the changes are saved back to the persistent store. In other words, writings are always consistent in the main managed object context and eventual consistent in the persistent store. Data are always available in the main managed object context. `Skiathos`/`Skopelos` are just subclasses of `DALService`, to give a nice name to the component. ## How to use To use this component, you could create a property of type `Skiathos` and instantiate it like so: ```objective-c self.skiathos = [Skiathos setupInMemoryStackWithDataModelFileName:@"<#DataModelFileName>"]; // or self.skiathos = [Skiathos setupSqliteStackWithDataModelFileName:@"<#DataModelFileName>"]; ``` the Skopelos version is: ```swift self.skopelos = SkopelosClient(inMemoryStack: "<#DataModelFileName>") // or self.skopelos = SkopelosClient(sqliteStack: "<#DataModelFileName>") ``` You could then pass around the objects to other parts of the app via dependency injection. It has to be said that it's perfectly acceptable to use a singleton for the Core Data stack. Also, allocating instances over and over is expensive. Generally speaking, we don't like singletons. They are not testable by nature, clients don't have control over the lifecycle of the object and they break some principles. For these reasons, the library comes free of singletons. There are 2 reasons why you should inherit from `Skiathos`/`Skopelos`: - to create a shared instance for global access - to override `handleError(error: NSError)` to perform specific actions when an error is encountered and this method is called To create a singleton, you should inherit from `Skiathos`/`Skopelos` like so: ### Singleton 🧐 ```objective-c @interface SkiathosClient : Skiathos + (SkiathosClient *)sharedInstance; @end static SkiathosClient *sharedInstance = nil; @implementation SkiathosClient + (SkiathosClient *)sharedInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [self setupSqliteStackWithDataModelFileName:@"<#DataModelFileName>"]; }); return sharedInstance; } - (void)handleError:(NSError *)error { // clients should do the right thing here NSLog(@"%@", error.description); } @end ``` or ```swift class SkopelosClient: Skopelos { static let sharedInstance = Skopelos(sqliteStack: "DataModel") override func handleError(error: NSError) { // clients should do the right thing here print(error.description) } } ``` ### Readings and writings Speaking of readings and writings, let's do now a comparison between some standard Core Data code and code written with these components. Standard Core Data readings: ```objective-c __block NSArray *results = nil; NSManagedObjectContext *context = ...; [context performBlockAndWait:^{ NSFetchRequest *request = [[NSFetchRequest alloc] init]; NSEntityDescription *entityDescription = [NSEntityDescription entityForName:NSStringFromClass(User) inManagedObjectContext:context]; [request setEntity:entityDescription]; NSError *error; results = [context executeFetchRequest:request error:&error]; }]; return results; ``` Standard Core Data writings: ```objective-c NSManagedObjectContext *context = ...; [context performBlockAndWait:^{ User *user = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass(User) inManagedObjectContext:context]; user.firstname = @"John"; user.lastname = @"Doe"; NSError *error; [context save:&error]; if (!error) { // continue to save back to the store } }]; ``` Skiathos readings: ```objective-c [[SkiathosClient sharedInstance] read:^(NSManagedObjectContext *context) { NSArray *allUsers = [User allInContext:context]; NSLog(@"All users: %@", allUsers); }]; ``` Skiathos writings: ```objective-c // Sync [[SkiathosClient sharedInstance] writeSync:^(NSManagedObjectContext *context) { User *user = [User createInContext:context]; user.firstname = @"John"; user.lastname = @"Doe"; }]; [[SkiathosClient sharedInstance] writeSync:^(NSManagedObjectContext *context) { User *user = [User createInContext:context]; user.firstname = @"John"; user.lastname = @"Doe"; } completion:^(NSError *error) { // changes are saved to the persistent store }]; // Async [[SkiathosClient sharedInstance] writeAsync:^(NSManagedObjectContext *context) { User *user = [User createInContext:context]; user.firstname = @"John"; user.lastname = @"Doe"; }]; [[SkiathosClient sharedInstance] writeAsync:^(NSManagedObjectContext *context) { User *user = [User createInContext:context]; user.firstname = @"John"; user.lastname = @"Doe"; } completion:^(NSError *error) { // changes are saved to the persistent store }]; ``` Skiathos also supports dot notation and chaining: ```objective-c __block User *user = nil; [SkiathosClient sharedInstance].write(^(NSManagedObjectContext *context) { user = [User createInContext:context]; user.firstname = @"John"; user.lastname = @"Doe"; }).write(^(NSManagedObjectContext *context) { User *userInContext = [user inContext:context]; [userInContext deleteInContext:context]; }).read(^(NSManagedObjectContext *context) { NSArray *users = [User allInContext:context]; }); ``` or in Swift, Skopelos readings: ```swift SkopelosClient.sharedInstance.read { context in let users = User.SK_all(context) print(users) } ``` Skopelos writings: ```swift // Sync SkopelosClient.sharedInstance.writeSync { context in let user = User.SK_create(context) user.firstname = "John" user.lastname = "Doe" } SkopelosClient.sharedInstance.writeSync({ context in let user = User.SK_create(context) user.firstname = "John" user.lastname = "Doe" }, completion: { (error: NSError?) in // changes are saved to the persistent store }) // Async SkopelosClient.sharedInstance.writeAsync { context in let user = User.SK_create(context) user.firstname = "John" user.lastname = "Doe" } SkopelosClient.sharedInstance.writeAsync({ context in let user = User.SK_create(context) user.firstname = "John" user.lastname = "Doe" }, completion: { (error: NSError?) in // changes are saved to the persistent store }) ``` Skopelos supports chaining too: ```swift SkopelosClient.sharedInstance.write { context in user = User.SK_create(context) user.firstname = "John" user.lastname = "Doe" }.write { context in if let userInContext = user.SK_inContext(context) { userInContext.SK_remove(context) } }.read { context in let users = User.SK_all(context) print(users) } ``` The `NSManagedObject` category provides CRUD methods always explicit on the context. The context passed as parameter should be the one received in the read or write block. You should always use these methods from within read/write blocks. Main methods are: ```objective-c + (instancetype)SK_createInContext:(NSManagedObjectContext *)context; + (NSUInteger)SK_numberOfEntitiesInContext:(NSManagedObjectContext *)context; - (void)SK_deleteInContext:(NSManagedObjectContext *)context; + (void)SK_deleteAllInContext:(NSManagedObjectContext *)context; + (NSArray *)SK_allInContext:(NSManagedObjectContext *)context; + (NSArray *)SK_allWithPredicate:(NSPredicate *)pred inContext:(NSManagedObjectContext *)context; + (instancetype)SK_firstInContext:(NSManagedObjectContext *)context; ``` ```swift static func SK_create(context: NSManagedObjectContext) -> Self static func SK_numberOfEntities(context: NSManagedObjectContext) -> Int func SK_remove(context: NSManagedObjectContext) -> Void static func SK_removeAll(context: NSManagedObjectContext) -> Void static func SK_all(context: NSManagedObjectContext) -> [Self] static func SK_all(predicate: NSPredicate, context:NSManagedObjectContext) -> [Self] static func SK_first(context: NSManagedObjectContext) -> Self? ``` Mind the usage of `SK_inContext(context: NSManagerObjectContext)` to retrieve an object in different read/write blocks (same read blocks are safe). ## Thread-safety notes All the accesses to the persistence layer done via a DALService instance are guaranteed to be thread-safe. It is highly suggested to enable the flag `-com.apple.CoreData.ConcurrencyDebug 1` in your project to make sure that you don't misuse Core Data in terms of threading and concurrency (by accessing managed objects from different threads and similar errors). This component doesn't aim to introduce interfaces with the goal of hiding the concept of `ManagedObjectContext`: it would open up the doors to threading issues in clients' code as developers should be responsible to check for the type of the calling thread at some level (that would be ignoring the benefits that Core Data gives to us). Therefore, our design forces to make all the readings and writings via the `DALService` and the `ManagedObject` category methods are intended to always be explicit on the context (e.g. `SK_createInContext(context: NSManagedObjectContext)`). ### Offline UI testing on iOS with stubs URL: https://albertodebortoli.com/2015/11/23/offline-ui-testing-on-ios-with-stubs/ Last updated: 2018-02-11T01:26:50.000Z **The original post is published on the Just Eat tech blog at this [URL](http://tech.just-eat.com/2015/11/23/offline-ui-testing-on-ios-with-stubs/?ref=albertodebortoli.com).** Here at Just Eat, while we have always used stubs in Unit Tests, we tested against production public APIs for our functional and UI Testing. This always caused us problems with APIs returning different data depending on external factors, such as time of day. We have recently adopted the UI testing framework that Apple introduced at the WWDC 2015 to run functional/automation tests on the iOS UK app and stubs for our APIs along with it. This has enabled us to solve the test failures caused by network requests gone wrong or returning unexpected results. ## Problem For out UI Testing we used to rely on [KIF](https://github.com/kif-framework/KIF?ref=albertodebortoli.com) but we have never been completely satisfied, for reasons such as: - The difficulty of reading KIF output because it was mixed in the app logs - The cumbersome process of taking screenshots of the app upon a test failure - General issues also reported by the community on the [GitHub page](https://github.com/kif-framework/KIF/issues?ref=albertodebortoli.com) We believe that Apple is providing developers with a full set of development tools and even though some of them are far from being reliable in their initial releases, we trust they will become more and more stable over time. Another pitfall for us is that our APIs return different values, based on the time of the day, because restaurants might be closed and/or their menu might change. As a consequence, the execution of automation tests against our public APIs was causing some tests not to pass. ## Proposed Solution Rethinking our functional tests from scratch allowed us to raise the bar and solve outstanding issues with a fresh mind. We realised we could use the same technology used in our Unit test to add support for offline testing in the automation tests, and therefore we designed around [OHHTTPStubs](https://github.com/AliSoftware/OHHTTPStubs?ref=albertodebortoli.com) to stub the API calls from the app. Doing this was not as trivial as it might seem at first. OHHTTPStubs works nicely when writing unit tests as stubs can be created and removed during the test, but when it comes to automation tests it simply doesn’t work. The tests and application run as different instances, meaning that there is no way to inject data directly from the test code. The solution here is to launch the application instance with some launch arguments for enabling a “testing mode” and therefore generating a different data flow. We pass parameters to the app either in the setup method (per test suite): ```swift override func setUp() { super.setUp() continueAfterFailure = false let app = XCUIApplication() app.launchArguments = ["STUB_API_CALLS_stubsTemplate_addresses", "RUNNING_AUTOMATION_TESTS"] app.launch() } ``` or per single test: ```swift func test_ApplePayAvailable_UserLoggedIn_ServiceTypeDelivery() { let app = XCUIApplication() app.launchArguments = ["STUB_API_CALLS_stubsTemplate_addresses", "RUNNING_AUTOMATION_TESTS"] app.launch() // test code } ``` In our example we pass two parameters to signal to the app that the automation tests are running. The first parameter is used to stub a particular set of API calls (we’ll come back to the naming later) while the second one is particularly useful to fake the reachability check or the network layer to avoid any kind of outgoing connections. This helps to make sure that the app is fully stubbed, because if not, tests could break in the future due to missing connectivity on the CI machine, API issues or time sensitive events (restaurants are closed etc). We enable the global stubbing at the end of the `application:didFinishLaunchingWithOptions:` method: ```objective-c #ifndef APP_STORE_BUILD [self _stubAPICallsIfNeeded]; #endif //... - (void)_stubAPICallsIfNeeded { // e.g. if 'STUB_API_CALLS_stubsTemplate_addresses' is received as argument // we globally stub the app using the 'stubsTemplate_addresses.bundle' NSString *stubPrefix = @"STUB_API_CALLS_"; NSString *bundleName = [[[[NSProcessInfo processInfo].arguments filterUsingBlock:^BOOL(NSString *arg) { return [arg hasPrefix:stubPrefix]; }] firstObject] stringByReplacingOccurrencesOfString:stubPrefix withString:@""]; if (bundleName) { [JEHTTPStubManager applyStubsInBundleWithName:bundleName]; } } ``` The launch arguments are retrieved from the application thanks to the [NSProcessInfo](https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSProcessInfo%5FClass/?ref=albertodebortoli.com) class. It should now be clearer why we used the `STUB_API_CALLS_stubsTemplate_addresses` argument: the suffix `stubsTemplate_addresses` is used to identify a special bundle folder in the app containing the necessary information to stub the API calls involved in the test. This way the Test Automation Engineers can prepare the bundle and drop it into the project without the hassle of writing code to stub the calls. In our design, each bundle folder contains a `stubsRules.plist` file with the relevant information to stub an API call with a given status code, HTTP method and, of course, the response body (provided in a file in the bundle). ![group_folders](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/group_folders.png) This is how the stubs rules are structured: ![plist](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/plist.png) At this point, there's nothing more left than showing some code responsible for doing the hard work of stubbing. Here is the `JEHTTPStubManager` class previously mentioned in the AppDelegate. ```objective-c #import @interface JEHTTPStubManager : NSObject + (void)applyStubsInBundleWithName:(NSString *)bundleName; @end ``` ```objectivec #import "JEHTTPStubManager.h" #import "OHHTTPStubs+JEAdditions.h" static NSString *je_mappingFilename = @"stubsMapping"; static NSString const *je_matchingURL = @"matching_url"; static NSString const *je_jsonFile = @"json_file"; static NSString const *je_statusCode = @"status_code"; static NSString const *je_httpMethod = @"http_method"; @implementation JEHTTPStubManager + (void)applyStubsInBundleWithName:(NSString *)bundleName { NSParameterAssert(bundleName); NSString *bundlePath = [[NSBundle mainBundle] pathForResource:bundleName ofType:@"bundle"]; NSBundle *bundle = [NSBundle bundleWithPath:bundlePath]; NSString *mappingFilePath = [bundle pathForResource:je_mappingFilename ofType:@"plist"]; NSArray *mapping = [NSArray arrayWithContentsOfFile:mappingFilePath]; [mapping enumerateObjectsUsingBlock:^(NSDictionary * _Nonnull stubInfo, NSUInteger idx, BOOL * _Nonnull stop) { NSString *matchingURL = stubInfo[je_matchingURL]; NSString *jsonFile = stubInfo[je_jsonFile]; NSNumber *statusCode = stubInfo[je_statusCode]; NSString *httpMethod = stubInfo[je_httpMethod]; NSString *inlineResponse = stubInfo[je_inlineResponse]; id stub = [OHHTTPStubs stubURLThatMatchesPattern:matchingURL withJSONFileName:jsonFile statusCode:[statusCode integerValue] HTTPMethod:httpMethod bundle:bundle]; }]; } @end ``` We created an utility category around OHHTTPStubs: ```objective-c #import "OHHTTPStubs.h" @interface OHHTTPStubs (JEAdditions) + (id)stubURLThatMatchesPattern:(NSString *)regexPattern withJSONFileName:(NSString *)jsonFileName statusCode:(NSInteger)statusCode HTTPMethod:(NSString *)HTTPMethod bundle:(NSBundle *)bundle; // ... @end ``` ```objective-c #import "OHHTTPStubs+JEAdditions.h" @implementation OHHTTPStubs (JEAdditions) #pragma mark - Public + (id)stubURLThatMatchesPattern:(NSString *)regexPattern withJSONFileName:(NSString *)jsonFileName statusCode:(NSInteger)statusCode HTTPMethod:(NSString *)HTTPMethod bundle:(NSBundle *)bundle { NSBundle *targetBundle = bundle ?: [NSBundle bundleForClass:[self class]]; NSString *path = [targetBundle pathForResource:jsonFileName ofType:@"json"]; NSString *responseString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil]; return [self _stubURLThatMatchesPattern:regexPattern withResponseString:responseString statusCode:statusCode HTTPMethod:HTTPMethod]; } // ... #pragma mark - Private + (id)_stubURLThatMatchesPattern:(NSString *)regexPattern withResponseString:(NSString *)responseString statusCode:(NSInteger)statusCode HTTPMethod:(NSString *)HTTPMethod { NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexPattern options:0 error:nil]; return [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) { if (HTTPMethod && ![request.HTTPMethod isEqualToString:HTTPMethod]) { return NO; } NSString *requestURLString = [request.URL absoluteString]; if ([regex firstMatchInString:requestURLString options:kNilOptions range:NSMakeRange(0, [requestURLString length])]) { return YES; } return NO; } withStubResponse:^OHHTTPStubsResponse*(NSURLRequest *request) { NSData *response = [responseString dataUsingEncoding:NSUTF8StringEncoding]; return [OHHTTPStubsResponse responseWithData:response statusCode:(int)statusCode headers:@{@"Content-Type":@"application/json; charset=utf-8"}]; }]; } @end ``` Having our automation tests running offline reduced the majority of red test reports we were seeing with our previous setup. For every non-trivial application, running all the test suites takes several minutes and the last thing you want to see is a red mark in C.I. due to a network request gone wrong. The combination of OHHTTPStubs and Apple’s test framework has enabled us to run the automation tests at anytime during the day and to completely remove the possibility of errors that arise as a result of network requests going wrong. **Update**: see also the similar and well-thought-out post [Test automation for iOS](https://tech.blacklane.com/2015/12/13/test-automation-for-ios/?ref=albertodebortoli.com) by Stanislav Pankevich [@svpankevich](http://twitter.com/svpankevich?ref=albertodebortoli.com). ### A mind-blowing Impression Tracking engine for iOS URL: https://albertodebortoli.com/2015/08/15/a-mind-blowing-impression-tracking-engine-for-ios/ Last updated: 2018-03-01T14:40:29.000Z In one of my previous companies, it happened from time to time I had the opportunity to do some R&D of experimental ideas. What came out once, was, in my opinion, pretty neat. It never saw the light in production and I don't want this amount of work to be forgotten, so here is, after years, a still valid outline of a powerful impression tracking engine on iOS. Before further reading, you should be familiar with AOP and you should read my previous article on [Analytics on iOS](https://albertodebortoli.com/2014/03/25/an-aspect-oriented-programming-approach-to-ios-analytics/). ## Problem - You have an app with a feed - You want to track the impressions of the items - You don't want to track items displayed on screen during a fast scroll - You only want to track impressions that stay on screen for more than n seconds Reasons for this are, for example, you want to collect data for the impressions to better sell ads. Prepare to read a lot of code to understand the overall design, not the implementation (for that you need quite some time). ## Proposal I'll use an approach similar to the one used in git: working directory, staging area & index have been used for tracking and discarding impressions, I'll use these terms in this article to leverage the analogy. These are the main components: - **ADBImpressionManager**: holds a store with the impressions (ADBImpressionData objects). When asked for the impressions, it flushes the store and returns the retrieved objects. Could be a stack with push and pop operations. Using an analogy with Git, it's a way for clients to simulate moving stuff away from the "working directory" into something else (the "staging area"). ```objective-c @interface ADBImpressionData : NSObject // things you want to track ... // mandatory fields to populate on appear and disappear of the element we want to track the impression of @property (nonatomic, strong) NSDate *startDate; @property (nonatomic, strong) NSDate *endDate; @end @interface ADBImpressionManager : NSObject - (void)addImpression:(ADBImpressionData *)impression; - (NSArray *)impressions; @end ``` - **ADBThreadSafeStore**: a thread-safe store user by the ImpressionManager. Accesses must be serial as we are putting objects here according to UI events (when cells are displayed). UI is intrinsically non thread-safe so, better be cautious. ```objective-c @protocol ADBStoreProtocol @property (nonatomic, strong, readonly) NSArray *objects; @property (nonatomic, readonly) NSUInteger count; - (id)objectAtIndex:(NSUInteger)index; - (NSUInteger)indexOfObject:(id)object; - (BOOL)containsObject:(id)object; - (void)addObject:(id)object; // ... etc @end @interface ADBThreadSafeStore : NSObject // this class can hold only objects of a specific class // in our case, ADBImpressionData - (instancetype)initWithClass:(Class)clazz; @end ``` - **ADBImpressionTracker**: responsible for actually sending the tracking events over to the service (e.g. Google Analytics). Impressions are batched by the caller before being sent with `trackImpressions:`. ```objective-c @protocol ADBImpressionTrackingServiceProtocol - (void)trackImpressions:(NSArray *)impressions; @end @interface ADBGoogleAnalyticsImpressionTracker : NSObject @end ``` - **ADBImpressionDataEncoder**: used to convert ADBImpressionData objects to a format that can be sent over to the service and in a form respecting a given specification. ```objective-c @protocol ADBObjectEncoderProtocol - (NSDictionary *)encodeObject:(id)object; @end @protocol ADBImpressionDataEncoderProtocol - (NSArray *)encodedImpressions:(NSArray *)impressions; @end @interface ADBImpressionDataEncoder : NSObject ``` - **ADBImpressionTrackingManager**: this component (ITM) put all of the above (sub)components together. A root level object is the right place for creating it (if not the AppDelegate 😷, something near). An impression tracker, an impression manager and an impression data converter are passed-in as dependencies. A configuration (something similar to what proposed [here](http://albertodebortoli.github.io/blog/2014/03/25/an-aspect-oriented-approach-programming-to-ios-analytics/?ref=albertodebortoli.com), including selectors for start and end tracking) is used to perform the necessary AOP. The number of seconds necessary to consider an impression to be a valid is also provided. In git terms, what the commitAndPush does is 1\. commit the impressions to the impression manager and 2\. push them remotely. When committing them to the impression manager we check for the startDate and endDate of the ADBImpressionData object to verify that the impression was on screen for at least `impressionTime` seconds, otherwise the impression is trashed away (simply ignored). When pushing the impressions remotely we grab the impressions from the manager (that flushes the store afterwards) and send them via the tracking service (impressions are batched before being sent). ```objective-c @interface ADBImpressionTrackingManager : NSObject - (instancetype)initWithTrackingService:(id)trackingService impressionManager:(ADBImpressionManager *)impressionManager impressionDataConverter:(id)impressionDataConverter configuration:(NSArray *)configuration impressionTime:(NSTimeInterval)impressionTime; - (void)commitAndPush; @end @interface ImpressionTrackingManager () - (void)_commitImpressionsToManager; - (void)_pushRemotely; @end ``` - **RepeatedTimer**: a simple component encapsulating the logic to trigger calls to selector on an object every n seconds (it's nothing more than a wrapper on top of NSTimer, but much nicer to use). ```objective-c @interface ADBRepeatedTimer : NSObject - (instancetype)initWithTarget:(id)target selector:(SEL)selector interval:(NSTimeInterval)timeInterval; - (void)start; - (void)stop; @end ``` All of the above put together: ```objective-c ADBImpressionManager *im = [[ADBImpressionManager alloc] init]; ADBGoogleAnalyticsImpressionTracker *it = [ADBGoogleAnalyticsImpressionTracker new]; ADBImpressionDataEncoder *idc = [[ADBImpressionDataEncoder alloc] init]; self.impressionTrackingManager = [[ADBImpressionTrackingManager alloc] initWithTrackingService:it impressionManager:im impressionDataConverter:idc configuration:configuration impressionTime:1.0f]; self.timer = [[ADBRepeatedTimer alloc] initWithTarget:self.impressionTrackingManager selector:@selector(commitAndPush) interval:10.0f]; [self.timer start]; ``` ## Let's try to explain Using the AOP approach we can (again, I'm not showing the implementation, let's just make the assumption it's possible) *do things* after some methods are called. If we consider the UITableView for our example, we want to AOP the `tableView:cellForRowAtIndexPath:` to set the startDate property of an ADBImpressionData object and the `tableView:didEndDisplayingCell:forRowAtIndexPath:` for the endDate property. Given the configuration provided (names of the classes and the selectors to bind to the start and end date), the ImpressionTrackingManager can do the hard AOP/swizzling and business logic. An internal data structure is used to keep the 'working directory' of the impressions where keys are the tracked classes, values are mutable dictionaries. The keys of those dictionaries are index paths, the values are ImpressionData objects (but that's way far from the scope). Commit actions must happen everytime an impression ends to avoid losing duplicate impressions of the same item. Impressions that (for some reason) don't hit the end via end selectors configured in the configuration (for example, they are still on the screen) are still evaluated on the next commit & push cycle to check if the minimum time of an impression to be valid (MT) has been reached. Commit & push cycles are triggered periodically and the interval for such period must strictly be greater than the MT. The following diagram should explain how impressions are committed to the ImpressionManager and when they are pushed remotely via the ImpressionTracker. ![impression_tracking_timeline](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/impression_tracking_timeline.png) ## Conclusion There are way many aspects not discussed in this article that make the outlined design work but they would need a deeper analysis that would be too much to take in for a single reading. For instance, Extended Analytics Info (EAI) (via associated objects on NSObject+ADBAnalytics) can also be used. ```objective-c @interface NSObject (ADBAnalytics) - (NSDictionary *)analyticsEntryForKey:(NSString *)key; - (void)setAnalyticsEntry:(id)object forKey:(NSString *)key; @end #define ADBSetAnalyticsEntry(__obj, __key, __value) [__obj setAnalyticsEntry:__value forKey:__key]; ``` The above macro could be used in your `tableView:cellForRowAtIndexPath:` or `collectionView:cellForItemAtIndexPath:` like so: ```objective-c UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; id myObj = self.myDataSource[indexPath.row]; ADBSetAnalyticsEntry(cell, @"someKey1", someProperty1); ADBSetAnalyticsEntry(myObj, @"someKey2", someProperty2); ``` With the necessary implementation in the ITM, it'd be possible to tie together tracking information for impression events. In order for the ITM to reach the objects enriched with extra information, the view controllers (classes provided in the AOP configuration) could implement the ADBImpressionsProtocol and return some transformation of the datasource objects displayed in the table/collection view. ```objective-c @protocol ADBImpressionsProtocol - (id)impressionItemAtIndexPath:(NSIndexPath *)indexPath; @end ``` It's quite a lot of code to digest, I know, but it should help to understand the overall design and how a solution to the original problem (something definitely not trivial) could be developed. With some crazy gymnastic and black magic, of course. ### The journey of Apple Pay at Just Eat URL: https://albertodebortoli.com/2015/07/14/the-journey-of-apple-pay-at-just-eat/ Last updated: 2018-02-12T23:17:44.000Z **The original post is published on the Just Eat tech blog at this [URL](http://tech.just-eat.com/2015/07/14/the-journey-of-apple-pay-at-just-eat/?ref=albertodebortoli.com).** ## Introduction Apple Pay has recently been released in UK and at JUST EAT we worked on the integration in the iOS app to better support all of our customers and to ease the experience to both existing and new users. Until version 10 of our iOS UK app, the checkout for completing an order was wrapped into a webview and the flow was as follows: ![web](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/web.png) Since Apple pushes developers to implement Apple Pay in a way that the checkout doesn't force the user to log in, the checkout flow had to be reworked, and we took the opportunity to make the majority of the checkout flow native. This enabled us to support both checkout flows: - standard checkout (now with a more native flavour) ![std](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/std.png) - Apple Pay checkout ![ap](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/ap.png) The latter is clearly a fantastic solution for completing the checkout in very few steps with a great and simple UX. Thanks to the information provided by Apple Pay (inserted by the user when registering a debit/credit card) the user details native screen is no longer necessary and more importantly for the user, there is no need to log in to the platform. A further detail on the checkout is that we support two different so-called "service types" for the orders: delivery and collection. Defined as so: ```objective-c typedef NS_ENUM(NSUInteger, JEServiceType) { JEServiceTypeUnknown = 0, JEServiceTypeDelivery, JEServiceTypeCollection }; ``` On a side note, these changes soon became a challenge during the development as JUST EAT need to treat Apple Pay users (guest users) in a similar manner to users that have registered previously to our service. ## How we designed around Apple Pay At the time of writing there are already a few very good articles about a basic integration with Apple Pay. Probably the best reference worth mentioning is the [NSHipster post](http://nshipster.com/apple-pay/?ref=albertodebortoli.com). Clearly also the [Apple Documentation](https://developer.apple.com/apple-pay/?ref=albertodebortoli.com) is a great start and the ["Apple Pay Within Apps"](https://developer.apple.com/videos/wwdc/2015/?id=702&ref=albertodebortoli.com) video from WWDC 2015 explains really clearly all the relevant steps to have your app ready for Apple Pay. Rather than discussing the basic concepts (creating the merchant ID, configuring the `PKPaymentRequest` object, handling the presentation of the PKPaymentAuthorizationViewController, sending the token to the Payment Service Provider, etc.), we think it'd be more useful to walk you through the architectural aspects we considered when designing the solution on iOS using Objective-C. In the architecture we are proposing, the relevant components for handling an Apple Pay payment are the following: - ApplePayService - ApplePayPaymentHandler - ApplePayPaymentRequestFactory Some additional components are also present in the big picture: - CheckoutService - ABRecordRefConverter - PaymentFlowController N.B. We haven't used the iOS SDK provided by our PSP (Payment Service Provider) to communicate directly to it from the iOS app, but rather we rely on a payment API to complete this communication. At JUST EAT we like dependency injection and composition when possible. Developing this new feature with these concepts in mind helped to develop components that are isolated, easily pluggable, easy to test and (sometimes) reusable. The above components have well-defined responsibilities. We'll provide simplified code for the interfaces. Let's go through them in a constructive order: N.B. Don't be alarmed if you see the usage of the `JEFuture` or the `JEProgress` symbols. Lots of parts in our codebase rely on JustPromises (the library about Future and Promises we open sourced on [GitHub](https://github.com/justeat/JustPromises?ref=albertodebortoli.com)). You'll also see the usage of some DTOs and the `JE` prefix. - **CheckoutService**: responsible for handling the basic flow for the checkout. This is very much platform dependant. It could include the logic to perform the necessary actions in the backend to prepare the order to be completed with Apple Pay. In our case we need to store the user delivery notes (things like "The door bell doesn't work please call me when you arrive.") and the preferred time for the delivery. ```objective-c @interface JECheckoutService : NSObject /** * Composition of operations that are needed to prepare the order to be payed. */ - (JEFuture *)checkoutWithProgress:(JEProgress *)progress basketID:(NSString *)basketID orderContactDetails:(JEOrderContactDetailsDTO *)orderContactDetails deliveryDate:(NSDate *)deliveryDate deliveryNotes:(NSString *)deliveryNotes; - (void)cancelCheckoutForBasketWithID:(NSString *)basketID; @end ``` - **ApplePayPaymentRequestFactory**: responsible for creating the `PKPaymentRequest` objects representing a transaction. In our case objects of this kind are initialised with a delivery method and a card fee. The input parameter for the method returning a `PKPaymentRequest` is a representation of the basket. ```objective-c @interface JEApplePayPaymentRequestFactory : NSObject - (instancetype)initWithServiceType:(JEServiceType)serviceType cardFee:(NSDecimalNumber *)cardFee NS_DESIGNATED_INITIALIZER; - (PKPaymentRequest *)paymentRequestForBasket:(JEBasketDTO *)basketDTO; - (NSArray *)summaryItemsForBasket:(JEBasketDTO *)basketDTO; @end ``` You might wonder why `summaryItemsForBasket:` is public rather than keep it private. The reason is related to the fact that the block parameter of`paymentAuthorizationViewController:didSelectShippingAddress:completion:` has the following signature: ```objective-c (void (^)( PKPaymentAuthorizationStatus status, NSArray *shippingMethods, NSArray *summaryItems)) ``` It might very well be that some items are not available to be shipped to a specific address, and therefore we need a way to provide the updated list of summary items for the new shipping address the user selected. - **ApplePayPaymentHandler**: objects of this class are responsible for handling the payment from beginning to end. It's initialised with a CheckoutService that covers the initial part of the flow (i.e. the steps that happen with the standard checkout). A `PKPaymentRequest` and a basket representation are provided (along with some other minor details) to objects of this class when asked to actually process a payment. ```objective-c @interface JEApplePayPaymentHandler : NSObject - (instancetype)initWithServiceType:(JEServiceType)serviceType checkoutService:(JECheckoutService *)checkoutService NS_DESIGNATED_INITIALIZER; /** * Handle an authorised PKPayment payment. * * @param payment The payment object provided by Apple Pay via the PassKit framework after the user authorised it. * @param basket The basket representation to use for creating the order. * @param paymentProvider The payment provider. * @param deliveryDate The delivery time of the order. * @param deliveryNotes THe delivery notes of the order. * * @return A future for the payment handling. Future can be successful, failed or canceled if 'cancelLastPaymentAttempt' is called during the initial state of the payment and it can therefore be aborted. */ - (JEFuture *)handlePayment:(PKPayment *)payment forOrderWithBasket:(JEBasketDTO *)basket paymentProvider:(NSString *)paymentProvider deliveryDate:(NSDate *)deliveryDate deliveryNotes:(NSString *)deliveryNotes; /** * Attempt to stop the payment process for a given payment. */ - (JEFuture *)attemptPaymentCancellation:(PKPayment *)payment; @end ``` - **ApplePayService**: this service is responsible for implementing the `PKPaymentAuthorizationViewControllerDelegate` protocol, for checking if Apple Pay is enabled on the device and for cancelling the payment (if it's not too late). It's initialised with a view controller (used to display the `PKPaymentAuthorizationViewController`), a `JEApplePayPaymentRequestFactory` and a `JEApplePayPaymentHandler`. The logic for handling the selection of shipping address or the shipping method is here. ```objective-c @protocol JEApplePayServiceDelegate @optional - (void)applePayServiceDidPresentApplePaySheet:(JEApplePayService *)service; - (void)applePayService:(JEApplePayService *)service didCompletePaymentProcessWithSuccessForOrderWithID:(NSString *)orderID; - (void)applePayService:(JEApplePayService *)service didCompletePaymentProcessWithError:(NSError *)error; - (void)applePayService:(JEApplePayService *)service didFinishWithPaymentStatus:(JEApplePayServicePaymentStatus)paymentStatus; @end @interface JEApplePayService : NSObject @property (nonatomic, weak) id delegate; - (instancetype)initWithPresentingViewController:(UIViewController *)presentingViewController paymentRequestFactory:(JEApplePayPaymentRequestFactory *)paymentRequestFactory paymentHandler:(JEApplePayPaymentHandler *)paymentHandler NS_DESIGNATED_INITIALIZER; + (BOOL)isApplePayAvailableOnDevice; /** * Request the receiver to handle an Apple Pay payment. * * @param basket The basket representation for the order to be paid. * @param paymentProvider The payment provider from the retrieved payment option. * @param deliveryDate The delivery date of the order. * @param deliveryNotes The delivery notes of the order. */ - (void)handlePaymentForOrderWithBasket:(JEBasketDTO *)basket paymentProvider:(NSString *)paymentProvider deliveryDate:(NSDate *)deliveryDate deliveryNotes:(NSString *)deliveryNotes; /** * Attempt to stop the payment process for a given basket. */ - (void)attemptPaymentCancellationForBasket:(JEBasketDTO *)basket; @end ``` - **ABRecordRefConverter**: just a bunch of class methods for isolating the logic for transforming the `ABRecordRef` to easy-to-use DTOs. Until iOS 8.4, the delegate method `paymentAuthorizationViewController:didSelectShippingAddress:completion:` of `PKPaymentAuthorizationViewControllerDelegate` provides an `ABRecordRef`. Starting with iOS 9.0, this method is deprecated and a similar one using a wrapper object (`PKContact`) on top of the Contacts framework is used. `ABRecordRefConverter` basically does the work that Apple did for us in iOS 9. ```objective-c @interface JEABRecordRefConverter : NSObject + (JEOrderContactDetailsDTO *)orderContactDetailsForABRecordRef:(ABRecordRef)recordRef requireShippingDetails:(BOOL)requireShippingDetails error:(NSError **)error; + (JEAddressDTO *)addressForABRecordRef:(ABRecordRef)recordRef error:(NSError **)error; @end ``` Similar to what happened in the`ApplePayPaymentRequestFactory`, you might wonder why the `addressForABRecordRef:` method is necessary. The reason is that the second parameter of`paymentAuthorizationViewController:didSelectShippingAddress:completion:` is an `ABRecordRef` populated with only the address information for privacy reasons. - **PaymentFlowController**: it is responsible for creating the necessary stack and interactions between of all the above components. It acts as the delegate of the `ApplePayService`to handle the navigation flow and it should be intended to be the starting point and glue around our standard checkout flow and the Apple Pay one. ```objective-c /* simplified code from JEPaymentFlowController */ JEApplePayPaymentRequestFactory *paymentRequestFactory = [[JEApplePayPaymentRequestFactory alloc] initWithServiceType:self.serviceType cardFee:self.applePayPaymentOption.fee]; self.checkoutService = [JECheckoutService new]; JEApplePayPaymentHandler *paymentHandler = [[JEApplePayPaymentHandler alloc] initWithServiceType:self.serviceType checkoutService:self.checkoutService]; self.applePayService = [[JEApplePayService alloc] initWithPresentingViewController:self.navigationController paymentRequestFactory:paymentRequestFactory paymentHandler:paymentHandler]; self.applePayService.delegate = self; [self.applePayService handlePaymentForOrderWithBasketID:self.basketID paymentProvider:self.applePayPaymentOption.paymentProvider deliveryDate:deliveryDate deliveryNotes:deliveryNotes]; ``` We strongly believe in unit testing (and automation and integration testing as well) and we strive to cover our code with the necessary tests for every new feature we develop. Payments are clearly a hot topic and a crucial part of our business, therefore structuring this feature in small and separated components allowed us to easily keep the code coverage for the above components close to 100%. ## PKPaymentRequest's pitfalls The way the payment request is populated dictates what the Apple Pay sheet will display. How to populate the shipping and billing address properties is not completely straightforward. An excerpt of code we have in production is as follows: ```objective-c /* basic PKPaymentRequest creation */ PKPaymentRequest *paymentRequest = [[PKPaymentRequest alloc] init]; paymentRequest.supportedNetworks = @[PKPaymentNetworkMasterCard, PKPaymentNetworkVisa]; paymentRequest.merchantCapabilities = PKMerchantCapability3DS; paymentRequest.countryCode = @"GB"; // ISO 3166-1 alpha-2 country code paymentRequest.currencyCode = @"GBP"; // ISO 4217 currency code paymentRequest.requiredBillingAddressFields = PKAddressFieldAll; if (__ORDER_IS_FOR_DELIVERY__) { paymentRequest.requiredShippingAddressFields = PKAddressFieldAll; paymentRequest.shippingType = PKShippingTypeDelivery; } else if (__ORDER_IS_FOR_COLLECTION__) { paymentRequest.requiredShippingAddressFields = PKAddressFieldPhone | PKAddressFieldEmail; } return paymentRequest; ``` Using the above configuration the Apple Pay sheets for delivery and collection orders appear like so: ![ap_delivery_sd](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/ap_delivery_sd.png) ![ap_collection_sd](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/ap_collection_sd.png) Note that, in the case of collection orders, even if we set the `requiredShippingAddressFields` property to something meaningful (which isn't `PKAddressFieldNone`), the associated cell in the sheet is not displayed. At JUST EAT we need to know upfront if the order is for delivery or collection in order to let the user fill the basket accordingly (e.g. some items might not be available for delivery) and for this reason we couldn't leverage the built-in capabilities of Apple Pay to handle different shipping methods. Moreover, since Apple defines the shipping type like so: ```objective-c typedef NS_ENUM(NSUInteger, PKShippingType) { PKShippingTypeShipping, PKShippingTypeDelivery, PKShippingTypeStorePickup, PKShippingTypeServicePickup } NS_ENUM_AVAILABLE(NA, 8_3); ``` for collection orders the correct value to use would be `PKShippingTypeStorePickup` but the address of the store must be present in the list of addresses the user has entered on the device. This isn't practical. Going back to how the payment request is configured, it's critical for JUST EAT to have the phone number and the email of the customer in order for customer services to contact them in case something goes wrong with the order. This applies to delivery orders as well as collection orders. At first we thought that, since for collection orders the shipping address was not necessary, the property `requiredShippingAddressFields`of the `PKPaymentRequest`could be set to `PKAddressFieldNone` and we could grab the phone number and email from the `billingAddress` property. Unfortunately, even setting the `requiredBillingAddressFields` to `PKAddressFieldAll` when fetching the info from the `billingAddress` property of the `PKPayment` the phone number and email values are not there. Crucially, to grab the necessary order details info we had to merge the information provided by the two properties (`billingAddress` and `shippingAddress`) as so: ```objective-c @interface JEAddressDTO : NSObject @property (nonatomic, copy, readonly) NSString *line1; @property (nonatomic, copy, readonly) NSString *line2; @property (nonatomic, copy, readonly) NSString *line3; @property (nonatomic, copy, readonly) NSString *city; @property (nonatomic, copy, readonly) NSString *postCode; + (instancetype)addressWithLine1:(NSString *)line1 line2:(NSString *)line2 line3:(NSString *)line3 city:(NSString *)city postCode:(NSString *)postCode; @end @interface JEOrderContactDetailsDTO : NSObject @property (nonatomic, copy, readonly) NSString *fullName; @property (nonatomic, copy, readonly) NSString *phoneNumber; @property (nonatomic, copy, readonly) NSString *email; @property (nonatomic, strong, readonly) JEAddressDTO *address; + (instancetype)orderContactDetailsWithFullName:(NSString *)fullName phoneNumber:(NSString *)phoneNumber email:(NSString *)email address:(JEAddressDTO *)address; @end ``` ```objective-c /* * simplified code for composing the necessary order details info from * billing and shipping addresses within the `JEApplePayPaymentHandler` */ - (JEOrderContactDetailsDTO *)orderContactDetailsForPayment:(PKPayment *)payment serviceType:(BOOL)serviceType error:(NSError **)error { BOOL isDeliveryOrder = (serviceType == JEServiceTypeDelivery); NSError *shippingAddressError = nil; JEOrderContactDetailsDTO *contactDetailsFromShippingAddress = [JEABRecordRefConverter orderContactDetailsForABRecordRef:payment.shippingAddress requireShippingDetails:isDeliveryOrder error:&shippingAddressError]; // at this point `contactDetailsFromShippingAddress` contains if (shippingAddressError && error != NULL) { *error = shippingAddressError; return nil; } JEOrderContactDetailsDTO *orderContactDetails = contactDetailsFromShippingAddress; if (!isDeliveryOrder) // collection orders { NSError *billingAddressError = nil; JEOrderContactDetailsDTO *contactDetailsFromBillingAddress = [JEABRecordRefConverter orderContactDetailsForABRecordRef:payment.billingAddress requireShippingDetails:NO error:&billingAddressError]; if (billingAddressError && error != NULL) { *error = billingAddressError; return nil; } // compose the order contact details orderContactDetails = [JEOrderContactDetailsDTO orderContactDetailsWithFullName:contactDetailsFromBillingAddress.fullName phoneNumber:contactDetailsFromShippingAddress.phoneNumber email:contactDetailsFromShippingAddress.email address:contactDetailsFromBillingAddress.address]; } return orderContactDetails; } ``` ## Integration considerations From our journey into the Apple Pay world we learned that Apple pushes a lot for the Apple Pay button to be prominent in your app's UI. Unless one starts an iOS application from scratch and provides only Apple Pay as a payment method, other methods are usually already available (cards, PayPal, etc.): showing the Apple Pay button as big as the other buttons leading to different payments is almost a mandatory requirement from Apple. Showing the button as a first option is equally important. What is not so mandatory, but still very nice to have, is to provide the user with the ability to complete the checkout without logging in. This is a good thing for a few reasons: - remove the friction and enable happy paths to help your customers pay quicker - no need to collect data from the user any more, leveraging what's already provided by Apple Pay - Apple and customer happiness As in our case, this is far from being trivial and logic in the backend usually needs to be tweaked accordingly to support what we call "guest" or "anonymous" users. On a side note, it wasn't completely clear from the beginning that the `ABRecordRef` object provided in the `paymentAuthorizationViewController:didSelectShippingAddress:completion:` delegate method does not contain the full user data but just the address, but that is sufficient information to calculate the shipping cost and the availability of the items to a specific address. The entire user information is provided in the `PKPayment` object via the `shippingAddress`and `billingAddress`properties once the user authorised the payment. In iOS 9 the API slightly changed mainly due to introduction of the Contacts framework and the deprecation of the AddressBook one. PassKit provides a new class `PKContact` that is a wrapper around objects from the Contacts framework. This means that the following properties of `PKPaymentRequest` objects: ```objective-c @property (nonatomic, assign, nullable) ABRecordRef billingAddress NS_DEPRECATED_IOS(8_0, 9_0, "Use billingContact instead"); @property (nonatomic, assign, nullable) ABRecordRef shippingAddress NS_DEPRECATED_IOS(8_0, 9_0, "Use shippingContact instead"); ``` are deprecated in favour of the following new ones: ```objective-c @property (nonatomic, retain, nullable) PKContact *billingContact NS_AVAILABLE_IOS(9_0); @property (nonatomic, retain, nullable) PKContact *shippingContact NS_AVAILABLE_IOS(9_0); ``` This will help a lot since the old `ABRecordRef` is defined as a `CFTypeRef` and the related APIs are not easy to consume in an object-oriented world. ### Notes on the Developer Portal. For Dummies. URL: https://albertodebortoli.com/2015/03/22/notes-on-the-developer-portal-for-dummies/ Last updated: 2018-02-10T15:03:37.000Z Let's make it clear. This is a post for dummies. There are some others tutorials online but I thought I could write something better focusing on the right details rather than wasting time on a brainless walkthrough. Well… here are my 50¢. It's since 2008 that I work with the iOS platform and still, when it comes to managing certificates and provisioning profiles within the Developer Portal, I lose my mind. Just like when you start a new bottle of vodka and you cannot remember the entire trip that brought you to the bottom of the previous bottle. After having read this post you'll hopefully have a better understanding of what and why you need to setup in terms of certificates and provisioning profiles for building an iOS app on device or for archiving for release. If not, at least you have some handy tutorial for dummies. Let's start from scratch. No keys, no certificates, no provisioning profiles, clean Mac installation. # Everything begins with a CSR A CSR or Certificate Signing request is a block of encrypted text that is generated on the machine that the certificate will be used on. It contains information that will be included in your certificate such as your organization name, common name (domain name), locality, and country. It also contains the **public key** that will be included in your certificate. A private key is usually created at the same time of the creation of the CSR. A certificate authority will use a CSR to create your SSL certificate, but it clearly does not need your private key. You need to keep your **private key** secret. What is a CSR and private key good for if someone else can potentially read your communications? The certificate created with a particular CSR will only work with the private key that was generated with it. So if you lose the private key, the certificate will no longer work. To create a CSR, launch the Keychain Access app on Mac OS X and select Keychain Access -> Certificate Assistant -> Request a Certificate From a Certificate Authority ![1-request-certificate-from-ca](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/1-request-certificate-from-ca.png) I think the menu title is misleading. We don't actually request a certificate, we are creating a request for it! Create a CSR with common name and a email address. This fields are enough for our purpose. ![2-csr-certificate-information](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/2-csr-certificate-information.png) Now you have the `CertificateSigningRequest.certSigningRequest` file. CSRs are created in the Base-64 encoded PEM format. This format includes the `-----BEGIN CERTIFICATE REQUEST-----` and `-----END CERTIFICATE REQUEST-----` lines at the beginning and end of the CSR. A PEM format CSR can be opened in a text editor and looks like the following example: ``` -----BEGIN CERTIFICATE REQUEST----- MIIByjCCATMCAQAwgYkxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlh MRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRMwEQYDVQQKEwpHb29nbGUgSW5jMR8w HQYDVQQLExZJbmZvcm1hdGlvbiBUZWNobm9sb2d5MRcwFQYDVQQDEw53d3cuZ29v Z2xlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEApZtYJCHJ4VpVXHfV IlstQTlO4qC03hjX+ZkPyvdYd1Q4+qbAeTwXmCUKYHThVRd5aXSqlPzyIBwieMZr WFlRQddZ1IzXAlVRDWwAo60KecqeAXnnUK+5fXoTI/UgWshre8tJ+x/TMHaQKR/J cIWPhqaQhsJuzZbvAdGA80BLxdMCAwEAAaAAMA0GCSqGSIb3DQEBBQUAA4GBAIhl 4PvFq+e7ipARgI5ZM+GZx6mpCz44DTo0JkwfRDf+BtrsaC0q68eTf2XhYOsq4fkH Q0uA0aVog3f5iJxCa3Hp5gxbJQ6zV6kJ0TEsuaaOhEko9sdpCoPOnRBm2i/XRD2D 6iNh8f8z0ShGsFqjDgFHyF3o+lUyj+UC6H1QW7bn -----END CERTIFICATE REQUEST----- ``` Now in the Keychain Access are also visible the new keys created along with the CSR. They will be 2048-bit by default. The name of the public/private keys is the common name chosen previously. There is nothing linked to these keys yet. These keys are super important data. If you lose them, everything we are about to create will be fuc\*ed up. ![3-csr-keys](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/3-csr-keys.png) ## Public and Private Keys If you missed *that* lesson about RSA at your Computer Science class, what you need to know is basically: - RSA is an algorithm invented by Rivest, Shamir, Adleman to encrypt data in a secure way; - It involves 2 keys: the public one can be shared with anyone, the private one must be kept secret; - It's secure, depending on the length of the keys (in bits, usually 2048 for our purposes, military grade), since it relies on strong math theorems such as Fermat's little theorem and the factoring problem (the difficulty of factoring the product of two large prime numbers); - In-depth explanation [here](http://crypto.stackexchange.com/a/398?ref=albertodebortoli.com); - The following flow diagram should be self-explanatory. ![4-public-private-keys](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/4-public-private-keys.png) ## Certificates, Identifiers & Profiles It's time to go to the Developer Portal and create some stuff. Sections are Development and Production. Let's go through the steps for creating the development certificate (similar steps apply for production). Let's pick "iOS App Development". You'll be asked to submit the CSR. Download the certificate, it'll be named `ios_development.cer`. ![5-dev-cert](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/5-dev-cert.png) When you install it, it will appear in the Keychain Access with an ID (the so-called Identity) in parenthesis like so: ![6-dev-certificate-added](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/6-dev-certificate-added.png) The name is the one of the Developer Program license, not to be confused with the one you set when creating the CSR. The certificate has been created for you and encrypted with the public key you sent with the CSR. Your machine can decrypt it since it has the private key. Now in the Keychain Access you'll have the certificate combined with the private key. ![7-cert-pk-link](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/7-cert-pk-link.png) and the other way around ![8-pk-cert-link](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/8-pk-cert-link.png) If you want to export the whole thing to another machine, select the couple and export. ![9-export-couple](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/9-export-couple.png) Pick a password and you'll get a `Certificates.p12`. If interested in the certificate info, select info from the drop down menu. It has been issued by an Apple CA (Certification Authority) and it includes the public key. ![10-cert-info](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/10-cert-info.png) At its core an X.509 certificate is a digital document that has been encoded and/or digitally signed according to [RFC 5280](https://www.ietf.org/rfc/rfc5280.txt?ref=albertodebortoli.com). In the X.509 system, a certification authority issues a certificate binding a public key to a particular distinguished name. Now it's time to create an App ID. Nothing too fancy here if you stick to the rule: **One App ID per App** Try to avoid wildcard to match multiple apps. It hurts when you decide to add App Services (like Push Notifications) later on. Insert the Bundle ID of your app. I usually stick to this reverse domain schema `com.yourdomain.yourapp.buildtype` (e.g. `com.albertodebortoli.iHarmony.beta`). At the very basic, you get something like so: ![11-iharmony-beta-app-id](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/11-iharmony-beta-app-id.png) Time to create a Provisioning Profile: it's something that allows your application to run on your devices. It is a collection of your iOS Development Certificate, UDID and your App Id. Without a provisioning profile you cannot install your apps on your development devices. Here sections are, one more time, Development and Production, like it was for the certificates. Let's go for through the Development flow. Select 'iOS App Development'. Select the App ID just created, select the devices you want to enable (if it's a development provisioning profile), link the certificate previously generated that will be included in the Provisioning Profile and pick a name for it. I usually stick with this schema ` PP`. ![12-pp-generated](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/12-pp-generated.png) Download and install it. It's named something like `iHarmony_Beta_220315_Development_PP.mobileprovision`. ## The project Create a new Xcode project. Set the bundle id to the one picked previously. Go to Build Settings and filter for "code signing". Set the values "Code Signing Identity" (the certificate) and "Provisioning Profile" (the provisioning profile) like so: ![13-code-signing](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/13-code-signing.png) Make sure you have setup the correct Bundle ID in the Info.plist file. When a new project is created it has something like `com.albertodebortoli.iHarmony.beta.$(PRODUCT_NAME:rfc1034identifier)`, you might want to remove the last part and prefer having an explicit string to avoid the following error in the General pane of your target (you might need to restart/play around with Xcode to make it get the change). ![14-general-pane-team](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/14-general-pane-team.png) If you already are part of other teams (i.e. different Developer Program Licenses), you have to pick the right one. Now you should be able to build on device and (if you've done the same step for Production) archive for App Store/Ad Hoc Distribution. ## Some key points - When you create a CSR within the Keychain Access the keys are generated automatically for you. - You can reuse the same CSR for generating different certificates or if some certificate is revoked. It's fine to have just one single public/private key pair for all the things related to a specific domain. Differently, when generating certificates for other services (like the push notifications) you might want to use a different key pair. - Everything can be regenerated. Always. You revoked some certificates and the CTO of your company got the mail stating “Your certificate has been revoked”? No worries, your business is still alive and the apps on the App Store are still valid. - Revoking a certificate used for signing apps distributed with an enterprise license will cause the app not to be valid anymore. - If you delete a Provisioning Profile, it can be regenerated using the same certificate. - If you lose/delete your private key, the certificate becomes invalid because you don't have a public/private key pair anymore. Therefore the provisioning profile containing the certificate cannot be used anymore. - The public/private key can be regenerated too, but you probably want to avoid this step and keep them between certificate/provisioning profiles regenerations. This means that you might want to store the CSR safely otherwise the next time around you'll have to Generate a certificate signing request (CSR) for an existing private key. - To pass all the data necessary to build your app to another machine you need to either: - export the private key and the certificate and download the provisioning profile from the Developer Portal. - export the private key, download the certificate and provisioning profile from the Developer Portal. There is so much more to say about code signing and especially about the errors and the headaches you get from it. Apple created some great [documentation](https://developer.apple.com/support/technical/code-signing/?ref=albertodebortoli.com) over the last 7 years. ### From the Eyes of an iOS Dev at JUST EAT URL: https://albertodebortoli.com/2015/03/15/from-the-eyes-of-an-ios-dev-at-just-eat/ Last updated: 2018-02-10T15:22:03.000Z It has been almost 6 months since my last blog post. Things have changed quite a lot since then. Six months ago I was still excited about my travel to San Francisco for the WWDC 2014, my girlfriend still had to move from Italy to London with me and definitely I wasn't planning to switch job again any time soon. # Overture I've been attracted by [JUST EAT](http://www.just-eat.com/?ref=albertodebortoli.com) as a company since March 2014 but at that time it was too early for me to consider to change job. I met Ben Chester (the tech lead of the iOS team) when he gave a talk at Badoo offices (when I was still working there) and that evening he blew my mind. Later, I had a few chances to have a chat with the passionated guy he is and I immediately thought "Damn! I want to work with this guy, with brilliant guys like him and I want to work at JUST EAT!". Since then, I heard people talking extremely good about JUST EAT as a job place because of the values, the work environment, the company culture and the engineeristic approach to things. Every time I started with "Do you know JUST EAT as a company?" the answer was something like "Oh yeah! They are freaking cool! I have a friend working there, they do amazing stuff and he's very very happy!". They definitely were all good signs. Signs I decided not to underestimate anymore these days after some terrible experiences in other companies where I've met so many, countless poor engineers. Another good sign was also the exposure and lots of information that the company promotes online with its [tech blog](http://tech.just-eat.com/?ref=albertodebortoli.com) giving a good insight of the technologies used, the [people and teams](http://tech.just-eat.com/the-team/?ref=albertodebortoli.com) working there and a good description of the [Engineering](http://tech.just-eat.com/engineering-at-just-eat/?ref=albertodebortoli.com). [Benefits](http://tech.just-eat.com/jobs/company-benefits/?ref=albertodebortoli.com) are also compelling. JUST EAT offices hosted [NSLondon](http://www.meetup.com/NSLondon/?ref=albertodebortoli.com) a few times. Meetups, you know, are the perfect occasions to reach out the developers' community. I noticed too many cool companies failing at this. After months of interest about JUST EAT and thoughts spinning in my head, I said to myself "let's see if I have what it takes". I decided to apply for the Senior iOS role in later October 2014 kicking off the process taking the [test task](https://github.com/justeat/JustEat.RecruitmentTest?ref=albertodebortoli.com). I joined the Consumer iOS app team at the begin of 2015 and after 2 months of excitement I'm summarizing some thoughts here. ![justeat_wall](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/justeat_wall.jpg) # The gangs Since the very first day it was clear how different teams cover the areas of the platform. Here's the list (hopefully up-to-date): - Consumer Web Apps - desktop and mobile consumer websites, along with embedded native app websites - Consumer iOS Apps - iOS apps for consumers - Consumer Android Apps - Android apps for consumers - Consumer Apps Test Automation - improving our test automation coverage across our consumer channels - Business Apps - the internal apps and tools used to administer our business and provide great customer care - Restaurant Apps - the devices and services used by our partner restaurants - EPOS - the full-feature point of sale system we provide to restaurants - Public APIs and web services - used by partners, internal clients and native apps - Payments - payment authorisation, fraud checking, invoicing and financial reporting - Order processing APIs - focusing on the APIs needed to place and fulfil orders - Search APIs - building intelligent restaurant & menu search and associated APIs - Platform Services - AWS automation, platform tools, infrastructure Even if your role is specific to a platform (like is mine) you often need to have chats and planning with other teams as happened to me for the first feature I developed. "Hey mate, can we have a talk about that particular API?", it's good to know that you can knock on the shoulders of people in other teams that are always open to discussions. This helps communication and the understanding of how things work in other departments. Developers of features are responsible for them. My first feature was… well, I won't really tell as it's not in production yet (it will be in the next release, 7.0) but I can tell you that the entire stack of things related to it on iOS was my responsibility and it involved a good amount of interaction with the API team. # The new Three's Company As I said, I'm working on the Consumer iOS App, the one that you can [download from the App Store](https://itunes.apple.com/gb/app/just-eat-takeaway/id566347057?mt=8&ref=albertodebortoli.com) and probably used sometimes to order delivery food in UK. Yes, That App. ![home70](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/home70.png) We have also recently published the [Consumer Apps page](http://www.just-eat.co.uk/apps?ref=albertodebortoli.com). It's important to notice that Consumer Apps are splitted into 2 main groups: UK and International. The International team (i.e. all the countries JUST EAT is available but UK) works on an app that is basically very similar to the UK one but the projects are actually separate. The UK Consumer iOS team is made up of the following highly talented gentlemen: [Ben Chester](https://twitter.com/benchester?ref=albertodebortoli.com) (Tech Lead), [Rui Peres](https://twitter.com/ruiaaperes?ref=albertodebortoli.com) (a.k.a. the guy behind [iOS Goodies](http://ios-goodies.com/?ref=albertodebortoli.com)) and me (a.k.a. Alberto De Bortoli 😜). You'd be no surprised to know that we use tools like CocoaPods, TeamCity, JIRA, Confluence, HockeyApp, gitflow, some third-party open source components etc. We run stand-up meetings every morning within the team on Google hangout and retrospectives for every feature developed. More on this on the [iOS tech page](http://tech.just-eat.com/ios/?ref=albertodebortoli.com). # Fingers and brains At JUST EAT we craft software in the way I always loved. We strive to give the maximum attention to the details, we promote culture for testing code the right way, we stick to clear and defined development processes but at the same time we leave the door open to introduce innovation in the project. Collaboration and sharing are the basis of our work. It's hard to find a stimulating environment like this, I can tell from experience. ![biscuit](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/biscuit.jpg) But a software developer is not just fast fingers on the keyboard. Principles here are something we really care about, definitely not just something made up to show off. - Do the right thing - Take responsibility - Be transparent - Continuously improve - Take pride Even before joining the company, the first one got my attention the most. It means a lot to always strive to do-the-thing-that-you-think-it's-best, for the team, for the product, for the company. It really has a broad meaning but if you try to stick to it using your expertise, skills, gut feelings and passion… I think it's hard to go wrong. Ben is a fantastic tech lead, he teaches to care about our product, our code, our output. A project is like a little baby, it grows in the right way only if someone cares about him. Nothing is more true that this! In the end, everything you do always pays off under different aspects. It's so hard to find such attitude and motivation in other companies (and I'm not even talking about agencies 😬). Every engineering member is asked to set some OKRs (Objectives and Key Results). Objectives are goals, they tell you where to go. Each objective has a few key results, they indicate how you will get there. OKRs as established on a quarter-basis. Another catchy aspect that makes you realize how much attention the company gives to their employees is the personalized development plan. It can include any aspect that you want to improve about yourself. You plan this with your line manager and the company will help you to reach the goals in the most appropriate way. If needed and/or requested by you, you can follow specific courses. You can set very clear and defined career goals as well. This is something that only the best companies do! If you're a geek like we are, you like open source too. And you probably lament that big enterprise companies are often very closed when talking about releasing to the public some hidden pieces of their hidden, obscure and very delicate internal tech infrastructure. It's not the case at JUST EAT, that has an established presence on GitHub with some useful components like [JustSaying](https://github.com/justeat/JustSaying?ref=albertodebortoli.com), [JustBehave](https://github.com/justeat/JustBehave?ref=albertodebortoli.com) and [JustPromises](https://github.com/justeat/JustPromises?ref=albertodebortoli.com). The latter is the first component released by the iOS team and I can't wait to release more stuff! # The smell of the air ![fleetplace](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/fleetplace.jpg) Recently some guys from Google popped by and took some photos 😲 You can now have a look at the entire 7th floor of 2 Fleet Place House, EC4M 7RF on [Google Street View](https://www.google.co.uk/maps/@51.516575,-0.103675,3a,75y,300.97h,75.99t/data=!3m5!1e1!3m3!1syyXCpAVBjgkAAAQpfAywUQ!2e0!3e2?ref=albertodebortoli.com). Take a quick tour, our offices are stunning! JUST EAT owns 3 floors at 2 Fleet Place House (6th, 7th and 8th). When things are getting hard during the day (usually tough bugs, developer's life…) it's good to have a break to reorder the ideas. In the kitchen there are facilities for preparing a good capuccino (I keep failing at this, no matter how many times I try to learn from the [master barista Joakim](https://twitter.com/joakimcarlgren?ref=albertodebortoli.com)) and to play ping-pong (I'm usually better at this, right [Rui Peres](https://twitter.com/ruiaaperes?ref=albertodebortoli.com)?). ![justeat_kitchen](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/justeat_kitchen.jpg) We also have offices in Bristol and apparently they will be even nicer than London's. The goal is that every new office is the best office we have 😎. # So… So you work at JUST EAT, [everything is awesome](https://www.youtube.com/watch?v=StTqXEQ2l-Y&ref=albertodebortoli.com) and I should cry in the darkness of my bedroom because I'm not lucky as you are, right? I know I know, maybe I put too much stress on how I think this company is cool. Well guys, I really wanted to share my experience so far 😄 and along with it also some tips on what to look for when you decide to move towards your next job. I'm sure you got them while reading this post. I can't wait to see what the next months will unfold but now I gotta go! I have some coding to do! 👓📱 ### Working on tasks with an eye on open source contributions URL: https://albertodebortoli.com/2014/09/28/working-on-tasks-with-an-eye-on-open-source-contributions/ Last updated: 2018-02-10T21:19:40.000Z Have you ever realized that developers are never happy with the legacy code? The definition of "legacy code" may vary: - code inherited from the previous developers of your company - code that doesn't have test suites - code that is older that 10 minutes (...) We all rarely find good code when joining a company, weird uh? Some reasons why good code is so hard to find can be: - the developer that worked on the code was good but couldn't care less about doing things properly - the developer that worked on the code was simply unexperienced and created bizzare things - the developer that worked on the code was terrible at architecture design - too many developers worked on the same code without understanding what was already been done - code was not developed using the [black box](http://en.wikipedia.org/wiki/Black%5Fbox?ref=albertodebortoli.com) approach and without reusability in mind All points but last are summarized here: ![developers](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/developers.jpg) Since I'm Italian and I don't go easy with my temper and my personal definition is simply: **Legacy code is code that sucks** Who knows me, knows that I'm fond of design and architecture; I'm not discussing the third point, here I'll elaborate on the last one. What I've always tried to do, among other things, while working in the different companies in my career is to develop reusable code. Let's call a bit of reusable code a 'component'. As you may guess, developing small pieces of reusable code always pays off and it turns out to be a great time saving in future. As an iOS developer I have tasks to do on a daily basis. Some of them are boring, others are challeging. Even if a task is boring I try to find the fun of it, identifying what can be reused and code it at my best. Sometimes it might be just a minor UI component, sometimes larger pieces of logic. Usually it's good to know in advance if you aim to create a component along the journey of the task you're working on rather then realizing later that something could have been 'componentized'. This also dictates how goodly you focus on the development of the component. Here comes the cool part: once you got a nice piece of self-contained code you might yearn to release it open source! This is the thing I love the most of the process because it makes me feel like I'm closing the circle. ![opensource](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/opensource.png) Having a component open sourced has some nice side effects: - you get feedback (appreciation or criticism) from the community - you get pull requests/bug fixes from the community - it raises the image of the company for what regards the technologies used For instance, at [Beamly](http://beamly.com/?ref=albertodebortoli.com) (the company I'm working at now) we are doing cool things and we have released 3 components so far: - [BMYCircularProgressPullToRefresh](https://github.com/beamly/BMYCircularProgressPullToRefresh?ref=albertodebortoli.com) - [BMYScrollableNavigationBar](https://github.com/beamly/BMYScrollableNavigationBar?ref=albertodebortoli.com) - [BMYCircleStepView](https://github.com/zeebox/BMYCircleStepView?ref=albertodebortoli.com) the process I used is like so: - identify a yet-to-be-written piece of code that can be reused - create a new projects and start coding it in isolation - if possible, test it - create a demo app for the component - go back to the main project and add the component as a development pod (you can read more about development pods in my previous article ["CocoaPods: Working With Internal Pods Without Hassle"](https://albertodebortoli.com/blog/2014/03/11/cocoapods-working-with-internal-pods/)) - if some fine tuning is needed, act accordingly on the development pod - open source it and make it available on CocoaPods Going this way while developing, you focus only on the component. In the end, the understanding of an isolated component is greater that the rest of the boilerplate for you and for the developers after you. So, back to the original point: think of joining a company where everything is reusable, pluggable like ruby gems or cocoa pods. This doesn't necessarily implies that the quality is good whatsoever, but for sure you'd complain less about the legacy code you have to deal with. ### Flow Controllers on iOS for a better navigation control URL: https://albertodebortoli.com/2014/09/03/flow-controllers-on-ios-for-a-better-navigation-control/ Last updated: 2018-02-10T21:24:52.000Z Since I'm in London conversations with iOS developers have reached high levels with no doubts. I love to discuss with friends and iOS devs about new ways to improve our coding. Often my best practices are very appreciated among them and a bunch of devs start applying day-by-day what they learnt. ["An Aspect Oriented Programming Approach to iOS Analytics"](https://albertodebortoli.com/2014/03/25/an-aspect-oriented-programming-approach-to-ios-analytics/) and ["CocoaPods: Working With Internal Pods Without Hassle"](https://albertodebortoli.com/2014/03/11/cocoapods-working-with-internal-pods-without-hassle/) are 2 examples of good best practices. A friend asked for a post about the specific topic of flow controllers so... here we go. :) ## Navigation on iOS There are very few ways to present UIViewControllers on iOS either through UINavigationController or UIViewController: ```objective-c // UIViewController [viewControllerInstance presentViewController:modalViewController animated:YES completion:^{ /* ... */ }]; // UINavigationController [navigationControllerInstance pushViewController:detailViewController animated:YES]; ``` The thing I never liked is that UIViewController instances have the ability to *push* things on their own using the associated UINavigationController and to *present* other UIViewController instances within their logic. It's not... their responsibility. ## The roots back to 2008 The design of the above APIs represents the easiest way to achieve the presentation of a detail view from a master one and I'm not surprised Apple approved it. With the launch of the iPhone in 2008, developers were given easy APIs to learn in order to ease the development learning curve: with just one line of code it was (and it is) possible to present other views. So... we got used to the Apple APIs, maybe too much. Some of the APIs are not so great and it's probably due to the old ones often ported from the Mac or from AppKit. You are free to argue about it, but the [UITableViewDelegate](https://developer.apple.com/library/ios/documentation/uikit/reference/UITableViewDelegate%5FProtocol/Reference/Reference.html?ref=albertodebortoli.com) and [UITableViewDataSource](https://developer.apple.com/library/ios/documentation/uikit/reference/UITableViewDataSource%5FProtocol/Reference/Reference.html?ref=albertodebortoli.com) are another example: they mix presentation, callbacks and real datasource. Just to say that not everything that comes from Apple is perfect: sometimes it is debatable. ## The need for a better world What I want to introduce here is a more elegant and clean way to handle the presentation of UIViewControllers fulfilling these point: - UIViewControllers shouldn't present other UIViewControllers - There should be a specific component responsible for handling the presentation flow Good architectures like [VIPER](http://www.objc.io/issue-13/viper.html?ref=albertodebortoli.com) have been proposed and they try to solve more general problems in a clean way. What I explain here are small concepts to improve the way things are presented on screen. Be aware not to confuse with routing systems like [JLRoutes](https://github.com/joeldev/JLRoutes?ref=albertodebortoli.com) whose main aim is to mimic the routing of web apps: we are not talking about them here. ## Some new friends: the Flow Controllers Over the years I spent time experimenting with different solutions to improve the code that handles the navigation of iOS Apps. Sometimes it involved a few components: Presenters, Factories, Flow Controllers... and in the end they all turned out to be over engineered and too much complicated solutions for a task that should be straightforward. Here is the minimum way to achieve a separation of concerns between UIViewController logic and the app navigation: - Subparts of an app that require proper navigation control should be handled by specific objects called *Flow Controllers* - Flow Controllers control the transitions from different state/screens of the app - Flow Controllers inherits from NSObject and are logic classes - Flow Controllers are initialized with a UINavigationController instance - Flow Controllers are domain specific - Flow Controllers are passed around to the view controller they handle - Flow Controllers are responsible for creating other view controllers ## Let's talk code It's September 2014 and no, no Swift, I still write only in Objective-C, please bear with me. Consider a user profile view controller that usually links to following/followers/settings view controllers: it makes a lot of sense to have a flow controller to control the flow of them all. Let's call the ProfileViewController the 'master' and the following/followers/settings view controllers the 'details'. ```objective-c @interface ADBProfileFlowController : NSObject - (instancetype)initWithNavigationController:(UINavigationController *)navigationController; - (void)showFollowingsScreen; - (void)showFollowersScreen; - (void)showSettingsScreen; @end ``` ```objective-c @interface ADBProfileFlowController () < ADBFollowingsViewControllerDelegate, ADBFollowersViewControllerDelegate, ADBSettingsViewControllerDelegate > @property (nonatomic, weak) UINavigationController *navigationController; @end @implementation ADBProfileFlowController - (instancetype)initWithNavigationController:(UINavigationController *)navigationController { /* ... */ } - (void)showFollowingsScreen { ADBDFollowingViewController *followingiewController = [[ADBDFollowingViewController alloc] initWithDependencies:...]; followingViewController.delegate = self; if ([self _profileIsTopViewController]) { [self.navigationController pushViewController:followingViewController animated:YES]; } } - (void)showFollowersScreen { ... } - (void)showSettingsScreen { ADBSettingsViewController *settingsViewController = [[ADBSettingsViewController alloc] initWithDependencies:...]; settingsViewController.delegate = self; if ([self _profileIsTopViewController]) { [self.navigationController presentViewController:settingsViewController animated:YES completion:nil]; } } #pragma mark - Private Methods - (void)_profileIsTopViewController { return ([self.navigationController.topViewController isKindOfClass:[ADBProfileViewController class]]); } #pragma mark - ADBFollowingViewControllerDelegate - (void)followingViewControllerDidReceiveTapOnCloseButton:(ADBFollowingViewController *)followingViewController { [self.navigationController popViewControllerAnimated:YES]; } #pragma mark - ADBFollowersViewControllerDelegate - (void)followersViewControllerDidReceiveTapOnCloseButton:(ADBFollowersViewController *)followersViewController { [self.navigationController popViewControllerAnimated:YES]; } #pragma mark - ADBSettingsViewControllerDelegate - (void)settingsViewControllerDidReceiveTapOnCloseButton:(ADBSetingsViewController *)settingsViewController { [settingsViewController savePreferences]; [self.navigationController popViewControllerAnimated:YES]; } ``` The public interface is very domain specific. The profile view controller is ```objective-c @interface ADBProfileViewController : NSObject - (instancetype)initWithFlowController:(ADBProfileFlowController *)flowController; @end ``` ```objective-c @interface ADBProfileViewController () @property (nonatomic, strong) ADBProfileFlowController *flowController; @end @implementation ADBProfileViewController - (instancetype)initWithFlowController:(ADBProfileFlowController *)flowController { /* ... */ } - (IBAction)settingsButtonTapped:(id)sender { [self.flowController showSettingsScreen]; } - (IBAction)followingsButtonTapped:(id)sender { [self.flowController showFollowingsScreen]; } - (IBAction)followersButtonTapped:(id)sender { [self.flowController showFollowersScreen]; } @end ``` To notice that the flow controller must not retain the navigation controller. Since the view controller retains the flow controller and the navigation controller retains the view controller, strongly referencing the navigation controller within the flow controller would cause a retain cycle. ## Conclusions I think this architecture has some advantages. In general, might be that the detail needs other dependencies to be created: an ID or A DTO object won't suffice and factories, providers, downloaders etc. might be necessary. We clearly don't want to make the master being aware of all the crap needed to instantiate the details. The fan-out (the number of imports) in the master would increase with no benefit. It'd be good to let the flow controller be responsible for the creation of the view controllers of a specific subpart of the app. The point here is to have domain specific flow controllers and the API that we want should reflect the intended behaviour. At this point, it's not important if the flow controller pushes, presents modally or play chess against Big Blue to do its job (to show the desired screen). Moreover, flow controllers can be reused on their own! They have all the necessary info to construct a specific sub-tree of navigation. Another advantage is... decoupling! It's always a good thing for ease of debugging in future. One can get confused and still think that a view controller can be created in the view controllers and passed to the flow controller for handling just the presentation. Nope. If we pass the detail from the master to the flow controller just to present it, then it would be ok to present it directly from the master. The flow controller would be just a navigation controller wrapper with API like - (void)presentViewController:(UIViewController \*)vc; - (void)pushViewController:(UIViewController \*)vc; Which is pointless and would create useless amount of code. This is totally against our goal: we aim for domain specific objects whose the goal to control the creation of specific objects and the presentation of them as well. ### The solution to FTP on iOS: GoldRaccoon URL: https://albertodebortoli.com/2014/08/01/the-solution-to-ftp-on-ios/ Last updated: 2019-04-20T09:21:57.000Z [GoldRaccoon](http://albertodebortoli.github.io/GoldRaccoon/?ref=albertodebortoli.com) is the iOS component to connect to a FTP service and do the following: - Download a file - Upload a file - Delete a file - Create a directory - Delete a directory - List a directory ## Why another Raccoon? First, because the humanity needs it. This project started on 29/06/2013 for the Objective-C Hackathon ([http://objectivechackathon.appspot.com/](http://objectivechackathon.appspot.com/?ref=albertodebortoli.com)). [GoldRaccoon](https://github.com/albertodebortoli/GoldRaccoon?ref=albertodebortoli.com) aims to be an evolution of [BlackRaccoon](https://github.com/lloydsargent/BlackRaccoon?ref=albertodebortoli.com) (which is an evolution of [WhiteRaccoon](https://github.com/valentinradu/WhiteRaccoon?ref=albertodebortoli.com)), maybe the best (or at least one of the few) third-party component out there for handling FTP operations on iOS. I forked the public repo of BlackRaccooon in May 2013 and added some improvements that have been merged into master to BlackRaccoon. Even though BlackRaccoon does what it says, I prefer to clean it a little and use a different and more extensible code structure. Most of the code is therefore written by [Valentin Radu](https://github.com/valentinradu?ref=albertodebortoli.com) and [Lloyd Sargent](https://github.com/lloydsargent?ref=albertodebortoli.com), the main extensions I ([Alberto De Bortoli](https://github.com/albertodebortoli?ref=albertodebortoli.com)) added are: - Done some deep refactoring for the bloating of the previous code; - Added missing (and reasonable) code conventions; - Added GRRequestsManager to manage all the different kind of requests using a FIFO queue; - Added a demo project. ## Usage If you'd like to include this component as a pod using [CocoaPods](http://cocoapods.org/?ref=albertodebortoli.com), just add the following line to your Podfile: `pod "GoldRaccoon"` otherwise - copy Sources folder into your project - add CFNetwork framework - import `GRRequestsManager.h` in your class - add a property for the manager ```objective-c @property (nonatomic, strong) GRRequestsManager *requestsManager; ``` - setup the manager somewhere (with hostname, username and password) ```objective-c self.requestsManager = [[GRRequestsManager alloc] initWithHostname: user: password:]; ``` - optionally make your class conform to `GRRequestsManagerDelegate`, implement the delegate methods (basically success, failure and progress callbacks) and set your instance of this class as delegate for the manager ```objective-c self.requestsManager.delegate = self; ``` - add the requests to the manager using the following methods: ```objective-c addRequestForListDirectoryAtPath: addRequestForCreateDirectoryAtPath: addRequestForDeleteFileAtPath: addRequestForDeleteDirectoryAtPath: addRequestForDownloadFileAtRemotePath:toLocalPath: addRequestForUploadFileAtLocalPath:toRemotePath: ``` - start the manager ```objective-c [self.requestsManager startProcessingRequests]; ``` ## Conclusion While we know that FTP is going obsolete more and more every day, we also ackowledge the fact that legacy technologies are always gonna stick around for longer than desired. ### Objective-C, Zen and some satisfaction URL: https://albertodebortoli.com/2014/07/11/objective-c-zen-and-some-satisfaction/ Last updated: 2018-02-10T15:17:36.000Z I'm very proud to announce my last work with [Luca Bernardi](http://twitter.com/luka%5Fbernardi?ref=albertodebortoli.com) ![zen-logo-thumb](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/zen-logo-thumb.png) **"Zen and the Art of the Objective-C Craftsmanship"** Available on [GitHub](https://github.com/objc-zen/objc-zen-book?ref=albertodebortoli.com). We started writing this book on November 2013\. The initial goal was to provide guidelines to write the most clean Objective-C code possible: there are too many guidelines out there and all of them are debatable. We didn't aim introducing hard rules but, instead, a way for writing code to be more uniform as possible across different developers. With time the scope moved to explain how to design and architecture good code. The idea underneath is that the code should not only compile, instead it should "validate". Good code has several characteristics: should be concise, self-explanatory, well organized, well documented, well named, well designed and stand the test of time. The main goals behind the curtain are that clarity always wins over performance and a rationale for a choice should always be provided. Some topics discussed here are general and independent from the language even if everything is tied up to Objective-C. Then something happened... On June 6th, 2014 Apple announced the new programming language to be used for iOS and Mac development in future: Swift. This new language is a radical departure from Objective-C and, of course, has caused a change in our plan for writing this book. It boiled down to the decision of releasing the current status of this essay without continuing our journey in unfolding the topics we originally planned to include. Objective-C is not going anywhere but at the same time continuing to write a book on a language that will not receive the same attention as it used to, is not a wise move. During the very first 2 days after the release, the fuzz in the iOS community on Twitter was great! We really hope you will enjoy it and will improve your craftsmanship skills ;-) ### Road to circular progress pull to refresh at Beamly URL: https://albertodebortoli.com/2014/06/30/road-to-circular-progress-pull-to-refresh-at-beamly/ Last updated: 2018-02-10T16:11:35.000Z ## Pull to refresh, this friend of ours The Pull to refresh became one of the most popular concepts used in mobile iOS apps. [Loren Brichter](https://twitter.com/lorenb?ref=albertodebortoli.com), the author of Tweetie for iOS introduced it for the first time in 2011 and it stood the test of time. Several implementations of the pull to refresh lie out there and the most used on iOS is for sure the [SVPullToRefresh](https://github.com/samvermette/SVPullToRefresh?ref=albertodebortoli.com) by [Sam Vermette](http://samvermette.com/?ref=albertodebortoli.com). Back in 2012, the concepts of Objective-C runtime and associated objects were still obscure to most of the iOS developers but Sam used the properly to add an extra view to the UIScrollView without the need for subclassing. Apple built a native pull to refresh publicly available as of iOS 6, called [UIRefreshControl](https://developer.apple.com/library/ios/documentation/uikit/reference/UIRefreshControl%5Fclass/Reference/Reference.html?ref=albertodebortoli.com), but customizations are hard to achieve and still, too often developers fallback to an ad hoc implementations. The most common customization is implementing a circular progress view like the one used in the Pinterest app. This leads to a much cooler UI rather than the well-known yet obsolete rotating arrow, and it is recognizable and intuitive to all iOS users. The concept proposed here has two main individual transitions that are dependent about the position of the finger: 1. App logo becomes visible (alpha/opacity property) 2. Circle progress becomes filled You can see the final behaviour in the gif below, but I definitely recommend downloading and running the [Beamly](https://itunes.apple.com/us/app/beamly-tv-by-zeebox/id513267737?mt=8&ref=albertodebortoli.com) iOS app by yourself to get the right feeling. ![pullToRefresh](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/pullToRefresh.gif) ## Yes... yes... calm down, we have it open sourced Go crazy with it on GitHub: [BMYCircularProgressPullToRefresh](https://github.com/beamly/BMYCircularProgressPullToRefresh?ref=albertodebortoli.com) ![ptr_1](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/ptr_1.png) ![ptr_2](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/ptr_2.png) ![ptr_3](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/ptr_3.png) ![ptr_4](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/ptr_4.png) The first transition is very straighforward. It is just changing the opacity of the layer. The second one is more complicated and we need two images with the full circles: 1. Light color circle image (for progress not filled) 2. Dark color circle image (for progress filled) the trick is to leave the first one untouched and to mask the second one based on the progress we want to display (that is dictated by how much the user scrolls). The mask we want to use is a a pie shape that become a full filled circle when the progress is 100%. We are developers and we talk code, so, here we go: ```objectivec - (void)_updatePie:(CAShapeLayer *)layer forAngle:(CGFloat)degrees { CGFloat angle = degToRad(-90); CGPoint center = CGPointMake(CGRectGetWidth(layer.frame)/2.0, CGRectGetWidth(layer.frame)/2.0); CGFloat radius = CGRectGetWidth(layer.frame)/2.0; UIBezierPath *piePath = [UIBezierPath bezierPath]; [piePath moveToPoint:center]; [piePath addLineToPoint:CGPointMake(center.x, center.y - radius)]; [piePath addArcWithCenter:center radius:radius startAngle:angle endAngle:degToRad(degrees - 90.0f) clockwise:YES]; [piePath addLineToPoint:center]; [piePath closePath]; layer.path = piePath.CGPath; } ``` The other big part of this component is the actual pull to refresh. Well, in the end there is no big surprise about it and the main idea behind the SVPullToRefresh has been used (just this implementation is nicer, protocol-based and with consistent code style, while the SVPullToRefresh is absolutely a combination of spaghetti and messed up code. Yes, I just said that). But again... I think that having an associated view on the UIScrollView is the only way to implement it properly. This is the first bit of code we open source at Beamly, and I'm kind of proud to be the one who pushed for it :) A peculiarity about it is the support for custom contentInsets on the underlying scrollview (as we needed in the Beamly app). I initially struggled on it for hours failing at least twice, eventually my smart colleague [Stefan Dougan-Hyde](https://github.com/stefandouganhyde?ref=albertodebortoli.com) added the [support](https://github.com/beamly/BMYCircularProgressPullToRefresh/commit/39206d52dcdcbd73968601bd20e954a4a16c22b8?ref=albertodebortoli.com) for it. ### Asynchronous message passing with Actors in Objective-C URL: https://albertodebortoli.com/2014/05/20/asynchronous-message-passing-with-actors-in-objective-c/ Last updated: 2018-02-12T23:25:30.000Z ## Actors, these strangers Although we are all in love with Objective-C, the power of a language itself is given by its inner features. Languages like [Ada](https://www.adacore.com/adaanswers/about/ada?ref=albertodebortoli.com) have a built-in concurrency model, while Objective-C needs external libraries (let's say `libdispatch`) to try to achieve the same power of expression found in richer languages. The same happened for the implementation of the Actor Model. The standout language for the feature of asynchronous message passing using the actor model is Erlang. From [Wikipedia](http://en.wikipedia.org/wiki/Actor%5Fmodel?ref=albertodebortoli.com): > The actor model in computer science is a mathematical model of concurrent computation that treats "actors" as the universal primitives of concurrent digital computation: in response to a message that it receives, an actor can make local decisions, create more actors, send more messages, and determine how to respond to the next message received. That said, languages like Ada and Erlang are semantically more powerful than Objective-C, as some features are expressed at the language level rather than through libraries provided in the user space. Adopting the Actor Model means avoiding the Object Orientation orthodoxy and forcing the developer to write software as a collection of smaller communicating programs that do not share state. Software written using the Actor Model approach is inevitably more "pure" than its traditional counterpart as the paradigm expresses a better level of abstraction, no matter which language is used. In the Actor Model concurrency paradigm, each Actor waits to receive a message. When it gets one it processes the message, and notifies – via another message – one or more other actors. Actors communicate asynchronously by message passing, rather than sharing resources or using primitive mechanisms (locks, semaphores, etc...) to guarantee mutual access to them. As is well known, the standard approach carries a high risk of race conditions, deadlocks and similar pesky problems. In their pure form, Actors can scale to thousands of threads spread out over hundreds of cores. ## Thread safety, the standard path Returning to the world of Objective-C from this little digression, a widely accepted way to deal with a locking mechanism is to use queues that provide an intrinsic way to stem the danger of thread safety. Grand Central Dispatch (GCD) should be the first choice that comes to mind, but it does not solve the thread safety issues – and the programmer still has to design carefully to guarantee the thread safety. The following code provides an intrinsic lock mechanism for a mutable data structure using a serial queue. ```objective-c @interface ThreadSafeStorage : NSObject - (id)objectForKey:(NSString *)key; - (void)addObject:(id)object forKey:(NSString *)key; - (void)removeObjectForKey:(NSString *)key; @end ``` ```objective-c @interface ThreadSafeStorage () @property (nonatomic, strong) NSMutableDictionary *data; @property (nonatomic, strong) dispatch_queue_t lockQueue; @end @implementation ThreadSafeStorage - (instancetype)init { if (self = [super init]) { _lockQueue = dispatch_queue_create("com.albertodebortoli.threadsafestorage", DISPATCH_QUEUE_SERIAL); _data = [NSMutableDictionary dictionary]; } return self; } - (id)objectForKey:(NSString *)key { __block id retVal = nil; dispatch_sync(self.lockQueue, ^{ retVal = [self.data objectForKey:key]; }); return retVal; } - (void)addObject:(id)object forKey:(NSString *)key { dispatch_async(self.lockQueue, ^{ [self.data setObject:object forKey:key]; }); } - (void)removeObjectForKey:(NSString *)key { dispatch_async(self.lockQueue, ^{ [self.data removeObjectForKey:key]; }); } @end ``` In the given class there are no explicit locks or similar primitives to guarantee mutual access, but still, any method will need to ensure that the data is accessed under mutual exclusion. This becomes particularly problematic when the class is modified at a later date and the real thread safety of the class is hard to prove. Consider a class whose responsibility is to persist data on a SQLite database, which is an example of shared data. Let's consider the threading issues dealing with different Managed Object Contexts in Core Data: one could try to solve the problem with an Actor (a thread) that intrinsically applies a lock mechanism to the shared data and thread safety. The actor semantics are learnable by most developers and 'safer' than their locked counterparts. They raise the abstraction level and allow developers to focus on coordinating access to the data rather than protecting all accesses to it with locks. ## Actors, talking business We are about to propose an Actor Model implementation inspired on the original implementation of [Valletta Ventures](https://github.com/vallettaventures/VVActors?ref=albertodebortoli.com) library. The main classes are Messages and Actors. Let's start with the Message: it is simply a wrapper for a selector. Interface file: ```objective-c @interface ADBMessage : NSObject @property (nonatomic, readonly) SEL selector; - (id)initWithSelector:(SEL)aSelector; @end ``` Implementation file: ```objective-c @interface ADBMessage () @property (nonatomic, assign) SEL selector; @end @implementation ADBMessage - (id)initWithSelector:(SEL)selector { self = [super init]; if (self) { _selector = selector; } return self; } - (id)copyWithZone:(NSZone *)zone { return self; } @end ``` The intention is to subclass `Message` to add custom fields appropriate to the message. It is important to note that in order to avoid sharing state, `Message` and any of its subclasses must be copied. The `Actor` class looks as follows: ```objective-c @interface ADBActor : NSThread @property (nonatomic, copy, readonly) NSString *uuid; - (void)executeMessage:(ADBMessage *)message; @end ``` with the following implementation: ```objective-c @interface ADBActor () @property (nonatomic, copy, readwrite) NSString *uuid; - (void)_processMessage:(ADBMessage *)message; @end @implementation ADBActor - (instancetype)init { self = [super init]; if (self) { _uuid = [[NSUUID UUID] UUIDString]; } return self; } - (void)main { @autoreleasepool { NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; BOOL shouldKeepRunning = YES; while (shouldKeepRunning && !self.isCancelled) { shouldKeepRunning = [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } } } - (void)executeMessage:(ADBMessage *)message { [self performSelector:@selector(_processMessage:) onThread:self withObject:[message copy] //copy to avoid shared state waitUntilDone:NO]; } - (NSString *)description { return [NSString stringWithFormat:@"%@ %p %@", NSStringFromClass([self class]), self, self.uuid]; } #pragma mark - Private Methods - (void)_processMessage:(ADBMessage *)message { if ([self respondsToSelector:message.selector]) { objc_msgSend(self, message.selector, message); } } @end ``` The important parts here are: - Each Actor is a `NSThread` subclass - Actors are meant to be subclassed to perform ad-hoc tasks - `executeMessage:` can be called from any thread, and this guarantees the thread safety An Actor, when booted, will start an NSRunLoop which will be used to dispatch any queued messages to the correct method. Using the NSRunLoop we will idle for free when no messages are queued. `executeMessage:` is used to pass a message to an Actor, which copies the message to avoid shared state and places the call in the run loop. When called by the run loop the `_processMessage:` selector calls the selector specified in the message with the message as an argument. The message is automatically released after the method has returned. ## Let me play, please The easiest way to see this in action is with a super simple example (taken from the original implementation of [Valletta Ventures](http://vallettaventures.com/2012/03/04/asynchronous-message-passing-in-Objective-C/?ref=albertodebortoli.com) library. First, subclass Actor: ```objective-c @interface TestActor : ADBActor @property (nonatomic, assign) ADBActor *nextInChain; - (void)passItOn:(ADBMessage *)message; @end ``` ```objective-c @implementation TestActor - (void)passItOn:(ADBMessage *)message { NSLog(@"<%@> passes to <%@>", self, self.nextInChain); [self.nextInChain executeMessage:message]; } @end ``` then create some actors, put them in a chain and fire the message passing: ```objective-c TestActor *actor1 = [[TestActor alloc] init]; TestActor *actor2 = [[TestActor alloc] init]; TestActor *actor3 = [[TestActor alloc] init]; actor1.nextInChain = actor2; actor2.nextInChain = actor3; actor3.nextInChain = actor1; [actor1 start]; [actor2 start]; [actor3 start]; ADBMessage *message = [[ADBMessage alloc] initWithSelector:@selector(passItOn:)]; [actor1 executeMessage:message]; sleep(10); [actor1 cancel]; [actor2 cancel]; [actor3 cancel]; ``` These actor will keep passing the message between them for 10 seconds, after which the actors will be cancelled. The full code can be found on GitHub in my [ADBActors repository](https://github.com/albertodebortoli/ADBActors?ref=albertodebortoli.com) ## Uh, cool stuff, so... You could consider to use the actor paradigm when: - you can decompose your problem into a set of independent tasks linked by a clear workflow - you want to be able to cancel jobs - you want to scale across threads and cores - you have a complex system that involves dependencies and shared state - you want to avoid the usage of explicit locks to protect shared state and actually make copies of that state (messages) and reacting to them locally - you want to code components that are intrinsically thread safe Thanks to [Balazs Balazs](https://github.com/balazsbalazs?ref=albertodebortoli.com), [Mark Hatton](https://github.com/markhatton?ref=albertodebortoli.com), [Joel Overton](https://github.com/joeloverton?ref=albertodebortoli.com), [Christopher Lyon Anderson](https://github.com/lyonanderson?ref=albertodebortoli.com) and [Luca Bernardi](https://github.com/lukabernardi?ref=albertodebortoli.com) for reviewing the article. ### An Aspect Oriented Programming approach to iOS Analytics URL: https://albertodebortoli.com/2014/03/25/an-aspect-oriented-programming-approach-to-ios-analytics/ Last updated: 2018-02-10T16:09:11.000Z **\[Update 09/06/2014\]** *On May 2014 [Peter Steinberger](https://twitter.com/steipete?ref=albertodebortoli.com) released [Aspects](https://github.com/steipete/Aspects?ref=albertodebortoli.com) inspired (a little :-) by this article and [Orta](https://twitter.com/orta?ref=albertodebortoli.com) and [Ash Furrow](https://twitter.com/ashfurrow?ref=albertodebortoli.com) improved [ARAnalytics](https://github.com/orta/ARAnalytics?ref=albertodebortoli.com) with a DSL based, again, on this article -> [Tweet](https://twitter.com/orta/status/463630336430469120?ref=albertodebortoli.com)* ![orta-tweet](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/orta-tweet.png) Analytics are a popular "feature" to include in iOS projects, with a huge variety of choices ranging from Google Analytics, Flurry, MixPanel, etc. Most of them have tutorials describing how to track specific views and events including a few lines of code inside each class. On Ray Wenderlich's blog there is a long [article](http://www.raywenderlich.com/53459/google-analytics-ios?ref=albertodebortoli.com) with some sample code to include in your view controller in order to track an event with [Google Analytics](https://developers.google.com/analytics/devguides/collection/ios/?ref=albertodebortoli.com): ```objective-c - (void)logButtonPress:(UIButton *)button { id tracker = [[GAI sharedInstance] defaultTracker]; [tracker send:[[GAIDictionaryBuilder createEventWithCategory:@"UX" action:@"touch" label:button.titleLabel.text value:nil] build]]; } ``` The code above sends an event with context information whenever a button is tapped. Things get worse when you want to track a screen view: ```objective-c - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; id tracker = [[GAI sharedInstance] defaultTracker]; [tracker set:kGAIScreenName value:@"Stopwatch"]; [tracker send:[[GAIDictionaryBuilder createAppView] build]]; } ``` This always looked like code smell to me. Do you see the nasty thing here? We are actually making the view controller dirty adding lines of code that should not belong there it as it's not responsibility of the view controller to track events. You could argue that you usually have a specific object responsible for analytics tracking and you inject this object inside the view controller but the problem is still there and no matter where you hide the tracking logic: you eventually end up inserting some lines of code in the `viewDidAppear:`. Here comes the idea. The idea is based on AOP, [Aspect Oriented Programming](http://en.wikipedia.org/wiki/Aspect-oriented%5Fprogramming?ref=albertodebortoli.com). From Wikipedia: > An aspect can alter the behavior of the base code (the non-aspect part of a program) by applying advice (additional behavior) at various join points (points in a program) specified in a quantification or query called a pointcut (that detects whether a given join point matches). In the world of Objective-C this means using the runtime features to add *aspects* to specific methods. The additional behaviours given by the aspect can be either: - add code to be performed before a specific method call on a specific class - add code to be performed after a specific method call on a specific class - add code to be performed instead of the original implementation of a specific method call on a specific class There a few implementations of AOP libraries for Objective-C out there and most of them are failed attempts. The only one I found extremely reliable and well-designed comes from Andras [codeshaker](http://codeshaker.blogspot.co.uk/?ref=albertodebortoli.com) at this [blog post](http://codeshaker.blogspot.co.uk/2012/01/aop-delivered.html?ref=albertodebortoli.com) and on [GitHub](https://github.com/ndcube/AOP-for-Objective-C?ref=albertodebortoli.com). We are going to use this library, you can do it through CocoaPods using my [public pods repo](https://github.com/albertodebortoli/ADBCocoaPodsRepository?ref=albertodebortoli.com) or copying the [podspec](https://github.com/albertodebortoli/ADBCocoaPodsRepository/blob/master/AOPAspect/1.0.1/AOPAspect.podspec?ref=albertodebortoli.com) I've created for it and add it to your personal repo of internal pods. The AOPAspect library does some cool magic with the runtime, replacing and adding methods (further tricks over the method swizzling technique). The API of AOPAspect are interesting and powerful: ```objective-c - (NSString *)interceptClass:(Class)aClass beforeExecutingSelector:(SEL)selector usingBlock:(aspect_block_t)block; - (NSString *)interceptClass:(Class)aClass afterExecutingSelector:(SEL)selector usingBlock:(aspect_block_t)block; - (NSString *)interceptClass:(Class)aClass insteadExecutingSelector:(SEL)selector usingBlock:(aspect_block_t)block; ``` For our purposes we need to use just the following method on the singleton instance: ```objective-c [[AOPAspect instance] interceptClass:[MyClass class] afterExecutingSelector:@selector(myMethod:) usingBlock:^(NSInvocation *invocation) { ... }]; ``` The code above will perform the block parameter after the execution of the instance method `myMethod:` on the class `MyClass` (the author pointed out that class methods are still an issue to be solved). In other words: the code provided in the block parameter will always be executed after each call of the `@selector` parameter on any object of type `MyClass`. We added an aspect on `MyClass` for the method `myMethod:`. Wow! This is a perfect example to apply AOP to track screen views on specific `viewDidAppear:` methods! Moreover, we could use the same approach to add event tracking in other methods we are interested in, for instance when the user taps on a button (i.e. trivially calling the corresponding IBAction). This approach is clean and unobtrusive: - the view controllers will not get dirty with code that does not naturally belongs to them - it becomes possible to specify a SPOC file (single point of customization) for all the aspects to add to our code - the SPOC should be used to add the aspects at the very startup of the app - if the SPOC file is malformed and at least one selector or class is not recognized, the app will crash at startup (which is cool for our purposes) - the team in the company responsible for managing the analytics usually provides a document with the list of *things* to track; this document could then be easily mapped to a SPOC file - as the logic for the tracking is now abstracted, it becomes possible to scale with a grater number of analytics providers - for screen views it is enough to specify in the SPOC file the classes involved (the corresponding aspect will be added to the `viewDidAppear:` method), for events it is necessary to specify the selectors. To send both screen views and events, a tracking label and maybe extra meta data are needed to provide extra information (depending on the analytics provider). We may want a SPOC file similar to the following (also a .plist file would perfectly fit as well): ```objective-c NSDictionary *analyticsConfiguration() { return @{ @"trackedScreens" : @[ @{ @"class" : @"ADBMainViewController", @"label" : @"Main screen" } ], @"trackedEvents" : @[ @{ @"class" : @"ADBMainViewController", @"selector" : @"loginViewFetchedUserInfo:user:", @"label" : @"Login with Facebook" }, @{ @"class" : @"ADBMainViewController", @"selector" : @"loginViewShowingLoggedOutUser:", @"label" : @"Logout with Facebook" }, @{ @"class" : @"ADBMainViewController", @"selector" : @"loginView:handleError:", @"label" : @"Login error with Facebook" }, @{ @"class" : @"ADBMainViewController", @"selector" : @"shareButtonPressed:", @"label" : @"Share button" } ] }; } ``` The architecture proposed is hosted on GitHub on the [EF Education First](https://github.com/ef-ctx/JohnnyEnglish/blob/master/Sources/CTXUserActivityTrackingManager.m?ref=albertodebortoli.com) profile. This was for sure the most cool tech thing I've coded this year so far and I hope you enjoyed this absolutely-not-well-known approach. That said, there are some cons when adding aspects with the AOP library: - when adding a screen view, all instances of that view controller will be tracked (there may be very rare cases when you want to track only specific instances, in this case different classes should be used) - when adding an event, the code base should provide a specific method to track (often the analytics tracking in legacy code is drowned inside blobs of code, in this case the code should be refactored properly to accommodate the need) - when adding a screen view or an event in legacy code bases there might be some conditional code to decide if do the tracking or not, in these cases the code should be refactored properly - when adding a screen view or an event, in legacy code bases there might be some math or logic to gather values to track as metadata: this can't be achieved with our approach because of the runtime calculations Give this approach a try using the [source code provided](https://github.com/ef-ctx/JohnnyEnglish/blob/master/Sources/CTXUserActivityTrackingManager.m?ref=albertodebortoli.com) and you'll be surprised, astonished, delighted. Enjoy. ### CocoaPods: working with internal pods without hassle URL: https://albertodebortoli.com/2014/03/11/cocoapods-working-with-internal-pods-without-hassle/ Last updated: 2019-12-14T17:09:29.000Z [CocoaPods](http://cocoapods.org/?ref=albertodebortoli.com) is cool. I was honoured to have a chat with [Fabio Pelosin](http://twitter.com/fabiopelosin?ref=albertodebortoli.com), the main contributor, at the [NSLondon](http://www.meetup.com/NSLondon/?ref=albertodebortoli.com) meetup some time ago and see how much passion those guys put in this project. I was one of the first supporters back in 2012 and I have some Pods in the Specs repo (ADB prefixed). Recently, I spent several days going through some hidden aspects of CocoaPods, ending up reading some source code from the Core and Xcodeproj. During one of the recent [NSLondon(s)](http://www.meetup.com/NSLondon/?ref=albertodebortoli.com) of 2013, [Orta](http://twitter.com/orta?ref=albertodebortoli.com) explained the advantages of CocoaPods and [Abizer Nasir](http://twitter.com/abizern?ref=albertodebortoli.com), in one of the following events, discussed the usage of Git submodules. Basically comparing their visions. What I'm going to explain here is a solution to a common scenario: **Manage the versioning of internal private pods within projects without hassle.** Ok, let's start. You have your project under our DVCS (which I hope is Git for your sanity). ``` ~/MyProject └── .git └── Sources └── MyProject.xcodeproj ``` You use some third-party components like AFNetworking and MagicalRecords and you have submodules for that, you hipster! ``` ~/MyProject └── .git └── Sources └── MyProject.xcodeproj └── Vendor └── AFNetworking (submodule) └── .git └── MagicalRecord (submodule) └── .git ``` You decide to use CocoaPods, install it following the [instructions](http://guides.cocoapods.org/using/getting-started.html?ref=albertodebortoli.com) and you add the `Podfile`. ```ruby platform :ios pod 'AFNetworking', '~> 2.0' pod 'MagicalRecord', '~> 2.2' ``` You remove the submodules and run `pod install`: the workspace is created for you and you're good to go. So far so good. ``` ~/MyProject └── .git └── Podfile └── Sources └── MyProject.xcodeproj └── MyProject.xcworkspace └── Pods └── Pods.xcodeproj └── AFNetworking └── MagicalRecord ``` Then you realize that it'd be cool to have your own private repo of pods and create private pods for some parts of your project. You like modular things and maybe, one day your pods will be ready for a pull request to the Specs repo to contribute to the open source community. So you create your own repo (mine is [https://github.com/albertodebortoli/ADBCocoaPodsRepository](https://github.com/albertodebortoli/ADBCocoaPodsRepository?ref=albertodebortoli.com)) and add it to CocoaPods. ``` $ pod repo add REPO_NAME SOURCE_URL ``` Time to create a spec for your [private pod](http://guides.cocoapods.org/making/index.html?ref=albertodebortoli.com), tag it and push it. Something like this: ```ruby Pod::Spec.new do |s| s.name = 'MyInternalLibrary' s.version = '1.0.0' s.platform = :ios, '7.0' s.summary = 'My first pod!!!!111' s.homepage = 'https://github.com/me/MyInternalLibrary' s.author = { 'John Doe' => 'john.doe@example.com' } s.source = { :git => 'https://github.com/me/MyInternalLibrary.git', :tag => s.version.to_s } s.license = { :type => 'New BSD License', :file => 'LICENSE' } s.source_files = '*.{h,m}' s.requires_arc = true end ``` `pod install` and the situation is as follows: ``` ~/MyProject └── .git └── Podfile └── Sources └── MyProject.xcodeproj └── MyProject.xcworkspace └── Pods └── Pods.xcodeproj └── AFNetworking └── MagicalRecord └── MyInternalLibrary ~/MyInternalLibrary └── .git └── MyInternalLibrary.podspec └── Sources └── MyInternalLibraryDemo.xcodeproj ``` You want to put the `Pods` folder in the `.gitignore` file. Cool! But... there's a but. You're actively developing on `MyInternalLibrary`, you're touching those files several times a day. The files you're touching will be overwritten the next time you run `pod install`. Oh shit. You don't wanna open `~/MyInternalLibrary`, touch things, `pod install` `~/MyProject` over and over. It's not practicable. Solution is to use *development pods*. When you install a development pod, its files are symbolically linked within `Pods.xcodeproj`. The `Podfile`, now, specifies that the pod needs to be fetched from a local directory (with the use of the `:path` directive). A development pod does not require to be versioned with a Git repo, but it must contain the podspec which describes how to retrieve the pod files. This is your new `Podfile`: ```ruby platform :ios pod 'AFNetworking', '~> 2.0' pod 'MagicalRecord', '~> 2.2' pod 'MyInternalLibrary', :path => '~/MyInternalLibrary' ``` That's great, now you can touch things safely and you can commit changes to `MyInternalLibrary` (in `~/MyInternalLibrary`) while you are working on `MyProject.xcworkspace`. Here comes the interesting bit. You have a CI and deployment system that need to have a specific version of your internal pod. You are tempted to tag changes to `MyInternalLibrary` when deploying and use a specific `Podfile` in your release branch of `MyProject`. ```ruby platform :ios pod 'AFNetworking', '~> 2.0' pod 'MagicalRecord', '~> 2.2' pod 'MyInternalLibrary', '~> 1.0.1' ``` Yeah... yeah... you're cool. No, you're not. Do you see the problem here? Keep the versioning/tagging of the internal pods updated is a pain in the ass. Developer's time should never be wasted this way. I see you thinking... > "Damn, pods are not quite right here. I'm a hipster and a submodule would be perfect! But ufff... submodules or CocoaPods? Uff uff...". The answer is... use both! Is the versioning/tagging of your library (just for accommodating CocoaPods) complex? Well, let the submodules handle it for you (using the `sha1` of the commit, you know... Git things to point to the correct revision). The ideal solution is to put your internal library inside the folder of the project and treat it as a git submodule: ``` ~/MyProject └── .git └── Podfile └── Sources └── MyProject.xcodeproj └── MyProject.xcworkspace └── MyInternalLibrary (submodule) └── .git └── MyInternalLibrary.podspec └── Sources └── MyInternalLibraryDemo.xcodeproj └── Pods └── Pods.xcodeproj └── AFNetworking └── MagicalRecord └── MyInternalLibrary ``` Your `Podfile` will be ```ruby platform :ios pod 'AFNetworking', '~> 2.0' pod 'MagicalRecord', '~> 2.2' pod 'MyInternalLibrary', :path => './MyInternalLibrary' ``` When running `pod install`, files in `~/MyProject/MyInternalLibrary` will be symbolically linked within `Pods.xcodeproj`. Now you can - work on your project and change your libraries/pods while developing - commit changes to your libraries/pods independently of your project - tag/version your internal libraries/pods when you want to (not when it is requested by your project) - avoid the need of a specific Podfile for the release branch on MyProject People way too often want to embrace CocoaPods fully, others prefer just submodules (and they are wrong, period). I think that the pragmatic solution to edge cases like this one is to use both! Work as if it was just a submodule but with the unleashed power of CocoaPods! ### Objective-C blocks caveat URL: https://albertodebortoli.com/2013/08/03/objective-c-blocks-caveat/ Last updated: 2018-02-10T15:43:43.000Z I like blocks. I really do. It's been at least two years now I've been using them and I still make syntax mistakes, but I like them. Recently I had a long discussion with my colleagues and friends about the use of self inside blocks. I can hear \[you\]\[8\] saying: "C'mon dude, it's sooo straightforward and ridiculous!". I'm sure there are some subtle things to consider about the `__weak` and the `__strong` qualifiers for self inside blocks. ### Preface As known by humanity, we can refer to self in three different ways inside a block. Here are the three cases: 1. using the keyword `self` directly inside the block 2. declaring a `__weak` reference to self outside the block and referring to the object via this weak reference inside the block 3. declaring a `__weak` reference to self outside the block and creating a `__strong` reference to self using the weak reference inside the block Well, in this article I'll try to describe these usages and give my take to them. ### Discussion #### Case 1: using the keyword `self` inside a block If we use directly the keyword `self` inside a block, the object is retained at block declaration time within the block (actually when the block is [copied](https://developer.apple.com/library/ios/documentation/cocoa/conceptual/Blocks/Articles/bxVariables.html?ref=albertodebortoli.com#//apple%5Fref/doc/uid/TP40007502-CH6-SW4) but for sake of simplicity we can forget about it) . A const reference to self has its place inside the block and this affects the reference counting of the object. If the block is used by other classes and/or passed around we may want to retain self as well as all the other objects used by the block since they are *needed* for the execution of the block. ```objective-c dispatch_block_t completionHandler = ^{ NSLog(@"%@", self); } MyViewController *myController = [[MyViewController alloc] init...]; [self presentViewController:myController animated:YES completion:completionHandler]; ``` No big deal. But... what if the block is retained by self in a property (as the following example) and therefore the object (self) is retained by the block? ```objective-c self.completionHandler = ^{ NSLog(@"%@", self); } MyViewController *myController = [[MyViewController alloc] init...]; [self presentViewController:myController animated:YES completion:self.completionHandler]; ``` This is what is well known as a retain cycle and retain cycles usually should be avoided. The warning we receive from CLANG is: ```objective-c Capturing 'self' strongly in this block is likely to lead to a retain cycle ``` Here comes in the `__weak` qualifier. #### Case 2: declaring a `__weak` reference to self outside the block and use it inside the block Declaring a `__weak` reference to self outside the block and referring to it via this weak reference inside the block avoids retain cycles. This is what we usually want to do if the block is already retained by self in a property. ```objective-c __weak typeof(self) weakSelf = self; self.completionHandler = ^{ NSLog(@"%@", weakSelf); }; MyViewController *myController = [[MyViewController alloc] init...]; [self presentViewController:myController animated:YES completion:self.completionHandler]; ``` In this example the block does not retain the object and the object retains the block in a property. Cool. We are sure that we can refer to self safely, at worst, it is nilled out by someone. The question is: how is it possible for self to be "destroyed" (deallocated) within the scope of a block? Consider the case of a block being copied from an object to another (let's say myController) as a result of the assignment of a property. The former object is then released before the copied block has had a chance to execute. The next step is interesting. #### Case 3: declaring a `__weak` reference to self outside the block and use a `__strong` reference inside the block You may think, at first, this is a trick to use self inside the block avoiding the retain cycle warning. This is not the case. The reference to self is created at *block execution time* while using self in the block is evaluated at *block declaration time*, thus retaining the object. [Apple documentation](http://developer.apple.com/library/mac/?ref=albertodebortoli.com#releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html) says that "For non-trivial cycles, however, you should use" this approach: ```objective-c MyViewController *myController = [[MyViewController alloc] init...]; // ... MyViewController * __weak weakMyController = myController; myController.completionHandler = ^(NSInteger result) { MyViewController *strongMyController = weakMyController; if (strongMyController) { // ... [strongMyController dismissViewControllerAnimated:YES completion:nil]; // ... } else { // Probably nothing... } }; ``` What is the meaning of "trivial block" for Apple? It is my understanding that a trivial block is a block that is not passed around, it's used within a well defined and controlled scope and therefore the usage of the weak qualifier is just to avoid a retain cycle. On the other hand the meaning of "non-trivial block" is a block where the weak reference is used more than once. As said at the beginning of this article, I made [a](http://dhoerl.wordpress.com/2013/04/23/i-finally-figured-out-weakself-and-strongself/?ref=albertodebortoli.com) [lot](http://blog.random-ideas.net/?p=160&ref=albertodebortoli.com) [of](http://stackoverflow.com/questions/7904568/disappearing-reference-to-self-in-a-block-under-arc?ref=albertodebortoli.com) [researches](http://stackoverflow.com/questions/12218767/objective-c-blocks-and-memory-management?ref=albertodebortoli.com) [online](https://github.com/AFNetworking/AFNetworking/issues/807?ref=albertodebortoli.com), checked some books ([Effective Objective-C 2.0](http://www.effectiveobjectivec.com/?ref=albertodebortoli.com) by [Matt Galloway](https://twitter.com/mattjgalloway?ref=albertodebortoli.com) and [Pro Multithreading and Memory Management for iOS and OS X](http://www.amazon.it/Pro-Multithreading-Memory-Management-Ios/dp/1430241160?ref=albertodebortoli.com) by Kazuki Sakamoto & Tomohiko Furumoto) and discussions with [some](https://twitter.com/andreagiavatto?ref=albertodebortoli.com) \[developers\]\[8\] and I came up realizing that the real benefit of using the strong reference inside of a block is to be robust to preemption. I'll explain this going through the cases I figured out when it is possible for self to 'disappear' (i.e. to be deallocated) during the execution of a block: ### Explanation #### Case 1: using the keyword `self` inside a block: If the block is retained by a property, a retain cycle is created between self and the block and both objects can't be destroyed anymore. If the block is passed around and copied by others, self is retained for each copy. #### Case 2: declaring a `__weak` reference to self outside the block and use it inside the block: There is no retain cycle and no matter if the block is retained or not by a property. If the block is passed around and copied by others, when executed, weakSelf can have been turned nil. The execution of the block can be preempted and different subsequent evaluations of the weakSelf pointer can lead to different values (i.e. weakSelf can become nil at a certain evaluation). ```objective-c __weak typeof(self) weakSelf = self; dispatch_block_t block = ^{ [weakSelf doSomething]; // weakSelf != nil // preemption, weakSelf turned nil [weakSelf doSomethingElse]; // weakSelf == nil }; ``` #### Case 3: declaring a `__weak` reference to self outside the block and use a `__strong` reference inside the block: There is no retain cycle and, again, no matter if the block is retained or not by a property. If the block is passed around and copied by others, when executed, weakSelf can have been turned nil. When the strong reference is assigned and it is not nil, we are sure that the object is retained for the entire execution of the block if preemption occurs and therefore subsequent evaluations of strongSelf will be consistent and will lead to the same value since the object is now retained. If strongSelf evaluates to nil usually the execution is returned since the block cannot execute properly. ```objective-c __weak typeof(self) weakSelf = self; myObj.myBlock = ^{ __strong typeof(self) strongSelf = weakSelf; if (strongSelf) { [strongSelf doSomething]; // strongSelf != nil // preemption occurs, strongSelf still not nil [strongSelf doSomethingElse]; // strongSelf != nil } else { // Probably nothing... return; } }; ``` [Andrea Giavatto](https://twitter.com/andreagiavatto?ref=albertodebortoli.com) showed me also that in an ARC-based environment, the compiler itself alerts us with an error if trying to access an instance variable using the `->` notation. The error is very clear and leaves no room for any doubt: ```objective-c Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first. ``` It can be shown with the following code: ```objective-c __weak typeof(self) weakSelf = self; myObj.myBlock = ^{ id localVal = weakSelf->someIVar; }; ``` ### TL;TR **Case 1** should be used only when the block is not assigned to a property, otherwise it will lead to a retain cycle. **Case 2** should be used when the block is assigned to a property and self is referenced only once and the block has a single statement. **Case 3** should be used when the block is assigned to a property and self is referenced more the once and the block has more than a statement. I really have to thank all my colleagues at [EF](https://ef.com/?ref=albertodebortoli.com) and especially [Andrea Giavatto](https://twitter.com/andreagiavatto?ref=albertodebortoli.com) for the time spent talking about this topic and for reviewing this article. ### Objective-C blocks under the hood URL: https://albertodebortoli.com/2013/04/21/objective-c-blocks-under-the-hood/ Last updated: 2018-02-10T17:09:34.000Z Recently I was asked to describe the 'under the hood' memory management of variable and objects in blocks. I think that in 2013 almost every iOS dev knows the practical usage and effect of putting the `__block` keyword before the declaration of a variable, but maybe what's happening for real is not so straightforward. Heads up: - Blocks are created on the stack - Blocks can be copied to the heap - Blocks have their own private const copies of stack variables (and pointers) - Mutable stack variables and pointers must be declared with the \_\_block keyword If blocks aren’t kept around anywhere, will remain on the stack and will go away when their stack frame returns. While on the stack, a block has no effect on the storage or lifetime of anything it accesses. If blocks need to exist after the stack frame returns, they can be copied to the heap and this action is an explicit operation. This way, a block will gain reference-counting as all objects in Cocoa. When they are copied, they take their captured scope with them, retaining any objects they refer. If a block references a stack variable or pointer, then when the block is initialized it is given its own copy of that variable declared const, so assignments won't work. When a block is copied, the `__block` stack variables it reference are copied to the heap and after the copy operation both block on the stack and brand new block on the heap refer to the variables on the heap. [Apple documentation](http://developer.apple.com/library/ios/?ref=albertodebortoli.com#documentation/cocoa/Conceptual/Blocks/Articles/00%5FIntroduction.html) helped me in some ways to make things clear, also [this article](http://ios-blog.co.uk/tutorials/programming-with-blocks-an-overview/?ref=albertodebortoli.com), but to clearly see what's going on in memory I arrange a little exercise: ```objective-c void (^function())() { int i1 = 4; __block int bi1 = 8; NSMutableString *ms1 = [@"mutable string 1" mutableCopy]; __block NSMutableString *bms1 = [@"__block mutable string 1" mutableCopy]; MyBlock block = ^(id arg1, id barg1, int iarg1, int biarg1) { NSLog(@"*** BLOCK CALLED ***"); NSLog(@"<%p>: %i", &i1, i1); NSLog(@"<%p>: %i", &bi1, bi1); NSLog(@"<%p - %p>: %@ [%i]", &ms1, ms1, ms1, [ms1 retainCount]); NSLog(@"<%p - %p>: %@ [%i]", &bms1, bms1, bms1, [bms1 retainCount]); NSLog(@"<%p>: %i", &iarg1, iarg1); NSLog(@"<%p>: %i", &biarg1, biarg1); NSLog(@"<%p - %p>: %@ [%i]", &arg1, arg1, arg1, [arg1 retainCount]); NSLog(@"<%p - %p>: %@ [%i]", &barg1, barg1, barg1, [barg1 retainCount]); NSLog(@"*** BLOCK CALL ENDED ***"); }; NSLog(@"<%p>: %i", &i1, i1); NSLog(@"<%p>: %i", &bi1, bi1); NSLog(@"<%p - %p>: %@ [%i]", &ms1, ms1, ms1, [ms1 retainCount]); NSLog(@"<%p - %p>: %@ [%i]", &bms1, bms1, bms1, [bms1 retainCount]); // 1st block invocation block(ms1, bms1, i1, bi1); NSLog(@"*** BLOCK <%p> [%i] ***", block, [block retainCount]); MyBlock copiedBlock = [[block copy] autorelease]; NSLog(@"*** COPIED BLOCK <%p> [%i] ***", copiedBlock, [copiedBlock retainCount]); // 2nd block invocation copiedBlock(ms1, bms1, i1, bi1); i1 = 42; bi1 = 108; ms1 = [@"mutable string 1 replaced" mutableCopy]; bms1 = [@"__block mutable string 1 replaced" mutableCopy]; NSLog(@"<%p>: %i", &i1, i1); NSLog(@"<%p>: %i", &bi1, bi1); NSLog(@"<%p - %p>: %@ [%i]", &ms1, ms1, ms1, [ms1 retainCount]); NSLog(@"<%p - %p>: %@ [%i]", &bms1, bms1, bms1, [bms1 retainCount]); // 3rd block invocation block(ms1, bms1, i1, bi1); // 4th block invocation copiedBlock(ms1, bms1, i1, bi1); } ``` LLDB shows that a block is a nice piece of things. ![blocks_debugger](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/blocks_debugger.png) The most important thing to note is that `__block` variables and pointers are treated inside the block as structs that obviously handle the reference to the real value/object. Ready? Here is the log. With explanation! ``` <0xbfffdd44>: 4 // stack address, int primitive type (4) <0xbfffdd40>: 8 // stack address, int primitive type (8) <0xbfffdd2c - 0x7180120>: mutable string 1 [1] // pointer (stack) and object (heap) addresses, NSMutableString object ("mutable string 1") <0xbfffdd20 - 0x7180160>: __block mutable string 1 [1] // pointer (stack) and object (heap) addresses, NSMutableString object ("__block mutable string 1") *** BLOCK CALLED *** <0xbfffdcf4>: 4 // stack address, int primitive type (4), int value copied inside the block at declaration time as const <0xbfffdd40>: 8 // stack address, int primitive type (8), inside the block there is a reference to it <0xbfffdcfc - 0x7180120>: mutable string 1 [1] // pointer (stack, copied in block as const) and object (heap) addresses, NSMutableString object ("mutable string 1") not retainded since the block resides on the stack <0xbfffdd20 - 0x7180160>: __block mutable string 1 [1] // pointer (stack, reference to pointer copied in block) and object (heap) addresses, NSMutableString object ("__block mutable string 1") not retained <0xbfffdc10>: 4 // stack address, int primitive type (4), int arg passed by value inside the block <0xbfffdc0c>: 8 // stack address, int primitive type (8), int arg passed by value inside the block <0xbfffdc18 - 0x71b3810>: mutable string 1 [1] // pointer (stack, arg pointer value used inside the block) and object (heap) addresses, NSMutableString object ("mutable string 1") <0xbfffdc14 - 0x71b3850>: __block mutable string 1 [1] // pointer (stack, arg pointer value used inside the block) and object (heap) addresses, NSMutableString object ("__block mutable string 1") *** BLOCK CALL ENDED *** *** BLOCK <0xbfffdce0> [1] *** *** COPIED BLOCK <0x759aa90> [1] *** // block copied to the heap *** BLOCK CALLED *** <0x759aaa4>: 4 // heap address, int primitive type (4), int value copied inside the block at declaration (copy) time as const <0x759aa60>: 8 // heap address, int primitive type (8), inside the block there is a reference to it (int copied to the heap due to [block copy]) <0x759aaac - 0x7180120>: mutable string 1 [2] // pointer (heap, copied in block) and object (heap) addresses, NSMutableString object ("mutable string 1") retained since this block now lives on the heap <0x759aa88 - 0x7180160>: __block mutable string 1 [1] // pointer (heap, reference to pointer copied in block) and object (heap) addresses, NSMutableString object ("__block mutable string 1") not retained <0xbfffdc10>: 4 // stack address, int primitive type (4), int arg passed by value inside the block <0xbfffdc0c>: 8 // stack address, int primitive type (8), int arg passed by value inside the block <0xbfffdc18 - 0x7180120>: mutable string 1 [2] // pointer (stack, arg pointer value used inside the block) and object (heap) addresses, NSMutableString object ("mutable string 1") <0xbfffdc14 - 0x7180160>: __block mutable string 1 [1] // pointer (stack, arg pointer value used inside the block) and object (heap) addresses, NSMutableString object ("__block mutable string 1") *** BLOCK CALL ENDED *** <0xbfffdd44>: 42 // stack address, int primitive type (4) <0x759aa60>: 108 // heap address, int primitive type (8) (int copied to the heap due to [block copy]) <0xbfffdd2c - 0x755a580>: mutable string 1 replaced [1] // pointer (stack) and object (heap) addresses, NSMutableString object ("mutable string 1 replaced") <0x759aa88 - 0x75595a0>: __block mutable string 1 replaced [1] // pointer (stack) and object (heap) addresses, NSMutableString object ("__block mutable string 1 replaced") *** BLOCK CALLED *** <0xbfffdcf4>: 4 // stack address, int primitive type (4), int value copied inside the block at declaration time <0x759aa60>: 108 // heap address, int primitive type (8), inside the block there is a reference to it (int copied to the heap due to [block copy]) <0xbfffdcfc - 0x7180120>: mutable string 1 [2] // pointer (stack, copied in block as const) and object (heap) addresses, NSMutableString object ("mutable string 1") (retained) <0x759aa88 - 0x75595a0>: __block mutable string 1 replaced [1] // pointer (stack, reference to pointer copied in block) and object (heap) addresses, NSMutableString object ("__block mutable string 1 replaced") (not retained) <0xbfffdc10>: 42 // stack address, int primitive type (4), int arg passed by value inside the block <0xbfffdc0c>: 108 // stack address, int primitive type (8), int arg passed by value inside the block <0xbfffdc18 - 0x755a580>: mutable string 1 replaced [1] // pointer (stack, arg pointer value used inside the block) and object (heap) addresses, NSMutableString object ("mutable string 1 replaced") <0xbfffdc14 - 0x75595a0>: __block mutable string 1 replaced [1] // pointer (stack, arg pointer value used inside the block) and object (heap) addresses, NSMutableString object ("__block mutable string 1 replaced") *** BLOCK CALL ENDED *** *** BLOCK CALLED *** <0x759aaa4>: 4 // heap address, int primitive type (4), int value copied inside the block at copy time as const <0x759aa60>: 108 // heap address, int primitive type (8), reference to int value (copied to the heap due to [block copy]) <0x759aaac - 0x7180120>: mutable string 1 [2] // pointer (heap, copied in block) and object (heap) addresses, NSMutableString object ("mutable string 1") (retained) <0x759aa88 - 0x75595a0>: __block mutable string 1 replaced [1] // pointer (heap, reference to pointer copied in block) and object (heap) addresses, NSMutableString object ("__block mutable string 1 replaced") (not retained) <0xbfffdc10>: 42 // stack address, int primitive type (4), int arg passed by value inside the block <0xbfffdc0c>: 108 // stack address, int primitive type (8), int arg passed by value inside the block <0xbfffdc18 - 0x755a580>: mutable string 1 replaced [1] // pointer (stack, arg pointer value used inside the block) and object (heap) addresses, NSMutableString object ("mutable string 1 replaced") <0xbfffdc14 - 0x75595a0>: __block mutable string 1 replaced [1] // pointer (stack, arg pointer value used inside the block) and object (heap) addresses, NSMutableString object ("__block mutable string 1 replaced") *** BLOCK CALL ENDED *** ``` Blocks are first-class citizens in the Objective-C runtime: they have an ‘isa’ pointer which defines a class through which the Objective-C runtime can access methods and storage. In a non-ARC environment you can mess things up for sure, and cause crashes due to dangling pointers. `__block` applies only when using the variables in the block, it simply says to the block: > Hey, this pointer or primitive type relies on the stack with its own address. Please refer to this little friend with a new variable on the heap. I mean… refer to the object with double dereference, and don’t retain it. > Thank you, sir. If at some time after the declaration but before the invocation of the block, the object has been released and deallocated, the execution of the block will cause a crash. `__block` variables are not retained within the block. In the deep end… it’s all about pointers, references, dereferences and retain count stuff. As expected. ### Sartoria Piquadro iPad app URL: https://albertodebortoli.com/2013/03/21/sartoria-piquadro-ipad-app/ Last updated: 2018-02-10T15:52:45.000Z [Sartoria](http://sartoria.piquadro.com/it/?ref=albertodebortoli.com) is a cool and important project for Piquadro, the famous fashion brand. I'm the sole developer of the official iPad app used in the Piquadro stores around the world. ![sartoria_2](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/sartoria_2.png) The app has been developed @ [H-umus](http://h-umus.it/?ref=albertodebortoli.com) during 2011 with minor updated once in a while in following years. The app can configure handmade products and cansend orders. It can be used only by merchants and shop assistants. ![sartoria_1](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/sartoria_1.png) ![sartoria_3](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/sartoria_3.png) ### iOS Lab @ Digital Accademia URL: https://albertodebortoli.com/2012/06/18/ios-lab-digital-accademia/ Last updated: 2018-02-10T15:55:04.000Z I’m very proud and excited to hold a course on Object Orientation programming and iOS developing @ Digital Accademia. 11 lessons starting on June 20th 2012. Details @ [digitalaccademia.com](http://www.digitalaccademia.com/labs/?ref=albertodebortoli.com) ![digital_accademia_ios_lab_manifesto](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/digital_accademia_ios_lab_manifesto.jpg) **UPDATE** The course is finished and me, Enrico Zeffiro and Alessandro Benvenuti are very proud! The lessons and the exercises are available on [iOSLabDigitalAccademia GitHub](https://github.com/albertodebortoli/iOSLab-DigitalAccademia-2012?ref=albertodebortoli.com). ![digital_accademia_ios_lab_class](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/digital_accademia_ios_lab_class.jpg) ### H-Farm, H-umus and cool fashion models URL: https://albertodebortoli.com/2011/12/10/h-farm-h-umus-and-cool-fashion-models/ Last updated: 2025-09-12T20:41:55.000Z I started working for a startup company in [H-Farm](http://h-farmventures.com/?ref=albertodebortoli.com), called [H-umus](http://h-umus.it/?ref=albertodebortoli.com). H-Farm is a Venture Incubator with the mission to accelerate the development of Internet startups via a combination of seed investment and incubation services. Some kind of Italian Silicon Valley ([1](http://www.youtube.com/watch?v=sLvnAdI2Wdc&ref=albertodebortoli.com) and [2](http://www.youtube.com/watch?v=0W6-8G16t0M&ref=albertodebortoli.com)). ![farm_summer_instagram](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/farm_summer_instagram.png) In H-umus we develop iOS applications for the most famous fashion brands in the world like Armani, Diesel, Nike… and this is the reason for the title of the post ;-) We are specialized in human interaction. What makes us different is our approach to solution design. H-Farm is a [wonderful](http://www.flickr.com/photos/hfarmventures/sets/72157625945794228/show/?ref=albertodebortoli.com) place to work at, but you know, what makes things great is the people you work with! ![umus_team](https://storage.ghost.io/c/ae/f4/aef4d625-32a2-417b-86f4-22c70a9b47a1/content/images/2018/02/umus_team.jpg) We are a multifaceted team composed of analysts, designers, software engineers, striving together to create innovative solutions that really make the difference.