I craft user-friendly applications

Test Logs in Xcode

Apple spent the week of WWDC touting the new testing features in Xcode 7. One thing they failed to mention both this year and last was the persistence of test results to disk. These files and folders are not officially documented, so if you decide to rely on them make sure to check a new version of Xcode doesn’t break anything. This post will be updated to reflect changes as needed.

If you run a set of tests in Xcode 6/7 or with xcodebuild test via the command line, a new folder inside of the app’s folder in DerivedData is created, called Test. It contains similar files to the other folders, but Xcode 7 adds some new things as well.

.xcactivitylog

An .xcactivitylog is a gzipped copy of the build log that’s generated any time a project is built. Each file is uniquely named with a UUID. Unfortunately for us, the UUID generated is unique per folder, so correlating a file in Build and Test cannot be done by file name alone. These appear to be segments of the output, not complete logs. At least one of these is generated per build.

TestSummaries.plist

This is the exciting part. This file contains the results of the tests in a machine readable format. At the root of this plist is 3 things: FormatVersion, RunDestination, and TestableSummaries.

FormatVersion

This is a string that is used to version the TestSummaries file format. Between Xcode 6 and 7, the version number did not change, though additional information is added to the files generated in 7. With this, we can assume that the version is not incremented when additions are made to the objects contained.

RunDestination

This contains information about what computer, device, and sdk were used to run the tests. All the items are fairly self-explanatory.

TestableSummaries

Each test target is an item in this array. As of Xcode 7, running test will automatically run all test targets associated with a project.

Within the Tests array there are records of the test, but they are quite buried. The top-level item distinguishes between All tests or Selected tests. The only difference between these two is how many levels of Subtests exist between the top level Test object and the individual test results.

In All tests the identifier hierarchy looks like so

All tests
    └── AppTests.xctest
        └── AppTests
            └── Individual tests   

For Selected tests the .xctest level is skipped.

Selected tests
    └── AppTests
        └── Individual tests   

Once we get down into individual tests, the Identifier is automatically generated based on the method name. so -(void)testExample becomes AppTests/testExample. When using Specta, the names are mangled slightly more.

Take this test for example:

describe(@"successful request", ^{
     it(@"calls success block when request succeeds", ^{
      ...
     });
});

The TestIdentifier generated will look something like this: AppTests/test_successful_request__calls_success_block_when_request_succeeds.

Tests written in swift will always end with ().

Xcode 7 adds ActivitySummaries and FailureSummaries

For Unit test targets, the useful piece of information here is the reason the test fails. For UI test targets, this is where the steps to perform the UI tests are recorded, along with any extra information about them. When running a UI test, each action taken on a UI object generates one of these sub activities.

Take this test:

    func testOne() {
        let app = XCUIApplication()
        app.buttons["One"].tap()
        let aTextFieldTextField = app.textFields["A Text Field"]
        aTextFieldTextField.tap()
        aTextFieldTextField.typeText("asd")
    }

Sidenote: the test must tap a textField before it can enter text into it.

This test generates 4 activities, with sub activities auto-generated:

    t =     1.77s     Wait for app to idle
    t =     2.42s     Tap the "One" Button
    t =     2.43s         Wait for app to idle
    t =     2.68s         Find the "One" Button
    t =     2.72s         Dispatch the event
    t =     2.97s         Wait for app to idle
    t =     3.62s     Tap the "A Text Field" TextField
    t =     3.62s         Wait for app to idle
    t =     3.62s         Find the "A Text Field" TextField
    t =     3.64s         Dispatch the event
    t =     3.87s         Wait for app to idle
    t =     4.49s     Type 'asd' into the "A Text Field" TextField
    t =     4.49s         Wait for app to idle
    t =     4.49s         Find the "A Text Field" TextField
    t =     4.55s         Dispatch the event
    t =     4.66s         Wait for app to idle

The step we care about most is “Dispatch the event” because it contains the attachments generated from the tests.

Attachments

Xcode 7 introduced UI testing, a big part of which is screenshots of the tests as they happen. The Attachments object contains information about where those screenshots live. But there’s more! In addition to a .png of the screen during that particular test step, there’s also a snapshot. What is a snapshot? It’s a archive containing the entire view hierarchy on screen when the screenshot was taken as a binary plist.

Failures

When a test fails, a summary is added to the FailureSummaries key. This is just a shortcut to the item in ActivitySummaries which has almost the same information.

Xcode vs xcodebuild

