๐Ÿ“– ๐Ÿ› All Things Wise and Wonderful

By James Herriot


๐ŸŽฅ Star Wars: Rogue One

with IT department


๐ŸŽฅ Moana

with four older kids


รšll 2016 Sketchnotes posted on @sketchnotable

Ull 2016 Sketchnotes

๐Ÿ“– Tuesdays with Morrie

By Mitch Albom


๐Ÿ“– ๐Ÿ› All Things Bright and Beautiful

By James Herriot


๐ŸŽฅ Jason Bourne

Alone


Takeaways from Release Notes

Indianapolis

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.

Action items

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.

  • Designate different locations and positions for different kinds of work
  • Have a lawyer review my LLC
  • Get liability insurance
  • Form a daily writing habit
  • Set writing goals for my blog and journal
  • Experiment with pricing in my apps
  • Prepare Pointedly for a subscription model
  • Answer all outstanding support requests
  • Schedule daily time for support
  • Answer incoming support within a day
  • Say exactly what I mean, without trying to make someone feel a certain way
  • Plan a solid demo for each app I want to show off and practice it
  • Find an official personal board of directors

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.

Sketchnotes

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.โ€

Tell a human


๐Ÿ“– Shadows in Flight

By Orson Scott Card


Release Notes 2016 Sketchnotes posted on @sketchnotable

Release Notes Sketchnotes

๐Ÿ“– Shadow of the Giant

By Orson Scott Card


๐Ÿ“– Shadow Puppets

By Orson Scott Card


Adding automation to open-source projects

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.

Steps

All examples are using my TextMagic framework. Remember to change the names to match your project.

  1. Set up Travis to start building your project
  2. Add travis.yml to project (see sample below)
  • Make sure to build either project or workspace, as needed
  1. Add slather.yml to to limit coverate reporting to Sources using Slather (see sample below)
  2. Include a call to Codecov after the build in Travis to collect code coverage information (see sample below)
  3. Test locally to make sure that it builds properly and then view the reports on Codecov
  • 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/
  1. Push to GitHub, and make sure Travis builds successfully
  2. Include build and coverage information in your README.md file

<div class=“badges”> <a href=“https://travis-ci.org/benjaminsnorris/TextMagic"&gt;&lt;img src=“https://img.shields.io/travis/benjaminsnorris/TextMagic.svg" alt=“Build Status”></a> <br/> <code class=“highlighter-rouge”> Build Status </code> <br/> <br/> <a href=“https://codecov.io/gh/benjaminsnorris/TextMagic"&gt;&lt;img src=“https://img.shields.io/codecov/c/github/benjaminsnorris/TextMagic.svg" alt=“Code Coverage”></a> <br/> <code class=“highlighter-rouge”> codecov </code> </div>

Sample travis.yml file

language: 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 &lt;(curl -s [codecov.io/bash](https://codecov.io/bash)) -J "$PROJECT_NAME" -f ./test_reports/cobertura.xml

Sample slather.yml file

# .slather.yml

coverage_service:   cobertura_xml
xcodeproj:          TextMagic.xcodeproj
workspace:          TextMagic.xcworkspace
scheme:             TextMagic
source_directory:   Sources
output_directory:   test_reports

Sample call to Codecov

bash &lt;(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 &lt;(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

Apple event 2016 sketchnotes

๐Ÿ“– Shadow of the Hegemon

By Orson Scott Card


๐Ÿงช The value of iOS test-driven development (TDD)

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.

The story

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:

  1. ๐Ÿ’ก Think of a great idea to add to the app
  2. ๐Ÿ–ฅ Write the code to update text and preserve cursor position
  3. โŒจ๏ธ Write tests to verify my code worked properly
  4. ๐ŸŽŠ Bask in the illusion of completion
  5. ๐Ÿ™„ Step away from the project and realize I should probably support text selection as well
  6. โŒจ๏ธ Write failing tests for all the scenarios I could think of
  7. ๐Ÿ–ฅ Implement the logic for text selection
  8. ๐Ÿ˜’ Run the tests and realize I had broken cursor position support
  9. ๐Ÿ˜… Repeat previous two steps until everything passed
  10. ๐ŸŽ‰ Write an exultant blog post

The realization

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.

The takeaway

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:

  1. Create a test case class and write empty test functions
  2. Create objects with properties and stubbed functions
  3. Fill in failing tests to verify behavior
  4. Implement the functions and logic needed for the tests to pass
  5. Adjust tests and code as needed

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.

The conclusion

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.


Handling live text reload elegantly

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 &lt; diffRange.startIndex {
        // Change is after current cursor
        moveCursorRelativeToBeginning(with: cursorOffset, rangeLength: selectedRangeLength)
    } else if cursorOffset &lt; diffRange.startIndex &amp;&amp; selectedEndOffset &gt; diffRange.endIndex {
        // Change occurs within selection
        moveCursorRelativeToBeginning(with: cursorOffset, rangeLength: selectedRangeLength + changedText.characters.count - diffRange.count)
    } else if cursorOffset &gt;= diffRange.endIndex {
        // Change occurs completely before current cursor
        moveCursorRelativeToBeginning(with: cursorOffset + changedText.characters.count - diffRange.count, rangeLength: selectedRangeLength)
    } else if diffRange.startIndex &lt; selectedEndOffset &amp;&amp; diffRange.startIndex &gt; cursorOffset {
        // Change starts in middle of selection
        moveCursorRelativeToBeginning(with: cursorOffset, rangeLength: selectedRangeLength - (selectedEndOffset - diffRange.startIndex))
    } else if diffRange.startIndex &lt;= cursorOffset &amp;&amp; cursorOffset &lt; 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 &gt; 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() -&gt; Int {
        guard let textView = textEditing.textView, selectedRange = textView.selectedTextRange else { return 0 }
        return textView.offsetFromPosition(textView.beginningOfDocument, toPosition: selectedRange.start)
    }

    private func selectedRangeLength() -&gt; 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.


Installing on iOS 10 with Xcode 7

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.

The problem

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.

Xcode 7 Error iOS 10

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
Xcode device support

The solution

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.

Xcode beta device 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
Xcode device support with link

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!

Xcode 7 running on iOS 10

๐Ÿ“– The Swarm

By Orson Scott Card


๐ŸŒ€ Lopsided relationships

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.

Meeting David Smith

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!


Retrieving iOS shared web credentials

The problem

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.

The solution

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.

The steps

  1. Add entitlement and configure app
  2. Upload site association file to server
  3. Add password to Safari on Simulator or device
  4. Add code to retrieve password
  5. Run app

Entitlement

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:

Associated domains

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.

Site association file

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.

App prefix

Safari password

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.

  1. Open the Settings app
  2. Tap on โ€œSafariโ€
  3. Tap on โ€œPasswordsโ€
  4. Enter 1234 for the passcode
  5. Tap on โ€œAdd Passwordโ€
  6. Enter your website and a user name and password

Safari passwords

Code

There 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?) -&gt; ()) {
  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()
  }
}

Execution

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:

Shared credentials

Summary

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