<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://benscheirman.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://benscheirman.com/" rel="alternate" type="text/html" /><updated>2024-01-21T14:48:25-06:00</updated><id>https://benscheirman.com/feed.xml</id><title type="html">Ben Scheirman</title><subtitle>Ben is an experienced software engineer from Houston, TX. Currently focused on Swift, iOS, Ruby, and Rust.</subtitle><entry><title type="html">Unfuddling the SwiftUI Alignment Guide API</title><link href="https://benscheirman.com/2024/01/swiftui-alignment-guide" rel="alternate" type="text/html" title="Unfuddling the SwiftUI Alignment Guide API" /><published>2024-01-20T12:30:00-06:00</published><updated>2024-01-20T12:30:00-06:00</updated><id>https://benscheirman.com/2024/01/swiftui-alignment-guide</id><content type="html" xml:base="https://benscheirman.com/2024/01/swiftui-alignment-guide"><![CDATA[<p>In general I think Apple’s APIs are pretty good. However there are the occasional exceptions where my brain, for whatever reason, can’t seem to get a hold on the essence of the API design and results in me constantly looking up the docs. In UIKit, the <code>UIAnimatedTransitioning</code> and <code>UIPresentationController</code> APIs come to mind.</p>

<p>In SwiftUI, I’d say the most confusing (to me) is the alignment guide system.</p>

<p>I’m working on an update to one of my apps and I want to have a draggable bar (like a vertical slider) that can split into two when you tap on it.</p>

<table>
<tr>
    <td>
        <img alt="one vertical bar" src="https://res.craft.do/user/full/377428a4-d293-6f1d-0310-8055f7763ca3/doc/62C8D9AE-478E-4E05-A5C2-CD897573BBC0/5136A437-0B05-4A95-8DA2-8AF109BACCF0_2/qXyWROrFM7ITIjzMyoU5VxkFQcnJURThgKozPksyuaYz/CleanShot%202024-01-20%20at%2011.32.232x.png" />
    </td>
    <td>
        <img alt="two vertical bars" src="https://res.craft.do/user/full/377428a4-d293-6f1d-0310-8055f7763ca3/doc/62C8D9AE-478E-4E05-A5C2-CD897573BBC0/3D392465-5E36-4E56-A7A6-AD5A70087C35_2/uQtyJv6RAykgnWKoueJNRgw1XZsjixmNA1RYB28wjsUz/CleanShot%202024-01-20%20at%2011.32.472x.png" />
    </td>
</tr>
</table>

<p>This type of interface <em>could</em> be written like this:</p>

<pre><code class="language-swift">struct DemoView: View {
  @State var isLinked = true

  var body: some View {
    HStack {
      BarView()
      if !isLinked {
        BarView()
      }
    }
    .onTapGesture {
      withAnimation {
        isLinked.toggle()        
      }
    }
  }
}
</code></pre>

<p>Doing this doesn’t look great when you see the animation, however.</p>

<div class="flex justify-center">
  <img src="https://res.craft.do/user/full/377428a4-d293-6f1d-0310-8055f7763ca3/doc/62C8D9AE-478E-4E05-A5C2-CD897573BBC0/700C1968-B44B-4B51-B191-5FAB75453733_2/uASzTP3byyKK1jWbLQlbEQFZDnaPrDToR7vRysVehvsz/AnimatedImage.gif" class="w-72 center" />
</div>

<p>The reason for this is that the <code>HStack</code> has aligned the left bar in the center, and then when it re-renders with 2 bars it slides the bar to the new position. The right bar however was never in the view hierarchy before, so it just fades in to the new position.</p>

<p>In an attempt to fix this, I reached for the <code>.alignmentGuide</code> API, however no matter what I tried, modifying the <code>HorizontalAlignment</code> properties had no effect.</p>

<p>I have probably tried this exact thing a dozen times and am frustrated every time it doesn’t work, hence this post. It’s as much for future me as it is for you, dear reader.</p>

<h2 id="how-alignment-guides-work">How Alignment Guides work</h2>

<p>Using the excellent <a href="https://apps.apple.com/us/app/a-companion-for-swiftui/id1485436674?mt=12">SwiftUI Companion</a> app (must buy if you work with SwiftUI in my opinion) I was able to generate a playground for visualizing how this works:</p>

<p><img src="https://res.craft.do/user/full/377428a4-d293-6f1d-0310-8055f7763ca3/doc/62C8D9AE-478E-4E05-A5C2-CD897573BBC0/648791C3-FC87-44BD-8534-13B3EEF508AC_2/GhxPkhOcpwRO1cJiPBdTzKR47jls5JqmKBQO8xPDAmQz/Screenshot%202024-01-20%20at%2011.40.58AM.png" alt="Screenshot 2024-01-20 at 11.40.58 AM.png" /></p>

<p>The <code>alignmentGuide</code> modifier takes the guide you want to modify (<code>HorizontalAlignment</code> or <code>VerticalAlignment</code> and then the closure lets you specify <em>how</em> you want to modify the view in relation to that.</p>

<p>So in the screenshot above, look at the middle bar. It’s <code>.leading</code> edge (of the parent <code>VStack</code>) is aligned with the <code>.center</code> of the yellow bar. Note that these are all <em>horizontal</em> guides in a vertical layout.</p>

<p>This seems confusing because it doesn’t <em>look</em> like the leading edge is there. But if you play with the toggles here you’ll see what is happening. The edges of the <code>VStack</code> are being pushed out because of the alignment guides. Without the last one enabled, it looks like this:</p>

<p><img src="https://res.craft.do/user/full/377428a4-d293-6f1d-0310-8055f7763ca3/doc/62C8D9AE-478E-4E05-A5C2-CD897573BBC0/8826681F-1E79-484C-B16C-54E90F64BAD9_2/fkIfU9SJYIugGecG9MggSS6qt6xnqm08n0OAEWehxx4z/Image.png" alt="Image.png" /></p>

<p><strong>There are two key insights here.</strong></p>

<ol>
  <li>the alignment guide passed to the <code>.alignmentGuide(…)</code> method refers to the container, not the view we’re modifying.</li>
  <li>the alignment guides influence the layout of the <em>cross</em> dimension of the stack. So for a <code>VStack</code> you can control the horizontal alignment (but clearly the bars here are still stacked vertically). For an <code>HStack</code> you can control the vertical alignment.</li>
</ol>

<p>So in my case I need a <code>ZStack</code> so I can have them vertically aligned <em>and</em> horizontally offset from each other in a way I can modify with the alignment guide.</p>

<h2 id="implementing-the-zstack-solution">Implementing the ZStack solution</h2>

<p>Changing our <code>HStack</code> to a <code>ZStack</code> means that the items will be laid on top of one another, centered in both dimensions (by default). We can change the horizontal alignment of each depending on the value of the <code>isLinked</code> state property.</p>

<p>We also want to remove the <code>if</code> condition in our body because this will cause the 2nd bar to be inserted and removed from the hierarchy as we tap the button. We don’t want this because we want to be in control of the animation.</p>

<pre><code class="language-swift">ZStack {
     BarView()
         .alignmentGuide(HorizontalAlignment.center) { d in
             d[isLinked ? .center : .trailing]
         }
     BarView()
         .alignmentGuide(HorizontalAlignment.center) { d in
             d[isLinked ? .center : .leading]
         }
 }
</code></pre>

<p>With this in place, our view is animating nicely.</p>

<div class="flex justify-center">
  <img src="https://res.craft.do/user/full/377428a4-d293-6f1d-0310-8055f7763ca3/doc/62C8D9AE-478E-4E05-A5C2-CD897573BBC0/EC1F6E9F-41C5-4F1E-A4B0-85C5C928D564_2/dKT7MUOBGcdtwwdJuPuNdVfWyg3JtvYt7xyWRBx6hRoz/CleanShot%202024-01-20%20at%2011.52.41.gif" class="w-72 center" />
</div>

<p>My hope is that by writing this I will now remember this API, and hopefully you will too!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[In general I think Apple’s APIs are pretty good. However there are the occasional exceptions where my brain, for whatever reason, can’t seem to get a hold on the essence of the API design and results in me constantly looking up the docs. In UIKit, the UIAnimatedTransitioning and UIPresentationController APIs come to mind.]]></summary></entry><entry><title type="html">Side Mirror 2.5</title><link href="https://benscheirman.com/2023/03/side-mirror-2-5" rel="alternate" type="text/html" title="Side Mirror 2.5" /><published>2023-03-06T19:39:00-06:00</published><updated>2023-03-06T19:39:00-06:00</updated><id>https://benscheirman.com/2023/03/side-mirror-2-5</id><content type="html" xml:base="https://benscheirman.com/2023/03/side-mirror-2-5"><![CDATA[<p>Today I released a big new update for <a href="https://sidemirrorapp.com">Side Mirror</a>, my macOS app for presenters.</p>

<p>Before I dive into the new features, let me first try to explain what Side Mirror does.</p>

<div class="italic text-center text-sm bg-sky-800 text-white p-4 rounded-lg">
This is truly the hardest part of developing this app. I have yet to find my good elevator pitch.
</div>

<h3 id="side-mirror-allows-you-to-select-any-attached-display-on-your-mac-and-run-it-in-a-window">Side Mirror allows you to select any attached display on your Mac and run it in a window.</h3>