xcodebuild

Thankfully for those of us who automate builds, xcodebuild test does the same thing that Xcode does, though it generates fewer files. Here’s what you’ll find in Logs after running on the command line:

├── Build
│   ├── Cache.db
│   └── FCDDA053-2DA1-4C1C-8716-B6C339CB5815.xcactivitylog
└── Test
    ├── 21ED62CB-4F14-4A87-805E-C594936F24E6_TestSummaries.plist
    └── Attachments
        ├── Screenshot_6CD674C6-4E0D-4DAC-BF66-016198C32780.png
        ├── Screenshot_8126D5ED-88B9-4F6D-8F38-A339F3A14AE5.png
        ├── Screenshot_94513284-AA2F-48DC-A9D7-C56A0EF9BC91.png
        ├── Screenshot_E6EBC6AC-7311-49C2-829D-AF9880141C4B.png
        ├── Snapshot_5638F5F5-C04C-4F5E-AE42-C517B6B1FCBC
        ├── Snapshot_57F67566-9E22-4005-B7D6-B822EF08CD2E
        ├── Snapshot_7AA349A8-CF54-4AC4-8139-A676167C8801
        └── Snapshot_A065B515-A4DE-431C-B72F-87952AA133A6

One caveat: The .xcactivitylog in the Build folder does not contain the full build log. It is missing the logs generated when tests are run.

Now this seems great, but remember this is inside of DervivedData so there will be many other files, so finding it programmatically is not easy. One solution is to grep the console output for the sessionIdentifier which is included in the XCTestConfiguration:

Found configuration <XCTestConfiguration: 0x7ff38a5894e0>
                      testBundleURL:file:///Users/michele/Library/Developer/Xcode/DerivedData/Buttons-amswwgmmhfjuzcegszalugciwdsj/Build/Products/Debug-iphonesimulator/ButtonsUITests-Runner.app/PlugIns/ButtonsUITests.xctest
                  productModuleName:ButtonsUITests
                        testsToSkip:(null)
                         testsToRun:(null)
                 reportResultsToIDE:YES
                  sessionIdentifier:<__NSConcreteUUID 0x7ff38a58bf70> CC966A22-0E28-4880-B445-9E401BB24CDB
         pathToXcodeReportingSocket:(null)
          disablePerformanceMetrics:no
    treatMissingBaselinesAsFailures:no
                    baselineFileURL:(null)
              targetApplicationPath:/Users/michele/Library/Developer/Xcode/DerivedData/Buttons-amswwgmmhfjuzcegszalugciwdsj/Build/Products/Debug-iphonesimulator/Buttons.app
          targetApplicationBundleID:com.mrt.Buttons

Manually sorting through Xcode’s logs is The Hard Way™ and finally with Xcode 7 there is an easier way to get that information.

There is a new xcodebuild argument resultBundlePath which will save the .xcactivitylog, TestSummary.plist, and related files to a directory of your choosing. As of Xcode 7b1, this must be an absolute path.

$ xcodebuild test -scheme 'Buttons' -destination 'platform=iOS Simulator,name=iPhone 6,OS=9.0' -resultBundlePath '~/Desktop/build'

Outputs:

├── 1_Test
│   ├── Attachments
│   │   ├── Screenshot_1236AC16-AF8C-4E87-A748-4867EAB120FC.png
│   │   ├── Screenshot_52E63346-D71F-4A6B-ACA9-736AD54854F3.png
│   │   ├── Screenshot_7A8F0CD6-3981-47FD-BC2D-DC6B35863656.png
│   │   ├── Screenshot_E4792BBC-9717-4BD1-8CD0-D560E19A491D.png
│   │   ├── Snapshot_273B4980-BE03-4EA8-81B7-03F237772558
│   │   ├── Snapshot_86786328-081A-4314-ADC9-213648471F5D
│   │   ├── Snapshot_AA3A1ADC-84C8-4FCC-91A6-590431B89210
│   │   └── Snapshot_CDCA9CA4-847D-46DF-852F-8BCEF0C468AE
│   ├── action.xcactivitylog
│   ├── action_TestSummaries.plist
│   └── build.xcactivitylog
├── Attachments
│   ├── Screenshot_2750BC84-575E-4849-9A39-820FB8D8E998.png
│   ├── Screenshot_59E390A6-9B39-4FD5-AA13-6915C211F6F8.png
│   ├── Screenshot_8F1BB0EE-1146-4CE0-BB5B-1ED473A12173.png
│   ├── Screenshot_DF5341F9-2763-47C6-821B-2B157E88AEC7.png
│   ├── Snapshot_21D3F473-0F28-4F4A-97FD-F30263461422
│   ├── Snapshot_5AF107B1-7CA0-4B14-9EEB-3918E4C4B5E9
│   ├── Snapshot_5F74DD59-6225-40D7-955A-21B366A563BD
│   └── Snapshot_FD2ED335-1729-4489-8B7F-0777E73D4EE8
├── Info.plist
└── TestSummaries.plist

