🎥 Jason Bourne
Alone
🎥 Jason Bourne
Alone
Old Union Station in Indianapolis
This week, I attended Release Notes, and it was great. I sketchnoted the talks, and posted a summary with all of them. Before I came to the conference, my wife asked what I was hoping to get out of it, and my first answer was to meet new people. She was a little surprised by that, and reminded me that networking is not something that I typically enjoy or do particularly well. So I set a goal to talk with at least one person each day for an extended time and get to know them better.
Accomplishing that goal was easier than I had thought, although it still required me to push myself outside of my comfort zone. The conference was organized intentionally for a group of introverted developers who want to meet other people, but are not skilled at that. We had plenty of time between talks to have conversations, and long breaks for lunch and before dinner. It was perfect for me. The other big thing that helped was my sketchnotes. After the first couple talks, people began to notice the sketchnotes, and it was an easy jumping off point to begin conversations with people. It is so much easier when people come up to talk to you instead of the inverse.
I want to force myself to turn my thoughts into action as I leave the conference. I found myself talking about Pointedly every time that I introduced myself, and realized that it is my true passion project right now, and I should do more with it.
Many of these actions items come down to discipline for me. I need to be better at time management and making sure that I am focusing on my most important tasks. My adherence to a planning and execution system has fluctuated wildly, but I know that I am most effective when I am consistent and methodical.
One unexpected outcome of this conference was a rekindling of my passion for sketchnoting. I have done some occasional sketchnotes over the past couple of years, but since I started focusing on iOS development, I have done little else. One thing that I want to let simmer in the back of my mind is an app to help with sketchnoting. I have no idea what shape that will take yet, but it would be a perfect marriage of my two biggest professional passions. In the meantime, I want to make sure that this is a skill that I continue to hone and exercise regularly. Creating sketchnotes is incredibly rewarding personally, but the best part is the connections that it forms with others. People are drawn to them, and it makes for great conversations that could not happen otherwise. Like most things, I need to carve out time to practice so that I can progress and avoid regressing.
I am so grateful that I had the opportunity to attend Release Notes this year. I look forward to working to implement these ideas and to continue to learn from others and nurture the relationships that I formed.
Fantastic reminder from Cafe Patachou:
“A human created the problem, a human can fix it.”
📖 Shadows in Flight
By Orson Scott Card
Release Notes 2016 Sketchnotes posted on @sketchnotable
📖 Shadow of the Giant
By Orson Scott Card
📖 Shadow Puppets
By Orson Scott Card
One goal that I have had for all of my open-source projects is to have run automated builds and have complete test coverage. Achieving this goals is a slow process, but something that I have wanted to learn and get more comfortable with so that I can be more disciplined. I decide to post the build status and coverage data for all of my libraries on my site, to provide myself additional incentive to hurry and get everything updated.
At my day job, we are using Jenkins and Fastlane to run all unit and UI tests after every commit that is pushed, and to submit to iTunes Connect after every merge to master
following a successful pull request. For my open-source libraries, I just wanted something to make sure that they build and run all the tests after changes. For now, I landed on using Travis to run the automated builds, and Codecov to collect coverage reports. I wanted to capture some of the process to be able to refer back to it, and hopefully it can help others with similar goals.
All examples are using my TextMagic framework. Remember to change the names to match your project.
travis.yml
to project (see sample below)slather.yml
to to limit coverate reporting to Sources
using Slather (see sample below)xcodebuild -workspace TextMagic.xcworkspace -scheme TextMagic -sdk iphonesimulator9.3 -destination="OS=9.3,name=iPhone 6S Plus" -configuration Debug GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES GCC_GENERATE_TEST_COVERAGE_FILES=YES ONLY_ACTIVE_ARCH=YES test | xcpretty -c
slather coverage -s --scheme TextMagic --workspace TextMagic.xcworkspace/ TextMagic.xcodeproj/
<div class=“badges”> <a href=“https://travis-ci.org/benjaminsnorris/TextMagic"><img src=“https://img.shields.io/travis/benjaminsnorris/TextMagic.svg" alt=“Build Status”></a> <br/> <code class=“highlighter-rouge”> </code> <br/> <br/> <a href=“https://codecov.io/gh/benjaminsnorris/TextMagic"><img src=“https://img.shields.io/codecov/c/github/benjaminsnorris/TextMagic.svg" alt=“Code Coverage”></a> <br/> <code class=“highlighter-rouge”> </code> </div>
travis.yml
filelanguage: objective-c
before_install:
- gem install xcpretty --no-rdoc --no-ri --no-document --quiet
- gem install slather
env:
global:
- LC_CTYPE=en_US.UTF-8
- LANG=en_US.UTF-8
- PROJECT_NAME="TextMagic"
- WORKSPACE_SUFFFIX=".xcworkspace"
- FRAMEWORK_SCHEME="TextMagic"
- IOS_SDK=iphonesimulator9.3
matrix:
include:
- osx_image: xcode7.3
env: DESTINATION="OS=9.0,name=iPhone 6S Plus" SCHEME="$FRAMEWORK_SCHEME" SDK="$IOS_SDK"
- osx_image: xcode7.3
env: DESTINATION="OS=9.1,name=iPhone 6S" SCHEME="$FRAMEWORK_SCHEME" SDK="$IOS_SDK"
- osx_image: xcode7.3
env: DESTINATION="OS=9.3,name=iPad Pro" SCHEME="$FRAMEWORK_SCHEME" SDK="$IOS_SDK"
script:
- set -o pipefail
- xcodebuild -version
- xcodebuild -showsdks
- xcodebuild
-workspace "$PROJECT_NAME$WORKSPACE_SUFFFIX"
-scheme "$SCHEME"
-sdk "$SDK"
-destination "$DESTINATION"
-configuration Debug
GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES
GCC_GENERATE_TEST_COVERAGE_FILES=YES
ONLY_ACTIVE_ARCH=YES
test
| xcpretty -c
after_success:
- slather
- bash <(curl -s [codecov.io/bash](https://codecov.io/bash)) -J "$PROJECT_NAME" -f ./test_reports/cobertura.xml
slather.yml
file# .slather.yml
coverage_service: cobertura_xml
xcodeproj: TextMagic.xcodeproj
workspace: TextMagic.xcworkspace
scheme: TextMagic
source_directory: Sources
output_directory: test_reports
bash <(curl -s [codecov.io/bash](https://codecov.io/bash)) -J "$PROJECT_NAME" -f ./test_reports/cobertura.xml
Even though this call is included already in the sample travis.yml
file, I want to explain it a bit more.
bash <(curl -s [codecov.io/bash](https://codecov.io/bash))
downloads the latest script from Codecov.
-J "$PROJECT_NAME"
specifies the packages to build coverage. This can significantly reduces time to build coverage reports.
-f ./test_reports/cobertura.xml
targets the file that Slather created so that it is not searching for all reports.
Apple Special Event Sep 2016 Sketchnotes posted on @sketchnotable
📖 Shadow of the Hegemon
By Orson Scott Card
This will not come as a surprise to most people—writing tests makes your code better. I have come to appreciate this more over the past few months, as I wrote about previously. Also not a surprise—writing tests first makes your code even better. From my experience with iOS developers, while we might know this, most of us do not do it. Even after coming to see the value, it is easy to skip straight to writing the code. I had an experience this week that highlighted the value of this for me again, and wanted to pause and reflect before allowing myself to lose the lesson.
I wrote earlier this week about handling live text reload elegantly, and wanted to share a bit more about the process I went though. It went something like this:
After I was done, it hit me how different it was to write the code before the tests instead of vice versa. When the code came first, my tests just verified my code. I had come up with all the scenarios I thought I needed to support, and had coded them, so I just made sure that they worked as I intended.
However, when I wrote the tests first, it was a completely different mental exercise. I was not worried about how I was going to implement the different scenarios—I was just focused on thinking of all possible scenarios. Not only did I think of many different cases for text selection, but I realized that I had omitted a number of cases for cursor position as well.
My adherence to TDD has ebbed and flowed over time. It is easy to want to jump straight into writing the code and seeing something work in the simulator. But taking the time at the beginning to consider the desired behavior, and writing tests for that behavior makes for better code and fewer rewrites and regressions.
Sometimes, it can feel impossible to start with tests. Your tests will not compile if the objects they are testing do not exist yet. One approach that has worked well for me is to start by writing descriptive function names for tests, and then moving on to writing the rest of everything. The process looks something like this:
One of the most exciting aspects to programming is how much you learn while you are working through something. Invariably, you will have to make changes to the tests as you think through the implementation details. The trick is to make sure that your tests remain focused on the behavior, and not the actual implementation details. That way, if you refactor the implementation, your tests can still verify that the behavior remains intact.
A major benefit to using this approach is that it helps you write more reusable code, which has been a focus of mine lately. You still need to learn about good design patterns and best practices. But starting with your tests means that you are exercising the API from the beginning, and that will help you think through having a good API as you build it.
Part of my purpose in writing this post is to help myself recommit to TDD in all of my projects. It is too easy to get lazy and skip the tests altogether. That is part of why I have put build, coverage and version badges on all of my open-source project pages on this site. I am hoping that the shame of not having tests and builds and coverage will help motivate me to get this done quickly. My future self will thank me later.
In my current project at my day job, we are using Firebase and ReSwift. I plan to write more about this powerful combination soon. One of the major advantages is that it allows us to easily support live reloading of concurrent editing. However, I ran into a problem in long-form text editing. It was impressive to see the text update while someone else edited the same data, but if you were also trying to type, it would get extremely frustrating. With every reload, your cursor would jump to the end of the text, making it nearly impossible to keep working.
One of my favorite podcasts, Runtime, recently mentioned approaches to diffing text. I reached out to Sam Soffes, who pointed me to a simple library he created for this, diff.
Using that library, I was ready to tackle preserving the cursor position and text selection when the underlying text changed. In the hopes that others can benefit from or improve this work, here is the code that I am using:
func updateText(with newString: String?) {
guard let textView = textView, newString = newString,
(diffRange, changedText) = diff(textView.text, newString) else { return }
guard let selectedRange = textView.selectedTextRange else { textView.text = newString; return }
textView.text = newString
let cursorOffset = textView.offsetFromPosition(textView.beginningOfDocument, toPosition: selectedRange.start)
let selectedEndOffset = textView.offsetFromPosition(textView.beginningOfDocument, toPosition: selectedRange.end)
let selectedRangeLength = selectedEndOffset - cursorOffset
if selectedEndOffset < diffRange.startIndex {
// Change is after current cursor
moveCursorRelativeToBeginning(with: cursorOffset, rangeLength: selectedRangeLength)
} else if cursorOffset < diffRange.startIndex && selectedEndOffset > diffRange.endIndex {
// Change occurs within selection
moveCursorRelativeToBeginning(with: cursorOffset, rangeLength: selectedRangeLength + changedText.characters.count - diffRange.count)
} else if cursorOffset >= diffRange.endIndex {
// Change occurs completely before current cursor
moveCursorRelativeToBeginning(with: cursorOffset + changedText.characters.count - diffRange.count, rangeLength: selectedRangeLength)
} else if diffRange.startIndex < selectedEndOffset && diffRange.startIndex > cursorOffset {
// Change starts in middle of selection
moveCursorRelativeToBeginning(with: cursorOffset, rangeLength: selectedRangeLength - (selectedEndOffset - diffRange.startIndex))
} else if diffRange.startIndex <= cursorOffset && cursorOffset < diffRange.endIndex {
// Change is a removal/change over the current cursor position
let rangeLength = selectedRangeLength - (diffRange.endIndex - cursorOffset)
moveCursorRelativeToBeginning(with: cursorOffset - (cursorOffset - diffRange.startIndex) + changedText.characters.count, rangeLength: rangeLength > 0 ? rangeLength : 0)
}
}
private func moveCursorRelativeToBeginning(with offset: Int, rangeLength: Int = 0) {
guard let textView = textView, startPosition = textView.positionFromPosition(textView.beginningOfDocument, offset: offset), endPosition = textView.positionFromPosition(startPosition, offset: rangeLength) else { return }
textView.selectedTextRange = textView.textRangeFromPosition(startPosition, toPosition: endPosition)
}
This is even more useful when combined with the automated tests that ensure that it is working properly. Here are all of the tests:
import XCTest
import Nimble
import Diff
@testable import align
class TextEditingSpec: XCTestCase {
var textEditing: TextEditing!
override func setUp() {
super.setUp()
textEditing = TextEditing.initializeFromStoryboard()
let _ = textEditing.view
textEditing.textView = UITextView()
}
/// test that it loads properly
func testThatItLoadsProperly() {
expect(self.textEditing.textView).toNot(beNil())
expect(self.textEditing.textView?.text) == ""
expect(self.textEditing.title).to(beNil())
}
// MARK: - Cursor position tests
// Original text: "Watch Bugger attack videos together and discuss strategy."
/// test that cursor position does not change if state changes but agenda is unchanged
func testThatCursorPositionDoesNotChangeIfStateChangesButAgendaIsUnchanged() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 10)
expect(self.cursorOffset()) == 10
expect(self.selectedRangeLength()) == 0
textEditing.updateText(with: "Watch Bugger attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 10
expect(self.selectedRangeLength()) == 0
}
/// test that cursor position does not change when agenda has changes after cursor
func testThatCursorPositionDoesNotChangeWhenAgendaHasChangesAfterCursor() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 10)
expect(self.cursorOffset()) == 10
expect(self.selectedRangeLength()) == 0
textEditing.updateText(with: "Watch Bugger attack videos together.")
expect(self.cursorOffset()) == 10
expect(self.selectedRangeLength()) == 0
}
/// test that cursor position changes when agenda has removed text before current cursor position
func testThatCursorPositionChangesWhenAgendaHasRemovedTextBeforeCurrentCursorPosition() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 10)
expect(self.cursorOffset()) == 10
expect(self.selectedRangeLength()) == 0
textEditing.updateText(with: "Bugger attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 4
expect(self.selectedRangeLength()) == 0
}
/// test that cursor position changes when agenda has changed text before current cursor position
func testThatCursorPositionChangesWhenAgendaHasChangedTextBeforeCurrentCursorPosition() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 10)
expect(self.cursorOffset()) == 10
expect(self.selectedRangeLength()) == 0
textEditing.updateText(with: "View Bugger attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 9
expect(self.selectedRangeLength()) == 0
}
/// test that cursor position changes when agenda has removed text that includes current cursor position
func testThatCursorPositionChangesWhenAgendaHasRemovedTextThatIncludesCurrentCursorPosition() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 10)
expect(self.cursorOffset()) == 10
expect(self.selectedRangeLength()) == 0
textEditing.updateText(with: "Watch attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 0
}
/// test that cursor position changes when agenda has changed text that includes current cursor position
func testThatCursorPositionChangesWhenAgendaHasChangedTextThatIncludesCurrentCursorPosition() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 10)
expect(self.cursorOffset()) == 10
expect(self.selectedRangeLength()) == 0
textEditing.updateText(with: "View recorded Bugger attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 18
expect(self.selectedRangeLength()) == 0
}
// MARK: - Selected text tests
/// test that text selection does not changes when text does not change
func testThatTextSelectionDoesNotChangesWhenTextDoesNotChange() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Watch Bugger attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
}
/// test that text selection does not change when text changes occur after selection
func testThatTextSelectionDoesNotChangeWhenTextChangesOccurAfterSelection() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Watch Bugger attack videos and discuss strategy.")
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
}
/// test that text selection remains same but moves when text is added before selection
func testThatTextSelectionRemainsSameButMovesWhenTextIsAddedBeforeSelection() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Watch the Bugger attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 10
expect(self.selectedRangeLength()) == 6
}
/// test that text selection adjusts to include changes that occur within the selection
func testThatTextSelectionAdjustsToIncludeChangesThatOccurWithinTheSelection() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Watch Bear attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 4
}
/// test that text selection expands to include additions that occur within the selection
func testThatTextSelectionExpandsToIncludeAdditionsThatOccurWithinTheSelection() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Watch Big bad bugger attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 14
}
/// test that text selection is truncated when the end of the selection is removed
func testThatTextSelectionIsTruncatedWhenTheEndOfTheSelectionIsRemoved() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Watch Bug attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 3
}
/// test that text selection is truncated when the end of the selection is changed
func testThatTextSelectionIsTruncatedWhenTheEndOfTheSelectionIsChanged() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Watch Bug vehicle attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 3
}
/// test that text selection is truncated when the beginning of the selection is removed
func testThatTextSelectionIsTruncatedWhenTheBeginningOfTheSelectionIsRemoved() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Watch er attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 2
}
/// test that text selection is truncated and moved when the beginning of the selection is changed
func testThatTextSelectionIsTruncatedAndMovedWhenTheBeginningOfTheSelectionIsChanged() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Watching some tiger attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 16
expect(self.selectedRangeLength()) == 3
}
/// test that cursor does not move when the exact selection is removed
func testThatCursorDoesNotMoveWhenTheExactSelectionIsRemoved() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Watch attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 0
}
/// test that cursor is moved when entire selection is removed
func testThatCursorIsMovedWhenEntireSelectionIsRemoved() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Wattack videos together and discuss strategy.")
expect(self.cursorOffset()) == 3
expect(self.selectedRangeLength()) == 0
}
/// test that cursor is moved when entire selection is changed
func testThatCursorIsMovedWhenEntireSelectionIsChanged() {
textEditing.textView?.text = "Watch Bugger attack videos together and discuss strategy."
textEditing.textView?.becomeFirstResponder()
moveCursorRelativeToBeginning(with: 6, length: 6)
expect(self.cursorOffset()) == 6
expect(self.selectedRangeLength()) == 6
textEditing.updateText(with: "Watching attack videos together and discuss strategy.")
expect(self.cursorOffset()) == 8
expect(self.selectedRangeLength()) == 0
}
}
// MARK: - Private functions
private extension TextEditingSpec {
private func cursorOffset() -> Int {
guard let textView = textEditing.textView, selectedRange = textView.selectedTextRange else { return 0 }
return textView.offsetFromPosition(textView.beginningOfDocument, toPosition: selectedRange.start)
}
private func selectedRangeLength() -> Int {
guard let textView = textEditing.textView, selectedRange = textView.selectedTextRange else { return 0 }
return textView.offsetFromPosition(textView.beginningOfDocument, toPosition: selectedRange.end) - cursorOffset()
}
private func moveCursorRelativeToBeginning(with offset: Int, length: Int = 0) {
guard let textView = textEditing.textView, startPosition = textView.positionFromPosition(textView.beginningOfDocument, offset: offset), endPosition = textView.positionFromPosition(startPosition, offset: length) else { return }
textView.selectedTextRange = textView.textRangeFromPosition(startPosition, toPosition: endPosition)
}
}
And in case it is easier to consume, I created a Gist with all of the code.
Edit: Since writing this, I decided to pull all of this code into a simple library: TextMagic.
Like many developers, I was anxious to install iOS 10 on my devices. This summer, I went all in and installed iOS 10 beta on my primary iPhone, iPad, as well as watchOS 3 beta on my Apple Watch. I have had remarkably few issues, but one of the major problems was installing apps that I still needed to work on that needed to be shipped before iOS 10 came out.
By default, you cannot install an app from Xcode 7 on a device running iOS 10. You will see an error that reads, “Could not find Developer Disk Image” as shown below.
The problem is that Xcode 7 does not have support for iOS 10. Using Terminal, you can easily see which operating systems Xcode is able to support.
$ cd /Applications/Xcode.app/Content/Developer/Platforms/iPhoneOS.platform/DeviceSupport
$ ls
In order for Xcode 7 to install apps on your device running iOS 10, you first need to make sure that it works with Xcode 8. Download the beta, and open a project with your device connected. Typically, you will need to wait for the symbol files to install, and then you should be able to install. In some cases, restarting your device is necessary, or even resetting the network settings (Settings -> General -> Reset -> Reset Network Settings
).
Once that it working, you can look in the Xcode beta device support folder to find iOS 10 support.
There are a couple options to give Xcode 7 the information that it needs. You could copy over the folder containing 10.0 support, but the simplest option is to create a symbolic link using the ln
command in Terminal (documentation). As you type this out in Terminal, be sure to use Tab
to help autocomplete the directory names in order to avoid typos. And naturally, if either Xcode app is in a different place, or the exact version of iOS 10 is different, you will make adjustments for your environment.
$ ln -s
/Applications/Xcode-beta.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/10.0\ \(14A5339a\)/
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/10.0
After adding the link, you will need to quit and restart Xcode 7. Connect your iOS 10 device, and you should now be able to install and run your app!
📖 The Swarm
By Orson Scott Card
Since deciding that I wanted to change careers to become a full-time developer, I have started listening to a number of podcasts. Some of my favorites are Under the Radar, the spiritual successor to my previous favorite Developing Perspective, Core Intuition, and Release Notes. There are many others that I have enjoyed off and on, including the Accidental Tech Podcast and The Talk Show, as well as some new ones I am loving, such as Runtime and Canvas. I am not sure how many hours of podcasts I have listened to, but I know that Overcast has saved me an extra 46 hours from Smart Speed alone, if that tells you anything.
One of the interesting things about listening to podcasts is that you spend hours and hours listening to someone share their thoughts and feelings on issues that are important in your life. You start to feel as if you know these people from having spent so much time with them. And since most friendships come from spending time together, you begin to feel as if you are friends.
But there is a problem.
The person on the other end of this relationship does not know you at all. They have no idea that they have spent countless hours with you. From their perspective, there is no relationship.
I had an experience recently where I was on the other end of this phenomenon. In addition to my day job as an iOS developer, I work at DevMountain, a coding bootcamp here in Utah, teaching iOS development. Many of the lessons that I have given were recorded, and current students watch those recordings as part of their curriculum. Over the past few weeks, I have had a few students come up and talk to me. They both said something to the effect of, “I feel like I know you from watching all your videos.” I had to chuckle when I realized that I had an lopsided relationship with these students.
My mind was taken back to WWDC 2015, when I had the chance to meet _David Smith, the man behind Developing Perspective. We got to chat for just a minute, and I told him how much his podcast and his willingness to share what he has learned have helped me in my own development career. It was a highlight of the conference for me.
Caleb Hicks, David Smith, Joshua Howland, and me
From these difference experiences, and from talking with other people, I have some thoughts about how to handle meeting someone with whom you are in one of these lopsided relationships. If you are on the side that believes that you are friends, start with a brief statement explaining how you know the other person. That can go a long way to alleviate an otherwise awkward encounter. If you are on the oblivious side, do your best to make the other person feel comfortable. Recognize that the person who is meeting you feels vulnerable and probably a little intimidated. If you are both introverted developers, chances are neither of you are particularly good at interpersonal interactions, so you will both be awkward together, and that is ok.
However these relationships form, they can turn into real, meaningful relationships with just a little effort. Almost everyone who produces work that you admire is just a normal, friendly person and would appreciate you saying a kind word letting them know what you appreciate about them. Next time you see someone in a lopsided relationship with you, go up and introduce yourself!
One of the most annoying interactions with any app or service is logging in. None of us likes having to remember usernames and passwords, and often they are difficult to type in. Even if they are easy, it is never what you actually want to do. You are accessing an app or service in order to accomplish a task, and logging in is a barrier to success. However, none of us want to feel like our sensitive information is not secure, so logging in is something of a necessary evil.
One thing that you can do as a developer is to make this experience as simple and painless as possible. One option, which I plan to discuss in the future, is to integrate with 1Password. This is a popular password management app, and integration is quite simple.
Another approach is what I want to cover today: integrating with Shared Web Credentials, or from a user’s perspective, integrating with Safari saved passwords. This is available since iOS 8, and is an effective way to simplify life for your users. You should read the official documentation, but I will go through the steps that I did in order to retrieve passwords successfully. In a future post, I will address creating an account and saving a new password.
Xcode does a great job of simplifying the process of adding entitlements. In order to use shared web credentials, you need to enable the Associated Domains
entitlement:
Then in place of example.com
, you need to put your domain. Read through the documentation for this section to make sure that you do not miss anything.
In order for the association to work, you have to upload a simple JSON file to your server. Even if you do not have a web app that users will sign in to, as long as you have a website for your app, you can upload the file and make it easier for your users to log in on additional devices. The file should be named apple-app-site-association
with no extension, and is simple JSON. It should look like this:
{
"webcredentials": {
"apps": [
"D3KQX62K1A.com.example.DemoApp"
]
}
}
Inside the apps
array, you should list all of the bundle identifiers for apps that should be able to share passwords from this site. The prefix to the bundle identifier is usually your Team ID. To be sure, pull up the app in developer.apple.com and copy the prefix from the App ID information.
Since we are focusing on retrieving passwords, and not saving them from the app, it will make testing easier to manually add the password to the device you are going to test on. I will include instructions for using the Simulator, but the same basic steps apply if you are using a physical device.
1234
for the passcodeThere are many approaches to retrieving the shared password. The method I have chosen is to make the call to retrieve the shared password when the user taps on the username or password field in the login form. That way, it is not as jarring for the user when first navigating to the form, but it is still helpful at the moment the user wants to take action.
The actual code for retrieving the credentials is fairly simple. Here is an example with a completion closure that returns an optional username and password.
func requestSharedPassword(completion: (username: String?, password: String?) -> ()) {
SecRequestSharedWebCredential(nil, nil) { credentials, error in
dispatch_async(dispatch_get_main_queue()) {
guard error == nil else {
completion(username: nil, password: nil)
return
}
guard let unwrappedCredentials = credentials else {
completion(username: nil, password: nil)
return
}
let arrayCredentials = unwrappedCredentials as [AnyObject]
guard let typedCredentials = arrayCredentials as? [[String: AnyObject]] else {
completion(username: nil, password: nil)
return
}
guard let credential = typedCredentials.first else {
completion(username: nil, password: nil)
return
}
guard let username = credential[String(kSecAttrAccount)] as? String,
password = credential[String(kSecSharedPassword)] as? String else {
completion(username: nil, password: nil)
return
}
completion(username: username, password: password)
}
}
}
func textFieldDidBeginEditing(textField: UITextField) {
requestSharedPassword { username, password in
guard let username = username, password = password else { return }
usernameField.text = username
passwordField.text = password
self.submit()
}
}
Finally, with the code in place, you can run the app again. When the user taps in the username or password field, call your requestSharedPassword
function, and if everything is set up properly, you will see something like this example from an app I am currently working on:
And that is it! These steps really are simple and straightforward, but the effect on the user experience of your app is tremendous.
📖 🛏 All Creatures Great and Small
By James Herriot
📖 Harry Potter and the Cursed Child
Play by J. K. Rowling, Jack Thorne, and John Tiffany
📖 Ender’s Shadow
By Orson Scott Card
🎥 Angry Birds
with children two and three
📖 Ender in Exile
By Orson Scott Card
As I approached the end of 2015, I was thinking of my goals for 2016, and how I wanted to grow and improve as a developer. One of the main areas in which I felt like I was lacking was in automated testing. That is a topic that many developers talk about, usually with a slight sense of shame, or a tinge of regret. There is almost always a tacit acknowledgement that it is important, and we should be doing it, but for many reasons, it is not happening.
As an iOS development community, we seem to largely have not yet embraced testing. There are many notable developers who are huge proponents of testing, but there seem to be even more who are not. I was talking with a Ruby developer about why that is. He commented that without tests, he has no way to know if his code is working—there is nothing he can easily pull up to see the code in action. With iOS development, we launch the simulator, and immediately conduct a manual test. So writing automated tests feels unnecessary.
One of the most common refrains that I hear is that writing automated tests feels like writing the app a second time, but with no obvious value to the end user. The conclusion to that line of thinking is that the time would be better spent building out more features that do benefit customers.
One experience that I had in late 2015 started to change my thinking about this. I participated in a Startup Weekend event in Salt Lake City, and was on a team that built a bowling app for the then-yet-to-be-released Apple TV. I had never built a game before, and I was tasked with creating the actual game play—the bowling lane, pin, balls, and logic of the game. After diving into SceneKit and figuring out how to get the ball to go down the lane and knock over pins, I had to implement scoring logic, and reset the pins correctly based on the results of the frame. The logic was fairly simple, up until the tenth frame. I realized that it was going to be extremely difficult to manually test all of the different options. So I wrote a suite of automated tests to make sure that everything worked properly. That suite saved me a number of times as I would make a tweak for something like a strike or a spare, only to find out that I had broken part of the tenth frame logic.
At the beginning of 2016, I started on a new team at my day job, and began working on a new project, Align. We started with a quick prototype that had no automated testing. But as we began work on the real app, I took the time to set up a continuous integration build with Jenkins and Fastlane that would run all of my tests and push to iTunes Connect for Test Flight. Then I began writing tests for every single thing that I implemented.
As I worked to maintain high test coverage, I found that writing automated tests, and thinking about writing automated tests did at least three things for me:
I worked hard to make sure that all non-UI code was as completely covered with unit tests as possible. And then I wrote UI tests both to test the UI code, and also to serve as integration tests for the whole app. There were a number of configuration things that I had to set up, and I hope to discuss those in future posts. Having a comprehensive suite of unit and UI tests has already saved me from some major bugs. I have made significant changes to the architecture and organization of the project, and have been able to verify within seconds where I had introduced problems in the app, or verify with confidence that everything was still working.
One practice that I have been pushing myself to adopt more is test-driven development or TDD. This has been most useful when discovering or hearing about a bug in the app. I write a failing test that highlights where the error is, and then write the code to fix the behavior. That way, I know when it is fixed, and that it will not break again in the future. When writing new code, TDD is often more difficult. I will create the basic objects that I need, and then write test methods whose names describe the behavior I want to have in the app. I will then add the property and functions necessary to define the API and write failing tests that illustrate what I expect to happen. Finally, I actually fill our the functions so that the tests pass. Invariably, there is some back and forth as I tweak the tests and tweak the code before everything is working properly.
Having a project with a robust test suite is such a comfort that I never want to go back. When I work on other projects that do not have tests in them, I start to get anxious and worry that I am breaking things without knowing about it. I have found that starting by adding some simple tests around any new behavior, or to illustrate bugs that are found is a great way to get started. You do not have to have the perfect project with complete test coverage in order to start reaping the benefits of automated tests. I look forward to learning more about testing and sharing more of what I am learning along the way.
📖 Ender’s Game
By Orson Scott Card
At the beginning of this year, I was put on a new team at work. We had the chance to a build a project completely from scratch. When I joined the team, they had already spent a few months interviewing potential customers and learning about the market, and were ready to build a prototype. I was tasked with creating a quick, simple version of the app that stored all data locally. I knew that I would be rebuilding the app a couple times, or at least refactoring major portions of the app. So I set out to build portions of the app in ways that I could easily reuse them multiple times, and across multiple projects.
One thing that has made this easier is that my employer has been generous in allowing large portions of my work to be open-source. As I worked on my project, I was conscious to separate app-specific code from reusable pieces that could be made into frameworks or libraries.
One of the first areas that I tackled was to create a network stack built on top of NSURLSession
that provided a simple interface to make network requests. The end result is somewhat specific to our projects at O.C. Tanner, but we made it open-source in case it is useful to anyone else: ios-network-stack.
Since then, I have made a number of libraries that I use across different apps. Most of the libraries are not to the level that I would like yet—I need to add automated tests, better code documentation, and useful readme files. But I have learned a number of things in creating and using these libraries, and thought it would be useful to go through some of the pros and cons.
For a long time, I resisted adding any dependencies to my projects. It felt much simpler to just focus on the project at hand, and build anything that was needed specific to the project. Dealing with a system such as Cocoapods or Carthage felt like too much overhead. Part of my reaction came from working on projects where previous developers had pulled in so many dependencies that the project became unwieldy and difficult to update and maintain. We experimented with different approaches and finally landed on using Carthage and Git submodules, which I described earlier.
Part of the key in managing dependencies reliably is being able to choose when to update those dependencies. If your code changes out from under you when you are not expecting it, development becomes much more difficult. Using Carthage and submodules means that I decide exactly when to update, and can choose which version of a given dependency to keep in my project. So if I need to use an older version for compatibility issues, or an experimental branch, that is easy to do.
One of the biggest frustrations with adding dependencies is the additional time investment at the beginning of a project. Especially if something goes wrong, it can be extremely frustrating to want to just start coding, and instead have to deal with dependency problems.
Again, this is one of the reasons that I have decided on the system that I currently use. I use Tower as a Git client, and it has made working with submodules extremely simple. If someone needs to check out the repo, they can just initialize the submodules, and they will have the entire project set up and ready to go. When starting a project, it is a simple process to set up the project, although there are a number of steps that can feel intimidating at first.
One significant issue that you have to be aware of when creating open-source projects is the possibility that they will become successful. Once an open-source project starts being used by more people, and especially when they start contributing, it requires much more of a time investment. There will come pull requests to review and merge, issues to triage, and questions to answer. Hopefully this also comes with the benefit of having the code improved by getting more real-word usage, and contributions back to the project. But as the project creator, you have to evaluate whether the benefit will be worth the cost.
In practice, I have found that the time balance comes out in favor of using dependencies. I only have a few third-party libraries that I am pulling in—many of my dependencies are internal libraries that I maintain and share across my projects. Splitting that code out into reusable libraries means that in each project, I can focus on the app-specific logic. I have been able to get new projects started much faster as a result.
Since I am using mainly internal libraries, this is code that I would have written anyway for each app. But since I am creating libraries, by necessity, it is decoupled from app logic, and more modular, making it easier to test. This has the benefit of creating more maintainable code and spending less time down the road debugging and fixing issues.
Even though many of my frameworks and libraries are only used by myself at this point, the fact that I am creating them as open-source projects means that they are available to the community. As I find interesting approaches to problems, or identify areas of boilerplate code that can easily be reused, it is a great feeling knowing that I am making it possible for others to benefit from that as well.
Of course, since I am still enjoying the luxury of obscurity, I do not feel the pressure of having all of my projects perfectly documented, tested, and maintained. I still plan on working toward that goal, but I know I have time. At this point, creating reusable libraries has given me great personal benefit, and I look forward to helping others as well in the future.
📖 Changer
By Matt Gemmell