<p>This can be useful for a number of reasons:</p>

<ul>
  <li>
    <p><strong>During live presentations</strong>, to avoid looking back at a projector screen. You can see it and monitor what the audience sees right on your Mac. This keeps your eyes front toward the audience. Your main screen can have presenter notes, snippets to copy &amp; paste, files to drag, etc.</p>
  </li>
  <li>
    <p>Using a cheap HDMI dummy adapter (<a href="https://www.amazon.com/Emulator-Headless-Compatible-Computer-fit-Headless/dp/B09P6DKF28/ref=sr_1_5?crid=3MYDCVBQ2BDCK&amp;keywords=cheap+hdmi+dummy&amp;qid=1678153696&amp;sprefix=cheap+hdmi+dumm%2Caps%2C133&amp;sr=8-5">like this one</a>), you can <strong>get a virtual screen in macOS</strong>. Using Side Mirror, you can put this screen inside a window. There are 2 amazing reasons why this is powerful:</p>
    <ul>
      <li>Presenting via Zoom. You can share your full “virtual” display and not have desktop icons or other windows visible to the audience.</li>
      <li>Recording with Screenflow. Similar to above, but this time recording a full screen video without having to “prep” your desktop. Additionally this is a great way to have a dedicated, resolution-perfect display to record to simplify your workflow.</li>
    </ul>
  </li>
</ul>

<p>So that’s the core of Side Mirror.</p>

<iframe src="https://mastodon.xyz/@bens/109932023746566990/embed" class="my-10 md:mx-12 mastodon-embed" style="max-width: 100%; border: 0" width="400" allowfullscreen="allowfullscreen"></iframe>
<script src="https://mastodon.xyz/embed.js" async="async"></script>

<h3 id="in-order-to-make-using-a-display-in-a-window-actually-usable-there-are-a-few-key-features">In order to make using a display-in-a-window actually usable, there are a few key features</h3>

<ul>
  <li>Teleporting your mouse clicks through the preview window and to where you actually clicked on the display (like magic 🪄)</li>
  <li>Keyboard shortcut to bring your mouse back to the Side Mirror window, to help you avoid getting lost</li>
  <li>Cursor preview hinting as your cursor moves to the external display, so you can easily tell if your mouse is on the display or on top of Side Mirror.</li>
</ul>

<hr />

<h2 id="25-update-features">2.5 update features</h2>

<p>So now that you (hopefully) understand what Side Mirror can do, here are the new features and fixes in 2.5.</p>

<p>✨<strong>New Resize option</strong></p>

<p>Resize the Side Mirror window to match the target display resolution. You’re given 3 options to scale the Side Mirror window to match the target display. If it the main display can’t accommodate it, it does a best fit.</p>

<p>✨<strong>Snap Back Cursor</strong></p>

<p>Enable this in preferences. This is a keyboard shortcut to have your mouse jump back to the Side Mirror window.</p>

<p>✨<strong>Cursor Hinting</strong></p>

<p>Enable this in preferences. This will highlight the Side Mirror preview when your mouse moves to the target display. This can be useful as a guide to let you know which display your mouse is on. You can customize the color in preferences as well.</p>

<p>✨<strong>Remember last display</strong></p>

<p>Side Mirror will now remember the last display used and automatically select it on subsequent launches, if it is available.</p>

<p>✨<strong>Keyboard Shortcuts</strong></p>

<p>There’s now a keyboard shortcut to stop/start the preview</p>

<p>🐞<strong>Bug Fixes</strong></p>

<ul>
  <li>Click-through is now disabled after you stop previewing</li>
  <li>The preview will now be stopped if a display suddenly becomes unavailable</li>
  <li>Displays that lack a title (or have an empty name) will now show “Untitled”</li>
  <li>Fixed an issue that would cause the preview to suddenly stop</li>
</ul>

<p>And one last thing, as I’m actually quite eager to start taking advantage of newer OS features, this will be the last release that supports macOS Catalina.</p>

<h3 id="available-on-the-mac-app-store">Available on the Mac App Store</h3>

<p>You can find Side Mirror on the <a href="https://itunes.apple.com/us/app/side-mirror-preview-whats/id944860108?mt=12">Mac App Store</a>. I hope you enjoy it!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Today I released a big new update for Side Mirror, my macOS app for presenters.]]></summary></entry><entry><title type="html">Async/Await and the Future of Combine</title><link href="https://benscheirman.com/2021/06/async-await-and-the-future-of-combine" rel="alternate" type="text/html" title="Async/Await and the Future of Combine" /><published>2021-06-09T13:31:30-05:00</published><updated>2021-06-09T13:31:30-05:00</updated><id>https://benscheirman.com/2021/06/async-await-and-the-future-of-combine</id><content type="html" xml:base="https://benscheirman.com/2021/06/async-await-and-the-future-of-combine"><![CDATA[<p>Swift 5.5 is here with Xcode 13 Beta and with it comes my favorite new addition to Swift: Async/Await.</p>

<p>But what does this mean for Combine?</p>

<!--more-->

<p>Async/await is a high level, structured model for concurrency in Swift that allows you to write expressive async code in shockingly little ceremony.</p>

<p>A code sample is probably best for this, so consider this example where we want to fetch an image showing the current weather conditions for the user’s location.</p>

<p>To do this requires multiple steps:</p>

<ul>
  <li>First get the user’s location. If that gives an error, fail early.</li>
  <li>Then use that to get the weather conditions</li>
  <li>Look in that response to get an image URL for the current condition</li>
</ul>

<h2 id="the-callback-way">The callback way</h2>

<p>Using traditional callbacks we might have these methods:</p>

<pre><code>func getLocation(completion: @escaping (Result&lt;CLLocation, Error&gt;) -&gt; Void) {
   // ...
}

func getWeather(for location: CLLocation, completion: @escaping (Result&lt;WeatherCondition, Error&gt;) -&gt; Void) {
   // ...
}
</code></pre>

<p>Putting these together gives you:</p>