There is duplicate data here. The TestSummaries.plist at the root of the directory is version 1.1, but inside 1_Test is the correct version. This also adds both build and test .xcactivitylog files conveniently in one place. Using CI? This argument will make your life a whole lot easier.

Xcode

It is common knowledge that when Xcode builds a project, it does not do the same thing that xcodebuild does. Just like xcodebuild, building in Xcode creates a Test folder inside Logs, and saves summary plists and attachments. However, Xcode itself creates a lot more logs.

├── Build
│   ├── Cache.db
│   └── D0C828D2-21CA-4182-9A0E-C80DE1F8FA86.xcactivitylog
├── Debug
│   ├── 130CC81E-789E-4FDD-A156-DB75FBEBC770.xcactivitylog
│   ├── 44845FEA-0D87-4266-AD62-502E0F4C30B2.xcactivitylog
│   └── Cache.db
├── Issues
│   ├── 0F1BB6CD-F28B-48DB-9A09-B3948257D698.xcactivitylog
│   ├── 4AFC3EE7-12EC-4E4C-BDF9-2ED7BCA6887E.xcactivitylog
│   ├── C32CA334-AAF9-43B4-AB00-D10373391A08.xcactivitylog
│   └── Cache.db
└── Test
    ├── 9309CF06-A5BB-418C-91D5-299788C685A3.xcactivitylog
    ├── 9309CF06-A5BB-418C-91D5-299788C685A3_TestSummaries.plist
    ├── Attachments
    │   ├── Screenshot_556D54FE-235B-4A3E-A00E-C34ED00AF043.png
    │   ├── Screenshot_786546BC-98C4-4788-B7F1-9A66DFA0EF51.png
    │   ├── Screenshot_976F8BF7-C2A6-4E8F-8BFC-EE6A4FA6D78E.png
    │   ├── Screenshot_BC2D4BF7-DC31-459F-B726-84651CFF52A3.png
    │   ├── Snapshot_31A23466-8A83-42B6-8AD7-4CBD2FB34BB7
    │   ├── Snapshot_626616A0-436F-4D45-822B-41E21E7E80CF
    │   ├── Snapshot_73A33739-49A0-4D8E-9295-338BDAE08C5C
    │   └── Snapshot_83BF8F36-C1EF-45E0-B55E-3320F7FE55D7
    └── Cache.db

When opening up the multiple .xcactivitylog files, there is some overlap. It appears that Xcode will split the different sections of the build into different activity logs. Unfortunately, because the UUIDs are unique and not related, putting a full log back together from these pieces is fairly difficult.

Conclusion

These new files and flags will make automated builds even better with Xcode 7. Apple has finally rounded out the toolset for automating tests, and CI providers are already starting to offer builds using Xcode 7.

The sample project used in this post is available on Github.

Thanks

  • Lawrence Lomax for telling me about the new resultBundlePath option.
  • Brian Nickel for fixing the sample project’s storyboard overriding accessibilityLabel.

The Project File Part 1: Composition

The project file, specifically the project.pbxproj, is the closest thing to taboo we deal with on a regular basis as iOS developers. We tip toe around it, because we don’t want to break it. After all, without this file we can’t compile, and if we can’t compile, we can’t build apps.

Object Graph

At it’s heart, this file is an object graph. All of these objects correspond with some sort of action you do in Xcode. This is the master list of files, target composition, build settings, etc. Each item is referenced by a UID.

One of the most important things to learn about the project file is that these UIDs must not be changed. They need to be consistent within the file, otherwise Xcode cannot open your project. Conversely, if there are extra, unused references, Xcode doesn’t care.

Traversing the graph is pretty simple. It’s just a matter of picking a UID and finding it elsewhere in the document. There are a number of intermediary objects to go through to find a particular target that contains a particular file, the files in a build phase etc.

Classes

