Jekyll2022-01-01T18:38:57+00:00https://mar.codes/feed.xmlMarcos GriselliAn iOS development blog about UIs and automation.Marcos GriselliSimulating side-by-side Xcode Previews2021-05-18T00:00:00+00:002021-05-18T00:00:00+00:00https://mar.codes/blog/simulate-side-by-side-Xcode-previews<p>Xcode Previews lays out our views vertically. This style can be inconvenient when we want to preview a portrait screen on different devices or configurations and iterate on it while seeing our changes live.</p>
<p>For example, fitting four iPhone 12 previews on a 16” MacBook Pro requires we zoom out to 12,5%, making it difficult to see our elements. Unless you have a vertical monitor, you’ll usually be better off with side-by-side previews.</p>
<h2 id="visual-setup">Visual Setup</h2>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/horizontal-previews.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/horizontal-previews.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/horizontal-previews.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/horizontal-previews.png 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/horizontal-previews.png 1280w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1600/https://mar.codes/blog/../assets/horizontal-previews.png 1600w" sizes="(min-width: 50rem) 50rem, 90vw" alt="side-by-side previews" width="1970" height="933" crossorigin="anonymous" /></p>
<p>Being able to display side-by-side previews means that we don’t need to scroll through multiple previews to confirm our changes didn’t introduce regressions on different color schemes or layout directions. The more previews you can fit, the less likely you are to overlook UI inconsistencies.</p>
<p>If you’re working on a single MacBook screen, you can use half of the Xcode window for code and half for previews and easily place eight previews without compromising zoom levels:</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/horizontal-previews-xcode-layout.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/horizontal-previews-xcode-layout.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/horizontal-previews-xcode-layout.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/horizontal-previews-xcode-layout.png 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/horizontal-previews-xcode-layout.png 1280w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1600/https://mar.codes/blog/../assets/horizontal-previews-xcode-layout.png 1600w" sizes="(min-width: 50rem) 50rem, 90vw" alt="side-by-side previews on Xcode" width="3072" height="1870" crossorigin="anonymous" /></p>
<p>If you have an iPad, you can use it as a “Previews Monitor” by running <a href="https://support.apple.com/en-us/HT210380">Sidecar</a>. You can duplicate your Xcode current tab by doing ⌘ + T and dragging the newly created tab over to the iPad. Once you have a duplicated tab, you can expand the previews section as much as possible on the iPad while hiding the previews section on your main screen.</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/horizontal-previews-ipad-layout.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/horizontal-previews-ipad-layout.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/horizontal-previews-ipad-layout.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/horizontal-previews-ipad-layout.png 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/horizontal-previews-ipad-layout.png 1280w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1600/https://mar.codes/blog/../assets/horizontal-previews-ipad-layout.png 1600w" sizes="(min-width: 50rem) 50rem, 90vw" alt="side-by-side previews on iPad running Sidecar" width="2224" height="1668" crossorigin="anonymous" /></p>
<p>In this post, we’ll go through what can and can’t be done when trying to simulate side-by-side previews.</p>
<div class="border-l-8 border-yellow-300 rounded-lg bg-yellow-50">
<div class="px-4 py-1">
<div class="text-gray-700">
<p>⚠️ This is an example of how this feature could work. I don't advice relying on it for critical work. See the <b>Downsides</b> section to get more context on why this is not recommended.</p>
</div>
</div>
</div>
<h2 id="swiftui-implementation">SwiftUI Implementation</h2>
<p>If you’re only interested in copying the code for this experiment, you can check out this <a href="https://gist.github.com/marcosgriselli/eb4b6f076fcc440259f34f8bbcfd8209">gist</a>.</p>
<p>First, we’ll need a <code>Device</code> type with the properties for each specific device. We’ll be passing these properties to our views <code>.environment</code> and <code>.frame</code> modifiers.</p>
<pre><code class="language-swift">struct Device {
let name: String
let size: CGSize
let horizontalSizeClass: UIUserInterfaceSizeClass
let verticalSizeClass: UIUserInterfaceSizeClass
let userInterfaceIdiom: UIUserInterfaceIdiom
let orientation: UIInterfaceOrientation // We'll stick to .portrait for the moment
let safeArea: UIEdgeInsets // Not considered at this point
let displayScale: CGFloat
static let iPhoneSE = Device(
name: "iPhone SE (1st Gen)",
size: CGSize(width: 320, height: 568),
horizontalSizeClass: .compact,
verticalSizeClass: .regular,
userInterfaceIdiom: .phone,
orientation: .portrait,
safeArea: .init(top: 20, left: 0, bottom: 0, right: 0),
displayScale: 2
)
static let iPhone12Pro = Device(
name: "iPhone 12 Pro",
size: CGSize(width: 390, height: 844),
horizontalSizeClass: .compact,
verticalSizeClass: .regular,
userInterfaceIdiom: .phone,
orientation: .portrait,
safeArea: .init(top: 44, left: 0, bottom: 34, right: 0),
displayScale: 3
)
static let iPadPro12_9 = Device(
name: "iPad Pro 12.9-inch",
size: CGSize(width: 1024, height: 1366),
horizontalSizeClass: .regular,
verticalSizeClass: .regular,
userInterfaceIdiom: .pad,
orientation: .portrait,
safeArea: .init(top: 20, left: 0, bottom: 0, right: 0),
displayScale: 2
)
}
</code></pre>
<p>We can then define the combination of device and environment variables passed on to each preview element. For the moment we’ll only focus on <code>\.colorScheme</code>,<code>\.sizeCategory</code>, <code>\.layoutDirection</code> and <code>\.locale</code> but any environment variable could be added to this struct.</p>
<pre><code class="language-swift">struct Preview {
let device: Device
let colorScheme: ColorScheme?
let sizeCategory: ContentSizeCategory?
let layoutDirection: LayoutDirection?
let locale: Locale?
}
</code></pre>
<p>With our types in place, we can now dive into creating a layout that will render a view multiple times in an <code>HStack</code>.</p>
<pre><code class="language-swift">struct Canvas<Content: View>: View {
var content: Content
var previews: [Preview]
init(content: Content, previews: [Preview]) {
self.content = content
self.previews = previews
}
var body: some View {
HStack(spacing: 32) {
ForEach(0..<previews.count) { index in
content
// You can add any environment variable you need on this part after adding it to the Preview type.
.ifLet(previews[index].colorScheme) {
$0.environment(\.colorScheme, $1)
}
.ifLet(previews[index].sizeCategory) {
$0.environment(\.sizeCategory, $1)
}
.ifLet(previews[index].layoutDirection) {
$0.environment(\.layoutDirection, $1)
}
.ifLet(previews[index].locale) {
$0.environment(\.locale, $1)
}
.environment(\.horizontalSizeClass, previews[index].device.horizontalSizeClass)
.environment(\.verticalSizeClass, previews[index].device.verticalSizeClass)
.environment(\.displayScale, previews[index].device.displayScale)
.frame(width: previews[index].device.size.width, height: previews[index].device.size.height)
}
}
.previewLayout(.sizeThatFits)
}
}
</code></pre>
<div class="border-l-8 border-blue-500 rounded-lg bg-blue-50">
<div class="px-4 py-1">
<div class="text-gray-700">
<p>The <a href="https://www.fivestars.blog/articles/conditional-modifiers/">ifLet</a> implementation will help us avoid overriding the default values when a environment variable is not set.</p>
</div>
</div>
</div>
<p>Now all we need to do is to pass the view to be previewed multiple times and the devices + configurations we want to display:</p>
<pre><code class="language-swift">struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Canvas(
content: ContentView(text: .constant("")),
previews: [
Preview(device: .iPhoneSE, colorScheme: .light, contentSizeCategory: .extraExtraExtraLarge),
Preview(device: .iPhone12Pro, colorScheme: .dark),
Preview(device: .iPhone12Pro, colorScheme: .dark, contentSizeCategory: .extraSmall, locale: Locale(identifier: "ja_JA")),
Preview(device: .iPadPro12_9, contentSizeCategory: .accessibilityExtraLarge, layoutDirection: .rightToLeft),
]
)
}
}
</code></pre>
<h2 id="extra-benefits">Extra Benefits</h2>
<p>Apart from being space-efficient, this method provides a few other benefits over individual previews.</p>
<p>We don’t need to boot multiple different simulators. If you use previews with various simulators, you probably saw an activity indicator while the simulators are booting. With this approach, we can use a single simulator to mimic the other layouts.</p>
<p>You can “run” multiple views at the same time. If you click the Previews run button, your views will become active, and you can interact with them. Keep in mind that when in “run” mode, the preview takes the size of the simulator, so you’d need to use an iPad simulator and a <code>ScrollView</code> to see multiple previews.</p>
<p>This approach can be helpful when testing animations/transitions without pausing and running in the other simulators. It also works great for testing data-driven state changes. Your view gets copied multiple times, and any change to a store class that is bind to your view would trigger an update on all previews displayed.</p>
<h2 id="downsides">Downsides</h2>
<p>There are <strong>multiple</strong> downsides to this approach:</p>
<ul>
<li>I’m still learning SwiftUI, so my assumptions that a different device is replicated correctly via environment values can be way off.</li>
<li>We can’t pass <code>safeArea</code> values as an environment value. So the simulated previews will fail in that aspect. There might be ways to do it with a container view and <code>GeometryReader</code>, but that’s beyond this experiment.</li>
<li>OS-specific styles such as iPad sidebar navigation layout won’t work.</li>
<li>The multiple <code>ifLet</code> statements made autocomplete struggle. Editing an environment value caused syntax highlight and autocomplete to break, which can be inconvenient when working on your personalised <code>Canvas</code> structure.</li>
</ul>Marcos GriselliXcode Previews lays out our views vertically. This style can be inconvenient when we want to preview a portrait screen on different devices or configurations and iterate on it while seeing our changes live.Mi experiencia consiguiendo clientes como un desarrollador iOS freelance2021-01-10T00:00:00+00:002021-01-10T00:00:00+00:00https://mar.codes/blog/freelance-ios-spanish<p>Conseguir un proyecto remoto freelance como desarrollador de software puede ser un poco distinto a conseguir un trabajo fijo con entrevistas on-site. Conocer a tu cliente solamente por calls puede ser un desafío para mostrar tus habilidades y demostrar que serías un buen fit para el proyecto. Sumado a esto, algunos clientes necesitan un desarrollador por un corto periodo de tiempo lo que hace que no haya tiempo de hacer extensas entrevistas con ejercicios de código para hacer en casa y entregar.</p>
<p>Este posteo es una recopilación de experiencias que tuve donde distintos factores determinantes hicieron que consiga el trabajo o clientes a lo largo de los años.</p>
<blockquote>
<p>Todas las experiencias que comparto a continuación están acompañadas de MUCHISIMA SUERTE. Estar en el momento y lugar correcto pesó mucho más a la hora de conseguir un proyecto que cualquiera de los puntos de esta lista.</p>
</blockquote>
<h2 id="conocer-las-nuevas-tecnologias">Conocer las nuevas tecnologias</h2>
<p>Comencé mi carrera como programador iOS en Marzo de 2014 con Objective-C en una software factory que se especializaba en desarrollar apps para terceros. Un par de meses después Apple dio un golpe a la mesa anunciando [Swift en la WWDC 2014](<a href="https://www.youtube.com/watch?v=MO7Ta0DvEWA">announcing Swift in WWDC 2014</a>. En aquel entonces no estaba muy seguro de lo que esto significaba, sabia que era una gran oportunidad ya que repentinamente todos los desarrolladores pasamos a tener el mismo nivel de experiencia en este nuevo lenguaje. Seguimos programando las aplicaciones de los clientes en Objective-C pero comencé a jugar con Swift por mi cuenta. Aunque tenia poca experiencia con la plataforma y no estaba seguro de lo que estaba haciendo seguí programando en Swift y aprendiendo todo lo que podia en ese periodo.</p>
<p>Adelantando algunos meses a principio de 2015 comencé a buscar distintas oportunidades de trabajo. Tener un buen Inglés abre numerosas oportunidades pero mi experiencia no era suficiente. Allí es cuando este punto de “conocer las nuevas tecnologías” entró en juego. Un compañero estaba aplicando para entrar a <a href="https://topt.al/mZczdk">Toptal</a> y me animó a hacerlo. No tenia nada que perder así que apliqué. Para mi sorpresa mis meses de experiencia con Swift pesaron más que mi falta de años programando en iOS. Los clientes pedían desarrolladores familiarizados con el nuevo lenguaje de programación solamente porque era lo ultimo que había presentado Apple. Durante los primeros meses en la plataforma conseguía proyectos simplemente por haber estado jugando con Swift desde el comienzo, no muchos desarrolladores habían invertido en hacer apps en Swift entonces mi perfil resaltaba. Esos pocos meses de experiencia se fueron acumulando con los nuevos proyectos y apps subidas al App Store. Cuando Swift cumplió un año yo ya tenia proyectos hechos en Swift desde cero y había contribuido a apps en el App Store que también usaban Swift. Esto siguió mejorando mi capacidad de conseguir proyectos.</p>
<h2 id="codigo-abierto-o-funcionalidadescodigo-que-puedas-mostrar">Codigo abierto o funcionalidades/codigo que puedas mostrar</h2>
<p>Quiero comenzar esta sección mencionando que <strong>no es necesario contribuir código a proyectos open source</strong> para ser un desarrollador de software. Personalmente comencé a hacerlo porque después de un par de años de trabajo me di cuenta que no tenia ni una linea de código creada por mí que pueda mostrar. <strong>Esto no es un requerimiento</strong> muchas empresas tienen su propio proceso de entrevistas donde te piden que resuelvas un problema y usan eso para evaluar tu código.</p>
<p>En 2017 compartí una librería en GitHub para hacer animaciones en iOS llamada <a href="https://github.com/marcosgriselli/ViewAnimator">ViewAnimator</a>, sin saberlo me abrió las puertas a uno de los proyectos más entretenidos que tuve en mi carrera. Un par de semanas después de compartir la librería <a href="https://twitter.com/MengTo">Meng To</a> creador de <a href="https://designcode.io">Design+Code</a> <a href="https://twitter.com/MengTo/status/920476848051638272">twiteo</a> que estaba buscando diseñadores y desarrolladores para que se sumen al equipo. Inmediatamente le escribí un mail mostrando mi trabajo previo y la librería nueva. Meng la reconoció ya que había sido compartida en algunos newsletters de iOS y estaba en la lista trending de GitHub Explore.</p>
<p>En ese mismo día tuvimos una call y al proximo día ya estaba escribiendo codigo para la app <a href="https://apps.apple.com/us/app/design-code/id1281776514">iOS de Design+Code</a> que salió al publico unos meses después. No estoy seguro que hubiera pasado si no hubiera tenido código para mostrar cuando le escribí a Meng. Pero en este escenario en particular poder mostrar mi propio código fue una gran ventaja.</p>
<h2 id="escribir-articulos">Escribir articulos</h2>
<p>Seguramente muchos bloggers pueden escribir una mejor sección sobre su experiencia consiguiendo proyectos mediante su sitio/blog. De todas formas creo que este punto puede ser util para desarrolladores como yo que tienen un sitio/blog donde rara vez escriben.</p>
<p>Blogging, de la misma forma que compartir código, te deja mostrar tus soluciones a un problema. En muchos casos puede ser hasta mejor que simplemente compartir código ya que nos da la libertad para mostrar todo nuestro proceso de pensamiento y también demostrar porque otras alternativas no funcionan.</p>
<p>Después de compartir <a href="/blog/appstore-search">mi primer post</a> en <a href="https://twitter.com/marcosgriselli/status/1019656899292168193">twitter</a> recibí un mensaje de una persona que trabajaba en un equipo de la NBA. Estaban haciendo un producto interno basado en búsquedas para sus scouts. El posteo explora un solución usando componentes nativos para lograr una UI muy similar a la del App Store. Esto era exactamente lo que querían en su herramienta y me contactaron para implementar una idea similar en toda su app ya que la búsqueda era una funcionalidad esencial del proyecto.</p>
<p>Una o dos semanas después de este chat ya estaba haciendo trabajo de consulting para armar una arquitectura que soporte todas sus funcionalidades relacionadas con búsquedas.</p>
<p>Como mencioné anteriormente todos estos puntos vienen acompañados de <strong>mucha suerte</strong> de estar en el lugar adecuado en el momento adecuado y tener la oportunidad de mostrar lo que puedo hacer a clientes que están buscando contratar a alguien.</p>MarcosConseguir un proyecto remoto freelance como desarrollador de software puede ser un poco distinto a conseguir un trabajo fijo con entrevistas on-site. Conocer a tu cliente solamente por calls puede ser un desafío para mostrar tus habilidades y demostrar que serías un buen fit para el proyecto. Sumado a esto, algunos clientes necesitan un desarrollador por un corto periodo de tiempo lo que hace que no haya tiempo de hacer extensas entrevistas con ejercicios de código para hacer en casa y entregar.My experience getting remote freelance projects as an iOS Developer2021-01-10T00:00:00+00:002021-01-10T00:00:00+00:00https://mar.codes/blog/my-experience-getting-freelance-projects-as-an-ios-developer<p><em>This post is also available in <a href="/blog/freelance-es">Spanish</a></em></p>
<p>Getting a remote freelance project as a software developer can be slightly different than getting a regular job through on-site interviews. Meeting another person only via internet calls can sometimes fail to show how good a fit you can be for a project/client. Short-term clients can sometimes look for a developer to jump on board as soon as possible to start moving forward, which means that there’s no time to interview ten candidates, send them code challenges to do at home, and then pick one.</p>
<p>This post is a compilation of past experiences I had that point to different determining factors that paved the way for me to get jobs or clients throughout the years.</p>
<blockquote>
<p>Keep in mind that all the experiences shared below are coupled with EXTREME LUCK. I believe being at the right place at the right time weighted far more than the points I list below when getting a project.</p>
</blockquote>
<h2 id="being-familiar-with-new-technologies">Being familiar with new technologies</h2>
<p>I started my iOS Development journey in March 2014 with Objective-C on a software factory focused on building mobile apps. A few months after, Apple rocked our worlds by <a href="https://www.youtube.com/watch?v=MO7Ta0DvEWA">announcing Swift in WWDC 2014</a>. Although I wasn’t exactly sure what that meant at the time, I knew that this a fresh start in a way because suddenly everyone had the same level of expertise with this particular new programming language. We still kept all our clients’ projects in Objective-C. Yet, I started playing with Swift independently. Even though I had minimal experience with the platform and didn’t know for sure what I was doing, I just kept writing Swift and learning everything I could.</p>
<p>Fast-forward a couple of months to the beginning of 2015, and I started looking at other job opportunities. Having good English communication skills opened up numerous possibilities, but I wasn’t sure I’d get any jobs due to my lack of experience. That’s when this whole “being familiar with new technologies” kicked in. A friend of mine was applying to <a href="https://topt.al/mZczdk">Toptal</a> and encouraged me to do it as well. I had nothing to lose, so I went for it. To my surprise, my short experience doing Swift weighted in a lot more than my lack of years and years doing iOS Development. Clients were requesting developers familiar with the “latest and greatest” and that’s when my 6-months experience doing Swift came in clutch. For the first few months on the platform, I got freelance projects simply because I had experience with this new programming language. That experience started to stack with successful projects and apps uploaded to the App Store. All this experience paved the way for future opportunities, gave me confidence, and provided me with a proven track of projects released successfully.</p>
<h2 id="open-source-or-featurescode-you-can-show">Open source or features/code you can show</h2>
<p>I want to start this section by emphasizing that <strong>you don’t need to do open source</strong> when being a Software Developer. I got started with it when I realized that I didn’t have a single line of code to show after a few years of developing apps when getting to an interview. <strong>This is not a requirement by any means</strong>, but I was finding myself in interviews that didn’t have code challenges, so this was the only way I could “show” my code.</p>
<p>I released an open source library for iOS animations called <a href="https://github.com/marcosgriselli/ViewAnimator">ViewAnimator</a> in 2017, and without knowing it, it opened up the gates to one of my favorite projects I worked on so far. A couple of weeks after releasing the library <a href="https://twitter.com/MengTo">Meng To</a> from <a href="https://designcode.io">Design+Code</a> <a href="https://twitter.com/MengTo/status/920476848051638272">tweeted</a> that he was looking for developers/designers to join the team. I immediately pinged him sharing my work and latest library release. Meng recognized my project since it was featured on multiple iOS community newsletters and was trending on GitHub’s Explore section.</p>
<p>We had a call on that day, and the very next day, I was writing code for the <a href="https://apps.apple.com/us/app/design-code/id1281776514">Design+Code iOS app</a>, which was released a few months later. I’m not sure what would’ve happened if I didn’t have any code to share when I emailed Meng, but in this particular scenario, being able to share code I created was a significant advantage.</p>
<h2 id="blogging">Blogging</h2>
<p>Many bloggers can write a better section on their experiences with getting job offers via their websites/blogs. Still, I feel like this point might be useful for other developers like me who have a site/blog and rarely add content.</p>
<p>Blogging, like sharing code, let’s you open up about your techniques/solutions to a problem. It’s sometimes even better than just code since you can show your entire path to the solution and even show why alternatives won’t work.</p>
<p>After sharing my <a href="/blog/appstore-search">first blog post</a> on <a href="https://twitter.com/marcosgriselli/status/1019656899292168193?s=20">Twitter</a> I got a DM from a person working for an NBA team that was building an internal heavily search-based native iOS app for their scouts. The blog post explored a solution to a problem they were facing, so they figured it would be better to have me on board and do what I did of the blog post for them since search was such a critical aspect of the project.</p>
<p>A week or two after this interaction, I was doing consulting work to set up the architecture for all search based functionalities.</p>
<p>All of these points come paired with <strong>extreme luck</strong> of being around in the right place at the right time and having the opportunity to show what I can do to clients that were looking to hire a profile I could fit.</p>MarcosThis post is also available in SpanishSolving ambiguous constraints without rerunning your app2019-05-28T00:00:00+00:002019-05-28T00:00:00+00:00https://mar.codes/blog/Solving-ambiguous-constraints-without-rerunning-your-app<h2 id="dealing-with-auto-layout">Dealing with Auto Layout</h2>
<p>Solving Auto Layout issues is always a hassle; we run our application expecting all our constraints work correctly to find a massive block of Auto Layout error logs on the console.</p>
<p>Interface Builder is of great help when creating layouts visually since it throws warnings and errors when the constraints are faulty, although our layout might not always follow the visuals on IB. Our screen UI might change depending on different elements such as network request responses or locally stored data. Moreover, we might have screens that are built entirely from server-side information, sizes, colors, shapes, everything!</p>
<div class="border-2 border-gray-300 noprose rounded-xl">
<div class="px-4 pb-4 text-gray-600 dark:text-gray-300">
<div class="-mt-2 -mb-6 ">
<a target="_blank" href="https://topt.al/mZczdk">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../assets/toptal_ad_dark.svg" />
<source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcset="../assets/toptal_ad.svg" />
<img width="150px" src="../assets/toptal_ad.svg" />
</picture>
</a>
</div>
Toptal is an exclusive network of the top freelance software developers, designers, and managers in the world. Top
companies hire Toptal freelancers for their most important projects.
<br />
<a target="_blank" href="https://topt.al/mZczdk" class="font-semibold"> Get up to $2500
signing bonus applying as Software Developer <span aria-hidden="true">→</span></a>
</div>
</div>
<p>It seems like there’s no other way than to deal with that chunk of Auto Layout error log. Well, there are more straightforward ways, for starters, there’s <a href="https://www.wtfautolayout.com">WTFAutoLayout</a> (WTF stands for ‘Why The Failure’) which turns our cryptic error message into a beautiful visual representation of the issue.</p>
<figure>
<img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/viewdebugger/wtf_auto_layout.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/viewdebugger/wtf_auto_layout.png 320w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/viewdebugger/wtf_auto_layout.png 640w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/viewdebugger/wtf_auto_layout.png 960w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/viewdebugger/wtf_auto_layout.png 1280w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1600/https://mar.codes/blog/../assets/viewdebugger/wtf_auto_layout.png 1600w" sizes="(min-width: 50rem) 50rem, 90vw" alt="WTF autolayout" width="1996" height="900" />
<figcaption><p>Thanks John Patrick Morgan (@jpmmusic) for creating this.</p>
</figcaption>
</figure>
<p>This method is already a significant leap forward when understanding constraint issues, but it still means you need to find the conflicting constraint, write a fix, rerun your app, navigate to the desired screen and hope that:</p>
<ol>
<li>Your screen looks as it should</li>
<li>The current layout matches the one with errors</li>
<li>The Auto Layout error message is gone</li>
</ol>
<p>Keep in mind that your current app state might not always be there, perhaps when you rerun your app the server response changed and the content that was generating issues is not there any longer, for example, excessive long labels on cells.</p>
<h2 id="view-debugger--lldb-to-the-rescue">View Debugger + LLDB to the rescue</h2>
<p>Xcode’s <a href="https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/debugging_with_xcode/chapters/special_debugging_workflows.html">View Debugger</a> is a fantastic tool for understanding how UI’s are composed and debugging Auto Layout issues. If you never used the View Debugger I recommend you take a look at this <a href="https://www.youtube.com/watch?v=Xx57T9MshBQ">Ray Wenderlich’s Screencast</a> on the topic since this is post does not dive into what the View Debugger is and how to use it.</p>
<p>One of the best features of the View Debugger is the <strong>Layout Issues</strong> warnings. This functionality saves us much time when trying to understand which view/constraint is causing Auto Layout to complain.</p>
<figure>
<img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/viewdebugger/xcode_view_debugger.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/viewdebugger/xcode_view_debugger.png 320w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/viewdebugger/xcode_view_debugger.png 640w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/viewdebugger/xcode_view_debugger.png 960w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/viewdebugger/xcode_view_debugger.png 1280w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1600/https://mar.codes/blog/../assets/viewdebugger/xcode_view_debugger.png 1600w" sizes="(min-width: 50rem) 50rem, 90vw" alt="xcode view debugger" width="2832" height="1978" />
<figcaption><p>Single view layout with ambiguous constraint</p>
</figcaption>
</figure>
<figure>
<img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/viewdebugger/layout_issue.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/viewdebugger/layout_issue.png 320w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_446/https://mar.codes/blog/../assets/viewdebugger/layout_issue.png 446w" sizes="(min-width: 50rem) 50rem, 90vw" alt="layout issue" width="446" height="216" />
<figcaption><p>Layout Issues list on the Issue Navigator</p>
</figcaption>
</figure>
<p>We’re now able to jump straight into the conflicting view with a human-readable description of the error: <em>“Height and vertical position are ambiguous”</em>. Now is time to dive into <strong>LLDB</strong>. We’ll implement the core concepts presented in 2018 WWDC talk <a href="https://developer.apple.com/videos/play/wwdc2018/412/">Advanced Debugging with Xcode and LLDB</a>. I highly recommend you watch it.</p>
<p>We’ll take a few steps to try and resolve our constraint issue <strong>without rerunning</strong> our app and <strong>without reloading</strong> our current screen. This approach saves a lot of time and it is a life-saver if we’re not able to replicate the layout issue.</p>
<p>Here’s our plan to do this:</p>
<ol>
<li>Launch the View Debugger.</li>
<li>Deactivate the conflicting constraint</li>
<li>Apply constraint fix</li>
<li>Update our layout from LLDB</li>
<li>Check that the Layout Issue warning is gone</li>
<li>Profit</li>
</ol>
<p>W’ll use a <strong>super</strong> simple single-view case, but you can apply the same concepts for complex screens. This is our demo <code>ViewController</code> class:</p>
<pre><code class="language-swift">class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let colorView = UIView()
colorView.translatesAutoresizingMaskIntoConstraints = false
colorView.backgroundColor = .lightGray
view.addSubview(colorView)
NSLayoutConstraint.activate([
colorView.heightAnchor.constraint(greaterThanOrEqualToConstant: 277),
colorView.widthAnchor.constraint(equalToConstant: 242),
colorView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
colorView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
}
</code></pre>
<p>With the app running and the simulator on foreground <strong>⌥ + click</strong> on the View Debugger icon:</p>
<figure>
<img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/viewdebugger/view_debugger_start.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/viewdebugger/view_debugger_start.png 320w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/viewdebugger/view_debugger_start.png 640w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/viewdebugger/view_debugger_start.png 960w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/viewdebugger/view_debugger_start.png 1280w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1368/https://mar.codes/blog/../assets/viewdebugger/view_debugger_start.png 1368w" sizes="(min-width: 50rem) 50rem, 90vw" alt="view debugger start" width="1368" height="920" />
<figcaption><p>Notice how the simulator is the active app while Xcode sits in the background</p>
</figcaption>
</figure>
<blockquote>
<p>Using <strong>⌥ + click</strong> while the simulator is on foreground opens the View Debugger and keeps the simulator app running. This trick is a crucial part of our method since it will let us check the UI updates live.</p>
</blockquote>
<p>This action opens the View Debugger and keeps your app in a “breakpoint” state. Copy the conflicting constraint, in this case <code>view.height >= 277 @ 1000</code>, with <strong>⌘ C</strong> and move to LLDB.</p>
<blockquote>
<p>You might need to import UIKit before any LLDB step so run this command: <code>expr @import UIKit</code></p>
</blockquote>
<p>Using copy under this circumstances copies the memory address with the type cast to the clipboard with this format: <code>((NSLayoutConstraint *)0x600002a01810</code>. With this we’re now able to deactivate the constraint with <code>e ((NSLayoutConstraint *)0x600002a01810).active = false;</code></p>
<blockquote>
<p>If you want to see the current state of your layout after deactivating the constraint, run <code>e [CATransaction flush]</code> on LLDB to see the changes reflected on the simulator (this is possible because the simulator is kept active in foreground).</p>
</blockquote>
<p>Now we can apply the constraint we expect will solve this issue. We’ll have to copy our view from the View Debugger as we did with the constraint and add an explicit constraint: <code>e [[((UIView *)0x7f925d407850) heightAnchor] constraintEqualToConstant: 277].active = YES;</code></p>
<p>We’ll run <code>e [CATransaction flush];</code> again to make sure our layout is correct and click on the play/pause button beside the ‘activate all breakpoints’ one to continue running your app on the simulator.</p>
<figure>
<img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/viewdebugger/console.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/viewdebugger/console.png 320w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/viewdebugger/console.png 640w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/viewdebugger/console.png 960w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/viewdebugger/console.png 1280w,
https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1424/https://mar.codes/blog/../assets/viewdebugger/console.png 1424w" sizes="(min-width: 50rem) 50rem, 90vw" alt="console" width="1424" height="440" />
<figcaption><p>Instructions sent to LLDB</p>
</figcaption>
</figure>
<p>To make sure our new constraint doesn’t bring any new layout issue, we’ll open the View Debugger again and the system will re-check for new Auto Layout warnings, we can check that we’re now warning-free.</p>
<p>That’s it, with those simple steps we were able to find our conflicting constraint, test a fix and evaluate that the Auto Layout warnings are gone <strong>without rerunning our app</strong>.</p>
<blockquote>
<p>LLDB supports aliases and scripting so I suggest you take a look at that to make your debugging process even faster. You can start <a href="https://github.com/DerekSelander/LLDB">here</a>.</p>
</blockquote>
<p>NOTE: While this approach can be done only by using Xcode and the simulator, there are other alternatives that avoid dealing with casting and LLDB. They present UIs that look very similar to IB where you can edit multiple properties and values and see the changes on the simulator on real-time! I recommend you take a look at them to see if they fit your needs:</p>
<ul>
<li><a href="http://revealapp.com">Reveal</a> by <a href="https://www.ittybittyapps.com">Itty Bitty Apps</a></li>
<li><a href="https://sherlock.inspiredcode.io">Sherlock</a> by Inspired Code</li>
</ul>
<p>For any suggestion, question or discussion please use this <a href="https://twitter.com/marcosgriselli/status/1133396589710594051">tweet</a></p>MarcosDealing with Auto LayoutSet up library releases from Bitrise2018-12-10T00:00:00+00:002018-12-10T00:00:00+00:00https://mar.codes/blog/Set-up-library-releases-from-CI<p>On the last post, we went over how to <a href="https://mar.codes/2018-11-14/Automate-open-source-libraries-releases-with-fastlane">automate open source libraries releases using fastlane</a>. Today we’ll look into a commodity rather than automation like releasing a library directly from a CI service. We’ll be using <a href="https://app.bitrise.io/users/sign_up?referrer=585975239c81bb08">Bitrise</a> as it supports fastlane projects out of the box.</p>
<p>Since we already removed all the manual steps required to make a release of a library with setting up fastlane, why would we want to support releasing from a CI service?</p>
<p>There are two main advantages of having the ability to release from CI:</p>
<ol>
<li>
<p><strong>Trigger a release from anywhere</strong><br />
Since the CI service will be doing all the work, we’re not longer tied to the terminal on a computer with fastlane installed. All we need is an internet connection. We can even trigger it with a Siri Shortcut.</p>
</li>
<li>
<p><strong>Handle versioning from commits</strong><br />
This will depend on your library in particular or group of people contributing to it. I’m currently working on a project that uses private pods and to avoid the hassle of who pushes a new release and when to push it, we automated this process by setting the next version on the PR and the CI releases on merge.</p>
</li>
</ol>
<p>We’ll be going through the entire process of configuring your library on Bitrise from scratch using one of my open source libraries called <a href="https://github.com/marcosgriselli/ViewAnimator">ViewAnimator</a>. I added two extra lanes to the <a href="https://github.com/marcosgriselli/ViewAnimator/blob/039f99e1c0c46d982199b3c2e1c66cdec70b8abe/fastlane/Fastfile">Fastfile</a> <code>test</code> and <code>release_current</code> we’ll be reviewing them in detail later, but you can take a look and set them up to be ready.</p>
<div class="border-2 border-gray-300 noprose rounded-xl">
<div class="px-4 pb-4 text-gray-600 dark:text-gray-300">
<div class="-mt-2 -mb-6 ">
<a target="_blank" href="https://topt.al/mZczdk">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../assets/toptal_ad_dark.svg" />
<source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcset="../assets/toptal_ad.svg" />
<img width="150px" src="../assets/toptal_ad.svg" />
</picture>
</a>
</div>
Toptal is an exclusive network of the top freelance software developers, designers, and managers in the world. Top
companies hire Toptal freelancers for their most important projects.
<br />
<a target="_blank" href="https://topt.al/mZczdk" class="font-semibold"> Get up to $2500
signing bonus applying as Software Developer <span aria-hidden="true">→</span></a>
</div>
</div>
<h2 id="adding-a-new-app-to-bitrise">Adding a new App to Bitrise</h2>
<p>You should log in to Bitrise with your GitHub account to add your library’s repo.</p>
<p>Click on the ‘+’ button from the top right corner and then on <strong>Add app</strong> option:</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/bitrise/bitrise_add_app.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/bitrise/bitrise_add_app.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/bitrise/bitrise_add_app.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_790/https://mar.codes/blog/../assets/bitrise/bitrise_add_app.png 790w" sizes="(min-width: 50rem) 50rem, 90vw" alt="bitrise add app" width="790" height="404" crossorigin="anonymous" /></p>
<p>This will present the Bitrise new app configuration wizard. To enable write access we need to make the Bitrise app private, so we can pass in a read and write SSH key. I highly recommend you <a href="https://help.github.com/articles/connecting-to-github-with-ssh/">read more about this topic</a> if you’re not familiar with it.</p>
<p>On the <em>Setup repository access section</em> we’ll choose ‘Automatic’ option plus ‘I need to’ option which will let us provide a read/write SSH key to push changes directly to our repo. Follow the ‘Manual setup’ from <a href="https://devcenter.bitrise.io/faq/how-to-generate-ssh-keypair/">these steps</a> to set the key up.</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/bitrise/bitrise_ssh.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/bitrise/bitrise_ssh.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/bitrise/bitrise_ssh.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/bitrise/bitrise_ssh.png 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/bitrise/bitrise_ssh.png 1280w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1323/https://mar.codes/blog/../assets/bitrise/bitrise_ssh.png 1323w" sizes="(min-width: 50rem) 50rem, 90vw" alt="bitrise ssh" width="1323" height="480" crossorigin="anonymous" /></p>
<p>Keep filling the wizard with your specific set up needs. If the configuration is correct, Bitrise will go and trigger an initial build with the default configuration.</p>
<p>Navigate to your app’s dashboard and you’ll find this UI:</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/bitrise/app_ui.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/bitrise/app_ui.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/bitrise/app_ui.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/bitrise/app_ui.png 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/bitrise/app_ui.png 1280w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1600/https://mar.codes/blog/../assets/bitrise/app_ui.png 1600w" sizes="(min-width: 50rem) 50rem, 90vw" alt="bitrise app ui" width="3554" height="1184" crossorigin="anonymous" /></p>
<h2 id="setting-up-releases">Setting up releases</h2>
<p>To let Bitrise push our library release, we need to set up an access token for <a href="https://guides.cocoapods.org/making/getting-setup-with-trunk.html">trunk</a>. Tackling this action would require a few extra steps. Luckily, <a href="https://twitter.com/kylefuller">Kyle Fuller</a> wrote a detailed <a href="https://fuller.li/posts/automated-cocoapods-releases-with-ci/">post</a> on how to do this and setup the automatic release with other CI services. So go ahead and generate the access token following the steps he described.</p>
<p>We’ll then add it as a secret variable to Bitrise. Under the <strong>Secrets</strong> tab add a new variable called <code>COCOAPODS_TRUNK_TOKEN</code> with the value being the token generated from Kyle’s post.</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/bitrise/secret_var.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/bitrise/secret_var.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/bitrise/secret_var.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/bitrise/secret_var.png 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/bitrise/secret_var.png 1280w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1600/https://mar.codes/blog/../assets/bitrise/secret_var.png 1600w" sizes="(min-width: 50rem) 50rem, 90vw" alt="secret var" width="3060" height="978" crossorigin="anonymous" /></p>
<h2 id="managing-workflows">Managing Workflows</h2>
<p>Heads up, if you don’t feel like creating all the workflows manually you can jump to the <strong>Bitrise.yml</strong> section and follow the steps there where everything will be created automatically.</p>
<p>We’ll need to edit our workflow so open the <strong>Workflows</strong> tab. By default the first workflow is called <strong>primary</strong>, it will run whatever lane you defined on the configuration wizard. Since I set it up to run the <code>test</code> lane, I’ll rename it to <strong>test</strong>. Also, we can get rid of some actions since we won’t be needing them like <em>Do anything with Script step</em> and <em>Certificate and profile installer</em>. Here’s what my plain <strong>test</strong> workflow looks like:</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/bitrise/test_workflow.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/bitrise/test_workflow.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/bitrise/test_workflow.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_714/https://mar.codes/blog/../assets/bitrise/test_workflow.png 714w" sizes="(min-width: 50rem) 50rem, 90vw" alt="test workflow" width="714" height="982" crossorigin="anonymous" /></p>
<p>I also changed the <strong>Triggers</strong> to run the <code>test</code> workflow when there’s a new pull request on the repo. This will run the test suite on each PR and notify on the PR if the tests passed or failed.</p>
<p>The final step is to create the workflows needed for our releases. Major, minor and patch. They will perform the same actions as in <code>test</code> but they will call their respective fastlane lane. We’ll create a new workflow by tapping the ‘+ Workflow’ button and create a new workflow called <code>major</code> based on <code>test</code></p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/bitrise/new_workflow_major.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/bitrise/new_workflow_major.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/bitrise/new_workflow_major.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/bitrise/new_workflow_major.png 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1004/https://mar.codes/blog/../assets/bitrise/new_workflow_major.png 1004w" sizes="(min-width: 50rem) 50rem, 90vw" alt="new workflow major" width="1004" height="562" crossorigin="anonymous" /></p>
<p>After the new workflow is created all we need to do is edit the fastlane step with the desired lane we’ll run:</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/bitrise/edit_fastlane_step.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/bitrise/edit_fastlane_step.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/bitrise/edit_fastlane_step.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/bitrise/edit_fastlane_step.png 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/bitrise/edit_fastlane_step.png 1280w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1600/https://mar.codes/blog/../assets/bitrise/edit_fastlane_step.png 1600w" sizes="(min-width: 50rem) 50rem, 90vw" alt="edit fastlane step" width="3514" height="1300" crossorigin="anonymous" /></p>
<p>We can now manually trigger a Bitrise workflow that will launch our fastlane lane that lints the library, bumps the version, commit the changes and releases a new version to Cocoapods. You’ll see signed commits by <a href="https://github.com/bitrise-infrabot">Bitrise InfraBot</a>. Here’s the release it made for ViewAnimator triggered from Bitrise: <a href="https://github.com/marcosgriselli/ViewAnimator/releases/tag/2.2.2">Release 2.2.2</a>. Don’t forget to add the <code>minor</code> and <code>patch</code> workflows too.</p>
<h2 id="automatic-version-handling">Automatic version handling</h2>
<p>If you don’t feel like triggering workflows manually for each new release you can create a new Workflow for the <code>release_current</code> lane:</p>
<pre><code class="language-ruby">lane :release_current do
version = version_get_podspec(path: @podspec_name)
if git_tag_exists(tag: version)
UI.user_error!("The tag #{version} already exists on the repo. To release a new version of the library bump the version on #{@podspec_name}")
end
pod_lib_lint
add_git_tag(tag: "#{version}")
push_git_tags
pod_push
end
</code></pre>
<p>This lane will grab the current version set up on the <code>.podspec</code> file and create a new release with that value. If you decide to take this approach for your library, you’ll need to add a new Trigger that calls the <code>release_current</code> workflow upon pushing to master</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/bitrise/release_current_trigger.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/bitrise/release_current_trigger.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/bitrise/release_current_trigger.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/bitrise/release_current_trigger.png 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/bitrise/release_current_trigger.png 1280w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1600/https://mar.codes/blog/../assets/bitrise/release_current_trigger.png 1600w" sizes="(min-width: 50rem) 50rem, 90vw" alt="release current trigger" width="3058" height="644" crossorigin="anonymous" /></p>
<p>Remember that you’ll need to push the version change on the podspec file if you want this to work automatically.</p>
<h2 id="bitriseyml">Bitrise.yml</h2>
<p>If you don’t feel like creating each of the workflows manually you can simply copy and paste the code found on <a href="https://gist.github.com/marcosgriselli/40e000e356e346e12faaee686cd5aa50">this gist</a> into the <strong>bitrise.yml</strong> tab and everything will be created automatically.</p>MarcosOn the last post, we went over how to automate open source libraries releases using fastlane. Today we’ll look into a commodity rather than automation like releasing a library directly from a CI service. We’ll be using Bitrise as it supports fastlane projects out of the box.Automate your library releases with Fastlane2018-11-14T00:00:00+00:002018-11-14T00:00:00+00:00https://mar.codes/blog/Automate-open-source-libraries-releases-with-fastlane<p>Today we’ll look into automating a <a href="https://cocoapods.org">CocoaPods</a> library releases with <a href="https://fastlane.tools">fastlane</a>. This post is part of a small series about automating these type of tasks. On upcoming posts, we’ll look into private Pods and automating releases from a CI service.</p>
<p>Let’s take a look at the manual release process of a CocoaPods library. If you need more information on how CocoaPods library releases are set up, I suggest you take a look at <a href="https://blog.cocoapods.org/CocoaPods-Trunk/">CocoaPods Trunk</a>, the Authentication and API service provided for releasing new versions of your library.</p>
<p>The manual steps we are going to automate are:</p>
<ul>
<li>Update the <code>.podspec</code> file version and commit</li>
<li>Add new git tag</li>
<li>Push git tag</li>
<li>Lint the podspec file</li>
<li>Release the library</li>
</ul>
<p>This process is particularly useful when maintaining multiple libraries and it will help you focus on growing your libraries, shipping features instead of spending time on manual releases.</p>
<div class="border-2 border-gray-300 noprose rounded-xl">
<div class="px-4 pb-4 text-gray-600 dark:text-gray-300">
<div class="-mt-2 -mb-6 ">
<a target="_blank" href="https://topt.al/mZczdk">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../assets/toptal_ad_dark.svg" />
<source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcset="../assets/toptal_ad.svg" />
<img width="150px" src="../assets/toptal_ad.svg" />
</picture>
</a>
</div>
Toptal is an exclusive network of the top freelance software developers, designers, and managers in the world. Top
companies hire Toptal freelancers for their most important projects.
<br />
<a target="_blank" href="https://topt.al/mZczdk" class="font-semibold"> Get up to $2500
signing bonus applying as Software Developer <span aria-hidden="true">→</span></a>
</div>
</div>
<h2 id="setting-up-fastlane">Setting up fastlane</h2>
<p>You can take a look at fastlane’s <a href="https://docs.fastlane.tools/getting-started/ios/setup/">installation guide</a> if you’re new to it. Check the section named <strong>Use a Gem</strong> since we’ll be using <strong>bundle</strong>.</p>
<p>We’ll start by creating a file named <code>Gemfile</code> on the root of your project where we will list our dependencies:</p>
<pre><code class="language-ruby">source "https://rubygems.org"
gem "cocoapods"
gem "fastlane"
</code></pre>
<p>If you don’t have fastlane set up on your machine, run <code>bundle install</code></p>
<p>Next, we’ll run <code>bundle exec fastlane init</code>, select <strong>manual setup</strong> and press enter until it’s finished.</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/fastlane/installation.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/fastlane/installation.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/fastlane/installation.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/fastlane/installation.png 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/fastlane/installation.png 1280w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1396/https://mar.codes/blog/../assets/fastlane/installation.png 1396w" sizes="(min-width: 50rem) 50rem, 90vw" alt="fastlane installation" width="1396" height="1078" crossorigin="anonymous" /></p>
<p>The previous step generates a <code>fastlane</code> folder with our <a href="https://docs.fastlane.tools/advanced/Fastfile/">Fastfile</a> inside.</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/fastlane/generated.png" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/fastlane/generated.png 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/fastlane/generated.png 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/fastlane/generated.png 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/fastlane/generated.png 1280w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1600/https://mar.codes/blog/../assets/fastlane/generated.png 1600w" sizes="(min-width: 50rem) 50rem, 90vw" alt="fastlane generate output" width="1716" height="1238" crossorigin="anonymous" /></p>
<p>Let’s start working on our Fastfile, here we’ll define our lanes and steps needed to release an update.</p>
<p>Below we can see the default Fastfile content.</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby">default_platform(:ios)
platform :ios do
desc "Description of what the lane does"
lane :custom_lane do
# add actions here: https://docs.fastlane.tools/actions
end
end</code></pre></figure>
<p>We’ll rename <code>custom_lane</code> to <code>release</code>. Here’s the list of fastlane actions that we’ll use:</p>
<ul>
<li><a href="https://docs.fastlane.tools/actions/pod_lib_lint/">pod_lib_lint</a></li>
<li><a href="https://docs.fastlane.tools/actions/version_bump_podspec/">version_bump_podspec</a></li>
<li><a href="https://docs.fastlane.tools/actions/git_add/">git_add</a></li>
<li><a href="https://docs.fastlane.tools/actions/git_commit/">git_commit</a></li>
<li><a href="https://docs.fastlane.tools/actions/add_git_tag/">add_git_tag</a></li>
<li><a href="https://docs.fastlane.tools/actions/push_to_git_remote/">push_to_git_remote</a></li>
<li><a href="https://docs.fastlane.tools/actions/pod_push">pod_push</a></li>
</ul>
<p>Each of these functions take parameters which give us flexibility when building our release process. Let’s edit our Fastfile with an initial release approach.</p>
<pre><code class="language-ruby">default_platform(:ios)
podspec_name = "MyPod.podspec" # Don’t forget to update the podspec_name to the name of your podspec file.
platform :ios do
desc "Releases a new version automatically"
lane :release
pod_lib_lint
version = version_bump_podspec(path: podspec_name)
git_add(path: podspec_name)
git_commit(path: [podspec_name],
message: "#{version} release")
add_git_tag(tag: "#{version}")
push_to_git_remote
pod_push
end
end
</code></pre>
<p>We now reduced multiple manual steps to one single fastlane lane. The <code>release</code> lane delivers a patch update <code>0.0.X</code> to the public CocoaPods specs repo. Let’s add a bit more flexibility for different bump types (major, minor and patch). Let’s extract the functionality and create new lanes for each bump type.</p>
<pre><code class="language-ruby">default_platform(:ios)
podspec_name = "MyPod.podspec"
platform :ios do
desc "Release a new version with a patch bump_type"
lane :patch do
release("patch") # we could use __method__.to_s instead of duplicating the name
end
desc "Release a new version with a minor bump_type"
lane :minor do
release("minor")
end
desc "Release a new version with a major bump_type"
lane :major do
release("major")
end
def release(type)
pod_lib_lint
version = version_bump_podspec(path: podspec_name,
bump_type: type)
git_add(path: podspec_name)
git_commit(path: [podspec_name],
message: "#{version} release")
add_git_tag(tag: "#{version}")
push_to_git_remote
pod_push
end
end
</code></pre>
<p>That looks a lot better. Now we can call <code>bundle exec fastlane (patch | minor | major)</code> to release the new version of the library automatically once we finished committing our fixes or new features 🎉!</p>
<p>Thanks for reading!</p>MarcosToday we’ll look into automating a CocoaPods library releases with fastlane. This post is part of a small series about automating these type of tasks. On upcoming posts, we’ll look into private Pods and automating releases from a CI service.Replicating the iOS App Store Search tab2018-07-17T00:00:00+00:002018-07-17T00:00:00+00:00https://mar.codes/blog/Replicating-iOS-11-App-Store-search-tab<p>Lately I’ve been working with <code>UISearchController</code> for a friends <a href="https://github.com/Canillitapp/headlines-iOS">open source app</a> and decided to share the approach we took to replicate the App Store search functionality.</p>
<p>The idea was to categorize the search type and use a state container view controller to swap the resulting view controller depending on the search type.</p>
<p>Here’s an overview of the hierarchy:</p>
<p><img src="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_800/https://mar.codes/blog/../assets/architecture.jpg" srcset="https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_320/https://mar.codes/blog/../assets/architecture.jpg 320w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_640/https://mar.codes/blog/../assets/architecture.jpg 640w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_960/https://mar.codes/blog/../assets/architecture.jpg 960w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1280/https://mar.codes/blog/../assets/architecture.jpg 1280w, https://res.cloudinary.com/marcodes/image/fetch/c_limit,f_auto,q_auto,w_1600/https://mar.codes/blog/../assets/architecture.jpg 1600w" sizes="(min-width: 50rem) 50rem, 90vw" alt="architecture" width="1600" height="1344" crossorigin="anonymous" /></p>
<p>Having our components split this way and using UISearchController means <strong>no handling of the current search state</strong>. We won’t have <code>if filtered {}</code> spread arround a massive view controller that changes the entire structure if the user types something on the searchbar.</p>
<div class="border-2 border-gray-300 noprose rounded-xl">
<div class="px-4 pb-4 text-gray-600 dark:text-gray-300">
<div class="-mt-2 -mb-6 ">
<a target="_blank" href="https://topt.al/mZczdk">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="../assets/toptal_ad_dark.svg" />
<source media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" srcset="../assets/toptal_ad.svg" />
<img width="150px" src="../assets/toptal_ad.svg" />
</picture>
</a>
</div>
Toptal is an exclusive network of the top freelance software developers, designers, and managers in the world. Top
companies hire Toptal freelancers for their most important projects.
<br />
<a target="_blank" href="https://topt.al/mZczdk" class="font-semibold"> Get up to $2500
signing bonus applying as Software Developer <span aria-hidden="true">→</span></a>
</div>
</div>
<h2 id="using-searchresultscontroller">Using SearchResultsController</h2>
<p>Taking advantage of the implementation of <code>UISearchController</code> <code>searchResultsController</code> makes it magically to switch between our original content and our filtered one without ever handing a search status. This approach becomes even easier when displaying the same UI as we’re currently showing as you can reuse the <code>UIViewController</code> but this is not our case.</p>
<pre><code class="language-swift">searchController = UISearchController(
searchResultsController: resultsContainerViewController
)
// Selecting a suggestion term
resultsContainerViewController.didSelect = search
// Search bar delegate to determine the type of search being performed
searchController.searchBar.delegate = self
searchController.searchBar.placeholder = "Search"
searchController.searchResultsUpdater = self
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
definesPresentationContext = true
</code></pre>
<h2 id="differentiating-searchtype">Differentiating SearchType</h2>
<pre><code class="language-swift">enum SearchType {
case partial
case `final`
}
</code></pre>
<p>Our flow can go two ways, while typing the ‘suggestion terms’ screen will pop up showing a few app recommendations you might be looking for. When you’re done and tap search or tap on a trending term or suggestion the apps screen will be displayed and fetch the apps matching that term.</p>
<p>We use the <code>search(term: String)</code> method when tapped on a term. This will define the <code>searchType</code> and activate the <code>UISearchController</code>.</p>
<pre><code class="language-swift">private func search(term: String) {
searchController.searchBar.text = term
searchType = .final
searchController.isActive = true
}
</code></pre>
<p>Each time we type something we define that the <code>searchType</code> is <code>.partial</code> so our result controller will display the suggestions screen. Unless the text is empty.</p>
<pre><code class="language-swift">extension SearchViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
searchType = searchText.isEmpty ? .final : .partial
navigationController?.navigationBar.setShadow(hidden: searchText.isEmpty)
}
}
</code></pre>
<h2 id="displaying-results">Displaying results</h2>
<p>Finally, since all our view controller swapping code is handled by the result view controller we only need to implement <code>UISearchResultUpdating</code> to perfom an action everytime the user types in the searchbar. <code>updateSearchResults</code> gets called everytime there is interaction with the <code>searchController</code> so it would be useful to handle those cases too.</p>
<pre><code class="language-swift">extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text, !text.isEmpty else {
return
}
resultsContainerViewController.handle(
term: text,
searchType: searchType
)
}
}
</code></pre>
<p>Our <code>ResultsContainerViewController</code> <code>handle(term: , searchType:)</code> implementation will look like this:</p>
<pre><code class="language-swift">class ResultsContainerViewController: ContentStateViewController {
func handle(term: String, searchType: SearchType) {
switch searchType {
case .partial:
suggestionsViewController.searchedTerm = term
transition(to: .render(suggestionsViewController))
case .final:
appsListViewController.reset()
appsListViewController.search(term: term)
transition(to: .render(appsListViewController))
}
}
}
</code></pre>
<p>The key to easily swap view controllers on <code>ResultsContainerViewController</code> class builds over the concept of child view controllers, you can read more about it these two articles by <a href="https://twitter.com/johnsundell">John Sundell</a>: <a href="https://www.swiftbysundell.com/posts/custom-container-view-controllers-in-swift">Custom container view controllers in Swift</a> and <a href="https://www.swiftbysundell.com/posts/using-child-view-controllers-as-plugins-in-swift?rq=plugin">Using child view controllers as plugins in Swift</a></p>
<p>This lets us switch between the suggestions and app results view controllers easily depending on the user’s action by adding/removing childs view controllers.</p>
<p>Final result:</p>
<video autoplay="" loop="" muted="" playsinline="">
<source src="../assets/app_store_search.mp4" type="video/mp4" />
</video>
<p>All the code of this post can be found on a working example project on <a href="https://github.com/marcosgriselli/AppStoreSearch">GitHub</a> or</p>
<p><a href="xcode://clone?repo=https%3A%2F%2Fgithub.com%2Fmarcosgriselli%2FAppStoreSearch">
<button type="button" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-indigo-600 border border-transparent shadow-sm rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
🔨 Open in Xcode
</button>
</a></p>MarcosLately I’ve been working with UISearchController for a friends open source app and decided to share the approach we took to replicate the App Store search functionality.