<pre><code>getLocation { result in 
    switch result {
    case .failed(let error):
       // do something with location error

    case .success(let location):
       getWeather(for: location) { result in 
		    switch result {
    		case .failed(let error):
       		// do something with weather error

		    case .success(let conditions):
             // fetch image
				let imageTask = URLSession.shared.dataTask(with: conditions.imageURL) { (data, response, error) in
               // you get the idea
      	}
		}	  
	 }
}
</code></pre>

<p>Nested completion callbacks each with their own failure path is a giant pile of yuck. If we fail to call back with a failure from a previous step we might end up with an application that seems to get stuck (maybe showing a spinner forever). There’s nothing the compiler does here to help us. And if we have to ensure to call back any completion handler on a specific queue, there’s quite a lot of boilerplate which makes the above code even worse.</p>

<p><em>Of course, Combine makes this much nicer, but more on that in a minute.</em></p>

<h2 id="the-asyncawait-way">The Async/Await Way</h2>

<p>Now with Swift 5.5 this gets a whole lot better.</p>

<pre><code>func userLocation() async throws -&gt; CLLocation {
    // ...
}

func weatherConditions(for location: CLLocation) async throws -&gt; WeatherConditions {
   // ...
}
</code></pre>

<p>Notice how these functions return actual values. They also indicate that they are <code>async</code> and they can <code>throw</code> errors.</p>

<p>There’s also lots of built-in SDK functions that have been adapted to be async, like <code>URLSession.data(for:)</code> .</p>

<p>Putting this all together:</p>

<pre><code>let location = try await userLocation()
let conditions = try await weatherConditions(for: location)
let (imageData, response) = try await URLSession.shared.data(from: conditions.imageURL)
if (response as? HTTPURLResponse)?.statusCode == 200 {
    let image = UIImage(data: imageData)
} else {
    // failed
}
</code></pre>

<p>Notice now the code reads from top to bottom. When we get to the <code>await</code> keyword, our execution is suspended without blocking the thread, so the system is free to do other work.</p>

<p>There’s a lot of nuance here that I’m hand-waving over, but the key point is this is <em>objectively</em> better:</p>

<ul>
  <li>It’s much easier to write</li>
  <li>It’s much easier to read</li>
  <li>The compiler can enforce we handle all execution paths and errors</li>
</ul>

<p>So this looks like the future, right? What about Combine?</p>

<h2 id="what-about-combine">What about Combine?</h2>

<p>Back in iOS 13 we saw the release of Combine, Apple’s take on functional reactive programming. With Combine you can leverage Publishers that produces values over time, and then transform those values using Operators.</p>

<p>Combine provides a ton of power, but comes at a steep learning curve. This is what led me to create <a href="https://combineswift.com">a full course on Combine</a>. So you can imagine that many of us were wondering what would be added to Combine this year in light of all the async/await stuff we’ve seen in Swift Evolution over the past few months.</p>

<p>But searching the API diffs for <em>anything</em> Combine related yielded 0 results. Nothing.</p>

<figure class="kg-card kg-embed-card"><blockquote class="twitter-tweet">
<p lang="und" dir="ltr">😭 <a href="https://twitter.com/hashtag/WWDC21?src=hash&amp;ref_src=twsrc%5Etfw">#WWDC21</a> <a href="https://t.co/OlXlXvo2ar">pic.twitter.com/OlXlXvo2ar</a></p>— Casey Liss (@caseyliss) <a href="https://twitter.com/caseyliss/status/1402013247281287168?ref_src=twsrc%5Etfw">June 7, 2021</a>
</blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
<figcaption>Searching the API diffs fro Combine yields nada</figcaption></figure>

<p>Of course Apple will not tell us what the future plans are, but if we read the tea leaves a bit, it seems to me that this is pretty telling.</p>

<p>Combine’s strengths lie in the fact that it is modeled after streams of events or values. The assumption is that a Publisher can (and often does) emit multiple values before finishing. Many Publishers fire once and complete, but that is an implementation detail.</p>

<p>Contrast that with your typical completion callback or the async example above. Each of these produce a single value and return it.</p>

<p>So what does async/await do for multiple values?</p>

<h2 id="meet-asyncsequence">Meet AsyncSequence</h2>

<p>AsyncSequence is a new type that allows you to concurrently iterate over a series of async values. If you look at the <a href="https://developer.apple.com/documentation/swift/asyncsequence/">API docs</a>, it sure does have a lot of the same operations you’d find in a Combine pipeline:</p>

<ul>
  <li>map</li>
  <li>filter</li>
  <li>prefix</li>
  <li>contains</li>
  <li>reduce</li>
  <li>flatMap</li>
  <li>etc</li>
</ul>

<p>I’ve yet to play around with <code>AsyncSequence</code> (at the time of writing this the session is not yet available), but it certainly appears that this is shaping up to be an eventual replacement for Combine.</p>

<h2 id="looking-ahead-at-asyncstream">Looking Ahead at AsyncStream</h2>

<p>The best way to get an idea of what is coming is to take a look at <a href="https://github.com/apple/swift-evolution/">Swift Evolution</a>. Here we can see a new proposal, <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0314-async-stream.md">SE-0314 AsyncStream and AsyncThrowingStream</a>.</p>

<blockquote>
  <p>The continuation types added in  <a href="https://github.com/apple/swift-evolution/blob/main/proposals/0300-continuation.md">SE-0300</a>  act as adaptors for synchronous code that signals completion by calling a delegate method or callback function. For code that instead yields multiple values over time, this proposal adds new types to support implementing an AsyncSequence interface.</p>
</blockquote>

<p>The key point that this proposal suggests is to bridge the async/await continuation model - which currently assumes a single result or error - into working with multiple values over time.</p>

<p>So it certainly seems that Apple is creating an async model capable of potentially replacing Combine, at least for the majority of use cases.</p>

<h2 id="is-combine-going-away">Is Combine Going Away?</h2>

<p>My previous stance was basically: “You should use Combine because it makes complex async code easier to write, reason about, and Apple will support it”.</p>

<p>I’m now second-guessing that statement somewhat.</p>

<p>Combine won’t <em>go away</em> (at least, not for a long while) but I think it’s use may be limited to the more Functional Reactive programming style and most of the async ergonomics we get from Swift 5.5 will be preferred by most developers. Things like back-pressure and demand management are important concepts for a fully reactive programming framework, but not every async pipeline needs that complexity.</p>

<p>Also, SwiftUI currently depends on Combine for reacting to state changes in your view models.</p>

<p>Maybe next year we will see some Publisher additions that adopt some of the async/await features to make working with Combine simpler. Or perhaps we’ll see AsyncStream take over the majority of these use-cases.</p>

<p>So should you avoid learning Combine? No! I think it’s extremely useful still, and will be more available than async/await since that currently requires macOS 12 / iOS 15. So for my projects that need to support older versions, I’ll still be leaning on Combine for my async needs.</p>

<p>And in the meantime, if you’d like to learn Combine, I’d love for you to <a href="https://combineswift.com">check out my course</a>.</p>]]></content><author><name></name></author><category term="swift" /><summary type="html"><![CDATA[Swift 5.5 is here with Xcode 13 Beta and with it comes my favorite new addition to Swift: Async/Await.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://images.unsplash.com/photo-1519401706-5cf17f6e70de?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDd8fGZvcmslMjByb2FkfGVufDB8fHx8MTYyMzI0NTI0Mg&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=1080" /><media:content medium="image" url="https://images.unsplash.com/photo-1519401706-5cf17f6e70de?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDd8fGZvcmslMjByb2FkfGVufDB8fHx8MTYyMzI0NTI0Mg&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=1080" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Combine for Mere Mortals</title><link href="https://benscheirman.com/2021/01/combine-swift" rel="alternate" type="text/html" title="Combine for Mere Mortals" /><published>2021-01-08T16:32:32-06:00</published><updated>2021-01-08T16:32:32-06:00</updated><id>https://benscheirman.com/2021/01/combine-swift</id><content type="html" xml:base="https://benscheirman.com/2021/01/combine-swift"><![CDATA[<p>I’m excited to announce that my <a href="https://combineswift.com">Combine Swift</a> Course is now complete and available for purchase.</p>

<p>Learning Combine has been extremely rewarding, but learning it can be daunting. I built this course because:</p>

<ul>
  <li>I love the <em>idea</em> of functional reactive programming, but on the surface I found it to be confusing</li>
  <li>Despite reading books and following articles nothing seemed to click for me</li>
</ul>

<p>So I decided to make the course I wish I had when I started out.</p>

<p><strong>The course is aimed at making you more confident in writing complex asynchronous code that is easier to maintain and has less potential for bugs.</strong></p>

<p>It has been a <em>long</em> road to get here, but I’m quite proud of the final result. I’d be thrilled if you checked it out!</p>

<p><a href="https://combineswift.com">Get the Course Here</a></p>

<h2 id="course-overview">Course Overview</h2>

<p>The course is broken up into modules, each building up your knowledge of the Combine framework. Then later modules take that knowledge to solve real world problems.</p>

<ol>
  <li>Introduction &amp; Theory</li>
  <li>Combine Basics</li>
  <li>Error Handling</li>
  <li>Networking</li>
  <li>Schedulers and Threading</li>
  <li>More Publishers</li>
  <li>App Architecture</li>
  <li>Build a Complete Weather App with Combine</li>
  <li>Advanced Combine</li>
  <li>Testing Combine Publishers</li>
  <li>Debugging Pipelines</li>
</ol>

<h2 id="the-back-story">The Back Story</h2>

<p>Learning Combine was a challenge for me because reactive programming in general is such a mind shift. But after I started to get used to it something clicked. I started to find more areas where it would be useful. And I was able to simplify some parts of my code that was previously really complicated.</p>

<p>Last year I started planning out some Combine content and realized that it was much bigger than just a collection of screencasts. I came to the conclusion that this deserved its own complete course.</p>

<p>This was also an opportunity for me to try something new (one-time purchase course versus subscriptions).</p>

<p>Building a course takes an enourmous amount of work and I severely underestimated how much time it would actually take to write, record, edit, and publish the entire thing.</p>

<h2 id="a-custom-course-platform">A custom course platform</h2>

<p>Part of the reason that I underestimated it is that I wanted to build a course platform that was somewhat reusable for future courses I might want to do. There are lots of off the shelf platforms that offer this sort of thing, but I like to own the experience, and nothing works quite how I wanted it to.</p>

<p>The course platform is a custom Ruby on Rails web app, hosted on Digital Ocean. I can write more about this setup in a future post if you’re interested.</p>

<h2 id="course-levels">Course Levels</h2>

<p>I decided to break down the course offerings into 3 packages:</p>

<ul>
  <li><strong>The Standard course</strong><br />
This unlocks Modules 1-4 and gives you a solid fundamental overview of Combine</li>
  <li><strong>The Complete Course</strong><br />
This unlocks all 11 modules and allows you to download the videos for offline use.</li>
  <li><strong>Team Package</strong><br />
These come in groups of 5 seats and offers the complete course for your entire team.</li>
</ul>

<h2 id="whats-next">What’s next?</h2>

<p>While I have talked about this course on Twitter a bit over the course of the past few months, I’ve never really gone into <em>Marketing Mode</em>™. I’ll admit this is firmly outside of my comfort zone, but I also think that this course is worthy of a wide audience of iOS developers, and it’s going to be my goal to try to reach a bigger audience than just my Twitter followers.</p>

<p>On that note, I’d love if you shared this post with your colleagues!</p>]]></content><author><name></name></author><category term="swift" /><summary type="html"><![CDATA[I’m excited to announce that my Combine Swift Course is now complete and available for purchase.]]></summary></entry><entry><title type="html">Managing Version Numbers with Fastlane</title><link href="https://benscheirman.com/2020/10/managing-version-numbers-with-fastlane" rel="alternate" type="text/html" title="Managing Version Numbers with Fastlane" /><published>2020-10-20T22:11:30-05:00</published><updated>2020-10-20T22:11:30-05:00</updated><id>https://benscheirman.com/2020/10/managing-version-numbers-with-fastlane</id><content type="html" xml:base="https://benscheirman.com/2020/10/managing-version-numbers-with-fastlane"><![CDATA[<p>In this post I will describe how I use fastlane to manage my iOS and macOS version numbers for my releases.</p>