As you scan through this file, you’ll note that every object has an isa field. These names (likely) correspond with the actual objects created when you open the file. For the purpose of this exploration, I’ll be referring to these objects as Classes. And, for the most part, they are all prefixed with PBX or XC for consistency. Here is the full list as of Xcode 6:

PBXAggregateTarget
PBXBuildRule
PBXContainerItemProxy
PBXCopyFilesBuildPhase
PBXFileReference
PBXFrameworksBuildPhase
PBXGroup
PBXHeadersBuildPhase
PBXLegacyTarget
PBXNativeTarget
PBXProject
PBXReferenceProxy
PBXRezBuildPhase
PBXShellScriptBuildPhase
PBXSourcesBuildPhase
PBXTargetDependency
PBXVariantGroup
XCBuildConfiguration
XCConfigurationList
XCVersionGroup

Just like most of Cocoa, these are fairly self explanatory. But there are a few that stand out.

XCBuildConfiguration

This object holds all of your build settings, including compiler version, provisioning, code signing, info plist, etc. By default, this is not very large because unlike other sections of the project file, it is essentially a diff versus the default build configurations.

PBXNativeTarget

This represents a target for your app. It contains references to the build configuration, build phases, product, etc. This is also how each target can have different settings from the project file. This UID is used both internally to the .pbxproj and externally, as you’ll see later.

PBXFileReference

Yes, this is exactly what you think it is. This is the most common item to find in the project file, and this is usually the spot that gets messed up. This contains the path to the file on disk, as well as meta-info about the file, such as its type. And of course, this also has a UID.

Build scripts/steps etc.

One of the most interesting aspects of the project file are the inclusion of build phases. This is one of the spots where the names don’t quite equate to what you’d assume. And, just like everything else, this section references other objects that represent each kind of build phase. This includes the two most common areas for file conflicts, PBXSourcesBuildPhase and PBXCopyFilesBuildPhase. When working with a version control system like git, these are the areas that most frequently change, and therefore have the most conflicts.

One of the most interesting bits I’ve found is that a run script build phase’s contents are added directly to the project file. So if you’re wondering why Xcode complains about quotes sometimes, this is why. I do not suggest you modify these directly.

Other important files (workspace)

When exploring the contents of the .xcodeproj you’ll notice a few other files floating around. The most important one is the .xcworkspace. As of Xcode 4, every .xcodeproj contains a workspace, but this fact is hidden from the user. This is also how Xcode handles subprojects, and can easily recruse through them. This is just a standard XML file.

The other files of note can be found in the xcschemes directory inside xcshareddata. By default, Xcode assumes that there is a single scheme linked to the single target. If the project contains any shared schemes they will be here. These are also plain XML documents. These contain information on how to build the scheme, with a reference to the UID representing the target in the project file. Just like build phases, if you have any pre- or post-action scripts, they will be included as strings in this file.

In Conclusion

The project file is a formidable document, but it’s not as complicated as most would think. Everything is laid out in an orderly fashion, and once you understand its paradigms you’ll find working with it much easier.

In Part 2 I plan on exploring strategies for some of the common pitfalls we run into when working on a team.

Why your early-stage startup doesn't need a lead engineer

I’ve seen the insides of many companies, and like most engineers am inundated with recruitment emails. One in particular gave me inspiration to explore the needs of early stage companies when it comes to hiring talent. And so Why your early-stage startup doesn’t need a lead engineer was born on Medium.

My #CHIMEHACK Experience

I was really excited to be involved in Women Who Code’s first Hackathon, as a judge. It was a fantastic 3 days, and I really enjoyed seeing all the teams and what they produced.

I was so impacted by that event, that I shared some takeaways, and a challenge on Medium. Head on over and take a look: My #CHIMEHACK Experience on Medium.

CocoaPods Under the Hood @ objc.io

This month I wrote on some of the internals of CocoaPods, and explored the bits that make it tick.

Check it out in objc.io Issue #6.

Automated Testing with GHUnit and KIF

Testing in iOS is something that we as developers don’t really think about, or do. Writing tests can help us write code that lasts for years to come. But, this post isn’t about why you should write tests, it’s about how to use GHUnit and KIF with Jenkins CI.

There are two main types of testing frameworks, Unit and UI. OCUnit, which ships with Xcode, and GHUnit are both Unit testing frameworks. UIAutomation, Frank, and KIF are examples are UI testing frameworks.

GHUnit