<p>There are many ways to deal with versioning, but I prefer to define them manually loosely following a semver model.</p>

<p>If you’re not familiar, that means:</p>

<ul>
  <li>Major versions for large/breaking changes</li>
  <li>Minor version for new features</li>
  <li>Patch for bug fixes</li>
</ul>

<p>You can of course set a version number in Xcode, but this only works for one target at a time, and if you have any Extension targets (for things like Push Notifications) then they need to be set to the same version.</p>

<figure class="kg-card kg-image-card">
  <img src="https://benpublic.s3.amazonaws.com/blog/fastlane/xcode-target-versions.png" />
  <figcaption>Setting a version number in Xcode is easy, but you need to keep it in sync with extensions.</figcaption>
</figure>

<p>Instead, I like to set it with fastlane when doing a release.</p>

<h2 id="bumping-the-version-manually">Bumping the Version Manually</h2>

<p>If you just want to increment one of the version components you can do something like this:</p>

<pre><code>%w{major minor patch}.each do |part|
  lane "bump_#{part}".to_sym do
    increment_version_number(bump_type: part)
  end
end
</code></pre>

<p>This is dynamically defining 3 new lanes that I can invoke from the CLI or from other lanes in my <code>Fastfile</code>:</p>

<pre><code>bump_major
bump_minor
bump_patch
</code></pre>

<p>This leans on the builtin action to increment the version number from Fastlane.</p>

<p>To invoke this from the command line I can run:</p>

<pre><code>bin/fastlane bump_patch
</code></pre>

<p>And I can do this once and be sure that both targets have the new version.</p>

<h2 id="prompting-for-the-version-number">Prompting for the Version Number</h2>

<p>Another approach I’ve taken is to prompt for the marketing version number when doing a release.</p>

<p>Again we’re leaning on builtin fastlane actions here, which make it easy:</p>

<pre><code>private_lane :prompt_for_marketing_version do |options|
  marketing_version = get_version_number
  new_version = UI.input("What marketing version? &lt;enter to keep it at #{marketing_version}&gt;")
  unless new_version.strip == ""
    increment_version_number(version_number: new_version)
    UI.message("Version number set to #{new_version}")
    marketing_version = new_version
  end
end
</code></pre>

<p>I’ll call this in my <code>release</code> lane:</p>

<figure>
  <img src="https://benpublic.s3.amazonaws.com/blog/fastlane/fastlane-version-prompt.png" />
  <figcaption>Fastlane prompts for a version</figcaption>
</figure>

<p>I can press enter to skip it, or type a new number and it will set the target at that version.</p>

<h2 id="committing-the-version-change-and-tagging-the-commit">Committing the Version Change and Tagging the Commit</h2>

<p>After the release it is important to note only commit the version change, but tag the commit in Git so that you have a solid version history you can refer back to later.</p>

<pre><code>if UI.confirm "Commit version bump and Tag this version?"
  commit_version_bump
  add_git_tag(tag: lane_context[SharedValues::VERSION_NUMBER])
  push_git_tags
end
</code></pre>

<p>If we say yes to the prompt, we’ll get a commit in the git history, then it will automatically tag the release with that version number we set earlier. Finally it will push the tags to the git remote (which is often forgotten).</p>

<p>This type of setup is especially important if multiple people are doing releases. You want to have a consistent strategy so that people don’t forget to tag (or forget to push them) and so that the tags all have the same naming consistency.</p>

<pre><code>ben@mac $ git tag
1.0
1.0.1
1.0.2
1.1
1.2
2.0
2.0.1
</code></pre>

<p>If a customer experiences an issue, you can always checkout the tag for that release and start building your fix from there.</p>]]></content><author><name></name></author><category term="ios" /><summary type="html"><![CDATA[In this post I will describe how I use fastlane to manage my iOS and macOS version numbers for my releases.]]></summary></entry><entry><title type="html">Exporting a Git History at a Specific Commit</title><link href="https://benscheirman.com/2020/10/exporting-a-git-history-at-a-specific-commit" rel="alternate" type="text/html" title="Exporting a Git History at a Specific Commit" /><published>2020-10-16T13:52:45-05:00</published><updated>2020-10-16T13:52:45-05:00</updated><id>https://benscheirman.com/2020/10/exporting-a-git-history-at-a-specific-commit</id><content type="html" xml:base="https://benscheirman.com/2020/10/exporting-a-git-history-at-a-specific-commit"><![CDATA[<p>I had a need today to take an existing git repo that has commits, and move the history as of one of those commits into a different git repo.</p>

<p>So my two requirements were:</p>

<ul>
  <li>Show the content as of <code>&lt;sha&gt;</code></li>
  <li>No <code>.git</code> repo or any ignored files in the export</li>
</ul>

<p>It took a few minutes to find the right incantation so I thought I’d blog it here mostly for my future self. Maybe it will help someone else out there.</p>

<div class="callout">
I should note that there are many ways to accomplish things in git, so if you have a better way I'm all ears!
</div>

<p>We start by finding the sha that we want to export:</p>

<pre><code>~/projects/repo $ git log --oneline
f8de9f7 (HEAD -&gt; master) execute shell commands and capture output
d3a8363 Add Files package
5b9242d Initial command line setup
</code></pre>

<p>I’m interested in the first commit, <code>5b9242d</code>.</p>

<p>We can run <code>git archive</code> to get an archive of the repo in a given format. The available formats can be listed:</p>

<pre><code>~/projects/repo $ git archive -l
tar
tgz
tar.gz
zip
</code></pre>

<p>I don’t need any compression, so <code>tar</code> will do, and this happens to be the default. If you want to specify a format you can do so with <code>--format</code>.</p>

<p>So we start by exporting into a tar file, whose filename is specified with the <code>-o</code> option:</p>

<pre><code>~/projects/repo $ git archive -o git.tar 5b9242d
</code></pre>

<p>Now I have the tar file, I can go to the destination folder and untar it:</p>

<pre><code>~/projects/other $ tar xvf ~/projects/repo/git.tar
x .gitignore
x .nova/
x .nova/Tasks/
x .nova/Tasks/Swift Build.json
x Package.resolved
x Package.swift
x README.md
x Sources/
x Sources/swift-encode/
x Sources/swift-encode/main.swift
x Tests/
x Tests/LinuxMain.swift
x Tests/swift-encodeTests/
x Tests/swift-encodeTests/XCTestManifests.swift
x Tests/swift-encodeTests/swift_encodeTests.swift
</code></pre>

<p>If there were files you didn’t want included in the destination folder, like the <code>.nova</code> folder for instance, you can specify that in the untar step:</p>

<pre><code>~/projects/other $ tar xvf ~/projects/repo/git.tar --exclude .nova
</code></pre>

<p>And if we want to combine these commands into one we can avoid the intermediary tar file:</p>

<pre><code>~/projects/repo $ git archive 5b9242d | tar x --exclude .nova --directory ~/projects/other
</code></pre>

<p>I’m sure I’ll need to look this up again, but handy to know!</p>]]></content><author><name></name></author><category term="git" /><category term="cli" /><summary type="html"><![CDATA[I had a need today to take an existing git repo that has commits, and move the history as of one of those commits into a different git repo.]]></summary></entry><entry><title type="html">Sign-in with Apple Profile Information and Security</title><link href="https://benscheirman.com/2020/03/sign-in-with-apple-profile-information-and-security" rel="alternate" type="text/html" title="Sign-in with Apple Profile Information and Security" /><published>2020-03-31T12:37:18-05:00</published><updated>2020-03-31T12:37:18-05:00</updated><id>https://benscheirman.com/2020/03/sign-in-with-apple-profile-information-and-security</id><content type="html" xml:base="https://benscheirman.com/2020/03/sign-in-with-apple-profile-information-and-security"><![CDATA[<p>Sign-in with Apple is a fantastically useful new feature in iOS 13 and macOS Catalina. I knew when it was announced that I would want to support it. I’m currently working on this for <a href="https://nsscreencast.com">NSScreencast</a> and things haven’t gone quite as smooth as I would have expected.</p>

<div class="callout">
    <strong>Update:</strong>
    Thankfully, Apple changed the implementation at some point to include the email address in the signed <code>id_token</code>.
    See down further for more details. Thanks to <a href="https://twitter.com/bastihubrich/status/1245008111485804548">Sebastian Hubrich</a> for correcting me on this.
</div>

<p>For comparison, I already support Sign-in with Google (as required by some larger companies with team accounts). In this implementation I use the <code>sorcery</code> rubygem which supports several social logins (Google and a few dozen others), but does not currently support Sign-in with Apple <em>(henceforth: “SIWA”)</em>.</p>

<p>I looked at providing support for it myself, however I couldn’t seem to follow the patterns that the rest of these plugins follow. The OAuth payload usually contains the requisite profile information, such as first name, last name, email address, etc. With SIWA, it doesn’t, and this presents a few challenges.</p>

<h2 id="profile-information-only-once">Profile Information Only Once</h2>

<p>Apple provides the profile information to your application (or website) only once. The callback payload for a web app looks something like this:</p>

<pre><code class="language-json">{
    "code": "1234781237213123...",
    "id_token": "PEHANCKakenAKDNFAhekqoioenakdfnn53i2kKK23k2k2nn3...",
    "state": "..."
    "user": {
        "name": {
            "firstName": "Kurt",
            "lastName": "Cobain"
        }
    },
    "email": "kurt@example.com"
}
</code></pre>