Unit tests are used to test vertical, isolated slices of functionality. Your app won’t actually be running when these tests are run. Instead, you’ll only import and create objects you want to test directly.

Setup

GHUnit has some fantastic setup steps. The most important thing to note is that GHUnit has its own target, which is not a duplicate target of your app. This means that your app will not actually be running when the tests run.

Writing Tests

GHTestCase
All of your tests will be subclasses of GHTestCase. And you can group these together by creating a GHTestGroup. If you’ve used OCUnit before, this should all sound familar.

Each test case will determine sucess or failure by using assert macros (you can see a full list of them on this page).

Automation

GHUnit also happens to have a great guide for setting up tests using CI. Note that if you’re running Xcode 4.5 or newer there is a bug where the old way of running tests does not work. This is being actively tracked in GitHub Issues for the project, as well as on openradar.

GHUnit also will output JUnit test results, which Jenkins can track over time. This is great for keeping track of code stability, and for finding trends in how well your tests are doing.

Gotchas

Arguably the hardest part of automated testing is the fact that it’s been broken since Developer Preview 3 of Xcode 4.5. This means that you have to work around the command line tools in order to get your tests to run. It’s something that is actively being tracked, and there are solutions, but they are far from ideal.

Since your app won’t be running during these tests, everything you test is very isolated. That might be great for your app, but not all apps can be tested thoroughly with only small pieces running at a time.

Also, since your app is not actually running, running UI tests with GHUnit is not ideal.

KIF

KIF is a framework created by the folks at Square. It uses the <UIAccessibility> protocol to interact with your app, which is a private API. When adding KIF to your project, you have the choice of using git submodules, or Cocoapods.

Setup

KIF setup is fairly simple, and is documented in the project’s README. The main difference between KIF and GHUnit is that KIF duplicates your application target, so all of your source code is added to KIF.

KIFTestController
The Test Controller determines at runtime what tests to run, if you don’t want to run all of them (which you won’t). You can choose to run different sets of tests based on just about anything, the iOS version of the device, the device idiom, etc.

KIFTestScenario & KIFTestStep The bulk of your test writing will be creating scenarios, like “user logs in” or “user visits cart”, which are comprised of steps. To create reuseable steps, you subclass KIFTestStep, and then can define your step using blocks. Likewise, you can subclass KIFTestScenario.

Automation

In the KIF README there is a section on Automation that provides instructions, and a bash script, for getting it running. However, I’ve found that ios-sim works a bit better, but both work.

KIF doesn’t automatically output test results in any useable format. There’s currently a pull request open to get that in. Or, you could get a patch from an older pull request, which is the one I’ve been using for months. Either way, you need to fork KIF.

Here’s a simple command to run KIF with an .app file stored in $APPFILE and then save the JUnit xml:

/usr/local/bin/ios-sim launch $APPFILE --family ipad > /tmp/KIF-ipad-$$.out 2>&1
cp "`grep "JUNIT XML RESULTS AT " /tmp/KIF-ipad-$$.out | sed 's/.*JUNIT XML RESULTS AT //'`" 'test-reports/KIF-ipad-results.xml'

Gotchas

KIF is fast

KIF runs at processer speed. It doesn’t have a “reaction time” like people do, so KIF will try to find that view on the screen before your navigation push animation finishes. To compensate for this, you’ll end up doing a lot of:

[scenario addStep:[KIFTestStep stepToWaitForTimeInterval:1 description:@"wait"]];
[scenario addStep:[KIFTestStep stepToWaitForViewWithAccessibilityLabel:@"Table"]];

Now, this isn’t necessarily a bad thing. But it will take time to get in the habit of thinking to add waits into your tests. And tests will take longer to run, of course.

KIF can’t recover

KIF has a handy way of adding a step, or set of steps, to be run before every scenario:

[KIFTestScenario setDefaultStepsToSetUp:[KIFTestStep setupSteps]];

However, in these default setup steps you need to be able to recover from a failure at any point in your app. If you don’t recover, then there’ll be a domino-effect of failed tests. This will mean having methods available to esentially reset the application state.

You’re code coverage will not be evenly distributed

If your app requires a login for example, it’s likely you’ll have something like this in many scenarios:

[scenario addStepsFromArray:[LoginTestStep stepsToLoginWithEmail:TEST_EMAIL password:TEST_PASSWORD]];

Because you want to “reset the application state” between scenarios, you’ll end up testing the parts of your code that deal with common actions more frequently than other parts of your app. This is both a pro and a con; the parts you test over and over will definitely be rock solid, but that means there will be parts of your ap that won’t be tested nearly as much.

KIF can’t see off the screen

Since KIF relies on <UIAccessibility> and that relies on view drawing, KIF can’t see things that are off the screen. You’ll end up frequently doing something like this:

[scenario addStep:[KIFTestStep stepToScrollToItemWithAccessibilityLabel:@"Settings"]];
Fork it

KIF is a great framework, but Square has an “Individual Contributor License Agreement (CLA)” that you must sign before any of your code can go into core. This means that there are a number of great things that developers have made, but never got merged in (like the JUnit code).

In Summary

Does writing tests help you write better code? Yes.

Will testing add to development time? Yes.

Is it worth the extra time? Yes.

If you’re interested in seeing some sample tests, head over to my Github repo.

Automated Testing with GHUnit and KIF from micheletitolo

How To Make APIs That Don't Suck

As a mobile developer, I use a lot of APIs. I’ve worked with some really great ones, and some ones with lots of room for improvement. Here are a few lessons I’ve learned that hopefully you can think about the next time you are developing, or working with, a new API.

For the sake of this argument, an API is any way a 3rd party can communicate with your software. This includes both web services, and releasing a library that other developers can add to their own code base.

Lesson 3: Follow Conventions

Conventions have been established for many reasons, most often because they just make sense. If you’re going down a path that you haven’t been before, following conventions is a good place to start. As a developer, you’ll be able to learn from the wisdom of many before you, and you’ll end up producing a product of greater quality than if you were to code in a box.

Another important reason to follow conventions is the fact that problems will be easier to debug. The last thing you want, when letting others interact with your software, is for you to do hours of support (which take away from development time). This will greatly help developers working with your API.

If you are going to establish your own convention, which you will for a variety of reasons, make sure you are consistent. Adding paging, for instance, should be the same across all of your calls that support it; don’t go changing one API in a set to be different. Consistency is a big part of following conventions, regardless of who defined them.

Lesson 2: Don’t Be Clever

As programmers, we like to think we’re pretty smart. We like to find the best solution to a problem. Sometimes, however, this leads to us being clever. Clever is a pitfall, and it makes everyone’s life more complicated.

When making an API it’s your responsibility to make things as dead simple as possible. Consumers of your API want access to your data as easily and quickly as possible. Keeping things simple will make everyone happy, especially in the long run.

Lesson 1: Document, Document, Document

Did I say document? The purpose of an API is to be reused. If it’s not documented well, then it will be very difficult to use in the future. Expect the code you’re writing now to have at least a 5 year lifespan. Will you still be working on this one thing in 5 years? Probably not; you may not even be at the same company by then.

Writing good documentation is a requirement these days when releasing an API to the public. Documenting code is a skill that every developer should have, so take this opportunity to better your skills at it.

Screenshots and examples are also good things to include. If you want to go above and beyond, include an API console on your website, so people can play with it without the mess of a local client.

Lesson 0: Expect the Unexpected

Once your code goes live, people will start using it. And since we’ve established a timeline of at least 5 years, whatever you build now will be used in a different purpose in the future.

If you follow the first 3 lessons, this one will be easy to handle. Documentation is by far the most important thing to have, so make sure it’s easy to find and well written.

In Summary

In summary, APIs are becoming a vital part of your software stack. These are just a few lessons I’ve learned over the years I’ve been working with APIs. Obviously, some rules are meant to be broken. We all work in different ways. These are just some tips to help you in your creation!

Using Foundation (not Bootstrap)

Twitter’s Bootstrap has become the go-to toolset for most startups and web apps these days. It’s easy to see why, as it has pretty much everything a web app would need to get off the ground, including JS tools.

New layout woo

I’m really excited about this new layout. It’s much more modern than the old one, but keep in mind this is still a work in progress. There are a few things I have left to fix, especially on mobile *cough*Android*cough*.

One thing to note. I have removed my ‘Work’ section entirely. Why?

There are great tools out there, now, that display my work experience in a more cohesive manner, as well as show things like connections and endorsements. It’s also one less dependency for me to keep updated. Honestly, who needs another thing to update these days? Checkout my Linkedin or Zerply profiles if that’s really what you’re looking for.

Oh yeah, and the ‘About’ page is gone too. Yep, 3 pages down to one. And it feels so good.

If you have any feedback, or find an issue with the new layout, please email me!

M0 out!