<p>The <code>id_token</code> is a signed JWT that contains information like:</p>

<ul>
  <li>Who issued this token?</li>
  <li>Who is it for?</li>
  <li>When does it expire?</li>
  <li>What crypto algorithm can we use to validate it?</li>
  <li>What is the cryptographic signature of the token to prove it hasn’t been tampered with or created by some other entity?</li>
</ul>

<p>As you can see the <code>id_token</code> is an important part of this puzzle. If you aren’t familiar with JWTs yet, you will need to <a href="https://jwt.io">read up on them</a>, as it is required knowledge for a SIWA implementation.</p>

<p>Alongside the token is a <code>user</code> object that contains profile information. We requested email address and full name, so those are provided here.  In my case - and I suspect in many implementations - this user profile information is required to sign up for a new account.</p>

<p>So what happens if the user starts the sign in process and then cancels at the last step? What if your app encounters and error and the user has to retry? This <code>user</code> object will not be sent to you.</p>

<p>The <a href="https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_js/configuring_your_webpage_for_sign_in_with_apple">docs warn you</a> of this:</p>

<figure>
<img src="https://benpublic.s3.amazonaws.com/blog/sign-in-with-apple/sign-in-with-apple-warning.png" />
<figcaption>Apple only returns the user object the first time the user authorizes the app. Persist this information from your app; subsequent authorization requests won’t contain the user object.</figcaption>
</figure>

<p><strong>This part is not only a nuisance, but a security concern that you will have to mitigate.</strong> More on the security aspect in a minute.</p>

<p>Most implementations will end up saving this profile information to later associate it with a successful sign in. And this produces a failure case you have to handle: What if you have no <code>user</code> hash above and also don’t have any saved profile information? The only recourse is to ask the user to navigate to the Apple ID account settings and remove your app from the list and try again. Ick.</p>

<p>In my case I have a Rails model to save this information in my database:</p>

<pre><code># Used to save user profile information from Sign-in w/ Apple
# We only get this information once, so if they abandon the sign-up
# process mid-way, we need to be able to retrieve it again if it is missing.
class AppleUserInfo &lt; ApplicationRecord
end
</code></pre>

<p>This model has a <code>sub</code> column, which contains the Apple unique user id value, and a <code>jsonb</code> column for the <code>user</code> object above.</p>

<p>So now when a user signs up, I can provide a fallback user hash if the callback payload doesn’t contain one:</p>

<pre><code>def callback(params)
  id_token = params.fetch(:id_token)

  parsed_token = verify(id_token) # decode and verify the authenticity of the JWT

  user_hash = params[:user] || AppleUserInfo.find_by!(sub: parsed_token[:sub]).user_hash

  # if we don't have a user hash here we have a problem and need to ask the user
  # to delete the app from their Apple ID account settings and try again :(

  # ...
end
</code></pre>

<p>So if I fall into that last case where I have no <code>user</code> hash saved and can’t fall back, we’re stuck and we can’t continue. So I have to ask the user to remove the app from their Apple ID account settings. This error message is awful, I know. And while it is a rare edge case it’s still technically possible to encounter.</p>

<figure class="kg-card kg-image-card">
    <img src="https://benpublic.s3.amazonaws.com/blog/sign-in-with-apple-profile-information-and-security/DraggedImage.a026524483c544a5b124c01439de3927.png" class="kg-image" />
    <figcaption>If you have no profile information and you haven't saved any prior, you have to resort to ugly error messages like this one, prompting the user to delete their login and try again.</figcaption>
</figure>

<p>The user doesn’t have an account on your system, but iCloud thinks they do. So you have to go to Apple ID settings on your device and remove the app from there. Once they do that it will be like a fresh sign-in experience again.</p>

<h2 id="similar-experience-on-ios">Similar Experience on iOS</h2>

<p>On iOS we have a similar experience, though the mechanics are different. Once a user taps a Sign-in with Apple button, you request authorization like this:</p>

<pre><code>let provider = ASAuthorizationAppleIDProvider()
let request = provider.createRequest()
request.requestedScopes = [
    .fullName, .email
]

let authorizationController = ASAuthorizationController(authorizationRequests: [request])
authorizationController.delegate = self
authorizationController.presentationContextProvider = self
authorizationController.performRequests()
</code></pre>

<p>Here we request <code>.fullName</code> and <code>.email</code> scopes, so we expect this information to be present when we get the callback:</p>

<pre><code>func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {

    if let appleIDCred = authorization.credential as? ASAuthorizationAppleIDCredential {

        let idToken = appleIDCred.identityToken
        let authCode = appleIDCred.authorizationCode

        let userId = appleIDCred.user // gives you Apple's unique id for this user. In the web callback id_token this is called 'sub'
		
        let fullName: PersonNameComponents? = appleIDCred.fullName
        let email: String? = appleIDCred.email

        // ...
    }
}
</code></pre>

<p>As you can see here, the credentials we get back from the Apple ID authorization callback include the <code>idToken</code>, as well as <code>fullName</code> and <code>email</code>, but these last two are both optional properties for the reasons mentioned above.</p>

<div class="callout">
  <strong>Update</strong>: The `idToken` here now includes the email address, so if that's all you need to sign up then you're good. 
  Just read it from the token instead of the credential object directly. 
  Your server will be decoding and verifying this token anyway, providing you with `email` and `email_verified` 
  keys in the payload section of the JWT.
</div>

<p>So here we’re in the same boat. We need to save this information somewhere and then fall back to it if it wasn’t provided. In my case I chose the keychain:</p>

<pre><code>KeychainHelper.save(KeychainKeys.userId, data: appleIDCred.user)

if let fullName = appleIDCred.fullName {
    if let givenName = fullName.givenName {
        KeychainHelper.save(KeychainKeys.firstName, data: givenName)
    }
    if let familyName = fullName.familyName {
        KeychainHelper.save(KeychainKeys.lastName, data: familyName)
    }
}

if let email = appleIDCred.email {
    KeychainHelper.save(KeychainKeys.email, data: email)
}
</code></pre>

<p>And then we follow a similar flow for falling back to this information when creating an account. Again, we can fall into a case where these don’t exist and have to ask the user to delete the login from their device’s Apple ID settings and try again.</p>

<h2 id="the-security-problem">The Security Problem</h2>

<div class="callout">
    <strong>Update</strong>: This problem is now much less of an issue since `email` is part of the `idToken`. I'll leave the description here nonetheless since you still probably want to avoid automatically linking accounts unless you've verified the email address you have is actually theirs.
</div>

<p>Earlier I mentioned that the fact that the user information is provided <em>outside</em> the <code>idToken</code> as a sibling node means that we can’t trust it. We can only trust the values embedded in the token, as those are signed with a signature that we can verify. Anything outside of that could be tampered with and your app’s server would not know.</p>

<p>What if an account already existed on your system with the provided email address? You might want to be friendly and allow the user to just link them up and use either credentials to log into the same a count.</p>

<p>Here’s an attack vector:</p>

<ul>
  <li><strong>Mary</strong> has an account on your system. Her email address is <strong>mary@example.com</strong> and she logs in with a username and password.</li>
  <li><strong>Anita</strong> <em>knows</em> that Mary has an account. She uses the mobile app and crafts a man-in-the-middle attack, intercepting the traffic coming from the mobile app to your server. Using her own Apple ID, she logs in to create an account. On the outgoing request, she modifies the above <code>user</code> hash to indicate a different email address, in this case <strong>mary@example.com</strong>.</li>
</ul>

<p>If we were to allow linking up accounts based on the provided email address, we’d now have allowed Anita to steal Mary’s account.</p>

<p><em>If</em> the profile information was provided in the JWT itself, Anita would be unable to do this, as modifying the JWT in any way would break the signature and the server would not trust it.</p>

<p>The answer to this situation is to not allow linking up based on email address alone, and demand instead that that user logs in with their password credentials first, then associate with an Apple ID later.</p>

<h2 id="summary">Summary</h2>

<p>I originally wrote a TL;DR for this post and then realized the situation is nuanced and requires some explanation. While SIWA is not without it’s faults, the end-user experience is largely a positive one and supporting it is likely worth your time.</p>

<p>I recently released support for SIWA on NSScreencast.com and currently have around 50% of sign-ups using this over username &amp; password. I find this to be quite encouraging, and would recommend you add support for your apps as well.</p>]]></content><author><name></name></author><category term="ios" /><summary type="html"><![CDATA[Sign-in with Apple is a fantastically useful new feature in iOS 13 and macOS Catalina. I knew when it was announced that I would want to support it. I’m currently working on this for NSScreencast and things haven’t gone quite as smooth as I would have expected.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://benpublic.s3.amazonaws.com/blog/sign-in-with-apple/siwa-header-1.png" /><media:content medium="image" url="https://benpublic.s3.amazonaws.com/blog/sign-in-with-apple/siwa-header-1.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Swift for Good</title><link href="https://benscheirman.com/2020/01/swift-for-good" rel="alternate" type="text/html" title="Swift for Good" /><published>2020-01-17T18:49:13-06:00</published><updated>2020-01-17T18:49:13-06:00</updated><id>https://benscheirman.com/2020/01/swift-for-good</id><content type="html" xml:base="https://benscheirman.com/2020/01/swift-for-good"><![CDATA[<p>A few months ago <a href="https://www.hackingwithswift.com">Paul Hudson</a> asked me if I would be interested in contributing to a new book. The idea of the book was this: many authors, each writing a chapter about Swift, with all proceeds going to charity. I quickly accepted and began writing.</p>

<p>This week, the book is a reality! The book is entitled <strong><a href="https://www.swiftforgood.com">Swift for Good</a></strong> and includes content from <em>twenty</em> different authors. All proceeds of the book benefit <a href="https://blackgirlscode.org">Black Girls Code</a>, a charity devoted to proving to the world that girls of every color have the skills to become software engineers.</p>

<figure class="bg-slate-100 rounded-md p-6">
    <img src="https://benpublic.s3.amazonaws.com/blog/swift-for-good/swift-for-good.png" />
    <figcaption>Swift for Good</figcaption>
</figure>

<p>My chapter covers writing a networking stack in Swift that uses <a href="https://www.themoviedb.org">TheMovieDB</a>’s API as an example. In the chapter, we build out everything we need to get a robust, extensible networking layer in our project that allows us to write our requests like this:</p>

<pre><code>MovieDB.api.send(request: .popularMovies({ result in
     switch result {
         case .success(let page):
             self.movies = page.results

         case .failure(let error):
             print("Error: \(error)")
     }
 }))
</code></pre>

<p>The chapter also goes into how we might leverage <em>Request Adapters</em> to provide things like authorization headers to every request or logging to get some insight into how our network requests are working. Along the way we leverage a number of Swift’s strengths such as Codable models, protocol extensions, enums, auto closures, and inline types.</p>

<p>I’m quite happy with how my chapter turned out. There’s also a load of excellent material from the other amazing authors in the book.</p>

<p>Thanks to Paul Hudson for putting this project (and the book, literally) together! Also thanks to the other 19 authors for donating their time and expertise to build a book for a good cause. I’m proud to have been a part of it!</p>

<p>You can buy the book here:  <a href="https://www.swiftforgood.com">https://www.swiftforgood.com</a></p>

<p>I do hope you check it out!</p>]]></content><author><name></name></author><category term="swift" /><summary type="html"><![CDATA[A few months ago Paul Hudson asked me if I would be interested in contributing to a new book. The idea of the book was this: many authors, each writing a chapter about Swift, with all proceeds going to charity. I quickly accepted and began writing.]]></summary></entry><entry><title type="html">Modern AppKit File Permissions</title><link href="https://benscheirman.com/2019/10/troubleshooting-appkit-file-permissions" rel="alternate" type="text/html" title="Modern AppKit File Permissions" /><published>2019-10-23T23:38:19-05:00</published><updated>2019-10-23T23:38:19-05:00</updated><id>https://benscheirman.com/2019/10/troubleshooting-appkit-file-permissions</id><content type="html" xml:base="https://benscheirman.com/2019/10/troubleshooting-appkit-file-permissions"><![CDATA[<p>Sandboxing has been a fact of macOS development for quite some time now. With each release of macOS we see an increasing number of features and new security constraints that we must live with.</p>

<p>This is great for consumers, but (as with most things Security) can be a pain in the neck for developers.</p>

<p>I decided to start building a little utility app to stitch together some scripts that I use to publish <a href="https://nsscreencast.com">NSScreencast</a>. This is largely for my own education, and gives me an excuse to do more AppKit development.</p>

<p>My first task was to just list the episodes from a folder on my local machine in a tableview.  The data for this would come from <code>FileManager</code> , so I could just list the contents of my directory, right? Right?</p>

<pre><code>let url = URL(fileURLWithPath: "/path/to/folder") 
try FileManager.default.contentsOfDirectory(at: url,
    includingPropertiesForKeys: nil,
    options: .skipsHiddenFiles) // permissions error
</code></pre>

<p>Wrong.</p>

<p>This code fails with an error indicating you don’t have permission to the folder.</p>

<p>A sandboxed macOS app doesn’t have access to the entire file system. Instead, apps have to be <em>granted permission</em> to read &amp; write to specific folders.</p>

<p>This isn’t true for your sandbox of course. Each app is given an isolated place to store data, documents, cache, and settings. But in this case I want to read files outside of my sandbox. For this I’ll need user permission.</p>

<figure class="kg-card kg-image-card">
  <img src="https://benpublic.s3.amazonaws.com/blog/appkit-file-permissions/6FCC20C6-7E3C-45FD-879E-120C5806F866.png" />
  <figcaption> This is an example of Bear's sandbox, the app I'm writing this post in. </figcaption>
</figure>

<p>So I need to somehow get access to a folder outside my sandbox.</p>

<h2 id="granting-permission-to-access-folders">Granting Permission to Access Folders</h2>

<p>There is no API for saying “Please prompt the user to access this folder”. Instead, this is done in one of three ways:</p>

<ul>
  <li>Full Disk Access</li>
  <li>Prompting the user to open a file/directory</li>
  <li>Dragging &amp; Dropping a folder onto the application</li>
</ul>

<p>Of course you could also turn off Sandboxing altogether and not deal with this stuff. Doing so, however, means you would not be able to distribute your app on the Mac App Store.  <em>I personally don’t have this requirement, but I’m also stubborn and wanted to know how to do this stuff, so here we are.</em></p>

<h2 id="full-disk-access">Full Disk Access</h2>

<p>There is of course the option to use Full Disk Access, but this is a sledgehammer, and isn’t appropriate for most applications. If you do need Full Disk Access, this would require opening the Security preference pane, going into Full Disk Access, unlocking the UI, and dragging the app into this list.</p>

<figure class="kg-card kg-image-card"><img src="https://benpublic.s3.amazonaws.com/blog/modern-appkit-file-permissions/7CA43971-0B29-44C0-A13F-1C59D455A99E.png" class="kg-image" /></figure>

<p>Users will need assistance doing this, so I’ve noticed that many apps have their own way of prompting and instructing the user how to do this dance.</p>

<p>To top it off, the user has to quit the app and relaunch for the app to see these new permissions.</p>

<h2 id="fine-grained-permissions-with-nsopenpanel">Fine Grained Permissions with NSOpenPanel</h2>

<p>In our case we want permission to read (and write) to a single folder that is outside of our app’s sandbox. We’l need to enable this ability.</p>

<p>First, click on your project in the navigator on the left, then click “Signing &amp; Capabilities”. Here you can manage a few common options when dealing with Sandboxing. Look for <strong>File Access - User Selected File</strong> and change this to <em>Read/Write</em>. This will give you a new entitlement in your entitlements file.</p>

<figure class="kg-card kg-image-card">
    <img class="border border-slate-200 shadow-xl rounded-md p-4" src="https://benpublic.s3.amazonaws.com/blog/modern-appkit-file-permissions/2C81482A-097B-4F01-8062-C3A50AAF03A2-1.png" />
</figure>

<p>If you only wanted to write to one of the blessed locations listed above, then you don’t need to do this step, just set the appropriate permission level and you can access that folder.</p>

<p>For the User Selected File case, we have to show user intent. Here we’ll pop a panel to “open the directory”, which will signal to the OS this intent:</p>

<pre><code>private func promptForWorkingDirectoryPermission() -&gt; URL? {
    let openPanel = NSOpenPanel()
    openPanel.message = "Choose your directory"
    openPanel.prompt = "Choose"
    openPanel.allowedFileTypes = ["none"]
    openPanel.allowsOtherFileTypes = false
    openPanel.canChooseFiles = false
    openPanel.canChooseDirectories = true

    let response = openPanel.runModal()
    print(openPanel.urls) // this contains the chosen folder
    return openPanel.urls.first
}
</code></pre>

<p>If we use the <code>URL</code> instance that we get from the above function, we can pass this to  <code>FileManager</code> to list the contents of the directory.</p>

<p>One caveat though: <em>this access will expire when the user quits the app</em>. Ugh.</p>

<h2 id="drag--drop">Drag &amp; Drop</h2>

<p>Another way of signaling intent is to have the user drag &amp; drop the folder to your app. To do this, you’ll have to go through the dance which will allow your app to accept a drag.</p>

<p>In my case I created a custom <code>DragHandlingView</code> and associated delegate to pass this information off to a controller:</p>

<pre><code>protocol DragDelegate : class {
    func handleDrag(from view: DragHandlingView, dragInfo: NSDraggingInfo) -&gt; Bool
}

class DragHandlingView: NSView {
    weak var delegate: DragDelegate?

    override func draggingEntered(_ sender: NSDraggingInfo) -&gt; NSDragOperation {
        return .generic // this controls what the icon does ("copy", "move", "link", etc)
    }

    override func performDragOperation(_ sender: NSDraggingInfo) -&gt; Bool {
        return delegate?.handleDrag(from: self, dragInfo: sender) ?? false
    }
}
</code></pre>

<p>Then in my controller, I request permission for the drag types I want to accept:</p>

<pre><code>class WorkingDirectoryPromptViewController: NSViewController, DragDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        registerForDragDrop()
    }

    private func registerForDragDrop() {
        (view as! DragHandlingView).delegate = self
  
        // we don't need file contents, just the location
        view.registerForDraggedTypes([.fileURL])
    }
    
    // ...
}
</code></pre>

<p>Then we can handle the drag:</p>

<p>Armed with these two methods, you now have a couple ways to be given a special URL that has access to the folder the user selects.</p>

<pre><code>func handleDrag(from view: DragHandlingView, dragInfo: NSDraggingInfo) -&gt; Bool {
    // the dragInfo contains a "pasteboard" with our data
    if let fileURL = NSURL(from: dragInfo.draggingPasteboard) as URL? {
        print("FILE: \(fileURL)")
        var isDir: ObjCBool = false
        _ = FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &amp;isDir)

        if !isDir.boolValue {
            let alert = NSAlert()
            alert.alertStyle = .warning
            alert.messageText = "Drag a folder instead."
            alert.runModal()
            return false
        } else {
            workingDirectoryChosen(fileURL)
        }
    }
    return true
}
</code></pre>

<h2 id="persistent-access-to-a-folder">Persistent Access to a Folder</h2>

<p>By default the approaches above grant you access <em>while the app remains open</em>. When you quit the app, any folder access you had is lost.</p>

<p>To gain persistent access to a folder even on subsequent launches, we’ll have to take advantage of a system called Security-Scoped Bookmarks.</p>

<p>It is helpful to <a href="https://developer.apple.com/library/archive/documentation/Security/Conceptual/AppSandboxDesignGuide/AppSandboxInDepth/AppSandboxInDepth.html#//apple_ref/doc/uid/TP40011183-CH3-SW16">read the docs</a> on this, as there is little information elsewhere on the web about this (hence this article).</p>

<p>To start we’ll need to add an entitlement to MyApp.entitlements:</p>

<pre><code class="language-xml">&lt;key&gt;com.apple.security.files.bookmarks.app-scope&lt;/key&gt;
&lt;true/&gt;
</code></pre>

<div class="callout">
<em>I was informed by <a href="https://twitter.com/lapcatsoftware">Jeff Johnson</a> that this entitlement is no longer required, despite it still being listed in the docs. I filed a Feedback for this (<strong>FB7405463</strong>).</em>
</div>

<p>Next, after you’ve been granted permission to a folder using one of the methods above, we need to create a bookmark to that folder with a a special flag:</p>

<pre><code class="language-swift">private func saveBookmarkData(for workDir: URL) {
    do {
        let bookmarkData = try workDir.bookmarkData(options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil)

        // save in UserDefaults
        Preferences.workingDirectoryBookmark = bookmarkData
    } catch {
        print("Failed to save bookmark data for \(workDir)", error)
    }
}
</code></pre>

<p>On a subsequent launch we can check for the existence of this data and try to load the URL back (preserving its permissions):</p>

<pre><code class="language-swift">private func restoreFileAccess(with bookmarkData: Data) -&gt; URL? {
    do {
        var isStale = false
        let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &amp;isStale)
        if isStale {
            // bookmarks could become stale as the OS changes
            print("Bookmark is stale, need to save a new one... ")
            saveBookmarkData(for: url)
        }
        return url
    } catch {
        print("Error resolving bookmark:", error)
        return nil
    }
}
</code></pre>

<p>With these approaches you should now have access to the folder even across app launches.</p>

<p>But there’s one more step! We have to tell the system when we want to use the “resource” and when we’re done.</p>

<h2 id="start--stop-requesting-access">Start / Stop Requesting Access</h2>

<p>When we’re ready to read files, we’ll have to wrap this in a pair of calls to signal that we want to access the resource:</p>

<pre><code>if !workingDir.startAccessingSecurityScopedResource() {
    print("startAccessingSecurityScopedResource returned false. This directory might not need it, or this URL might not be a security scoped URL, or maybe something's wrong?")
}

paths = try FileManager.default.contentsOfDirectory(at: workingDir, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
            .map {
                $0.relativePath.replacingOccurrences(of: workingDir.path, with: "")
            }
workingDir.stopAccessingSecurityScopedResource()
</code></pre>

<p>Here we call <code>startAccessingSecurityScopedResource</code> on the URL we’ve been given. This might return <code>false</code>  under a few conditions:</p>

<ul>
  <li>The user really doesn’t have access to this</li>
  <li>The URL isn’t a security scoped URL</li>
  <li>This directory doesn’t need it (remember <code>~/Downloads</code>, etc above?)</li>
</ul>

<p>So we’re just logging this value and continuing anyway. We might still get an error when accessing the <code>contentsOfDirectory</code> and we’ll have to handle that by prompting for access again.</p>

<h2 id="some-gotchas">Some Gotchas</h2>

<p>In working through this issue I ran into numerous gotchas.</p>

<ul>
  <li><strong>Watch out for symlinks</strong>. My working directory is full of them, and I wanted to list contents of a nested folder that was actually a symlink, and this doesn’t work. You have to grant permissions to the real folder, which may involve additional prompts to grant all the permissions you need.</li>
  <li><strong>Watch out for folders that already have access</strong>. Even though we’re prompting for access, the user might select <code>~/Downloads</code> or <code>~/Pictures</code> or something. This will work fine, but you might run into confusion when <code>startAccessingSecurityScopedResource</code> returns <code>false</code>. In these cases we can just continue.</li>
  <li><strong>Watch out for Dropbox and other filesystem-bending plugins</strong>. Dropbox does some useful but unholy things to Finder and the file system in order to do its job. This might result in strange behavior when trying to access those files &amp; folders.</li>
</ul>

<h2 id="special-thanks">Special Thanks</h2>

<p>I want to thank to <a href="https://twitter.com/danielpunkass">Daniel Jalkut</a>, <a href="https://twitter.com/ikenndac">Daniel Kennett</a>, and <a href="https://twitter.com/liscio">Chris Liscio</a> for their invaluable help shedding some light on this. Also thanks to Jeff Johnson for noting <a href="https://twitter.com/lapcatsoftware/status/1187207846963924992">the security scoped bookmark entitlement is no longer needed</a>.</p>

<hr />

<div class="text-sm text-slate-300 dark:text-slate-800 italic mb-8">
Header graphic by Chris Panas.
</div>]]></content><author><name></name></author><category term="macos" /><category term="swift" /><summary type="html"><![CDATA[Sandboxing has been a fact of macOS development for quite some time now. With each release of macOS we see an increasing number of features and new security constraints that we must live with.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://images.unsplash.com/photo-1648834153013-2bb82a72bba4?ixlib=rb-4.0.3&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=4470&amp;q=80" /><media:content medium="image" url="https://images.unsplash.com/photo-1648834153013-2bb82a72bba4?ixlib=rb-4.0.3&amp;ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;auto=format&amp;fit=crop&amp;w=4470&amp;q=80" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Xcode Environment Specific Configuration</title><link href="https://benscheirman.com/2018/10/xcode-environment-specific-configuration" rel="alternate" type="text/html" title="Xcode Environment Specific Configuration" /><published>2018-10-02T15:50:15-05:00</published><updated>2018-10-02T15:50:15-05:00</updated><id>https://benscheirman.com/2018/10/xcode-environment-specific-configuration</id><content type="html" xml:base="https://benscheirman.com/2018/10/xcode-environment-specific-configuration"><![CDATA[<p>Almost every app you build with Xcode will need some sort of configuration. Whether it is API Keys for 3rd party SDKs, the URL of your API, feature toggles, or a logging level of verbosity, it’s a good idea to keep this configuration separate from your code.</p>

<p>The biggest reason you would want to do this is so that you can provide a different value depending on your build settings. For development builds, use a development API. For production, use the live API.</p>

<p>You might even want to isolate analytics between your beta distribution and your App Store distribution.</p>

<p>The way I approached this on the <a href="https://nsscreencast">NSScreencast</a> app is by using the <code>Info.plist</code> along with a custom configuration plist that I write a wrapper for.</p>

<div class="callout">
It should be noted that the method described in this post does not imply any amount of secrecy whatsoever. If you need to hide keys and other sensitive data in your binary, take a look at 
<a href="https://github.com/orta/cocoapods-keys">cocoapods-keys</a>,
<a href="https://github.com/rogerluan/arkana">Arkana</a>. I covered both of these in screencast form
<a href="https://nsscreencast.com/episodes/308-managing-secrets" title="Managing Secrets">here</a>
and <a href="https://nsscreencast.com/episodes/542-managing-secrets-with-arkana" title="Managing Secrets with Arkana">here</a>.
</div>

<h2 id="defining-configuration-per-environment">Defining configuration per environment</h2>

<p>In my projects I start by adding a <code>Configuration</code> group, along with a propertly list called <code>config.plist</code>.</p>

<figure class="kg-card kg-image-card"><img src="https://benpublic.s3.amazonaws.com/blog/xcode-environment-specific-configuration/configplist.png" class="kg-image" /><figcaption>config.plist from NSScreencast’s iOS app</figcaption></figure>

<p>In the plist there are three root keys:</p>

<ul>
  <li><code>Common</code> - These will hold settings that apply no matter the environment</li>
  <li><code>Local</code> - These apply when running a specific scheme for local development</li>
  <li><code>Production</code> - This applies to any release build</li>
</ul>

<p>Aside from <code>Common</code> you can name the environments whatever you like. You aren’t limited to these either, you can add as many as you need for your project.</p>

<h2 id="how-do-i-specify-which-environment-to-use">How do I specify which environment to use?</h2>

<p>The approach I’ve taken for this is to leverage Xcode build configurations along with the <code>Info.plist</code>. The reason is that the <code>Info.plist</code> is easy to read from your application <em>and</em> can contain substitutions based on build settings.</p>

<p>We’ll start by making an xcconfig file structure:</p>

<ul>
  <li><code>Configuration/Shared.xcconfig</code></li>
  <li><code>Configuration/Debug.xcconfig</code></li>
  <li><code>Configuration/Debug.LocalServer.xcconfig</code></li>
  <li><code>Configuration/Release.xcconfig</code></li>
</ul>

<p>You’ll want one of these for each build configuration you maintain. Here I’ve added a <code>Shared.xcconfig</code> in case there are any settings that would otherwise be duplicated in all of the config files.</p>

<p>You can also see that I have an extra one called <code>Debug.LocalServer.xcconfig</code>. This  is because I added an extra build configuration for the sole purpose of changing configuration like this. My build configurations look like this:</p>

<figure class="kg-card kg-image-card"><img src="https://benpublic.s3.amazonaws.com/blog/xcode-environment-specific-configuration/buildsettings.png" class="kg-image" /><figcaption>Choosing the xcconfig files for each build configuration</figcaption></figure>
<h3 id="what-if-i-use-cocoapods-doesnt-it-already-use-xcconfig-files">What if I use CocoaPods? Doesn’t it already use xcconfig files?</h3>

<p>Yep, you’ll have to take one extra step if you already have xcconfig files in use, which is true if you use CocoaPods.</p>

<p>Take note of the xcconfig file that is in use already, then simply include it in your own xcconfig file.</p>

<p>Here’s my <code>Release.xcconfig</code>:</p>

<pre><code>#include "../Pods/Target Support Files/Pods-NSScreencast-Base-NSScreencast/Pods-NSScreencast-Base-NSScreencast.release.xcconfig"
#include "Shared.xcconfig"

ConfigEnvironment=Production
</code></pre>

<p>And here is my <code>Debug.LocalServer.xcconfig</code>:</p>

<pre><code>#include "../Pods/Target Support Files/Pods-NSScreencast-Base-NSScreencast/Pods-NSScreencast-Base-NSScreencast.release.xcconfig"
#include "Shared.xcconfig"

ConfigEnvironment=LocalServer
</code></pre>

<p>All we are doing here is setting a build setting called <code>ConfigEnvironment</code>.</p>

<blockquote>
  <p>You can do loads of other things with these xcconfig files, including keeping interesting build settings under version control with comments. If you’re curious, take a look at the screencasts I did on the topic: <a href="https://nsscreencast.com/episodes/154-xcconfig-files">xcconfig files</a> (and <a href="https://nsscreencast.com/episodes/155-xcconfig-files-part-2">part 2</a>). Also worth mentioning is <a href="https://jamesdempsey.net/2015/01/31/generating-xcode-build-configuration-files-with-buildsettingextractor-xcodeproj-to-xcconfig/">James Dempsey’s BuildSettingExtractor tool</a>.</p>
</blockquote>

<p>Now that we have a different build setting per environment, we need to make this available at runtime. Open the <code>Info.plist</code>.</p>

<p>We’re going to add one line here at the top level:</p>

<figure class="kg-card kg-image-card"><img src="https://benpublic.s3.amazonaws.com/blog/xcode-environment-specific-configuration/infoplist.png" class="kg-image" /><figcaption>Adding ConfigEnvironment to Info.plist</figcaption></figure>

<p>This dynamic value gives us the missing piece to be able to determine the configuration at runtime.</p>

<div class="callout">You might be asking why we don’t just put all of our variables in the <code>Info.plist</code>. My answer is that this file already has a lot of uses, and is owned primarily by the operating system. I prefer isolating my configuration so that it easy to understand and change. There's also another benefit which I’ll get to at the end of this post.</div>

<h2 id="tying-it-all-together">Tying it all together</h2>

<p>Now that we have our configuration values, and we know what environment we’re in, we need an easy way to refer to these values at runtime. For this, we’ll create a class called <code>EnvironmentConfiguration</code>:</p>

<pre><code>final class EnvironmentConfiguration {
    private let config: NSDictionary
    
    init(dictionary: NSDictionary) {
        config = dictionary
    }
    
    convenience init() {
        let bundle = Bundle.main
        let configPath = bundle.path(forResource: "config", ofType: "plist")!
        let config = NSDictionary(contentsOfFile: configPath)!
        
        let dict = NSMutableDictionary()
        if let commonConfig = config["Common"] as? [AnyHashable: Any] {
        
            dict.addEntries(from: commonConfig)
            
        }
        if let environment = bundle.infoDictionary!["ConfigEnvironment"] as? String {
            if let environmentConfig = config[environment] as? [AnyHashable: Any] {
                dict.addEntries(from: environmentConfig)
            }
        }
        
        self.init(dictionary: dict)
    }
}
</code></pre>

<p>Note that the values in <code>Common</code> <em>can</em> be overridden by the environment specific values. This way you can provide defaults in the <code>Common</code> category, and then provide an override just in one environment if you need to.</p>

<p>This parses our plist, but doesn’t provide a way to read the values. This is where I add an extension and provide strongly-typed accessors for each key I want to by able to read:</p>

<pre><code>extension EnvironmentConfiguration {
    var baseApiUrl : String {
        return config["BaseApiUrl"] as! String
    }
    
    var logLevel : String {
        return config["LogLevel"] as! String
    }
    
    var googleClientId: String {
        return config["GoogleClientId"] as! String
    }
    
    var oneSignalAppId: String {
        return config["OneSignalAppId"] as! String
    }
    
    var appCenterKey: String {
        return config["AppCenterKey"] as! String
    }
}
</code></pre>

<p>Doing it this way means I can share the <code>EnvironmentConfiguration.swift</code> between projects, and then you just add an extension for the keys specific to your application.</p>

<p>Now, when I need a value from configuration, I can do this:</p>

<pre><code>let config = EnvironmentConfiguration()
let apiClient = ApiClient(baseUrl: config.baseUrl)
</code></pre>

<h2 id="what-about-testing">What about Testing?</h2>

<p>I’m glad you asked! With this structure it’s also trivial to initialize with your own in-memory dictionary instead of reading from the file. The only thing to worry about here is to avoid instantiating this configuration object from everywhere in your code. In fact, it may be beneficial to have your view controllers and other components depend on an abstraction:</p>

<pre><code>protocol NSScreencastConfiguration {
    var baseApiUrl: String { get }
    var logLevel: String { get }
    var googleClientId: String { get }
    var oneSignalAppId: String { get }
    var appCenterKey: String { get }
}

extension EnvironmentConfiguration : NSScreencastConfiguration { }
</code></pre>

<p>With this in place, any component in your system that needs configuration should refer to the protocol instead of the concrete type.</p>

<pre><code>class MyViewController : UIViewController {
    private let config: NSScreencastConfiguration
    
    init(config: NSScreencastConfiguration) {
        self.config = config
        super.init(nibName: nil, bundle: nil)
    }
    
    required init(coder: NSCoder) { fatalError() }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        if config.logLevel == "verbose" {
            // log all the things!
        }
    }
}
</code></pre>

<p>Passing around dependencies like this is a difficult practice to get into (and adhere to). It adds friction, but it is a good idea to make your dependencies <em>explicit</em>.</p>

<h3 id="what-else-can-you-do">What else can you do?</h3>

<p>This post so far describes static configuration. This is configuration that you set at development time and never change. But what if you could modify these values… at runtime?</p>

<p>This requires adding some additional layers to the approach, but essentially is the same underlying idea: Your configuration is stored in a dictionary in memory, and you pass around the configuration object to components that need it.</p>

<p>This means it would be possible for you to build a sort of live debug screen in your debug and beta configurations, allowing you to tweak settings, target a different version of the API, or turn up log levels dynamically.</p>

<p>It would be like having a Quake Console for your app.</p>

<figure class="kg-card kg-image-card"><img src="https://benpublic.s3.amazonaws.com/blog/xcode-environment-specific-configuration/quakeconsole-1.jpg" class="kg-image" /><figcaption>The Quake Console enabled debug info, cheats, and other abilities to players by pressing the backtick key.</figcaption></figure>

<p>Configuration is an important piece of most applications. By providing a flexible structure for providing configuration values, altering them based on environment, you can have a clean separation of code and configuration.</p>

<p>How do you handle configuration in your apps? I’d love to hear about alternative approaches to this concept. <a href="https://twitter.com/subdigital">Hit me up on Twitter</a>.</p>]]></content><author><name></name></author><category term="swift" /><summary type="html"><![CDATA[Almost every app you build with Xcode will need some sort of configuration. Whether it is API Keys for 3rd party SDKs, the URL of your API, feature toggles, or a logging level of verbosity, it’s a good idea to keep this configuration separate from your code.]]></summary></entry></feed>