{ "version": "https://jsonfeed.org/version/1.1", "title": "Jesse Squires", "home_page_url": "https://www.jessesquires.com", "feed_url": "https://www.jessesquires.com/feed.json", "description": "Turing complete with a stack of 0xdeadbeef", "icon": "https://www.jessesquires.com/img/logo.png", "favicon": "https://www.jessesquires.com/favicon.ico", "expired": false, "language": "en-US", "authors": [ { "name": "Jesse Squires", "url": "https://www.jessesquires.com", "avatar": "https://www.jessesquires.com/img/me.jpg" } ], "items": [ { "id": "https://www.jessesquires.com/blog/2024/01/22/swift-scripting-broken-macos-14/", "url": "https://www.jessesquires.com/blog/2024/01/22/swift-scripting-broken-macos-14/", "title": "Workaround: Swift scripts importing Cocoa frameworks broken on macOS 14", "date_published": "2024-01-22T14:31:32-08:00", "date_modified": "2024-01-22T14:31:32-08:00", "summary": "
On macOS 14 Sonoma there is a regression in Swift 5.9 which causes Swift scripts that import Cocoa frameworks to fail. This issue was first reported by @rdj. I discovered it myself shortly after. There is a ticket open at #68785 on the main Swift repo on GitHub to track the issue.
\n\n", "tags": [ "xcode","swift","macos","bugs","swift-scripting", "software-dev" ], "content_html": "On macOS 14 Sonoma there is a regression in Swift 5.9 which causes Swift scripts that import Cocoa frameworks to fail. This issue was first reported by @rdj. I discovered it myself shortly after. There is a ticket open at #68785 on the main Swift repo on GitHub to track the issue.
\n\n\n\nConsidering the following Swift script:
\n\n#!/usr/bin/swift\n\nimport AppKit\nimport Foundation\n\nNSPasteboard.general.clearContents()\nNSPasteboard.general.setString(\"Hello, Swift!\", forType: .string)\n\nprint(\"Hello, Swift!\")\n
You can run directly from the command line:
\n\n$ ./hello.swift\nHello, Swift!\n
On macOS 13 and earlier, this works. Unfortunately, on macOS 14 it now fails with an error: “JIT session error: Symbols not found”.
\n\nJIT session error: Symbols not found: [ _OBJC_CLASS_$_NSPasteboard, _NSPasteboardTypeString ]\nFailed to materialize symbols:\n\n[...]\n
The current workaround (also posted by @rdj) is to update the shebang, #!/usr/bin/swift
, by replacing it with the following:
#!/usr/bin/env DYLD_FRAMEWORK_PATH=/System/Library/Frameworks /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift\n
Note: you must also have Xcode installed for this to work.
\n\nI’ve verified that this does indeed fix the problem!
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
I recently setup fastlane
for one of my indie apps, Taxatio, to automate uploading builds and metadata to the App Store — by far, one of the most tedious tasks of app development. While I had used fastlane
extensively before when working on teams at companies, I had never actually set it up from scratch. In this post, I want to share how to do that, as well as a lightweight configuration that I think works well for solo indie developers — folks on a team of one!
I recently setup fastlane
for one of my indie apps, Taxatio, to automate uploading builds and metadata to the App Store — by far, one of the most tedious tasks of app development. While I had used fastlane
extensively before when working on teams at companies, I had never actually set it up from scratch. In this post, I want to share how to do that, as well as a lightweight configuration that I think works well for solo indie developers — folks on a team of one!
My general philosophy when it comes to software development is do the simplest thing first. (Thanks to Mike Krieger for that one.) The first step is to get something that works that is not complicated and not over-engineered. You can always make it complicated, incomprehensible, and over-engineer it later — so why start now? 😉
\n\nIn this particular situation with fastlane
, this meant that I avoided setting it up for the initial 1.0 release. I knew I would likely run into issues with setup and configuration (I did) and I did not want to waste a bunch of time figuring out (somewhat unfamiliar) tooling when I could instead spend that time simply logging on to App Store Connect to manually input my metadata and upload screenshots.
After the initial release, I set out to get fastlane
configured so that I would never have to do that manually again. And that’s great, because App Store Connect is not a very good website.
Because I’m a team of one, I do not use any CI/CD service. If a team (of at least a few people) told me they did not use a CI/CD service, I would scream and implement one on my first day. The benefits of using a CI/CD service with a team are clear. But there are good reasons not to do this as a solo indie dev.
\n\nInitially, I was torn about this decision. After working on teams for years with robust CI/CD automation, I am convinced it is the right way™ to do things. However, it simply does not make sense for a solo indie developer.
\n\nCI/CD services may not be that expensive in terms of absolute value, depending on what you consider expensive. GitHub Actions and Bitrise both have free tiers, but they are pretty limited. You will likely out-grow these, especially if you are working on multiple projects. In the next tier, Bitrise charges $90/month, while GitHub charges $4/month for the first year (after which it is unclear what they charge). My experience with Bitrise in the past was great. They tend deploy timely updates for macOS and Xcode releases. GitHub on the other hand is an exercise in frustration — their current default runners are using macOS 12, machines with macOS 13 are still in beta, and there is no mention of macOS 14, the latest release since last fall.
\n\nIn order to determine if the cost of these services is worth it, you have to ask yourself what value do they provide? They will run your tests every time you push new commits or open a pull request, build your app, and upload your app to App Store Connect (either via their own infrastructure, or by running fastlane
). But… couldn’t I just do all of that myself? Running my unit tests locally is not a problem — my projects are not massive like at big companies, so my test suites finish in a few minutes or less. Invoking fastlane
locally is also fast and easy. I don’t need some random machines on the internet to do this for me, especially not for $1,000 per year (with Bitrise).
More importantly, CI/CD services are a maintenance burden — a trade-off that does not make sense for a team of one. Even though I had a good experiences with Bitrise working on teams, it still broke occasionally. Updating a configuration would inadvertently break something, or communicating with App Store Connect would break (not Bitrise’s fault), or codesigning and provisioning profiles would break, or our tests would be reported as passing even though a minor configuration change actually prevented them from running at all. On GitHub actions, you’re dealing with constantly being multiple versions behind.
\n\nSo, is a CI/CD service worth the financial cost as well as the maintenance cost? For me, the answer is a clear no. The cost-benefit ratio simply does not add up. I do not want to spend time maintaining a service that only I use, when I could be using that time to work on features and fix bugs. No longer do I have to suffer through “it works on my machine, but not on CI” scenarios.
\n\nRather than complete automation, which is what CI/CD services offer, I have opted for human-initiated automation. I run my tests locally during development. When ready to submit to the App Store, I simply run fastlane
. I find this to be the best way to maximize the benefits of automation, while minimizing maintenance costs and financial costs.
While fastlane
does seem to be actively maintained, my impression is that much of it feels like it is languishing in maintenance mode. The documentation feels quite dated, sometimes referencing Xcode 7 or 8. I also ran into a handful of tiny bugs and had to experiment with various parameters. But it does still work, so that’s great!
For initial setup, you can follow the docs and follow the prompts.
\n\nfastlane init\n
You’ll be prompted to authenticate to App Store Connect. For now, you can just authenticate with your Apple ID when prompted. You can switch to using an API Key after initial setup.
\n\nIf you already have App Store Connect configured with your app metadata and screenshots, fastlane
can download this for you. The initial setup will prompt to do this for you, but I ran into issues. I would suggest doing this manually, especially if your app runs on multiple platforms.
# download metadata for each platform\nfastlane deliver download_metadata --platform osx --use_live_version true\nfastlane deliver download_metadata --platform ios --use_live_version true\n\n# download screenshots for each platform\nfastlane deliver download_screenshots --platform osx --use_live_version true\nfastlane deliver download_screenshots --platform ios --use_live_version true\n
You’ll only need to do this once to bootstrap your initial setup. Per the recommendations in the docs, I would git ignore the fastlane/screenshots/
directory. These image files will be large, and they can easily be regenerated at any time.
The docs encourage you to use Bundler with fastlane
. I agree that this is important for teams and CI/CD setups to ensure everyone (and everywhere) is using the exact same versions of everything. However, as a solo indie dev working on only my own machine, I find this to be overkill — especially for a single gem. I simply install fastlane
and invoke it directly. Then I don’t need to check-in a Gemfile
and Gemfile.lock
into git.
Setting up an API Key is the easiest method of authentication with App Store Connect, especially when working on multiple apps. You will not be constantly prompted for your Apple ID credentials and you won’t be bothered by 2-factor auth. I found that using a fastlane
API Key JSON file was the simplest approach for me. I save this on my machine at ~/Developer/AppStoreConnect/api_key.json
.
{\n \"key_id\": \"XXXXXXXXXX\",\n \"issuer_id\": \"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\",\n \"key\": \"-----BEGIN PRIVATE KEY-----\\nXXXXXXXXXX\\n-----END PRIVATE KEY-----\"\n}\n
Again, not having to configure all of this on a CI machine is a joy. I don’t have to worry about accidentally leaking secrets because I misconfigured something. I also don’t have to worry about data breaches on a CI service.
\n\nWhen working on a team, using Xcode’s “automatic codesigning” feature is usually a nightmare. This is why fastlane
has its own entire infrastructure for this. It is even more complicated and burdensome and tedious to get everything working on a CI/CD service, not to mention for everyone on a team. But, lucky for me, none of that applies to solo indie development!
When working alone, Xcode’s automatic codesigning works fine. You can configure fastlane
accordingly. Even better, you do not have to waste your time trying to get all of this working on a CI/CD service!
I initially had some trouble getting fastlane
to work with automatic codesigning. First, you need to configure your Xcode project to use automatic codesigning for all relevant targets. Second, when building via fastlane
, you need to pass -allowProvisioningUpdates
to the export_xcargs
parameter. See the configuration files below.
Here’s the minimal configuration you’ll need for building and uploading your app.
\n\n\n\napp_identifier(\"com.example.MyApp\")\napple_id(\"email@example.com\")\nitc_team_id(\"123456\")\nteam_id(\"XXXXXXXXX\")\n\n# set other parameters as needed\n
api_key_path(\"/PATH/TO/YOUR/api_key.json\")\n\n# set other parameters as needed\n
output_directory(\"./build\") # relative to your project\n\n# Enable automatic code signing and provisioning\nexport_xcargs(\"-allowProvisioningUpdates\")\n
The App Store screenshot requirements are unpleasant to deal with. Luckily, fastlane snapshot
can help with this for iOS. Unfortunately, it simply does not work for macOS. (See #11092, #11092-comment-349012721, #11092-comment-349273411, #19864) The good news though is that you can use fastlane to upload your macOS screenshots to App Store Connect.
For iOS, you can configure a Snapfile
with all the parameters you need.
Here’s my lane for generating iOS screenshots. Because fastlane
makes it trivial, I generate screenshots for all devices. Although, I really wish this weren’t necessary.
screenshots_path_ios = \"./fastlane/screenshots/ios\"\n\nplatform :ios do\n desc \"Capture iOS screenshots\"\n lane :screenshots do\n capture_screenshots(\n scheme: \"SnapshotTests-iOS\",\n output_directory: screenshots_path_ios,\n concurrent_simulators: true,\n devices: [\n \"iPhone 15 Pro Max\",\n \"iPhone 14 Plus\",\n \"iPhone 15 Pro\",\n \"iPhone 14\",\n \"iPhone SE 3\",\n \"iPad Pro (12.9-inch) (6th generation)\",\n \"iPad Pro (12.9-inch) (2nd generation)\",\n \"iPad Pro (11-inch) (4th generation)\",\n \"iPad (9th generation)\",\n \"iPad mini (5th generation)\"\n ]\n )\n end\nend\n
With all of this configuration complete, you can now write your Fastfile
.
Below is my Fastfile
for Taxatio, which runs on iOS and macOS. Like I mentioned, fastlane
does not work for generating macOS screenshots, but it does work for uploading them. All you need to do is place your screenshots in fastlane/screenshots/
for fastlane
to find them.
screenshots_path_ios = \"./fastlane/screenshots/ios\"\nscreenshots_path_macos = \"./fastlane/screenshots/macos\"\n\nplatform :ios do\n desc \"Upload iOS app, metadata, and screenshots to the App Store\"\n lane :appstore_upload do\n run_tests(scheme: \"Taxatio-iOS\")\n\n build_app(\n scheme: \"Taxatio-iOS\",\n output_name: \"Taxatio-iOS\"\n )\n\n upload_to_app_store(\n screenshots_path: screenshots_path_ios\n )\n end\nend\n\nplatform :mac do\n desc \"Upload macOS app, metadata, and screenshots to the App Store\"\n lane :appstore_upload do\n run_tests(scheme: \"Taxatio-macOS\")\n\n build_app(\n scheme: \"Taxatio-macOS\",\n output_name: \"Taxatio-macOS\"\n )\n\n upload_to_app_store(\n screenshots_path: screenshots_path_macos\n )\n end\nend\n
When ready to submit, I run the following commands:
\n\nfastlane ios appstore_upload\nfastlane mac appstore_upload\n
Important Notes:
\n\nfastlane
seems to get confused with screenshots for multiple platforms. If you provide explicit, distinct directories for each platform’s screenshots like I have above, then everything works. To me, this is also a nicer method of organization. The upload_to_app_store
action uses the API Key defined in the Deliverfile
, so all you need to do is provide the screenshots_path
.
Even though I run tests frequently during development, I have fastlane
run all unit tests first as a sanity check — just in case I forget to run them.
For building the app, I only need to provide the scheme because I’m using automatic codesigning and my Gymfile
passes -allowProvisioningUpdates
. I also provide unique names for the output binaries to differentiate between platforms.
With all of this in place, my overall workflow is the following:
\n\nfastlane ios screenshots
fastlane/screenshots/macos/
.fastlane [ios,macOS] appstore_upload
.I am very happy with this lightweight setup. I have automated the tedious and error-prone aspects of dealing with the App Store, without the hassle of maintaining a CI/CD service. Even better, now that I have found a solution that works, I can bring this over to other apps in the future beginning with the 1.0 release.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
My Mac menu bar apps, Red Eye and Lucifer, are now for sale on my new Gumroad store.
\n\n", "tags": [ "macos","apps","mac-app-store", "software-dev" ], "content_html": "My Mac menu bar apps, Red Eye and Lucifer, are now for sale on my new Gumroad store.
\n\n\n\nPreviously, these were free downloads on my apps website. If you currently have them installed, nothing will change for you. They will keep working. However, if you have ever been helped by one of my blog posts, enjoyed one of my conference talks, or used one of my open source libraries, then buying these apps is a great way to support my work!
\n\nI have listed them both for $2, but Gumroad allows you to pay more if you like — similar to artists selling albums on Bandcamp. I realize it’s a lot to ask you to pay for something that has been free until now, but your support is very much appreciated! And if you are able to leave a rating, that would also be great.
\n\nYou might be wondering, why now? Well, I am trying to get more serious about monetization and experimenting with some different sales and distribution models outside of the Mac App Store. Since going independent, I have been financially supported primarily through freelance and contracting work. My goal is to start shifting more of my income towards my indie apps and away from freelance gigs. With these tiny menu bar apps on Gumroad, I’m dipping my toes in the water. Also, I find selling something to be a bit more compelling than simple donations like GitHub Sponsors as a way of supporting someone. (Although, if you would like to sponsor me on GitHub, that would be great too!)
\n\nMore importantly, I have another Mac app that I’m planning to release this year — not on the Mac App Store. I likely will not be selling and distributing that via Gumroad. I’m currently looking into FastSpring and Paddle to determine which is a better fit. We’ll see how it all works out!
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
Perhaps “quirks” is not the correct description, but I recently encountered some unexpected behavior when modifying a protocol in Swift. While I was initially slightly confused, how Swift handles protocol requirements does make sense — conformances are more lenient than you might think!
\n\n", "tags": [ "swift", "software-dev" ], "content_html": "Perhaps “quirks” is not the correct description, but I recently encountered some unexpected behavior when modifying a protocol in Swift. While I was initially slightly confused, how Swift handles protocol requirements does make sense — conformances are more lenient than you might think!
\n\n\n\nI was updating one of my open source Swift packages, Foil, which provides a UserDefaults
property wrapper. Prior to the latest release, it defined the following protocol:
protocol UserDefaultsSerializable {\n associatedtype StoredValue\n\n var storedValue: StoredValue { get }\n\n init(storedValue: StoredValue)\n}\n
And I was changing the protocol to make the initializer failable.
\n\n- init(storedValue: StoredValue)\n+ init?(storedValue: StoredValue)\n
I anticipated getting lots of compiler errors (or at least warnings) about conformers who declared init
instead of init?
— but that did not happen. It turns out, this is by design. In some situations, protocol witnesses (values or types that satisfy protocol requirements) do not have to exactly match a protocol requirement. Protocol witnesses can sometimes mismatch requirements with more “lenient” alternatives. You can see this in the example above: a non-failable init
still satisfies the protocol requirements when a failable init?
is specified. It might seem like a mistake at first, but this makes sense.
Consider another scenario in Swift: a non-optional value can be passed to an optional parameter in a function or set to an optional property, but not the other way around — that is, you cannot pass an optional value to a function parameter that accepts a non-optional value.
\n\nMany thanks to Olivier Halligon, Jordan Rose, and Ole Begemann for the help in understanding this in our discussion on Mastodon. You can find a thorough discussion on the Swift Forums in this post, Protocol Witness Matching Roadmap, started by Suyash Srijan.
\n\nA handful of protocol witness mismatches are currently allowed:
\n\n@escaping
protocol requirementsmutating
protocol requirementsstatic
function protocol requirementsasync
protocol requirementsMost other protocol witness mismatches are forbidden and will produce a compiler error.
\n\nAn easy way to think about this is that the requirements above can be satisfied by more specific or more specialized declarations, but not less specific or less constrained declarations. A square is a rectangle, but a rectangle is not a square.
\n\nThe forum discussion outlines further details and potential plans for the future. I recommend reading it!
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
Providing screenshots for the App Store has always been a tedious and time-consuming process. But as the number of differently-sized iOS devices has grown and changed over the years, it has become more difficult to manage. (This is why the developer community built tools like fastlane snapshot.) The screenshot requirements for the App Store have increasingly become a burden for developers, especially indies. With the Mac App Store, there are fewer hurdles and less strict requirements. However, if you are now targeting only the latest OS releases and latest hardware, the screenshot requirements for both App Stores are not only burdensome but they no longer makes any sense!
\n\n", "tags": [ "app-store","mac-app-store","ios","macos","apple","screenshots", "software-dev" ], "content_html": "Providing screenshots for the App Store has always been a tedious and time-consuming process. But as the number of differently-sized iOS devices has grown and changed over the years, it has become more difficult to manage. (This is why the developer community built tools like fastlane snapshot.) The screenshot requirements for the App Store have increasingly become a burden for developers, especially indies. With the Mac App Store, there are fewer hurdles and less strict requirements. However, if you are now targeting only the latest OS releases and latest hardware, the screenshot requirements for both App Stores are not only burdensome but they no longer makes any sense!
\n\n\n\nFor the first ~8 years of the iPhone, screen sizes and resolutions did not change. (Yes, the iPhone 5 got a little bit taller, but it was not a dramatic change.) All devices had 3.5-inch or 4-inch displays. Similarly, iPad screen sizes and resolutions did not change for the first ~5 years. Each device has a history that can be broken up into distinct phases.
\n\nThe first phase of iPhone was the original 320 x 480
display. The second phase and the first fundamental changes for iPhone came with iPhone 6 and iPhone 6 Plus (up to the 8 and 8 Plus), which had larger and higher resolution displays. These displays were 4.7-inch and 5.5-inch. The next fundamental shift occurred with iPhone X, which began the third (and current) phase. This includes the current lineup of iPhones 15, with the Plus, Pro, and Pro Max. Again, this phase brought larger screen sizes, higher resolution displays, and — importantly — different aspect ratios. The current generation of display sizes for iPhone include 5.8-inch, 6.1-inch, 6.5-inch, and 6.7-inch.
The first fundamental changes for iPad occurred with iPad Pro and then again with iPad Air. Overall, iPad screen size changes have been more subtle and resolutions have been much more consistent. The majority of iPad models have a 3:4
aspect ratio, the outliers are the recent generations of iPad Air and iPad Mini. The current generation of display sizes for iPad include 8.3-inch, 9.7-inch, 10.5-inch, 11-inch, and 12.9-inch.
If you want to browse through all the details, I highly recommend ScreenSizes.app and iOSRef.com.
\n\nOn the Mac, the story is (expectedly) quite different. Obviously, the Mac is significantly older than iPhone and iPad and a significantly different machine. However, the Mac App Store is about 2.5 years younger than the iOS App Store. MacBooks have been pretty consistent in screen size and resolution. The first fundamental shift regarding resolution occurred with the Retina Displays. Over the years, there have been Mac laptops with displays ranging from 11-inches to 17-inches across the MacBook, MacBook Air, and MacBook Pro lineups. (Fun fact: with the introduction of the M-series MacBook Pro, there has been a MacBook display size for every 1-inch increment from 11 to 17.) Then there are the desktop Macs, some of which include a display like the iMac (ranging from 20-inch to 27-inch, which eventually shifted to Retina Displays) while the others require an external display. With external displays, there are too many possibilities to enumerate.
\n\nUnfortunately, I have been unable to find comprehensive resources about Mac Models and their specs like ScreenSizes.app for iOS devices. Apple does maintain a thorough Tech Specs page, but it is rather tedious to examine each product manually and individually.
\n\nI should also briefly mention that the discussion above is only considering screenshots for a single localization. If you need to provide screenshots in multiple languages, this is even more difficult to manage. Again, shoutout to fastlane for helping with this on iOS.
\n\nThe most significant metric that affects screenshots is not device size, but aspect ratio. The aspect ratio of a display is what can produce the most notable differences in the content that is displayed on screen.
\n\niPhones with a 5.8-inch screen and larger have a 19.5:9
aspect ratio, while all other modern iPhones (5.5-inch and smaller) have 16:9
aspect ratio. The now-obsolete iPhones with a 3.5-inch screen have a 3:2
aspect ratio.
Almost all iPads have a 4:3
aspect ratio. iPads with an 11-inch screen have a 10:7
aspect ratio and iPads with a 10.9-inch screen have a 23:16
aspect ratio. The single outlier is the 6th Gen iPad Mini, which is the only model with a 6.1:4
aspect ratio.
Most Macs and Apple Displays have either a 16:10
or 16:9
aspect ratio. However, the current generation of M-series MacBooks (Air and Pro) have an obscure 16:10.35
aspect ratio. So weird.
You can currently upload screenshots for all device sizes, if you wish. There was a time, if I remember correctly, when Apple required screenshots for all device sizes. However, this was eventually relaxed to include only some of the larger devices, which can then be scaled down to display for smaller devices with the same aspect ratio. You can find the current screenshot specification reference here.
\n\nScreenshot requirements for iPhone:
\n\nHowever, if your app is targeting iOS 17 and later, generating 5.5-inch screenshots is impossible! There are no 5.5-inch devices that can run iOS 17! The iPhones 6s/7/8 Plus stopped at iOS 16. It doesn’t make any sense to provide these screenshots if your app only runs on iOS 17 and later.
\n\nEven more ridiculous, App Store Connect still allows you to submit 3.5-inch screenshots. Not only are those devices obsolete, it is impossible to release an app today that even runs on those devices. Xcode prevents you from setting a minimum deployment target lower than iOS 12, which does not run on any 3.5-inch device!
\n\nLuckily, for tvOS and watchOS (and now visionOS) you only need to choose a single size for all of your screenshots.
\n\nScreenshot requirements for iPad:
\n\nAll other iPad sizes (9.7-inch, 10.5-inch, 11-inch) will use scaled versions of these. Notably, there is no option to upload screenshots for iPad Mini (not the old 7.9-inch display, nor the latest 8.3-inch display). Talk about consistency. This is certainly less cumbersome, at least there is a device of each size capable of running iOS 17.
\n\nBut you might have noticed… these screenshots are exactly the same size, resolution, and aspect ratio — so why the duplication? The only difference is whether or not the device has a home button (with Touch ID) or a virtual home indicator (with Face ID). Thus, the only difference when generating these two sets of screenshots is whether or not the home indicator is present at the bottom of the screen. I understand these are fundamental differences in the hardware, but do we really need to provide both sets of screenshots just for a difference of a few pixels at the bottom of the screen?
\n\nIs a home button versus a home indicator truly a meaningful distinction for screenshots? I don’t think so, especially considering that Apple provides an API that allows your app to hide the home indicator! In this case, screenshots would be identical. I do not see a good justification for the extra work involved here to generate two sets of the same screenshots.
\n\nI suspect, similar to the iPad requirements, the primary reason that a 5.5-inch screenshots are required for iPhone — aside from the difference in aspect ratio — is that this is the largest device with a home button (Touch ID). All other sizes (5.8-inch and larger) are for devices with a home indicator (Face ID), that is, no home button.
\n\nHere are the changes I would like to see:
\n\nWith these changes, developers would only need to provide the following screenshots:
\n\nThat’s 2 sets of screenshots instead of 4, with 1 optional set. I prefer that iPhone 4.7-inch screenshots be optional so that you only need one set per device, but I can see a valid argument against that because they have different aspect ratios. But hopefully, iPhones with 4.7-inch screens will be obsoleted with iOS 18, which then addresses this concern.
\n\nAs I mentioned above, the screen sizes for macOS are much more nebulous considering the numerous possibilities of externals displays. Thus, App Store Connect requires screenshots in any of the following sizes:
\n\n1280 x 800
1440 x 900
2560 x 1600
2880 x 1800
Luckily, you only have to choose one.
\n\nNotably, these all have a 16:10
aspect ratio — and that’s the problem. There is only one modern Mac that has a 16:10
aspect ratio and it’s the 13-inch MacBook Air M1. The other MacBook Air models and all MacBook Pro models have an obscure 16:10.35
aspect ratio — almost there, but just different enough to be unable to simply take a desktop screenshot and scale it down to 2880 x 1800
. Instead, you’ll have to scale it down and crop it. Thanks notch! The current iMac models along with the Studio Display and Pro XDR Display all have 16:9
aspect ratios — though perhaps this is a moot point considering 4K+ images are obviously too massive.
You could simply take screenshots of your app windows at one of the specified sizes, and that will certainly work for some use cases. But what about menu bar apps? Or apps with multiple windows? Generally, I think screenshots look much nicer when your Mac app is shown within the context of a full desktop screenshot.
\n\nLooking at the apps that Apple ships via the Mac App Store, you’ll see both types of screenshots for all of them. For example, Pages for Mac only has screenshots of the app itself in fullscreen. On the other hand, the Developer App shows the app window on a full Mac desktop. Both are valid approaches and both should be easy for developers to generate.
\n\nAllowing 4K or larger desktop screenshots is obviously not feasible given their size. However, I think the App Store should allow additional aspect ratios and sizes.
\n\nHere are the changes I would like to see:
\n\n16:9
aspect ratios.16:10.35
aspect ratios for the latest MacBook lineup.2560 x 1664
(MacBook Air 13” M2, at 16:10.35
)3024 x 1964
(MacBook Pro 14”, at 16:10.35
)This would make it significantly easier in many scenarios to generate screenshots without having scale and/or crop everything.
\n\nI’ve alluded to solutions above, but I’ll enumerate them here for clarity.
\n\nFor iOS, you can automate generating all screenshots with fastlane snapshot, which alleviates the majority of these issues. The tool is somewhat clunky and definitely feels like it is aging, but it still works. It would be nice if third-party tools like this were not necessary.
\n\nTo address having to provide 5.5-inch screenshots for an app that only runs on iOS 17 and later, there are a few steps and caveats. You need to satisfy the nonsensical requirement by uploading 5.5-inch screenshots, but you should also provide proper 4.7-inch screenshots, which display on devices that can run your app. Providing both is important, as Jeff Johnson has pointed out, if you only provide 5.5-inch screenshots they will be used for the 4.7-inch screen sizes. This is not ideal, because we have to manually resize them, which doesn’t look great. Here’s my process:
\n\nTechnically (again, Jeff notes) you only need to provide one 5.5-inch screenshot. Users will never see these because they’ll instead see the correct 4.7-inch screenshots. However, my automation with Retrobatch and fastlane make it trivial, so I do upload a full set for 5.5-inch screens.
\n\nFor macOS screenshots, you can choose to only upload screenshots of your app windows. If you have an M-series MacBook and want to take full desktop screenshots, you’ll have to resize and crop them to 2880 x 1800
. Retrobatch can streamline this process, too. Unfortunately, fastlane snapshot remains broken for macOS.
Finally, I should note that all of this only applies if you are taking proper screenshots. Unfortunately, the majority of listings in the App Store these days simply use various promotional images (sized appropriately) that were made in Photoshop, etc. But even if this is your strategy, generating screenshots in all the various sizes is still a burden. Being able to provide only a single set of screenshots per device would help.
\n\nI would love to see these changes (and fixes!) come to App Store Connect that make dealing with screenshots easier. Hopefully someone at Apple is listening!
\n\nHere are all the resources I used to find and confirm device and screenshot specifications:
\n\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
While working on updating iOS screenshots for the App Store recently, I discovered that simctl status_bar
is still broken. And unfortunately, I do not expect it to be fixed any time soon.
While working on updating iOS screenshots for the App Store recently, I discovered that simctl status_bar
is still broken. And unfortunately, I do not expect it to be fixed any time soon.
I’ve written before about automating perfect status bar overrides for the iOS simulator using my utility, Nine41. The current bug in simctl status_bar
not only prevents Nine41 from working, but also breaks every third-party tool like fastlane, SimGenie, etc. that offer a status bar override feature — because those all use simctl status_bar
under-the-hood.
Previously, I realized you could still use the iOS 16.0 simulators and earlier. However, if your app is targeting iOS 17 and above — my current situation since dropping iOS 16 — then this workaround will no longer suffice because your app no longer runs on iOS 16.
\n\nThe only workaround I have found is to use our good old friend, SimulatorStatusMagic — which Dave wrote years ago before simctl status_bar
existed. (It is now maintained by other great folks.) In my earlier post, I tried to guess (incorrectly) what might have caused the breakage in simctl status_bar
, but the project’s README explains the problem:
\n\n\n1) Injecting into Springboard (Required on iOS 17+)
\n\ntl;dr Running
\n\nbuild_and_inject.sh booted
will apply a default status bar to the running simulator. Replace “booted” with a simulator UDID to target a specific simulator.As of iOS 17, the API used by
\n\nSimulatorStatusMagic
is not accessible to processes other than Springboard. So, in iOS 17+ we need to injectSimulatorStatusMagic
into the Springboard process itself, which we do by building it as a dynamic library, and then updating Springboard’s launchd configuration to load our dynamic library.Running
\nbuild_and_inject.sh
will do all of this for you. If you want to change anything about the values used in the status bar, you will need to updateDynamicLibrary/main.m
.
And there’s our answer: “As of iOS 17, the API used by SimulatorStatusMagic is not accessible to processes other than Springboard.” Presumably, this is why simctl status_bar
no longer works. From the outside, it sometimes feels like teams within Apple have little to no communication with each other, resulting in bugs like this. Or, perhaps this is just the typical dysfunction that manifests with very large organizations.
Luckily, the status bar overrides applied using SimulatorStatusMagic will persist across simulator launches. Thus, you only need to run build_and_inject.sh booted
once for each simulator. I decided to just do this for all of my simulators so that I never have to worry about this. (Honestly, there’s really no reason for them to ever show the current date and time.)
If you fully reset a simulator using Erase All Content and Settings...
, then you will need to re-run the script for that simulator. Once you apply the overrides to the simulators, you can use whatever screenshot automation tools you prefer (Xcode UITests, fastlane snapshot, etc.) and the status bar overrides will remain. If you need this functionality on a CI service, you are in for a bit more work, but it is possible.
This is far from ideal, but at least we have a workaround that allows us to continue creating great screenshots.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
I’m happy to share that I released an update to Taxatio today, but unfortunately it was not without a lot of friction and hassle with the App Store approval process.
\n\n", "tags": [ "ios","macos","apple","app-store","mac-app-store","app-store-rejection", "software-dev" ], "content_html": "I’m happy to share that I released an update to Taxatio today, but unfortunately it was not without a lot of friction and hassle with the App Store approval process.
\n\n\n\nTaxatio is a universal app for iOS and macOS. I submitted the same update (version 1.3.0) for both platforms on December 30. The iOS app was accepted within a few hours, which was excellent. However, the macOS update was delayed for a ridiculous reason, after waiting to be reviewed for three entire days.
\n\nIt is important to note — and I cannot stress this enough — the iOS app and macOS app are nearly identical. They are SwiftUI apps with the exact same functionality and they share 90 percent of their code. The metadata in the App Store for each is exactly the same, the only exception is the screenshots. So, it was curious to me how one could be approved within hours while the other was waiting to be reviewed for multiple days — ultimately, only to be rejected.
\n\nSo what happened? On January 2, three days after submitting to App Review and three days after the iOS app was approved, I received this message from App Store Review (yes, with this formatting and typos):
\n\n\n\n\nBug Fix Submissions
\n\nThe issues we’ve identified below are eligible to be resolved on your next update. If this submission includes bug fixes and you’d like to have it approved at this time, reply to this message and let us know. You do not need to resubmit your app for us to proceed.
\n\nAlternatively, if you’d like to resolve these issues now, please review the details, make the appropriate changes, and resubmit.
\n\nGuideline 2.3 - Performance - Accurate Metadata
\n\nWe noticed that your app’s metadata includes the following information, which is not relevant to the app’s content and functionality:
\n\nWhat’ new text showed iOS reference.
\n\nNext Steps
\n\nTo resolve this issue, please revise or remove this content from your app’s metadata. For resources on metadata best practices, you may want to review the App Store Product Page information available on the Apple Developer website.
\n
In my release notes for this update for both the iOS app and the macOS app, I wrote:
\n\n\n\n\n\n
\n- Dropped support for iOS 16 and macOS 13. Now requires minimum iOS 17 and macOS 14.
\n
So acknowledging that my app is a universal app — a fact that users are aware of — is grounds for a rejection? That is beyond ridiculous, especially considering the iOS app contained the exact same release notes and got approved. I mean, seriously! What a fucking joke! Not to mention, Apple actively encourages developers to support universal purchases for apps available on their platforms since they started supporting this in 2020. I do not think that mentioning OS support changes for a universal app is “not relevant” and I certainly do not think it should result in a rejection. It’s harmless.
\n\nI replied asking to be approved anyway and “resolve” the “issues” on my next update — but surprise, my release notes for the next update will be completely different anyway. I replied shortly after receiving the rejection notice, hoping that the turn around would be quick — I had already waited three days! Of course, it took another entire day to finally be approved on January 3. That means time from submission to approval was four days in total.
\n\nI have two major complaints about this process.
\n\nFirst, the inconsistency is absolutely maddening. The time discrepancy between review times between platforms is annoying, especially considering you can submit multiple items for review for a universal app at once — and the fact that, you know, the app is universal. Wouldn’t it make sense to review and approve both at the same time? More obviously and more infuriating is the fact that these apps are identical with identical release notes. There were no issues with the iOS app release notes mentioning macOS, but apparently you cannot mention iOS in the release notes for a macOS app.
\n\nSecond, while I appreciate Apple’s new approach to allowing apps to be approved for minor “violations” without having to resubmit, it does not work well in practice (obviously). In 2020, Apple announced “for apps that are already on the App Store, bug fixes will no longer be delayed over guideline violations except for those related to legal issues. Developers will instead be able to address the issue in their next submission.” (Emphasis mine.) This sounded great in 2020, but the problem is that my app release was still delayed. I’m glad I did not have to wait another three days, but I did have to wait another entire day because of this inconsistent, bureaucratic nonsense. Instead of the back-and-forth, why not simply approve the submission immediately and then mandate changes on the next submission?
\n\nIt is so disappointing, after so many years, that the App Store approval process continues to be so erratic, unpredictable, petty, and (often) slow for such benign app updates — all the while scam apps continue to get approved and promoted in the top charts, much less get rejected.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
Continuing another tradition, here are the books I read in 2023. Similar to my previous post highlighting my top posts of 2023, I also skipped publishing my reading list last year, for 2022. Again, I was simply too burnt out by the end of that year. I also did not read that much compared to previous years — thanks again to burnout. You can find previous years here under the #reading-list tag.
\n\n", "tags": [ "books","reading-list", "reading-notes" ], "content_html": "Continuing another tradition, here are the books I read in 2023. Similar to my previous post highlighting my top posts of 2023, I also skipped publishing my reading list last year, for 2022. Again, I was simply too burnt out by the end of that year. I also did not read that much compared to previous years — thanks again to burnout. You can find previous years here under the #reading-list tag.
\n\n\n\nSince I skipped last year, I’ll include everything for 2022 and 2023 in this list. As usual, these are in no particular order but I’ve grouped books by the same author together.
\n\nHopefully, I can get back into a better reading routine for 2024.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
To continue my (almost) tradition of sharing my top posts, here are my most popular posts of 2023. You can find previous years here under the #top-posts tag. Last year, 2022, is notably absent from this series — I was too burnt out last year (for a number of reasons) to write one of these posts. So as a bonus, I’ll include my top posts of 2022 here as well! As usual, all of my analytics data is publicly available, made possible by the excellent GoatCounter Analytics, so you can view it too.
\n\n", "tags": [ "top-posts", "essays" ], "content_html": "To continue my (almost) tradition of sharing my top posts, here are my most popular posts of 2023. You can find previous years here under the #top-posts tag. Last year, 2022, is notably absent from this series — I was too burnt out last year (for a number of reasons) to write one of these posts. So as a bonus, I’ll include my top posts of 2022 here as well! As usual, all of my analytics data is publicly available, made possible by the excellent GoatCounter Analytics, so you can view it too.
\n\n\n\nI would like thank everyone who continues to read my blog. I truly appreciate being able to share my writing with all of you.
\n\nYou can view the data here for this year’s top posts, which is filtered to display site visits for all of 2023 and only for posts written in 2023 (that is, URLs matching /blog/2023/
). To see everything I’ve written in 2023, you can browse the archive.
Similarly to 2023, you can view the 2022 data here.
\n\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
Last week I wrote about setting up a new MacBook Pro — my first Apple Silicon Mac, and thus my first MacBook with a notch. I lamented how poorly macOS interacts with the notch, specifically how menu bar apps and icons simply get hidden if you have too many to display. Lots of folks on Mastodon offered various solutions, and some readers emailed me with options as well. I figured it was worth making a separate post about this specific issue to list all of the workarounds and alternatives. It is clear that this is a widespread problem that users are having.
\n\n", "tags": [ "apple","macbook-pro","macbook","macos", "essays" ], "content_html": "Last week I wrote about setting up a new MacBook Pro — my first Apple Silicon Mac, and thus my first MacBook with a notch. I lamented how poorly macOS interacts with the notch, specifically how menu bar apps and icons simply get hidden if you have too many to display. Lots of folks on Mastodon offered various solutions, and some readers emailed me with options as well. I figured it was worth making a separate post about this specific issue to list all of the workarounds and alternatives. It is clear that this is a widespread problem that users are having.
\n\n\n\n\n\n\nI have gripes about the notch. There isn’t enough room to display all of my menu bar apps and icons, so… they just get hidden!? Apparently, everyone in Cupertino thinks the best solution to this problem is to hide them with zero indication that there are more that simply can’t be displayed because of the notch. I wasted so much time trying to figure out why Little Snitch and 1Password were not running on my new machine. Was there a compatibility issue with Apple Silicon that I didn’t know about? That couldn’t be. In turns out, they were running the whole time but they were hidden by the notch.
\n\n[…]
\n\nThis “design” (or lack thereof) is so dumb. It is utterly ridiculous to me that this is still how it “works” two years after the introduction of the redesigned MacBook Pro with a notch. How hard could it be to add an overflow menu with a “«” (or should it be “»”?) button that shows the remaining apps and icons that can’t be displayed? This entire situation with the notch is ironic, because the iPhone notch and “dynamic island” are so thoughtfully designed with zero compromises regarding the functionality of iOS. In fact, they actually provide a better user experience. Yet on the Mac, how the notch interacts with macOS is laughably incompetent. It is shockingly lazy regarding attention to detail, and results in an outright disruptive and confusing user experience.
\n
And as Michael Tsai pointed out, the situation from a developer’s perspective is just as bad:
\n\n\n\n\nAside from the problem of the icons being hidden, there’s no API for an app to tell whether its icon is hidden.
\nNSStatusItem.isVisible
tells you whether the app or user wants the icon to be visible, but it will returntrue
if the icon is hidden in the notch—or even if it’s hidden behind a menu title.
If you prefer not to install any third-party apps (like me), there are a number of steps you can take to alleviate your crowded menu bar and possibly remedy the issue entirely (which I’ve successfully done). My goal with the steps below is to get all of my apps and icons to display when Finder is active. Some applications have more menu items that span across the other side of the notch, and in this scenario there is not much we can do to prevent hidden menu bar icons — which was actually the case before the notch existed for applications with a very large number of menu items.
\n\nMove macOS system-provided icons into Control Center. A number of icons can be configured to display only in Control Center, which frees up space in your menu bar for icons and third-party apps that must be displayed in the menu bar. For example, you can move WiFi, Bluetooth, Battery level, AirDrop, etc. into Control Center. Additionally, some icons can be configured to only display when they are active, like Focus status or Screen Mirroring.
\nReorder your apps and icons from right to left to display the ones that are most important to you on the right and least important on the left. The result is that the least important icons are the ones that get hidden by application menus or the notch, while the most important icons are more likely to remain visible. For example, I like having Time Machine in the menu bar, but it doesn’t matter if it gets hidden while I’m working in an app with a large menu that spans to the other side of the notch, thus hiding Time Machine. Therefore, I have placed Time Machine in the left-most position. Furthermore, I place system icons that display only when active (like Focus status) in the right most position. This is important for me, because I want to make sure I always see when these things are active.
\nReduce the menu bar item spacing and padding via UserDefaults
. (Thanks to Oliver Busch for the tip. Also see this Reddit post.) There are two defaults settings you can configure via Terminal, NSStatusItemSpacing
and NSStatusItemSelectionPadding
.
Read the current defaults:
\n\ndefaults -currentHost read -globalDomain NSStatusItemSpacing\ndefaults -currentHost read -globalDomain NSStatusItemSelectionPadding\n
Note: These values are not set by default. This means you will get an error that the keys and values do not exist if you have not previously set them.
\n\nWrite the defaults by providing an integer value:
\n\ndefaults -currentHost write -globalDomain NSStatusItemSpacing -int 12\ndefaults -currentHost write -globalDomain NSStatusItemSelectionPadding -int 8\n
Note: Unfortunately, you will need to logout and login for the changes to take effect.
\n\nAfter some experimentation, I landed on the values above — 12
for spacing and 8
for padding fit my needs. You should experiment as well. The smallest tolerable values are probably around 6
or 8
.
Remove the values to restore the default behavior:
\n\ndefaults -currentHost delete -globalDomain NSStatusItemSpacing\ndefaults -currentHost delete -globalDomain NSStatusItemSelectionPadding\n
Again, you will need to logout and login for the changes to take effect.
\n\nThere are several third-party applications to help organize and manage all of your menu bar apps and icons. The apps I have listed below seem to capture the three broad categories of possible solutions. I know there are a few others out there with similar functionality, but I think these are the most representative.
\n\nBartender. (Paid with Free Trial. Website). This one is very advanced and complex with elaborate customization options, including styling the entire menu bar. It works well, but I found it to be very heavy-handed and a bit cumbersome for my needs. It felt clunky and glitchy to me, in ways that I think the developer has tried hard to mitigate, but I imagine Apple does not make developing this sort of app easy. (I don’t think there are any public APIs for this stuff.) Bartender seems to rely on some hacks (like screen recording) to dynamically hide and show your overflowed menu bar apps. Like iOS, macOS now has a privacy feature where you are notified when apps are using the camera, microphone, screen recording, etc. On iOS there’s a little dot indicator that shows in the status bar, on macOS there’s an icon that displays in your menu bar. One specific issue I had with Bartender, was that the privacy indicator icon for screen recording appears very frequently, which was annoying.
\nHidden Bar. (Free. Mac App Store, GitHub). This one is extremely lightweight, providing a simple chevron “<” icon to expand and collapse the extra icons. You can customize which icons are always shown, and which are hidden when the menu is collapsed.
\nSay No to Notch. (Free with IAPs, Mac App Store). This is another heavy-handed approach. This one adds a letterboxed-style black bar to the top of your screen and shifts the entire menu bar down below the notch — along with the entire contents of your screen. I do not like the reduced screen real estate that this creates.
\nAll of these apps have pros and cons. Unfortunately, I was not quite satisfied with any of them. However, your experience and preferences might be very different!
\n\nAs noted above, I have opted for the collection of first-party workarounds. For me, all of the third-party apps felt cumbersome, wonky, lacking, or simply did not match my aesthetic taste. That’s not to say they aren’t great apps and creative solutions — they just aren’t for me. In any case, I hope this post has been helpful if you’ve been searching for solutions to the MacBook notch problem.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
With the recent announcement that Threads.net is starting to integrate with ActivityPub, I figured it would be a good time to remind folks that you can follow me on Mastodon at @jsq@mastodon.social. Many of my connections have moved over from Twitter — but sadly, not all of them. Although, some have moved over to Threads. If you would like to join Mastodon.social, you can use my invite link.
\n\n", "tags": [ "social-media","mastodon", "essays" ], "content_html": "With the recent announcement that Threads.net is starting to integrate with ActivityPub, I figured it would be a good time to remind folks that you can follow me on Mastodon at @jsq@mastodon.social. Many of my connections have moved over from Twitter — but sadly, not all of them. Although, some have moved over to Threads. If you would like to join Mastodon.social, you can use my invite link.
\n\n\n\nAs I previously wrote, Mastodon is pretty fucking awesome and I really enjoy it. It is currently my only (active) social media account. I am no longer using Twitter, for reasons I hope should be obvious by now. Seriously, what a shitshow. However, my posts on this blog are still automatically posted to Twitter for now. (That’s another reason for this post, so I don’t have to login to that awful website.)
\n\nI have no plans to create an account on Threads. Let’s be honest, Facebook is truly not that much better than Twitter. All (for-profit) social media is predatory, manipulative, invasive, and socially destructive.
\n\nIf you were hoping to find me on Threads, sorry. But if Threads follows through on its promise to federate, then that shouldn’t be a problem for either of us.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
Welcome to the fourth (and I think final!) part of my going indie series! Previously, I discussed the exciting topics of business structure, taxes, and retirement. Today, I’m going to discuss two final topics: bookkeeping and invoicing. The first is about how to correctly track your income and expenses, and the second is about making sure you get paid! Because this is capitalism, baby — we are not here for passion, we are here to pay those bills.
\n\n", "tags": [ "series-going-indie","indie-dev","contracting","freelance","consulting", "essays" ], "content_html": "Welcome to the fourth (and I think final!) part of my going indie series! Previously, I discussed the exciting topics of business structure, taxes, and retirement. Today, I’m going to discuss two final topics: bookkeeping and invoicing. The first is about how to correctly track your income and expenses, and the second is about making sure you get paid! Because this is capitalism, baby — we are not here for passion, we are here to pay those bills.
\n\n\n\n\n\n\nDisclaimer: As I have previously disclaimed, I am not an accountant. But the content in this post is what I have learned from my accountant, as well as multiple years of being indie. Accountants are pretty rad, you should get one! They will teach you a lot and when the IRS comes knocking with an audit, you’ll be glad to have someone on your side who understands our deliberately esoteric and obtuse tax code that was written and paid for by neoliberal sociopaths.
\n
I mentioned bank accounts in the previous post, but I’ll reiterate briefly. It would be wise to open new bank accounts that are solely for your business. I would recommend having one checking account and one credit card, at the very least.
\n\nIf you are a sole proprietor, you can open regular accounts. Sole proprietors do not need to open “business” accounts (although you can, if you want). If you have another business structure (as discussed previously), then you might require business accounts.
\n\nAnyway — the purpose of opening new accounts is to keep your business income and expenses separate from all of your personal finances. Do the simple thing first, and make adjustments as needed.
\n\nYou are going to need software for this, preferably something that automatically pulls in transactions from your checking and credit card accounts. Then you can categorize your income and expenses accordingly, which can hopefully be automated as well.
\n\nI use QuickBooks and I highly recommend it. Yes, it is a bit “corporate” and maybe not the most delightful piece of software — but it works, it is basically an industry standard, and your accountant is most likely already very familiar with it. That last point is key. QuickBooks makes working with your accountant easy because you can give them special accountant access. There are a lot of different versions and plans with various features. I use QuickBooks Self-Employed, which is pretty lightweight and fits my needs just fine. If you have an LLC or S-Corp, you might want to consider their other offerings.
\n\nFor the rest of this post, I’ll assume you are using QuickBooks Self-Employed. However, if you are using an alternative I’m sure it has similar features.
\n\nIf you have ever used a personal finance tracking tool like Mint, then you’ll feel comfortable in QuickBooks. The main tasks at hand are:
\n\nIf you do these things and keep track regularly (for example, I do my bookkeeping every Friday), then at the end of the year you can use QuickBooks to generate all the various reports you need for your taxes. Receipts are necessary documentation in case you get audited, and after uploading them to QuickBooks your accountant will be able to access them.
\n\nThe most difficult part is categorization, but that’s not too hard. Once you learn how all of your different expenses should be tracked, this becomes easy to maintain. Many items have obvious categories: meals, travel, computers, software, etc. Here are some of the not-so-obvious things to track:
\n\nIf you need to deposit money into your business account, categorize this as a “personal deposit”. The only time I ever did this was when I first started out and had an empty bank account. The point here is to not mistakenly track a deposit of your own cash as “income”. QuickBooks will omit “personal deposits” in your annual income reports.
\nWhen you withdraw money to pay yourself, categorize this as a “personal withdrawal”. Typically, when money is leaving your account this would be considered an expense, but that is not the case in this situation. Thus, you want to avoid accidentally mislabeling this transaction. QuickBooks will omit “personal withdrawals” in your annual expense reports.
\nSimilar to paying yourself, withdrawals for retirement accounts like a Solo 401(k) contributions should be labeled as a “personal withdrawal”. Be sure to track your contributions and consult with your accountant and financial advisor.
\nClaim a home office. After doing this, you can claim rent, energy (gas and electricity), and renter’s insurance. (Or if you own a home, your mortgage and homeowner’s insurance.) The formula for this involves finding the square footage of your office as a percentage of the total square footage of your entire apartment or home. Then you can split expenses accordingly. For example, if your home office is 10% of your apartment, then you can deduct 10% of your rent as a business expense.
\nHealth insurance premiums. These monthly expenses can be deducted.
\nPay other utilities like Internet and cell phone via your business account and split them 50-50 as personal and business expenses.
\nLarge computer expenses, like a new laptop should usually be categorized as a “fixed asset” rather than a normal business expense. This is because these devices are kept in use for multiple years and depreciate over time. Consult your accountant.
\nFinally, for subscription-based software I highly recommend paying annually instead of monthly, if you can. A single transaction with a single receipt is much easier to track than 12 transactions and 12 receipts. Plus, many services offer discounts if you pay annually.
\nI have already named and alluded to a number of different expenses you can claim. My understanding is that what can be considered an expense is quite broad. According to my accountant, anything that helps you run your business is usually a valid expense. As I mentioned above, your home office and any related costs can be expensed. Expense all software and services you use. Conferences, along with all associated costs (travel, food, etc.), are valid expenses. Anything broadly relating to “professional development” is a valid expense. If working from a coffee shop for the day, anything you purchase can be expensed.
\n\nAgain, consult your accountant. But one thing I learned over the years is that when I first started, I was way too conservative in tracking expenses. For my first year, I had hardly any expenses. I thought, “well, all I really did was sit at my desk at home.” I definitely missed out on claiming some valid expenses that year.
\n\nSo, my advice is to freely claim as many expenses as possible but stay organized with your bookkeeping. Then during tax season, if you claimed something that is not a valid expense, it is easy to throw out. It is much harder, if not impossible, to try to track down expenses you might have missed over the last 12 months.
\n\nInvoices are another “first impression” for your clients. They should be professional and high quality. They should look professional and high quality. Don’t pick some random, shitty invoice template. Take the time to make a nice template — you only have to do it once.
\n\nQuickBooks has some built-in invoicing and payment processing features, but I have never used them. The generated invoices are rather bland and cannot be customized. I don’t think they look very nice, nor professional. As far as payments go, all of my clients use ACH transfers — so I give them my account information and they direct deposit payments. I imagine this is pretty common, so you probably do not need to find payment processing / invoicing software.
\n\nI have been very happy using Pages to create my invoices. Pages has a great invoice template that you can use as a starting point. I recommend customizing it with your own branding, or create a template from scratch and use the provided template as a guide. You can export any document in Pages as a template via File > Save as Template
. If you don’t want to use Pages, I’m sure there are plenty of other options out there.
As far as content goes, my invoices contain:
\n\n#19760401
. I find that this makes it the easiest to keep track of. Not to mention, it is consistent and makes them easy to sort.My preference is to send invoices every two weeks and request that they be fulfilled within two weeks. To me, this is “the sweet spot”. I prefer getting paid as early as possible because I am not in the business of giving interest-free loans to companies. The sooner you can make your monthly contribution to your retirement account (or other investments), the better. Even in full-time positions, there is no real reason that workers can’t get paid at the end of every day for that day’s work. This would be especially impactful for low-wage workers for whom waiting two weeks for payment can be seriously precarious. Delayed payments for labor are about power and subjugation. But, I digress.
\n\nThe downside of more frequent invoicing is that you have more work to do for bookkeeping. But, it’s not that bad. Unfortunately, due to the archaic bureaucracy present in many organizations, my preferences are often not possible to accommodate. However, sometimes clients are, surprisingly, interested in more frequent invoicing. I have experienced all of the following: weekly invoices with weekly payments, bi-weekly invoices and bi-weekly payments, monthly invoices with monthly payments.
\n\nKnow your preferences and communicate them, but know that your client may not be able to accommodate you. The frequency of invoicing and payments is something to be negotiated with each client. Make sure you establish and confirm these details at the start of your contract to avoid confusion later.
\n\nFor all of my work so far, I have sent pdf invoices via email to the client. Depending on the organization, this sometimes goes to the accounting department, accounting@company.com
or something similar.
This is a bonus section to share a bit of my workflow. I have yet to figure out how to fully automate invoices, but I’ve gotten pretty close by eliminating a lot of tedious, repetitive steps. If you have additional tips, let me know! Here’s how I handle this:
\n\nI setup an email template for the client. Basically, you want to write a sample email and save it as a draft. Enter the client’s email in the “To:” field as if it is a real email. Add all of your content. I keep my emails brief, “Hello NAME
! Here’s my invoice for the month. Let me know if you have any questions!” Something like that. I always set the subject to “Invoice - DATE
” and I have an email signature with my contact info.
For Fastmail and Mail.app on macOS, you can drag this saved draft to a new folder called Templates/
to save it as a template. I’m sure other email clients and services are similar. When you are ready to send an invoice, navigate to the correct template email. In Mail.app, you can right-click and click “Send Again”, which generates a new email from the template. Then fill-in the details, attach the pdf invoice, and send! (Again, I’m sure other mail clients can do this.)
While working, every day I track and record my hours on the current invoice. That is, updating the .pages
file. Then, I have an AppleScript to automatically export the pages file as a pdf. (You can find the script here.) This saves me a handful of tedious clicks.
That’s everything I have to share about bookkeeping and invoicing, and how I deal with these tasks as an indie developer. And this post concludes my series on Going Indie — at least for now. If I think of other topics or if readers have a lot of questions, I will certainly write more.
\n\nIf you’ve been following this series and you’ve reached this point, thank you for letting me share my thoughts and experience with you. I hope you’ve enjoyed it!
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
I recently discovered, while setting up my first Apple Silicon Mac, that Xcode does not have access to your shell environment. But there’s one caveat to that. (Thanks to Boris for confirming!) This post will hopefully be a reminder to my future self when I encounter this issue again.
\n\n", "tags": [ "xcode","apple-silicon","m3","homebrew", "software-dev" ], "content_html": "I recently discovered, while setting up my first Apple Silicon Mac, that Xcode does not have access to your shell environment. But there’s one caveat to that. (Thanks to Boris for confirming!) This post will hopefully be a reminder to my future self when I encounter this issue again.
\n\n\n\nI realized the problem when an Xcode build failed because a Run Script Build Phase that runs SwiftLint failed because the SwiftLint binary could not be found. I have SwiftLint installed via Homebrew. I knew SwiftLint was in my PATH
because I could run it successfully on the command line. This meant Xcode did not have access to my PATH
(and, as I learned later, my entire shell environment).
This only started to happen after switching from an Intel Mac to an Apple Silicon Mac — and that was the issue. Homebrew installs at different locations for Intel (/usr/local
) and Apple Silicon (/opt/homebrew
). Invoking SwiftLint from a Run Script Build Phase in Xcode on Intel Macs just happens to work because /usr/local
is part of your default PATH
. To fix the issue on Apple Silicon Macs, you need to tell Xcode where to look for Homebrew packages in your script.
export PATH=\"$PATH:/opt/homebrew/bin\"\n
Anyway, the moral of this story is that Xcode does not have access to your shell environment. This is not necessarily an obvious thing, especially on Intel machines. Xcode only gets whatever the default PATH
is, configured via launch services — unless you launch it through a terminal. What this means is that xcodebuild
run from terminal and Xcode could end up with potentially different behavior in their Run Script Build Phases because they expose different environments. Good to know!
Thanks to Dave for linking to this post this week and sharing a very relevant tip on this topic:
\n\n\n\n\nDon’t forget you can still set environment variables from your project’s scheme configuration. Edit your scheme from the Product menu, select the Arguments tab against the Run behaviour, and set Environment Variables.
\n
\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
After setting up my new M3 MacBook Pro, I decided to do some quick performance comparisons with my old Intel machine. Anecdotally, I would have told you that it is insanely faster but seeing the data made my jaw drop.
\n\n", "tags": [ "apple-silicon","m3","xcode", "software-dev" ], "content_html": "After setting up my new M3 MacBook Pro, I decided to do some quick performance comparisons with my old Intel machine. Anecdotally, I would have told you that it is insanely faster but seeing the data made my jaw drop.
\n\n\n\nFirst, here are the specs for the two machines:
\n\nDo I really need an M3 Max? Probably not, but I waited until the third generation of M-series chips to upgrade, I wanted to make it count, and I want to keep this machine for years to come. Also, as an indie dev I get to write this off on my taxes as a business expense. :)
\n\nBefore erasing and resetting my old laptop, I wanted to see how Xcode performance compared between the Intel and the M3. I did not do an incredibly “scientific” comparison between these two machines, but more of a real world experiment. For example, I did not quit every single running application, etc. However, I was not actively using either machine during the test. I used Migration Assistant to setup the new M3 MacBook from the old Intel MacBook, so they were at least configured as similarly as possible.
\n\nI built and ran the same Xcode project on both. The project is massive, with over 100 monthly committers according to GitHub stats. It’s an iOS app that has started to remind me of what it used to be like working on the Instagram iOS app. If you’ve ever worked on one of these extremely large apps, you know that Xcode is painfully slow — which is why these big companies implement alternative build systems like Buck and Bazel. This project contains around 1.5 million lines of code, including third-party dependencies — based on a naive run of wc -l
. It is Objective-C and Swift, but mostly Swift. The majority of Objective-C code comes from third-party dependencies.
Below are the results for various tasks. I have listed the build times as reported by Xcode’s build logs in the sidebar.
\n\nA clean build of the project in Xcode:
\n\n2m 23s
11m 32s
That’s about a 5.5x speed up! And, of course, the fans on the Intel were going full speed the entire time. The fans on the M3 turned on briefly and quietly for maybe 30-60 seconds, which I almost didn’t notice because the fans on the Intel were so damn loud.
\n\nNext, I tried running an incremental build:
\n\n20s
66s
And finally, I did a build-and-run, measuring a cold launch of the iOS simulator:
\n\n51s
2m 31s
The performance improvement is simply unbelievable. Using this new M3 machine is an insanely different experience. The impact on my development workflow is dramatic and, honestly, unthinkable. I have never experienced such a profound hardware upgrade. My iteration cycles in Xcode are almost instant now. Previously, I dreaded having to do a clean build on the Intel machine — nearly 12 minutes is enough time to do all sorts of other things! Now, a clean build is simply no issue. And for smaller projects, every action in Xcode feels nearly instantaneous on the M3.
\n\nBattery life is incredible. Even when using Xcode, the battery life is impressive. On the Intel machine, the battery would be nearly drained after only 1-2 hours of using Xcode. On the M3 machine, I’ve been able to go entire work days on a single charge.
\n\nI did not measure generally daily usage and tasks, but I can say with confidence that the M3 simply outperforms the Intel by orders of magnitude in every way imaginable. Everything is faster. Typically slow-to-launch apps like Photoshop launch almost instantly. Simply opening large Xcode projects on the Intel machine was usually a chore, but for the M3 it is effortless.
\n\nWhen using Xcode for large projects like the one I described above, my Intel machine would crawl — sometimes it would get so bogged down that it was almost unusable, preventing me from doing anything else. With the M3, I don’t even notice that Xcode is running when I switch to other tasks in other applications. My Intel machine would often beach-ball on just a normal day with normal tasks. I have yet to see a loading spinner on this new M3.
\n\nI am very happy I finally upgraded to Apple Silicon. If, like me, you have been waiting “for the right moment” to upgrade from an Intel machine, now is the time.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
I typically try to keep devices for as long as I can — historically, that’s been around 5-6 years for laptops. During the Intel era of MacBooks, the year-over-year spec bumps were usually not impactful enough to justify more frequent upgrades. However, the transition to Apple Silicon has changed that. I recently upgraded to my first M-series MacBook Pro.
\n\n", "tags": [ "apple-silicon","m3", "essays" ], "content_html": "I typically try to keep devices for as long as I can — historically, that’s been around 5-6 years for laptops. During the Intel era of MacBooks, the year-over-year spec bumps were usually not impactful enough to justify more frequent upgrades. However, the transition to Apple Silicon has changed that. I recently upgraded to my first M-series MacBook Pro.
\n\n\n\nI replaced my 2020 Intel 13” MacBook Pro, which had a 2.3 Ghz Quad-Core i7. I can’t remember, but I think this was the highest-end processor you could get at the time for that model. This was the last generation of Intel Machines, which I waited to get until they fixed those defective keyboards. I anticipated keeping that Intel machine well into 2025, but the insane performance of the M-series chips was too incredible to wait any longer. I upgraded to the newly released 14” MacBook Pro with an M3 Max — and in Space Black, of course. I hope to keep this machine for a while, and based on my experience so far, I think that will be easy to do.
\n\nOverall, this is — by far — simply the best laptop I have ever used. It is the best Mac I have ever used.
\n\nUnfortunately, unboxing and initial setup were not without problems. I quickly realized that the laptop was not charging. The charging indicator light on the MagSafe cable connector also refused to light up. What it just the cable, or was it the MagSafe port on the laptop as well? Or worse? I tried charging with the USB-C cable and charger from my old Intel machine, along with every possible combination of the old and new charger with the old and new laptop with a USB-C cable. Everything worked with USB-C. It appeared that it was only the MagSafe cable. Finally, I ran Apple Diagnostics to be sure that nothing was wrong with the actual machine, which reported that everything was working properly. What a relief!
\n\nStill, minor hardware issues like these do not instill confidence. Apple has a solid track record with hardware, but their serious issues with software quality are widely documented by now. All I could think was, what else could be wrong? Seeing how this was my first M-series machine, I did not have an extra MagSafe cable around to verify that the machine was, in fact, ok. I stopped the setup process and waited until I could get to an Apple Store — the last thing I wanted was to fully setup and configure this new machine, only to find out it needed to be replaced.
\n\nMy experience at the Apple Store was frustrating, and honestly, somewhat laughable. The “genius” that assisted me plugged in my defective cable to his machine to verify it did not work — and that was it. I had to ask that we also plug-in a working MagSafe cable to verify that the MacBook Pro was also functioning properly. He did not run a diagnostic in the store, since I explained I had already done that.
\n\nBecause everything was still under warranty (of course), I owed nothing and we ordered a new replacement MagSafe cable because none were in stock. In the meantime, I could charge via USB-C. However, if my warranty had been expired, in addition to paying for a replacement MagSafe cable, Apple would have charged me $100 for “labor”. That’s right, one-hundred-fucking-dollars for a “genius” to plug in a cable. I think that is absurd, especially considering I did all of the actual work — debugging the entire issue at home, running diagnostics, and commuting to the store.
\n\nApparently, plugging in a cable is a “LEVEL 1 HARDWARE REPAIR” — so call me a genius, because I plug in at least half a dozen cables every day. If my warranty had been expired, I would gladly pay for the replacement cable but there is no way I would have paid for this “repair”. What a joke!
\n\nI used Migration Assistant to move all of my data from the Intel machine to the new M3 machine. It mostly worked. A few apps did not transfer at all, for unknown reasons. Some apps transferred without their licenses, so I had to re-enter those. Not a big deal. The biggest issue was that homebrew installations differ between Intel and Apple Silicon machines. I had to remove the old, migrated homebrew installation and re-install. Some of my dot files, configurations, and System Settings also had to be fixed. Overall, it was a decent experience and, while not perfect, Migration Assistant did save me a lot of time. Yet because of its imperfection and my paranoia about software quality, I double-checked everything.
\n\nFortunately for me, I did not have to deal with hundreds of security-theater permissions prompts, like what Jason Snell (and many others) have experienced when upgrading. However, I think your experience depends on how you use Migration Assistant. Snell explains that he migrated using a backup. I followed Howard Oakley’s instructions on using Migration Assistant, which specify skipping Migration Assistant during initial setup, creating a new admin account on the new mac, and then running Migration Assistant on both Macs to replace the admin account on the new Mac with the account from the old one.
\n\nI have gripes about the notch. There isn’t enough room to display all of my menu bar apps and icons, so… they just get hidden!? Apparently, everyone in Cupertino thinks the best solution to this problem is to hide them with zero indication that there are more that simply can’t be displayed because of the notch. I wasted so much time trying to figure out why Little Snitch and 1Password were not running on my new machine. Was there a compatibility issue with Apple Silicon that I didn’t know about? That couldn’t be. In turns out, they were running the whole time but they were hidden by the notch.
\n\nHere are the apps and icons I had in my menu bar: Little Snitch, 1Password, Pause, Lucifer, Red Eye, Tot, NordVPN, Time Machine, Script Menu, Audio/Volume, Bluetooth, WiFi, Battery with percentage, Input Sources, Control Center, Date and Time with Day of the Week. I don’t think that’s unreasonable — 10 system icons and 7 third-party menu bar apps. On my 13” Intel MacBook Pro, the icons reached to about halfway across the screen. On the 14” M3 MacBook Pro, ironically a machine with a larger display, at least 3 icons get hidden.
\n\nThis “design” (or lack thereof) is so dumb. It is utterly ridiculous to me that this is still how it “works” two years after the introduction of the redesigned MacBook Pro with a notch. How hard could it be to add an overflow menu with a “«” (or should it be “»”?) button that shows the remaining apps and icons that can’t be displayed? This entire situation with the notch is ironic, because the iPhone notch and “dynamic island” are so thoughtfully designed with zero compromises regarding the functionality of iOS. In fact, they actually provide a better user experience. Yet on the Mac, how the notch interacts with macOS is laughably incompetent. It is shockingly lazy regarding attention to detail, and results in an outright disruptive and confusing user experience.
\n\nThe best advice I got was to fix this with a 3rd-party app, Bartender. I tried Bartender — and kudos to the developer for making a great app — however, I just didn’t like it. It felt clunky and glitchy to me, in ways that I think the developer has tried hard to mitigate, but I imagine Apple does not make developing this sort of app easy. It seems to rely on a lot of hacks (like screen recording!?) to dynamically hide and show your overflowed menu bar apps.
\n\nThe other option is Say No to Notch, which adds a letterboxed-style black bar to the top of your screen and shifts the entire menu bar down below the notch. Again, kudos to the developer for creativity. But, yuck! I did not like this solution either. Also, part of the appeal of the 14” display is that I’m getting a bit more screen real estate than my 13” Intel.
\n\nThe final option, and ultimately what I decided to do, is embrace the notch and deal with it. (And, of course, install Notchmeister.) I removed as many of the system icons as I could from the menu bar — Bluetooth, WiFi, and Battery are now only in Control Center. Seeing the status of these now requires the extra click into Control Center, but it could be worse. I set Audio/Volume to only display when active — namely, so that the AirPods icon will appear in the menu bar when using them. Finally, I removed “Day of the Week” from showing with the Date and Time. Not a huge loss. For now, this configuration seems to work for my needs most of the time for the menu bar apps that I always keep open.
\n\nHowever, some applications (like Xcode) have large file menus that extend to the other side of the notch, thus hiding the menu bar apps. While this was an issue on my Intel machine as well, it is more apparent and disruptive with the notch. There are also some menu bar apps I open temporarily, like Sim Genie. This also causes problems. What I’ve done to address this is to order my menu bar apps and icons from least important on the left to most important on the right. The result is that the least important icons are the ones that get hidden by application menus or the notch, while the most important icons are more likely to remain visible. For example, Time Machine — which I want in the menu bar, but it doesn’t matter if this gets hidden while I’m working in Xcode and using Sim Genie.
\n\nDespite a rough unboxing experience due to a defective cable and gripes about the notch, I absolutely love this M3 MacBook Pro. Migration Assistant worked reasonably well and after resolving or working around the issues I described above, this machine is fantastic. As I said in the beginning of this post, this is simply the best laptop I have ever used. Performance is insane — outright, but especially in comparison to my old Intel machine — and I’ll write about that soon.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
A few weeks ago my AirPods Pro 2 (with Lightning, not USB-C) suddenly started acting buggy and weird. The case no longer made the chime sound when plugging them in to charge. I stopped getting “Left Behind” notifications from the Find My app. After updating to iOS 17, I could not get them to install the latest firmware, which enables the new features for Adaptive Audio, Conversation Awareness, and Personalized Volume. I had been trying for weeks to follow the magic steps that will trigger a firmware update with no luck. Even worse, after a full charge of both the AirPods and the case, the batteries for all of them would drain to zero percent within 2-3 days.
\n\n", "tags": [ "airpods", "software-dev" ], "content_html": "A few weeks ago my AirPods Pro 2 (with Lightning, not USB-C) suddenly started acting buggy and weird. The case no longer made the chime sound when plugging them in to charge. I stopped getting “Left Behind” notifications from the Find My app. After updating to iOS 17, I could not get them to install the latest firmware, which enables the new features for Adaptive Audio, Conversation Awareness, and Personalized Volume. I had been trying for weeks to follow the magic steps that will trigger a firmware update with no luck. Even worse, after a full charge of both the AirPods and the case, the batteries for all of them would drain to zero percent within 2-3 days.
\n\n\n\nI have no idea what triggered all of these issues, but my current theory is that I interrupted a firmware update that put them in an inconsistent state. But, who knows. In any case, I’m happy to report that a hard reset seems to have fixed all of these problems! If you’ve been experiencing any of these (or similar) issues with your AirPods, you should try resetting them.
\n\nI found this support guide from Apple on how to reset your AirPods, which states “You might need to reset your AirPods if they won’t charge, or to fix a different issue.” It isn’t clear to me exactly what resetting does — aside from the obvious steps of completely unpairing them, removing them from your iCloud account, forgetting them in your Bluetooth settings, and then repairing them. I suspect it might also restore the last-known good firmware?
\n\nHere are the steps from the support guide:
\n\n\n\n\n\n
\n- Put your AirPods in their charging case, and close the lid.
\n- Wait 30 seconds.
\n- Open the lid of your charging case, and put your AirPods in your ears.
\n- Go to Settings > Bluetooth. Or go to Settings > [your AirPods].
\n- If your AirPods appear there as connected, tap the More Info button next to your AirPods, tap Forget This Device, then tap again to confirm.
\n- If your AirPods don’t appear there, continue to the next step.
\n- Put your AirPods in their charging case, and keep the lid open.
\n- Press and hold the setup button on the back of the case for about 15 seconds, until the status light on the front of the case flashes amber, then white.
\n- Reconnect your AirPods: With your AirPods in their charging case and the lid open, place your AirPods close to your iPhone or iPad. Follow the steps on your device’s screen.
\n
Note that on step (8), it took longer than 15 seconds for me. It was more like 30 seconds. The light on the case was initially white, and then amber, then white again. Make sure you hold the button until you see the amber light, and then white.
\n\nAfter following these steps, I then tried to trigger a firmware update following the random magic steps that people have experimented with to make this happen implicitly. I wish Apple would just add an “Update Firmware” button in the Settings! Anyway — the firmware update installed successfully almost immediately and the case even chimed after it completed!
\n\nAs far as I can tell, my AirPods are as good as new.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
When you work on a large team and are participating in many pull requests on GitHub, it can be difficult to keep track of everything you are working on. In addition to opening your own pull requests, you can be assigned to them, you can be requested as a reviewer, you can comment in discussion threads, and you can be mentioned by others. Each of these occurrences requires your attention — perhaps immediately, but always eventually.
\n\n", "tags": [ "github","productivity", "software-dev" ], "content_html": "When you work on a large team and are participating in many pull requests on GitHub, it can be difficult to keep track of everything you are working on. In addition to opening your own pull requests, you can be assigned to them, you can be requested as a reviewer, you can comment in discussion threads, and you can be mentioned by others. Each of these occurrences requires your attention — perhaps immediately, but always eventually.
\n\n\n\nGitHub provides some convenient “built-in” filters to help you navigate the matters above. You can see all of your own pull requests, everything that’s assigned to you, or everything where you are mentioned.
\n\nHowever, wouldn’t it also be nice to see all of these pull requests at once? That is, all of your own pull requests, everything assigned to review, and everything where you are actively in discussion? You can, with the filter involves:@me
, which will show you all the pull requests you are involved in, in any capacity. In other words, it shows you everything that requires your attention. I have found this super helpful when I’m participating in lots of pull request review discussions as well as opening a lot of pull requests, and I want to see everything together. Better yet, it also works when filtering issues.
\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
Foundation’s URL
(née NSURL
) is a nearly ubiquitous API on Apple platforms. One of its shortcomings is that it is heavily overloaded – an instance of URL
could represent a web URL or a file URL. While there are many similarities between accessing resources on a local disk or on a web server, I think there should be explicit types for each, say WebURL
and FileURL
.
Foundation’s URL
(née NSURL
) is a nearly ubiquitous API on Apple platforms. One of its shortcomings is that it is heavily overloaded – an instance of URL
could represent a web URL or a file URL. While there are many similarities between accessing resources on a local disk or on a web server, I think there should be explicit types for each, say WebURL
and FileURL
.
What makes URL
more confusing is how other APIs often interchangeably use String
and URL
or provide multiple APIs with the same functionality where some use URL
and others use String
. The best example of this is FileManager
, which can’t seem to decide on one or the other. You can construct a URL
from a single String
or build a URL
incrementally from multiple String
values, and you can convert a URL
back to a String
. Also when working with networking APIs, it is common to move back and forth between String
and URL
.
Even after years of working with Foundation on Apple platforms, I make the same mistake using URL
with files all the time. When you are working with networking code and you need a string representation of a URL, you need to call URL.absoluteString
. This is so common and it is always the first thing I reach for — it even has “string” in the name! But when working with file URLs and FileManager
, this is not what you want.
I was recently writing some code dealing with files using FileManager
and, out of habit, I tried to pass URL.absoluteString
to an API that needed a String
file path. It took way too long for me to figure out the bug, because I am so used to seeing and using absoluteString
. It was not an obvious error.
For file URLs, URL.absoluteString
will produce:
file:///Users/jsq/Documents/file.txt\n
Note that the string includes the file://
scheme. For file-based APIs that require a String
file path, passing the value returned by absoluteString
will fail.
However, URL.path
will produce the full file path without the file://
scheme prefix.
/Users/jsq/Documents/file.txt\n
When working with file URLs, the correct way to convert to a String
value is to use URL.path
. This is confusing, because URL.path
for web URLs means something very different. And this helps illustrate the problem with the overloaded behavior of URL
— many of the APIs behave differently depending on the type of URL
you have. Even more confusing, some APIs do not make sense to include for both kinds of URLs. For example, URL.host
and URL.query
do not apply to file URLs.
Of course, URL.isFileURL
exists to allow you to distinguish between the two if needed, but this further exposes the poor design of URL
and emphasizes the need for two explicit types. Using FileManager
would be much more intuitive if all of its APIs worked with a single FileURL
type instead of mixing String
and URL
.
\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
Welcome to the third part of my going indie series! In the previous post, I discussed building a foundation, getting started, and finding clients. In this post, I am going to discuss many of the decidedly un-fun administrative aspects of being freelance and contracting like saving for retirement and — everyone’s favorite — taxes. Most folks consider these topics to be boring and tedious, but understanding them is critical to your success. The best approach is one of curiosity. As a software developer, you might find the task of optimizing (and minimizing!) your tax burden to be an interesting problem to solve — I definitely do!
\n\n", "tags": [ "series-going-indie","indie-dev","contracting","freelance","consulting", "essays" ], "content_html": "Welcome to the third part of my going indie series! In the previous post, I discussed building a foundation, getting started, and finding clients. In this post, I am going to discuss many of the decidedly un-fun administrative aspects of being freelance and contracting like saving for retirement and — everyone’s favorite — taxes. Most folks consider these topics to be boring and tedious, but understanding them is critical to your success. The best approach is one of curiosity. As a software developer, you might find the task of optimizing (and minimizing!) your tax burden to be an interesting problem to solve — I definitely do!
\n\n\n\n\n\n\nDisclaimer: I am not an accountant, nor a financial advisor. I highly recommend you get both! All the information in this post is derived from my experience, conversations with my accountant, and advice from my financial advisor. Of course, your tax situation and financial situation differ from mine, so “your mileage may vary” as they say. For example, I do not have children, but if you do that has a significant impact on your taxes and finances.
\n\nAlso, as I noted in my first post, this series assumes you are based in the United States. While many aspects of this series are widely applicable, this post is very specific to the US.
\n
What you may not realize is that going indie means starting your own business. Congratulations, you are now a small business owner as far as the IRS is concerned. Don’t worry, that does not mean some sort of formal business entity is necessary (as you will see below). It only means you need to shift your thinking a bit. My goal with this post is to give you a head start on learning how to structure your business, and what to expect regarding taxes. My hope is that you can begin your journey with more information than I had — which was literally zero.
\n\nIf you do not have an accountant or financial advisor, I highly recommend you get both. Find someone you trust. Their expertise will help guide you in making the correct decisions for yourself and they will save you a ton of time and potential headaches. Likely, someone in your network can make a recommendation for both.
\n\nThe first task after deciding to go independent and do freelance/contract work is deciding how to structure your self-employed business. You have a few options, each with its own pros and cons. The factors that differ between the options primarily center around taxes, reporting, paperwork, and bookkeeping. There are many resources on the internet that explain the differences, benefits, and drawbacks of each self-employed business entity — so I will be brief. I am not an expert on these topics, so you should ultimately consult your accountant. My goal here is to make you aware of your options and give you the gist of each.
\n\nIn all scenarios, you should open a new bank account for your business. It’s also a good idea to get a separate credit card that you use solely for business expenses. This is not strictly necessary as a sole proprietor (and the accounts can be normal accounts in your name, not actual business accounts), but it is a good practice to keep everything separate. This will make bookkeeping significantly easier for you.
\n\nSole Proprietorship. [IRS, Wiki] A Sole Proprietorship is owned and run by one person and in which there is no legal distinction between the owner and the business entity. Being a sole proprietor is the simplest and easiest structure. You do not need to establish a formal legal entity. There is no paperwork or setup. You operate as yourself, as an individual, under your legal name. Similar to being a full-time W-2 worker, you file taxes using your social security number (SSN), etc. You can (optionally) “level-up” by getting a Fictitious Business Name (FBN), which allows you to operate under a different “business” name. However, as the name indicates, this is not a “real” business entity. It merely provides a facade to your sole proprietorship, which can be useful in some situations. If you opt to have an FBN, then you also need to get an Employer Identification Number (EIN) to associate with it. An EIN is basically an “SSN for your business” — you file your taxes using the EIN instead of your SSN and you provide your EIN to your clients for your 1099 forms. In addition to income tax, you also pay self-employment tax. You “pay yourself” simply by withdrawing or transferring funds from your business account.
\nS Corporation (S-Corp). [IRS, Wiki] An S-Corp is a more formal structure that requires you to establish a payroll system and pay yourself a salary. You need an EIN and there is various paperwork to complete to establish the S-Corp entity. The process can take a couple weeks or up to 3 months, depending on your circumstances. It differs in that you can have multiple shareholders. An S-Corp is similar to a sole proprietorship in that it is a “pass-through entity” meaning the corporation’s income and losses are divided among and passed through to its shareholders (i.e., you). In this scenario, you are the singular shareholder so you more or less file taxes like an individual would. However, S-Corps have access to additional deductions and can yield higher tax savings. Notably, an S-Corp is not a “full” corporation and alleviates you from double taxation where you would otherwise pay corporate tax as well as individual income tax. You still pay self-employment tax like a sole proprietor, and because you have to run a payroll system you also pay payroll taxes. You “pay yourself” via your payroll system. S-Corps also come with some additional fees and specific states impose their own taxes on S-Corps. Also note that some states tax S-Corps as C-Corps, negating the pass-through benefit. Because of the additional fees and taxes, it usually does not make financial sense to establish an S-Corp until your annual earnings pass a threshold of around $100,000 — at which point you are better positioned to reap the tax benefits. (Again, consult your accountant here.)
\nLimited Liability Company (LLC). [IRS, Wiki] An LLC is what I know the least about. It is an established business entity that you also have to file paperwork to legally setup, pay fees, get an EIN, etc. An LLC does not need to have payroll. It is not a corporation but it is a legal form of a company. It is a hybrid business structure that can combine the pass-through taxation of a sole proprietorship with the limited liability of a corporation. Regulation of LLCs varies from state to state. LLCs generally have fewer formalities and reporting requirements than S-Corps, which can make them easier and less expensive to manage.
\nMy advice is: do the simplest thing first. If you are just getting started, operate as a sole proprietor. You do not need to complete any legal paperwork and you can start working immediately. This is beneficial because if you decide that you do not like freelancing and contracting, then you can stop and go back to full-time work without having to do anything else. At the same time, if you do decide to stay independent, then it is easy to transition to an S-Corp or LLC later. I have a friend that established an LLC once for a project that eventually fizzled out, and he said the process to dissolve it was a nightmare. He wished he had just gone with a sole proprietorship first.
\n\nFinally, one important caveat is that sole proprietorships do not shield you from liability, whereas S-Corps and LLCs do. For example, say something terrible happens and a client files a lawsuit against you. With an S-Corp or an LLC, your personal assets are protected. That is, the client is suing the business entity and they can only go after the business entity’s assets. If you are a sole proprietor, then the client is suing you personally. Technically, that puts everything you own at risk of seizure and liquidation — bank accounts, investments, property. However — this is extremely unlikely and especially rare in our line of work. You are not going to get sued for a software bug. (I mean, maybe you could? But let’s be real.)
\n\nIf you were starting a construction business or a restaurant, that is a very different story with very real scenarios involving liability. So do not allow the issue of liability scare you away from starting with a sole proprietorship! Tons of people are sole proprietors without any problems. It is important to weigh the potential risks of your type of business when choosing the appropriate structure. For software development and design, that risk is extremely low. Still, ask your accountant!
\n\nTaxes are terrible. Everyone knows this. And they are even worse when you are self-employed. However, you can mitigate your tax burden with deductions, which I’ll discuss below. Regardless of your business structure you will pay your normal income tax, which is the same as when you are a full-time worker at a company. The same federal and state income tax brackets apply.
\n\nWhat differs when you are self-employed is that you must also pay self-employment tax, or SECA (named after the Self-Employed Contributions Act). SECA taxes include Social Security and Medicare. When you are a full-time worker at a company, you also pay these taxes (called FICA, per the Federal Insurance Contributions Act) and you can see the deductions taken from your paychecks. So what’s the difference? When you work at a company, the company pays half of these taxes (FICA) and you pay the other half. When you are self-employed, you pay the full amount (SECA) — because you are the employee and the employer. The total self-employment tax is 15.3 percent. When you work at a company, you and the company each pay 7.65 percent.
\n\nThere are a lot of details that go into these calculations that I will not cover in this post, but you are in luck — I made an app for that! Taxatio is tax calculator for freelancers that I released last year. It is available for iOS and macOS. It will help you with these calculations and tax estimations, as well as explain what’s going on with detailed breakdowns.
\n\nI would be remiss if I did not take this opportunity to highlight the injustices of our tax system. SECA/FICA taxes are regressive taxes — the more money you make, the less you pay as a percentage of your total income. This imposes an undue burden on the poor and lower wage workers, not to mention the rich usually pay nothing. SECA/FICA are taxes on earned income, that is, income you earn through labor. Because the rich do not work, but rather “earn” money through their pre-existing assets and investments, they pay nothing into Social Security and Medicare. Furthermore, their wealth — that is, their property, investments, inheritance, and other assets — is taxed at significantly lower rates. Thus, the rich get richer simply because they own things all while doing no labor whatsoever, yet they still reap the benefits of our decaying social safety net that real workers provide.
\n\nThe more you learn about our tax system, the more obvious it becomes that income taxes are designed to punish poor and working class people — and if you must work to pay your bills and survive, then you are working class. You may consider yourself middle class, but this is a facade propped up by our precarious and predatory credit system. Being “middle class” is a status that can be revoked at a moment’s notice, as we witnessed during the 2008 housing crisis. The middle class is an artificial bifurcation of working people manufactured by the rich via credit that deludes us into thinking there exists a ladder we can simply climb to join the wealthy elite at the top. It is a scam. No one earns a billion dollars — they steal it. (And before someone tries to lecture me about the “risk” of investments — there is no “risk” when you have a literal mountain of money and wealth at your disposal, not to mention a government that will bail you out after you make billions of dollars in derivatives evaporate overnight.)
\n\nSECA taxes have a significant impact on independent workers. It is wrong that you must pay double as a single person, as if you have the same influence and wealth as an entire corporation. Even full-time workers should not have to split the burden of FICA taxes with corporations reap millions in profits while suppressing wages and destroying our environment with impunity.
\n\nBut, I digress.
\n\nThe most significant difference regarding taxes when you are self-employed and doing 1099 freelance/contract work is how and when you pay taxes. Your clients do not do any tax withholding. When you are a full-time W-2 worker, FICA taxes are withheld from each paycheck you receive. When you are self-employed, you must pay quarterly estimated taxes. This includes federal and state income tax (unless, of course, you live in a state that does not collect income tax). Instead of paying a small amount on each paycheck, you pay lump sum once per quarter.
\n\nTake note — quarterly estimated taxes are no joke. As an example, suppose your adjusted gross income for the year is $200,000. Your quarterly estimated tax payments may be as high as $18,000 per quarter. That’s a lot of money to drop at once. This means you need to be prepared. When clients pay you, do not withdraw the full amount from your business account to pay yourself. You always need to leave money in your business account for tax payments. Again, you are in luck because Taxatio will help you here by showing you the complete quarterly payments breakdown.
\n\nThe hardest part about estimated taxes is… well, estimating them! When you first get started, this is a moving target. You are not sure how much you will make for the year. Do your best to make projections. Your accountant can help you here, too. After your first year of being independent is over, you can use the previous year’s income as your estimate for the next year. If you do not pay your quarterlies on time, in full, or at all, you will get hit with fees and penalties when you file your tax returns.
\n\nOne reason I recommend going independent at the beginning of the calendar year is to simplify your taxes and estimates. If you start your freelance/contract work during the same year that you quit your full-time job, then you will have mixed income: some W-2, some 1099. This makes your estimations a bit more of a moving target, and they will not stabilize until after your second year — that is, your first full calendar year of being independent. However, you should not worry about this too much.
\n\nThere is one nice thing about estimated taxes (and using Taxatio to plan). If you have accurate projections for your annual income, like me, that means you have an accurate understanding of your annual tax burden. This allows me to optimize my time and work. For example, once I have made enough money for the year and know I have enough to cover my taxes, I can stop working. Historically, this means I can not work for 2-3 months each year.
\n\nConveniently, you can pay (and automatically schedule!) your federal estimated tax payments by creating an account with EFTPS.gov. For state estimated taxes, you will need to figure out the equivalent for your state. If you live in California, like me, then you can create an account with the California FTB where you can similarly schedule and pay your estimated payments.
\n\nOf course, we cannot discuss taxes without also discussing the available mechanisms we can employ to reduce them. In the example above, we discovered than an annual income of $200,000 could result in quarterly tax payments around $18,000. However, you can dramatically decrease this amount with deductions.
\n\nIt is important to understand the difference between your gross income (the total amount of money you earn) and your taxable income (the portion of your income that is subject to income tax). Deductions are subtracted from your gross income, which results in reducing your taxable income. Some of the most common deductions include health insurance premiums and a home office. Consult your accountant for advice.
\n\nIn part one I mentioned that you can purchase health insurance via the Affordable Care Act (ACA). Every state is different, but I have been happy with my experience in California. If you get health insurance (which, you probably should), the entirety of your premiums are tax deductible. Also note that failing to purchase insurance coverage will result in a tax penalty.
\n\nAnother substantial deduction is self-employment tax (SECA). You are allowed to deduct half of SECA — that is, the “employer” portion. While I think paying double is an unnecessary burden on sole proprietors, it does help to be able to deduct this.
\n\nFinally, and most importantly, you should be keeping track of your expenses. Expenses are subtracted from your gross income, which again results in reducing your taxable income. I will discuss bookkeeping in detail in the next part of this series.
\n\nAnd, of course, Taxatio does these calculations and will show you a breakdown. The current version is limited with a single field for your total amount of deductions, but a future update will expand this functionality to allow a full itemized list.
\n\nThe last significant piece of this puzzle involving business structure, taxes, and deductions is how you can continue to save for retirement despite no longer working full-time at a company. Typically, most companies provide retirement plans and many often offer matching contributions. Most commonly, you will have the opportunity to open a 401(k) plan. If you are lucky, the company will match a portion of your contributions.
\n\nIn my experience, many folks are unaware that as a self-employed business owner you can open a Solo 401(k) plan. The rules are the same in terms of the maximum annual contribution amounts — except you are the employee and the company. This means you can contribute as an individual and then “match yourself” as the company. In 2023, the maximum individual contribution is $22,500. The maximum company contribution is $43,500 or 25% of your earned income — whichever is smaller. This means you can put up to $66,000 into your Solo 401(k) per year, assuming you make enough to maximize the company contribution. Yes — that is a sweet fucking deal. You might be missing out on stock options at a company, but you can dump a ton of savings into retirement.
\n\nWith a 401(k), your contributions can be traditional (pre-tax) or Roth (after-tax). The difference is that traditional contributions reduce your taxable income, thus reducing the taxes you pay now. However, you will then pay taxes on the funds you withdraw during retirement. For Roth, you pay taxes now before making contributions and then when you withdraw funds during retirement, they are not taxed. The question to ask yourself is: do you want to pay taxes now, or later?
\n\nThe other popular option is a Roth IRA. However, the maximum contribution for 2023 is only $6,500. You can have both a Solo 401(k) and an IRA, if you want. Be aware that there are many rules and regulations regarding contributions. I do not recommend trying to setup these accounts on your own. If you must choose one, a Solo 401(k) is often the better choice as it allows you to save significantly more. However, paperwork for an IRA can be much simpler, so that might be a good start depending on your circumstances.
\n\nFor all of these important decisions, you should consult your financial advisor to make the choices that work best for you and your financial goals.
\n\nFinally, do I need to say it? Taxatio does these calculations for you. Currently, it only supports Solo 401(k) plans, but I would like to add support for Roth IRAs in a future update.
\n\nToday we covered some ugly, mundane — but nonetheless important — details of going independent. I hope this post was helpful if you are considering going independent. There is still more to come in this series. In the next part, I will discuss bookkeeping and invoicing! Stay tuned.
\n\nMany thanks to Michael Tsai and @ljmatkins for their feedback on this post regarding details about LLCs and retirement plans. I have updated this post to reflect their corrections and other notes.
\n\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
In UIKit, UITableViewCell
has a customizable accessory view. You can use one of the few accessory options that is provided by iOS by setting the accessoryType
property, or you can provide a custom view using accessoryView
, which can be any UIView
. The equivalent of constructing a UITableViewCell
with a chevron accessory in SwiftUI is using a NavigationLink
. Unfortunately, however, SwiftUI does not provide an API to customize the accessory view for a NavigationLink
— you are stuck with the default chevron.
In UIKit, UITableViewCell
has a customizable accessory view. You can use one of the few accessory options that is provided by iOS by setting the accessoryType
property, or you can provide a custom view using accessoryView
, which can be any UIView
. The equivalent of constructing a UITableViewCell
with a chevron accessory in SwiftUI is using a NavigationLink
. Unfortunately, however, SwiftUI does not provide an API to customize the accessory view for a NavigationLink
— you are stuck with the default chevron.
Leading by example, UIKit demonstrates the need for different accessory views to communicate to the user what to expect when they tap a table view cell. You can find all the different accessory types in use in the apps built-in to iOS — chevrons, the information icons (“i” with a circle), and checkmarks. It is very common for apps to customize the accessory view for cells in a UITableView
or views in a SwiftUI List
. It is surprising that SwiftUI still lacks an official API for this.
What’s worse than a default chevron that cannot be customized is that there is also no API to opt-out and simply hide it. And what’s even worse than that is that the default chevron for a NavigationLink
is different than the chevron provided by SFSymbols. If used together, they clash and it’s ugly.
So, like many things in SwiftUI, we have to resort to hacks and obscure workarounds to achieve decades-old UIKit behavior. The best way I’ve found to hide the default chevron in a NavigationLink
is to hide the entire thing underneath another view using a ZStack
.
Suppose we are using a NavigationLink
to display an “About” view in our app:
// HACK: ZStack with zero opacity + EmptyView\n// Hides default chevron accessory view for NavigationLink\nZStack {\n NavigationLink {\n AboutView()\n } label: {\n EmptyView()\n }\n .opacity(0)\n\n Label(title: \"About\", icon: Image(systemName: \"info.circle\"))\n}\n
This allows you to provide an entirely custom View
for the NavigationLink
. In this case, that’s the foremost Label
in the ZStack
. Obviously, you would not want to copy and paste this snippet every time you need a NavigationLink
, so we can write a better version of NavigationLink
to encapsulate this for us. We can mimic the NavigationLink
API.
struct BetterNavigationLink<Label: View, Destination: View>: View {\n let label: () -> Label\n let destination: () -> Destination\n\n init(@ViewBuilder label: @escaping () -> Label,\n @ViewBuilder destination: @escaping () -> Destination) {\n self.label = label\n self.destination = destination\n }\n\n var body: some View {\n // HACK: ZStack with zero opacity + EmptyView\n // Hides default chevron accessory view for NavigationLink\n ZStack {\n NavigationLink {\n self.destination()\n } label: {\n EmptyView()\n }\n .opacity(0)\n\n self.label()\n }\n }\n}\n
With that, we have a drop-in replacement for NavigationLink
and can update all call sites to use BetterNavigationLink
instead.
BetterNavigationLink {\n Label(title: \"About\", icon: Image(systemName: \"info.circle\"))\n} destination: {\n AboutView()\n}\n
I think NavigationLink
can be made significantly better with some small changes. First, I do not think there should be any accessory view by default — accessories should be opt-in, just like with UITableViewCell
. Second, I think NavigationLink
should allow you to set any SFSymbol as the accessory. Perhaps this could be a new view modifier for NavigationLink
.
NavigationLink {\n AboutView()\n} label: {\n Text(\"About\")\n}\n.navigationAccessory(Image(systemName: \"info.circle\"))\n
Wouldn’t that be nice?
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
UserDefaults
is probably one of the most popular APIs on Apple Platforms. It is a highly-optimized key-value persisted store that is backed by a property list, and it is most commonly used for saving small pieces of data like user preferences. Despite its ease-of-use, there is one common anti-pattern I see developers use often.
UserDefaults
is probably one of the most popular APIs on Apple Platforms. It is a highly-optimized key-value persisted store that is backed by a property list, and it is most commonly used for saving small pieces of data like user preferences. Despite its ease-of-use, there is one common anti-pattern I see developers use often.
In dozens of iOS projects over the years, I find that developers are prefixing key names with the app’s bundle identifier. For example, instead of naming a key \"sounds-enabled\"
it will be named \"com.mycompany.MyApp.sounds-enabled\"
. I am writing this post as a PSA to tell you that you do not need to do this!
On iOS, in your application’s user data sandbox you will find the backing plist for UserDefaults
at ~/Library/Preferences/com.mycompany.MyApp.plist
. On macOS, for a non-sanboxed app you will find the plist at the same path and for an app that is sandboxed you will find the plist at ~/Library/Containers/MyApp/Data/Library/Preferences/com.mycompany.MyApp.plist
.
As you can see, your instance of UserDefaults
is already namespaced by your app’s bundle identifier! Prefixing your keys is redundant. So please — do not prefix your key names with your app’s bundle identifier!
You might argue that prefixing key names is harmless, and that could be true sometimes. However, there are known issues with UserDefaults
key-value observing if key names have periods in the name — KVO does not work in that scenario. Furthermore, if you use debug tools (like FLEX) that allow you to inspect the values saved in UserDefaults
or if you manually open the plist (which is just XML) to examine it, it gets really cumbersome to search and read through if every single key is prefixed with the same ~30 characters.
Lastly, if you are convinced to drop the unnecessary prefixes, beware that you will need to write migration code to save your values from the old key to the new one, otherwise you will lose data.
\n\nA few folks on Mastodon have pointed out situations where prefixing might be warranted. Or rather, pointed out problematic scenarios to generally watch out for.
\n\nGreg Heo mentioned that you should carefully consider your key names if you are using UserDefaults
shared domains. Typically, this scenario would be sharing data between your main app and an extension — so hopefully you are not colliding with your own key names.
Matt Massicotte noted that some system frameworks store values in your app’s UserDefaults
. For example, AppKit and SwiftUI on macOS store state restoration data, most commonly window size and location. I had forgotten about this. However, I highly doubt you’ll have naming collisions. For example, here are a couple of keys for one of my apps: NSWindow Frame com_apple_SwiftUI_Settings_window
, com_apple_SwiftUI_Settings_selectedTabIndex
.
More importantly, Andy Ibanez warned that a third-party dependency might read and write to the same key name. That is very bad! If you are a library author, you should not be writing to UserDefaults.standard
. This is why UserDefaults.init(suiteName:)
exists — your library should initialize its own suite.
All of these are valid concerns. Still, I think for the majority of use cases, prefixing is not necessary.
\n\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
Xcode 15 introduces a new “bookmarks” feature, which lets you bookmark lines or entire files. It is a welcome change that has sherlocked my hack for using breakpoints as bookmarks.
\n\n", "tags": [ "xcode","xcode-tips", "software-dev" ], "content_html": "Xcode 15 introduces a new “bookmarks” feature, which lets you bookmark lines or entire files. It is a welcome change that has sherlocked my hack for using breakpoints as bookmarks.
\n\n\n\nHowever, one advantage of breakpoints is that they can be shared. This is useful for symbolic breakpoints that can be shared across projects. But it could also be useful for teams to share breakpoints within a project. As I was updating my old post, I was wondering if the same was possible for bookmarks. The question then is, how and where does Xcode save them?
\n\nBookmarks are stored in a plist at the following path:
\n\nPROJECT_NAME.xcodeproj/project.xcworkspace/xcuserdata/USER.xcuserdatad/Bookmarks/bookmarks.plist\n
where PROJECT_NAME
is the name of your project and USER
is the current user – that is, the output of $USER
or whoami
.
Typically, xcuserdata/
is added to .gitignore
so you likely would not notice any changes to this directory. Importantly, bookmarks are user-specific and there is a unique .xcuserdatad/
directory for each user. So if multiple users create bookmarks, you would have something like the following:
MyApp.xcodeproj/project.xcworkspace/xcuserdata/jsq.xcuserdatad/Bookmarks/bookmarks.plist\nMyApp.xcodeproj/project.xcworkspace/xcuserdata/gregheo.xcuserdatad/Bookmarks/bookmarks.plist\n
Furthermore, Xcode filters bookmarks in the UI based on the current $USER
. This means if you do check-in xcuserdata/
and bookmarks.plist
for each user, Xcode will only display your bookmarks in the Bookmarks Navigator panel.
I think this is a reasonable design choice. However, I can imagine scenarios where it would be useful to share bookmarks with your team, similar to breakpoints. For example, if you are trying to debug a problem with your remote team member and you’ve tracked down the issue to a few specific files and lines, you could bookmark those locations, push your branch, and have the other person checkout that branch. That’s a nicer, more precise experience than listing a bunch of filenames and line numbers in a Slack message that will eventually get lost. Another example would be for an interview exercise or a coding tutorial. You could prepare an Xcode project with bookmarks to guide someone through an exercise.
\n\nFor now, this is not possible in Xcode’s UI, but you could easily write a script to move a pre-populated bookmarks.plist
file to the correct location, based on the current $USER
. If you examine the data in bookmarks.plist
, you will see that each bookmark specifies a relative path key, <key>relative-path</key>
. Thus, if working on a team, everyone will need to checkout the project repo into a directory with the same name (which is the default behavior of git clone
). If a user changes the name of the root project directory, then the bookmarks will break, as the relative-path
will be invalid. If this is a possibility on your team, your script for sharing bookmarks will need to account for this by updating each entry in bookmarks.plist
with the correct root project directory name for the relative path.
While I’m here, there is one other usability issue with bookmarks. You have to right-click in a file to bring up the contextual menu to create one. It’s a bit cumbersome. There are keyboard shortcuts, but it would also be nice if you could create bookmarks by clicking in the line number gutter — similar to how you create breakpoints.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
Beginning with the introduction of dark mode in iOS 13, colors in iOS are now (optionally) dynamic. You can provide light and dark variants for all colors in your app. However, I was surprised to find that SwiftUI — which also made its first appearance on the platform in iOS 13 — still does not provide any API for creating dynamic colors.
\n\n", "tags": [ "swiftui","ios","macos","uikit","appkit","dark-mode", "software-dev" ], "content_html": "Beginning with the introduction of dark mode in iOS 13, colors in iOS are now (optionally) dynamic. You can provide light and dark variants for all colors in your app. However, I was surprised to find that SwiftUI — which also made its first appearance on the platform in iOS 13 — still does not provide any API for creating dynamic colors.
\n\n\n\nIn UIKit, UIColor
provides a dynamic initializer, init(dynamicProvider:)
, which I wrote about here. AppKit provides the equivalent API for NSColor
. Unfortunately, an equivalent API for SwiftUI’s Color
is missing.
UIKit also allows you to extract a specific variant from a UIColor
using resolvedColor(with:)
, which will return either the dark or light variant based on the provided trait collection. Again, AppKit provides the equivalent API for NSColor
. Surprisingly, in iOS 17 SwiftUI’s Color
gained a new API for color resolution, resolve(in:)
, which returns the resolved color value based on the provided EnvironmentValues
.
The result is that SwiftUI’s Color
API is oddly incomplete. Color
has no equivalent API to UIColor.init(dynamicProvider:)
, but it does provide its own version of UIColor.resolvedColor(with:)
. This is not only inconvenient, but very confusing.
Of course, you can use Asset Catalogs to define dynamic colors and reference them in SwiftUI, and Xcode 15 makes that easier! But if you need to programmatically initialize dynamic colors in SwiftUI, you are out of luck due to this glaring omission. Instead, you must resort to UIKit and AppKit. So, here’s a helpful extension that accommodates the missing API for all platforms.
\n\nimport SwiftUI\n\n#if canImport(AppKit)\nimport AppKit\n#endif\n\n#if canImport(UIKit)\nimport UIKit\n#endif\n\nextension Color {\n init(light: Color, dark: Color) {\n #if canImport(UIKit)\n self.init(light: UIColor(light), dark: UIColor(dark))\n #else\n self.init(light: NSColor(light), dark: NSColor(dark))\n #endif\n }\n\n #if canImport(UIKit)\n init(light: UIColor, dark: UIColor) {\n #if os(watchOS)\n // watchOS does not support light mode / dark mode\n // Per Apple HIG, prefer dark-style interfaces\n self.init(uiColor: dark)\n #else\n self.init(uiColor: UIColor(dynamicProvider: { traits in\n switch traits.userInterfaceStyle {\n case .light, .unspecified:\n return light\n\n case .dark:\n return dark\n\n @unknown default:\n assertionFailure(\"Unknown userInterfaceStyle: \\(traits.userInterfaceStyle)\")\n return light\n }\n }))\n #endif\n }\n #endif\n\n #if canImport(AppKit)\n init(light: NSColor, dark: NSColor) {\n self.init(nsColor: NSColor(name: nil, dynamicProvider: { appearance in\n switch appearance.name {\n case .aqua,\n .vibrantLight,\n .accessibilityHighContrastAqua,\n .accessibilityHighContrastVibrantLight:\n return light\n\n case .darkAqua,\n .vibrantDark,\n .accessibilityHighContrastDarkAqua,\n .accessibilityHighContrastVibrantDark:\n return dark\n\n default:\n assertionFailure(\"Unknown appearance: \\(appearance.name)\")\n return light\n }\n }))\n }\n #endif\n}\n
And now you can initialize a SwiftUI Color
programmatically with a light and dark variant.
let textColor = Color(light: someColor, dark: anotherColor)\n
\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
Welcome to my next post on going indie! In the previous one, I provided a high-level overview and introduction, and answered some of the most common questions I get from folks. In this post I’m going to dive deeper into how you can prepare to go independent, how to find clients, and other tips.
\n\n", "tags": [ "series-going-indie","indie-dev","contracting","freelance","consulting", "essays" ], "content_html": "Welcome to my next post on going indie! In the previous one, I provided a high-level overview and introduction, and answered some of the most common questions I get from folks. In this post I’m going to dive deeper into how you can prepare to go independent, how to find clients, and other tips.
\n\n\n\n\n\n\nDisclaimer: As I mentioned before, certain aspects of this series will be US-centric and I acknowledge that folks from underrepresented identities in the tech industry may not be able to replicate my trajectory or have the same experience.
\n
When I look around the indie dev community within the broader Apple developer community, there is one characteristic that most indie devs share — they do more than just write code. There are too many indie devs that I admire to attempt to list them all here, but they are all involved in more than only writing apps. They write blogs, they speak at conferences, they produce podcasts, they are involved in open source, they publish newsletters.
\n\nI recognize that not everyone is able to do open source, or blog, or speak at conferences for various reasons — time constraints, childcare, etc. While these extra activities are certainly not required to do freelance work or make your own apps, they will help tremendously.
\n\nThere is no single thing you can do to prepare for going indie, and it is definitely a long-term plan you should make. My career trajectory and experience set me up very well to eventually transition to being independent, but I was not necessarily conscious of this in the moment. Much of what I have been able to do originates from a place of privilege. I have had a lot of spare time to dedicate to “extracurricular activities” (open source, blogging, etc.) outside normal working hours when I was working at my full-time jobs in the past.
\n\nI got involved in the open source iOS community very early in my career with a number of projects, but JSQMessagesViewController (RIP) in particular was a huge success story. I also started this blog almost a decade ago (!!) and got unexpectedly Fireballed very early — that was pure luck, and it kickstarted my online following. My open source work and blogging helped me get recognized in the industry and build a following on Twitter (also RIP), which eventually lead to me speaking at local meetups, and then at indie conferences.
\n\nIn many ways, I really lucked out on the timing of my involvement in iOS development — iOS was still somewhat nascent (I started around iOS 5) and there were more opportunities back then for open source to fill-in gaps in the SDKs and improve the APIs. (We did not even have UICollectionView
back then!) There was also a strong iOS meetup scene in San Francisco, with multiple groups meeting regularly, like SLUG (RIP). Many factors contributed to the decline of the iOS meetup scene in SF, but the pandemic expedited that process and left little room for others to try to revive it. After a few years of blogging, open source, meetups, and conferences, I eventually started the Swift Weekly Brief newsletter (RIP) after Swift was announced. The newsletter project led to starting the Swift Unwrapped podcast (RIP) with my friend JP Simard — who I first met through SLUG! And then eventually, somehow, I ended up hosting a panel with Chris Lattner at WWDC 2017. And the rest, as they say, is history.
In addition to everything above, I worked full-time jobs at various companies in the industry for about 7 years. That industry experience was important. One aspect of full-time work that I did not intentionally plan, but that ended up being incredibly valuable was working for a diverse set of companies — they were all different sizes, they were at different stages, they made very different products, and they had very different business models. That breadth of exposure and experience was extremely beneficial when I started working with clients — which were, of course, all very different.
\n\nWhen you put all of these things together, you end up with multiple positive feedback loops. Open source gives you valuable experience in programming and project management, it gives you topics to blog about, and it helps build your portfolio. Those experiences and portfolio pieces help you land competitive jobs. Blogging gives you exposure and recognition, which can help you speak at conferences. Notably, I link to my “Hire Me” page at the bottom of every blog post in case a potential client is reading (you never know!). Speaking at conferences helps promote your open source work, blog, or podcast. Each of these contribute to building your résumé, leading to even better job prospects. Everything provides more experience to learn from and write about on your blog or present at a conference. And, so on. Each of these things has the potential to lead you to a new client, or better yet, have a client discover you. Everything feeds into everything else creating multiple intertwined positive feedback loops, culminating in a body of work and evidence that you can show potential clients to get contracts and freelance gigs. Then, add your indie apps on top of all of this. Your experience helps you write better apps, your apps build your portfolio and provide more content for your blog or conference talks, and so on.
\n\nWhile I do not think everyone needs to follow my exact footsteps in order to successfully go independent or start freelancing, I attribute my success so far to the accumulation of all of these experiences. They all had an incredibly important impact on me, but the most important aspect was all the compounding effects and positive feedback loops. Also recognize that you do not have to do all of these things, especially not all at once. Pick one, maybe two, that interest you the most. Maybe you start a blog first and after you feel established with that, try to speak at a conference.
\n\nThe last point I want to emphasize is that none of this happened instantly. None of the indie devs you look up to launched a blog with 100 popular posts overnight, nor published a podcast with 100 episodes in a single day. All projects are a gradual process. I did not actively work on all the projects I mentioned above at the same time. Little by little, I slowly built up my portfolio. I spent 7 years doing a mix of working full-time, contributing to open source, blogging, speaking, and podcasting — and notably, not all at once, nor consistently. I had plenty of low points where I did very little outside of my full-time job. I also had periods of time where I focused on a single project outside of work, like the newsletter or the podcast.
\n\nEach of these projects is a long-term investment and overtime your body of work grows. Not to mention, your past work often continues to pay dividends in the future. For example, some of my current most-visited blog posts were written years ago! Just like building an app, you have to start somewhere. Furthermore, the majority of the popular projects mentioned above, the ones I am most well-known for, are all now defunct! Not everything needs to last forever, nor should it.
\n\nThe stronger the foundation you build, the easier it will be to find clients. An important aspect of everything I discussed above is that each of these things is building your network. You meet a lot of people in the industry through conferences, full-time jobs, open source projects, etc. I have met so many people over the years and made a lot of friends. Again, doing all of these extra things outside of a full-time job is not a prerequisite to going indie and contracting, but it gives you a much better place to start — not only because of all of the positive feedback loops, but also because of how it expands your network. And it is important to note that working at multiple different companies when you are full-time expands your network, too. Each time you change jobs to work with new people, your network grows. Furthermore, when your coworkers leave for a new job your secondary network grows.
\n\nSo far, for the past 3 years, all of my clients have come to me through friends and acquaintances — former coworkers, fellow conference speakers, folks in open source, and other people that I have met during my time in the tech industry. I think this is the best way to find clients rather than reaching out to complete strangers because you begin the conversation with some baseline rapport through your mutual connections. If you do not have the experience I have had with all the “extracurricular activities”, or if you are unable to do so, do not worry. A great first step to finding clients is to reach out to your former coworkers and managers. They may be at a different company now that is looking for contractors, or they might have another connection that is looking for freelancers. However, you would also be surprised how often folks quit a full-time job and return to the same company as contractor, so do not be shy to ask your previous employer about contracting opportunities.
\n\nThis is probably obvious, but the best way to keep clients is to impress them with your skills. Do the best work you can. Build rapport. Work on having clear, pro-active communication with them. Often, one client can lead to another. If a client has a great experience working with you, they are more likely to recommend you to someone else in their network. As you can see, there is a recurring theme of building positive feedback loops.
\n\nAnother critical task is to ask for feedback. You should do this regularly, during your contracts with your clients. This is for your benefit, to identify your strengths and your weaknesses. Otherwise, you will never know how you can improve. Finally, when a contract ends, ask your client for a testimonial. They may decline, and that’s ok, but it never hurts to ask. Be sure to ask permission to publish the testimonial. Testimonials are so valuable to your continued success. They provide a source of legitimacy for new clients and can help address hesitations they may have when deciding whether or not to work with you. You can find the testimonials I have collected here. Also note that some of my testimonials are from when I worked full-time jobs. If you need to jump-start your collection, ask one of your former managers for a testimonial.
\n\nThe final piece of advice I have regarding clients is to avoid the predatory freelance platforms that are attempting to gig-ify contract and freelance work. These are platforms like Upwork, Freelancer.com, Fiverr, etc. They all facilitate and foster a race-to-the-bottom for hourly rates, impose excessive fees (up to 20% of your hourly rate!), and are generally nothing more than extractive middlemen. Think Uber, but for freelancers. You are their product. These platforms exist solely to further commodify your labor (beyond capitalism’s existing commodification of labor), de-value your highly-trained skills, and disguise wage theft through the collection of predatory fees. If your only option is using one of these platforms, you are better off working a full-time job.
\n\nNegotiating hourly rates or project rates is difficult to discuss generally. There are so many variables to consider with any given client and project, as well as your skill level. I cannot comment on other areas of expertise in the tech industry, but based on my experience with freelance iOS development, hourly rates range from $100/hour to $300/hour. Most commonly, what I see is $150-200/hour. If you are just starting out, it may be worth your time and energy to take some lower paying contracts to build on your experience and collect testimonials. But our goal, obviously, is to get the highest hourly rate possible.
\n\nYou should determine your limits and requirements before negotiating with clients. You must ask yourself, “am I willing to lose this potential client over hourly rates?” If you do not want to work for less than $150/hour and a potential client is unwilling (or unable) to meet this requirement, then you need to be comfortable and confident enough to not only walk away, but also not regret passing on that project. Will you later think to yourself, “damn, I really should have taken that project for $125/hour”? If so, then you did not accurately determine your limit. It does not matter what your limit is, and everyone’s will be different. What matters is knowing your limit and sticking to it, otherwise you will be miserable working for too little, or regretting a missed opportunity. It is also worth considering what types of projects you will not work on. For example, I do not fuck with cryptocurrency and NFTs. If a potential client project deals with those things, I am not interested.
\n\nOne important thing to remember is that all parts of a contract can be negotiable. For example, suppose a client offers $125/hour at 30 hours per week, but you have recently been working contracts for $175/hour. You could instead offer $175/hour at only 20 hours per week, which is still within their weekly budget. And you can propose that you try this for one month and then re-evaluate. I have been in a similar situation that ended up working out very well — the client was pleasantly surprised and more than pleased with what I could accomplish with only 20 hours each week. Many clients think they need someone full-time, but they rarely do. As a contractor, you are removed from much of the company bureaucracy, which gives you more time to do actual work.
\n\nAnother strategy is to negotiate working at a lower rate for a specific duration, at which point you and the client decide if you both want to continue. If so, you both agree to increase your rate. For example, you can propose working for one month at $125/hour. At the end of that month, you and the client re-evaluate — are you both happy with the progress so far and do you want to continue working together? If yes, then your rate goes up to $175/hour. You could also take this approach without adjusting your rates, and work for the first month at your full rate. This strategy gives both you and the client an opportunity to evaluate the pros and cons, then commit or walk away. Most often, clients are hesitant to pay higher rates because they do not understand what they are getting. Offering these introductory, “probationary” periods is a low-barrier way to show them what you can do.
\n\nPhilosophically, I have issues with hourly rates as they do not capture the true value of your labor. If you are highly skilled and great at what you do, then you can complete tasks efficiently and quickly. But getting work done quickly at an hourly rate punishes you for being extremely good at what you do. If a task takes you 1 hour at $200/hour, that’s $200. But if it takes you 5 hours, that’s $1,000. The logic does not make sense. If you can complete a project in one month versus someone else that will take 4 months — aren’t you more valuable than the other person? Yes, and so you should be paid more. This should factor into how you determine your hourly rates. The better you are, the more you should charge. This line of reasoning is how you convince your client to pay more — clients do not place more value on a project taking longer.
\n\nAnyway, I could write an entirely separate post on hourly rates and the value of labor. Unfortunately, hourly rates are the norm and project-based rates are not very common. This is what we’ve got to work with for now, which is why your rates should be higher rather than lower. I have never worked a contract for a project-based rate (yet). In the meantime, if this topic is interesting to you, you should watch this brief video from Chris Do that clearly articulates this conundrum. His other videos are great, too!
\n\nToday we covered how to prepare yourself for going independent, creating positive feedback loops, the importance of networking, finding clients, and negotiating contracts. I hope this post was helpful if you are considering going independent. There is still more to come in this series. In future posts I will discuss bookkeeping, taxes, saving for retirement, and more! Stay tuned.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
I recently asked on Mastodon for tips on estimating your total number of RSS subscribers. It turns out it is rather easy to do. While I do have (privacy-aware) analytics for my site, this only tracks page views.
\n\n", "tags": [ "rss","json","nfsn","nearlyfreespeech","web", "software-dev" ], "content_html": "I recently asked on Mastodon for tips on estimating your total number of RSS subscribers. It turns out it is rather easy to do. While I do have (privacy-aware) analytics for my site, this only tracks page views.
\n\n\n\nSome friends pointed me to this blog post which describes how to do this by simply searching through your web server logs. If you use NearlyFreeSpeech.net for hosting, like me, there are some additional steps.
\n\nFirst, logs are not enabled by default on NFSN. You need to navigate to your site settings to turn on logging. Logs are then stored in /home/logs
. You can find all the information you need about log files on NFSN in these FAQs.
You should wait at least a few days or a week after enabling logs before accessing them so that you can collect some data. Then you can grep
the logs to see the requests for your feeds. I publish an RSS feed as well as a JSON feed.
cat access_log | grep feed.xml\ncat access_log | grep feed.json\n
Unsurprisingly, the number of subscribers to the RSS feed is significantly higher than the JSON feed.
\n\nSome centralized services put the number of subscribers in the “user agent” string of their HTTP requests. You’ll see log entries like this:
\n\n\"GET /feed.xml HTTP/1.1\" 304 - \"-\" \"Feedbin feed-id:1344882 - 122 subscribers\"\n\"GET /feed.xml HTTP/1.1\" 304 - \"-\" \"Feedly/1.0 (+http://www.feedly.com/fetcher.html; 265 subscribers; like FeedFetcher-Google)\"\n
I anticipated having at least a few thousand subscribers based on my site analytics, but it appears to be more on the order of a few hundred. It’s a shame that RSS readers are not more popular.
\n\nObviously, decentralized RSS reader apps cannot provide subscriber data. But it was still neat to see what apps people are using. For example, it was nice to see NetNewsWire in the logs!
\n\n\"GET /feed.json HTTP/1.1\" 304 - \"-\" \"NetNewsWire (RSS Reader; https://netnewswire.com/)\"\n
If you are interested in seeing the total number of requests for your feed, you can pipe the output to wc
.
cat access_log | grep feed.* | wc -l\n
It is important to note that this number does not represent unique users, but it was still interesting for me to see. In one week, my feeds were requested over 78,000 times. That was much higher than I expected.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
When you display text in your app, you might come across situations where the text layout produces undesirable results under certain layout constraints. The text could wrap on smaller devices or be truncated in certain localizations. At this point, we are well-equipped with adaptive APIs to make our layouts work on all screen sizes — for example, we have Dynamic Type, Auto Layout, and UITraitCollection
.
When you display text in your app, you might come across situations where the text layout produces undesirable results under certain layout constraints. The text could wrap on smaller devices or be truncated in certain localizations. At this point, we are well-equipped with adaptive APIs to make our layouts work on all screen sizes — for example, we have Dynamic Type, Auto Layout, and UITraitCollection
.
However, some situations require fine tuning the formatting and layout of text using TextKit. Text layout APIs on Apple Platforms are provided by TextKit, which is available as part of UIKit and AppKit. TextKit is what powers UITextView
and NSTextView
.
One scenario where adaptivity APIs will not help is when you layout a body of text that results in an orphan word on the final line. Often, this simply looks bad. The text does not look balanced. Orphaned words are not aesthetically pleasing. This is especially true if you have only two lines of text, where the second line contains a single word. It is even worse if the text is center-aligned. How can we solve this issue?
\n\nImage our text view is laying out text like this:
\n\nTextKit manages text storage and performs layout of text-based content on iOS and\nmacOS.\n
The TextKit APIs we need to use are part of NSParagraphStyle
. The first API you might try to use is NSLineBreakMode
which has been available since iOS 6 and macOS 10. The .lineBreakMode
of an NSParagraphStyle
specifies what happens when a line is too long for a container. Your options are .byWordWrapping
, .byCharWrapping
, .byClipping
, and more. However, none of these will address the issue of orphan words.
let textView = UITextView()\nvar text = AttributedString(\"TextKit manages text storage and performs layout of text-based content on iOS and macOS.\")\n\n// Does Not Work!\nvar paragraphStyle = NSMutableParagraphStyle()\nparagraphStyle.lineBreakMode = .byWordWrapping\n\ntext.paragraphStyle = paragraphStyle\ntextView.attributedText = NSAttributedString(text)\n
The NSParagraphStyle
API we need to use is the similarly named, but much newer LineBreakStrategy
, which was added in iOS 14 and macOS 11. This property specifies how the text system breaks lines while laying out paragraphs. The option we want is .pushOut
, which “pushes out individual lines to avoid an orphan word on the last line of the paragraph.”
let textView = UITextView()\nvar text = AttributedString(\"TextKit manages text storage and performs layout of text-based content on iOS and macOS.\")\n\n// Fixed!\nvar paragraphStyle = NSMutableParagraphStyle()\nparagraphStyle.lineBreakStrategy = .pushOut\n\ntext.paragraphStyle = paragraphStyle\ntextView.attributedText = NSAttributedString(text)\n
This will adjust the layout so that there are at least two words on the second line:
\n\nTextKit manages text storage and performs layout of text-based content on iOS\nand macOS.\n
\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
I’m excited to share that I recently released a new app, a tax calculator for freelancers called Taxatio. It is specifically for self-employed sole proprietors based in the United States — freelancers, consultants, independent contractors, and indie developers (like me!). One of the more confusing and difficult aspects of going independent is taxes. And that’s why I made this. It is a multiplatform SwiftUI app for iOS and macOS available as a universal purchase on the App Store.
\n\n", "tags": [ "ios","macos","swiftui","apps","mac-app-store","indie-dev","contracting","freelance","consulting","taxatio-journal", "software-dev" ], "content_html": "I’m excited to share that I recently released a new app, a tax calculator for freelancers called Taxatio. It is specifically for self-employed sole proprietors based in the United States — freelancers, consultants, independent contractors, and indie developers (like me!). One of the more confusing and difficult aspects of going independent is taxes. And that’s why I made this. It is a multiplatform SwiftUI app for iOS and macOS available as a universal purchase on the App Store.
\n\n\n\nI did a soft launch back in December and released the first update last month. It has been a long process and a fun project, and I’m glad I can finally share it. Ultimately, I made this app for myself. As an indie dev and contractor, taxes are confusing and hard — at least at first. There is much more to consider as opposed to being a full-time W-2 employee. Notably, you have to pay quarterly estimated taxes since deductions are not automatically taken from your payments from clients as a 1099 contractor.
\n\nThis app started out as a note in the Notes app with my rough calculations and estimations. When that got too ridiculous, I made a Swift Playground. When that grew unwieldy, I moved all the calculation code into a proper Swift Package with unit tests. After that, I realized that all I needed to do was build a UI and then anyone could use this tool and benefit from it. Now here I am, with a fully-fledged app for iOS and macOS, built with SwiftUI.
\n\nThe word taxatio is a Latin noun meaning “valuing” or “estimation” and it is the root of the English word taxation. Note that it is not pronounced “Tax a tio” (tax a TEE-OH) like one of my good friends guessed, which I think is how you say “tax an uncle” in Spanish. You can listen to the correct pronunciation here. So, that’s the name!
\n\nI am not a designer, but I attempted to make an icon anyway. It is a stylized dollar sign with abstract flames in the background. The intent is to subtly communicate that paying taxes is similar to lighting your money on fire (at least in the US where tax money is primarily used to subsidize the lives of the rich). The more I learn about taxes in general, the more I realize that income tax is specifically designed to punish poor people, whose only means to earn income is through wage labor. Meanwhile, other forms of “income” that ultra wealthy people have — like simply owning property, inheritance, and stonks — are taxed at significantly lower rates. But, I digress.
\n\nI want to emphasize that this app is nothing like TurboTax, QuickBooks, or similar apps. It is a simple estimator. If you have an independent business, you probably use QuickBooks for bookkeeping. I do too. However, you might notice that the quarterly estimated taxes that QuickBooks attempts to estimate are always wrong. This was a big motivation for me to do these calculations myself. QuickBooks simply tries to be too smart based on historical data (which you do not have when you first start out) and projections based on current data (which may not be accurate depending on your upcoming contracts). Additionally, QuickBooks does not know anything about retirement contributions which may also affect your taxes.
\n\nThis app is obviously very niche and is mostly for myself, but it serves a few purposes. I wanted to learn SwiftUI and specifically experiment with making a multiplatform app for iOS (iPhone and iPad) and macOS (not using Catalyst). It is a tool I wanted to have and use to get a better understanding of my taxes and to do financial planning during the year. And finally, it is a good project to add to my portfolio. This was my first time ever using SwiftUI.
\n\nThe icing on the cake is that I could make this a real app and sell it on the App Store — and hopefully help out other contractors and freelancers like myself, which is my other primary motivation. You could type “tax calculator” into your favorite search engine and you will find a bunch of poorly-written, sleazy-looking websites to estimate your taxes — they all have a rather terrible experience and limited functionality. I knew I could do much better in general, but more specifically, I wanted Taxatio to also be a tool for folks to learn and understand what is going on with their taxes. Taxatio has clear breakdowns to show you how things are computed, and each part of the app has helpful tooltips that explain the calculations and tax rules.
\n\nI had a few constraints for building it. I wanted to experiment with out-of-the-box, vanilla iOS and macOS development. That meant no third-party libraries or assets, only use SwiftUI (when possible), only use SFSymbols, customize behavior as little as possible, etc. In other words, what can you do with just Xcode and everything else that Apple provides with minimal customization? This was important, because this is not an app that I want to spend that much time on — it is a small, fun project with a limited scope. There were only a few occasions where I needed to use AppKit and UIKit. Much to my surprise and delight, you can get quite far with only the SDK and no third-party help nowadays!
\n\nAs I mentioned, Taxatio has an intentionally limited scope — there is no way for me to reasonably account for all tax scenarios unless I want to spend my entire life writing tax software. The biggest constraints are that the app assumes you are a single-filer and have no dependents. This may change in the future, but those are not scenarios I need to accommodate for myself. Presently, deductions are limited to a single field but I plan to expand on this in future updates. I might also expand on different types of retirement accounts if there are enough requests. Even with these constraints, I suspect the app will be useful to quite a few people.
\n\nAfter you enter your income, expenses, deductions, etc. you are presented with a detailed breakdown of your adjusted gross income, federal income tax, state income tax, and self-employment tax. Each field also has tooltips that explain how the value was computed. You can also see your tax brackets, along with a breakdown of how much you pay in each bracket. You can select the specific tax year to do historical comparisons, too. It begins with 2020, which is the year I went independent and started doing my own calculations in the Notes app, so this is sort of a small “easter egg”.
\n\nThis project reminded me how damn hard it is to actually release software. The core functionality only took around a month or two to write, building the initial UI (for iOS and macOS) took about another month or two, and then I spent maybe another month on polish, bug fixes, and refinements. Of course, these were not 4-5 consecutive months, but days and weeks spread out over the course of last year. I had a finished app for months before actually submitting to the App Store. I really dragged my feet on getting all of the metadata together (screenshots, description, etc.) as well as building a product page. For me, all of this stuff at the end is the hardest part — it’s not as fun as programming.
\n\nThere is a blog post I read long ago about shipping software and deciding on what will suffice for a minimum viable product (MVP). The author wrote something like, “You should never be embarrassed by what you ship as an MVP, but you should be embarrassed by what you do not ship.” The idea was that MVPs are, by definition, not what you want to ship but what you have to ship. You have to stop somewhere for 1.0, otherwise you will be developing forever and never release anything. I cannot remember who wrote that post. If you know, please tell me and I will link to it!
\n\nAnyway, this is so true. If you are not embarrassed by all the features your app is missing then you waited too long. (Again, you should be proud of what you do ship.) I shipped 1.0 without state tax calculations. It only computed federal income tax and self-employment tax. Was that embarrassing? Yes, but I’m glad I shipped sooner rather than later and I’m proud of all the features that 1.0 did have — including a bunch of easter eggs. Did I prioritize easter eggs over actual features? Yes, absolutely. (I’ll write more on this another time.)
\n\nThe main reason I waited to write this post was so I could ship an update that included state taxes and be less embarrassed about this announcement. Implementing state tax calculations was difficult and took soooo long because all the states are different. Not to mention, I only ever needed to compute California income tax for myself, so this was the first big feature that provided functionality beyond my own needs.
\n\nLike I mentioned above, the total time it took to make this app was only a few months, but it was spread out over many more. The most difficult part was understanding how to do all the various calculations correctly and finding all of the information about tax brackets, etc. which change every year. There is no centralized database for all of this information. I was learning SwiftUI as I went. I was developing for three devices — iPhone, iPad, and Mac — and two platforms. Given all of that, I feel like the timeline was reasonable.
\n\nOverall, this was a great experience and when SwiftUI just works it feels magical. However, when SwiftUI does not work as advertised, it is significantly more confusing than UIKit or AppKit. SwiftUI often has surprising, unexpected behavior. It is very limited compared to UIKit and AppKit, yet what it does provide is typically extremely good. One thing I know for certain is that I would not have been able to build for both iOS and macOS this quickly without SwiftUI.
\n\nI think SwiftUI was very well suited for this tiny, simple app. However, I will not always reach for SwiftUI in the future. For more complex apps, I still prefer using UIKit/AppKit — or, at least I’ll start with a UIKit/AppKit shell. I have a few more app ideas I want to pursue and only some of them would be good candidates for being entirely SwiftUI.
\n\nI mentioned above that an early phase of this app was just a Swift Package and a Playground. That package is called TaxCalc
and I plan to make it open source eventually — hopefully by the end of this year. This package encapsulates all the core functionality in the app. I do not plan to open source the entire app. But in theory, you take the package and build your own UI on top of it.
Finally, I plan to write a short series of posts about how and why I implemented various parts of the app. I learned a lot while making it. I hope you enjoyed this introductory post. Stay tuned for more! And if you appreciate my blog, consider going to the App Store and giving me some money!
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
A number of folks have reached out to me recently (and over the years) asking me about my experience going indie. I originally wanted to write this reflection after my first year, but I could not find the time nor motivation. The early days of the pandemic really drained me. However, I hope these past three years of experience will only make this post more valuable to those seeking to do the same thing I did. I am writing this for anyone who is interested in trying to go independent — either with your own app development business, solo contracting and freelancing, or both.
\n\n", "tags": [ "series-going-indie","indie-dev","contracting","freelance","consulting", "essays" ], "content_html": "A number of folks have reached out to me recently (and over the years) asking me about my experience going indie. I originally wanted to write this reflection after my first year, but I could not find the time nor motivation. The early days of the pandemic really drained me. However, I hope these past three years of experience will only make this post more valuable to those seeking to do the same thing I did. I am writing this for anyone who is interested in trying to go independent — either with your own app development business, solo contracting and freelancing, or both.
\n\n\n\n\n\n\nThis post aims to be a sort of introduction and reflection. I plan to publish a series of posts about going independent and doing contract work that will elaborate more in-depth on various topics — how to prepare, finding clients, bookkeeping, taxes, and more. So stay tuned!
\n\nAlso, this post assumes that for contracting work, you will be based in the United States and classified as a sole proprietor for tax purposes. Unfortunately, I cannot comment on what it is like to go independent in other countries when it comes to taxes and business structure. However, other aspects of this post should still be helpful for folks living outside the US.
\n
I quit my full-time job in the summer of 2019 with the goal of pursuing indie iOS and macOS development and do freelance/contract work. I planned to take time off for the last half of 2019, and then get started at the beginning of the new year in 2020. I am now in my fourth year, which feels surreal. I cannot believe it has been so long and that I have made it this far. Honestly, I thought after one or two years, I would be back at a company in a full-time role. However, after three full years things are still going great — despite the pandemic and recent layoffs throughout the tech industry.
\n\nMy current arrangement is that I spend the majority of my time on client work — contracting/freelancing. This is what pays the bills and allows me to work on my indie apps. The split is probably about 70-30 or 80-20, depending on the contracting workload I have each month. Slowly but surely, I plan to rebalance my time to reach a 50-50 split between client work and indie work. After that, I want to progress toward 80 percent indie work and 20 percent client work. My goal is to eventually drop client work altogether and focus all of my time on my indie apps — but being an indie dev is incredibly hard and I am not sure if I will ever get to this point. But I will try!
\n\nOverall, this has been an incredibly valuable and rewarding experience for me. Despite periods of stress and a few mistakes along the way, I love being independent. I have learned so much doing this. Unless something significant in my life changes (which is possible!), I do not see myself returning to full-time work at a company in the near future, or possibly ever.
\n\nBefore continuing, I want to acknowledge that not everyone will be able to replicate my trajectory and experience going independent — especially folks from underrepresented identities in our industry. Not everyone will be able to do what I have done in exactly the same way. Some folks’ identities will be a barrier, some folks may have important obligations like childcare, and others may face financial constraints.
\n\nAs a cis white guy, I have a lot of privilege. Clients typically approach me with a baseline of respect and trust that may not be afforded to people of other skin tones, genders, or orientations. I still have to negotiate rates with clients, but my skills have never been seriously questioned. I rarely have to “prove myself” and I am usually taken at my word when it comes to my abilities and expertise. To anyone from an underrepresented identity reading this, if you are interested in going indie and freelancing, I am more than happy to help however I can — answering questions, giving advice, etc. Please get in touch.
\n\nIf you want to go independent, you really need to want it. You cannot half-ass this work and lifestyle. It takes a ton of work and effort — but the benefits and rewards are so worth it. You must be detail-oriented and be able to do self-directed work. There is no structure to your days like working at a company. Outside of working on client projects and your own apps, you have to be willing to do all the administrative work, too — bookkeeping, invoicing, taxes, finding new clients, etc. But do not allow these things to scare you away! (I will cover these topics in future posts in this series.)
\n\nAfter your first year or two, you will have learned mostly everything you need to know. For me, the first year was full of learning — how to keep my books, how to deal with taxes, how to continue saving for retirement, how to structure my days, how to manage my time, how to get shit done, how to take time off, and the list goes on. Be prepared for this in your first year and do not give up. My second year was all about making refinements and optimizations to all the things I learned in year one. Finally, in my third year I started to feel like I had everything figured out — I was on autopilot and coasting through all those tasks that were previously bumps in the road. Currently, administrative tasks are a breeze, I have consistent work with long-term clients, and I am able to make time to work on my indie apps. I know how to structure my days to get work done, and I know when I need to take time off.
\n\nPhilosophically, I have always wanted to be independent. I have never been a “school spirit” or “company pride” kind of guy. Many workers in the tech industry love all the free company swag. I will accept a free company-branded coffee mug, but I will never wear a free t-shirt with the company logo on it. I simply have never identified with a corporation. I never “drank the koolaid” like some folks. I dislike feeling as if a company “owns me” (most employment contracts claim ownership of all intellectual property you produce while employed, even if it occurs outside of working hours). I also dislike how management and executives at big tech companies often seem to feel so entitled to workers’ time. However, working full-time in the industry did give me valuable experience.
\n\nBeing an indie dev is something I have always wanted to do, or at least attempt. I want to write and release my own apps — without the explicit goal of starting a company™. That is, I have no interest in creating a startup, finding investors, and raising money. My goal is to be independent, rather than be beholden to a group of capitalist investors seeking to extract as much value out of me as they can (and ruining whatever product I create in the process). Investors are just another type of boss — like a CEO, but worse. Investors want increasing financial returns, exponential growth at all costs, and ultimately, control. Like Moxie always says, bad business models produce bad technology — and I am interested in neither. If one of my app ideas takes off and grows enough organically to become a company (without external investment), that would be great, but my only goal is to make a living for myself and make useful software that does not suck.
\n\nI will elaborate on preparation in a future post in this series. For this post, I only want to emphasize how valuable it was for me work at a diverse set of companies when I was working as a full-time worker. These experiences were crucial for me to be successful as a contractor. There is no single thing you can do to prepare for going indie. It is a long-term game you have to play. But know that your experience working in the industry is helping you build a solid foundation for working with clients, as well as building your own apps. The more depth and breadth you can cover, the better. Every company I worked at was very different — from small startups to large tech corporations, all with very different products and business models.
\n\nAlso, I want to briefly mention other opportunities for growth, development, and building a foundation: blogging, speaking at conferences, doing open source, and making side projects. Of course, none of these things are required, especially not all of them. And, many folks do not have the privilege to participate in these “extracurricular activities”. If you are able to choose one or two, it will help.
\n\nFirst, it is almost never going to feel like a good time to quit your job. Tech companies often move at a relentless pace. There will always be a new big project coming up. There will always be another vesting date. You have to find the time that is right for you to quit, because there will never be a time that is right for the company.
\n\nOnce I felt established in the industry and felt like I had enough experience to go solo, I started planning how to make it happen. When I quit my full-time job in the summer of 2019 I had around 8-12 months of living expenses saved — depending on how frugal I wanted to be. I did not intend to be out of work for an entire year, but I wanted some extra padding in case I had trouble finding client work, or if something went wrong.
\n\nOnce you have the savings, you just have to jump. I sat on my savings for longer than I needed, waiting for the right time to leave my job. It is difficult to abandon the comfort and stability of a full-time position. When I finally quit during the summer of 2019, I felt a mixture of relief, anxiety, excitement, and stress. After a few weeks, most of that stress was gone and I knew I made the right decision. I took some time off and traveled. I picked up a brief one-month contract in October 2019 through a friend, then I traveled a bit more, and did not work for rest of the year. At the start of 2020, I picked up my first long-term clients and I have had consistent work since. I will write more on finding clients later in this series of posts.
\n\nI recommend quitting full-time work sometime in the second half of the year — not the first half. If you are struggling to “find the right time”, often the end of the year is best. Most companies slow down during the holidays in November and December. Those months are a great time to reset and not work! It is also convenient to begin a new calendar year with a clean slate as a contractor (or unemployed) because it will simplify your tax situation. Personally, I loved having most of the summer to be free. I am so glad I had that time for myself, free from the obligations and toil of labor. Having that break was incredible. Eventually, I want to take off for a long period of time like that again.
\n\nWhen you start out — unless you have a ton of money saved — contracting is how you will finance your indie development, like I mentioned above. Unfortunately, splitting time between client work and indie projects is much easier said than done. You must prioritize what actually earns you money, which is contracting/freelancing. It is very difficult to balance both types of work when you are first getting started. Similar to having a full-time job, after a long day of client work, I often do not want to work even more on my indie apps. (If you are only interested in contracting, then this will not be a problem for you!)
\n\nWhat I have found is that it is best to allocate full days to one or the other. Each week I try to do only client work Monday through Thursday, and do indie work on Friday. Sometimes, I will take Thursday for indie work as well, depending on my schedule. Sometimes I work on indie projects in the evening, if I have the energy and nothing better to do. I have also experimented with half days where I spend the morning on indie work and the afternoon on client work, or vice versa. Depending on the clients and projects, that can work well too.
\n\nI have not made as much progress as I would like on my indie projects at this point. However, my first year was spent essentially 100% on client work. I was trying to get established and organized and was learning a lot about this new way of working. My second year was similar, with mostly client work. I was still struggling to find the right balance, and I was working with multiple clients at once — which was exciting and financially lucrative, but it left little time for my indie projects. Also, there was a fucking pandemic, so I try not to be too hard on myself. Toward the end of year two and during year three, I feel like I finally found a better balance and workflow.
\n\nAs always with software development, getting 1.0 released is the hardest goddamn part. Once you finally ship an indie project, subsequent updates are significantly easier to manage. You can make point-releases as large or as small as you want. That incremental progress is very satisfying once you get over the hump of the initial release. Releasing updates for existing apps is also substantially easier to balance with client work.
\n\nAside from the obligations of client work, the other main contributor to my lack of progress on indie apps is… life. Life does not stop. Just like with a full-time job, fucked up things happen (because capitalism), or there is a global pandemic, or you get sick, or someone in your family dies, or you go through a breakup, or you get depressed (because capitalism) — and yet, you are still expected to work every day (because capitalism). Being independent does not solve any of these issues, and it arguably makes dealing with them worse. As a contractor, you only get paid for the hours you work. There is no paid time off. So, when worst comes to worst, it is always the indie projects that must take a back seat to everything else.
\n\nIf you are not careful financially, you could end up in a tough spot. The main way to mitigate this is to account for unexpected life events in your hourly rates and maintain an emergency “nest egg” in your savings. Higher hourly rates and a solid amount of savings means you can afford to take a week off because you get sick, or because you are depressed as fuck and tired of staring at code all day. A comfortable amount of savings means you will not feel stressed and anxious when you need to take a mental health break, or a vacation. Remember to be good to yourself! Your self-worth is not defined by your capacity for production under capitalism. I will discuss finances in more detail later in this series.
\n\nThe rest of this post is a collection of thoughts that do not fit in the sections above and answers to questions that folks often ask me. I will elaborate on many of these topics in more detail later in this series. It is extremely difficult to quantify the pros and cons between indie dev and contracting work versus full-time employment. They are so different in so many ways. It really depends on what you value. Let’s begin with the big one that everyone cares about the most.
\n\nDoes freelancing/contracting make more money than full-time employment? Well, the answer is not straight-forward. In my experience, I have been able to meet or exceed my base salary from when I was working full-time, and according to levels.fyi I am still comfortably within current salary ranges. However, depending on the company, once you add stock options and bonuses, usually a full-time position pays more overall. But — importantly, I am only working 20-30 hours per week on client work and I usually take time off for a total of 1-3 months each year. I am not working full-time, which is why this comparison is difficult. In other words, I am making roughly the same amount of money, but working only like half the amount of time. The question you have to ask yourself is, do you care more about total compensation, or more about your time? Personally, I would rather forgo stock and bonuses in exchange for a 3-day or 4-day workweek. There are precisely two currencies in the predatory global capitalist system we live in — time and money. For me, time will always be more important than money. The other impact on income is taxes. As a sole proprietor, you pay more in taxes because of self-employment tax — however, this can be mitigated with deductions.
\n\nHow do you compensate for the lack of other benefits and perks? Most tech companies offer a range of benefits and perks that you obviously do not receive as a contractor. For health insurance, you can get a plan independently via the Affordable Care Act. For tax purposes, health insurance premiums are a business expense that you can write-off. Alternatively, if you have a spouse that works full-time, you can be added to their plan. You do not get free lunch at the office anymore, but you can expense meals and coffee, for example when working at cafes or meeting with clients. In general, any purchase that helps your business (as a self-employed contractor and indie developer), can be written-off on your taxes. Make sure you also take a home office deduction. All your expenses and deductions help recover “lost” benefits and perks by reducing your tax burden.
\n\nNo sick days and no PTO. Obviously, there are no paid sick days and no paid time off for vacation. However, as I previously mentioned, you account for this in your hourly rates. The higher your hourly rate, the more cushion you have regarding being sick or taking vacation. More importantly, you are not beholden to oppressive, bullshit PTO policies that give you a paltry 15-20 days off each year. You can pretend like you are a European and take an entire two months off in the summer. Like I mentioned above, I am making roughly the same salary while working part-time and every year I have been able to take off for 1-3 months. As a contractor you do not have to ask permission to leave for the day or to take time off. Of course, some negotiation of time-off is necessary — but you are not beholden to a PTO policy. As long as you give enough notice, you can come and go as you please. I have never had a problem telling a client that I will be gone for 1-2 months. I cannot emphasize how valuable it is to be able to have large chunks of time off. Only working part-time helps prevent burnout, too.
\n\nHow do you manage working from home and work-life balance? Due to the pandemic, everyone in a software engineering role has most likely experienced long-term working from home over the past few years. So I will keep this brief. For me, it is important to leave my house at least once a day — leaving for coffee and perhaps working from the cafe, going out for lunch, running errands, going for a run or bike ride, etc. I also try to make regular plans to see friends throughout the week. It is important to break up your days and stay social without an office, and as a contractor you have the flexibility to do that. This gives me a massively better work-life balance. I personally loathe tech offices. If I never have to enter one again, I will be content. You can also reclaim the time you would have spent commuting to and from the office.
\n\nHow do you stay current on what’s happening in the industry? There used to be a vibrant scene of iOS and Swift meetups in the Bay Area, but the pandemic essentially ended all of that. The best options for me now are talking with other devs on public Slacks and on Mastodon (RIP Twitter). I also subscribe to dozens of RSS feeds from various developer blogs. However, working with bigger clients that have bigger teams provides that sort of “industry community” as well.
\n\nTypical 9-5 workdays are sadly still a thing. As a contractor, I anticipated having immense freedom and eschewing the archaic 9-5 workday. This is mostly true, as I described above — I often do errands or go for a run in the middle of the day. However, most clients you work with will probably still be bound to the 9-5 workday. This was something I did not think about initially. This does not mean you have to work 9-5, but you will likely need to maintain roughly those core hours of availability. For me, that sometimes means I am answering Slack messages while standing in line at the grocery store in the middle of the afternoon. This trade-off does not bother me much. Rarely are my days interrupted with true work emergencies.
\n\nEstablishing boundaries. One thing I enjoy about being a contractor is that I can be as involved or as removed from a client’s company culture and politics as I like, within certain contexts. For example, I cannot attend a client’s monthly All Hands meeting — but that is good news for me! I hate meetings! Clients do not want to waste billable hours. They want productivity. This means you can focus most of your time on simply writing code and getting shit done. You are not beholden to mandatory meetings or any of the inner-workings of your client’s company. Because you exist outside of the company hierarchy, all sorts of baggage that exists for permanent workers does not exist for you. Overall, there are simply better boundaries around your time because you are external to the client and working hourly. They do not want to waste your time because they know they are paying for it. Ironically, this is also true when you are a full-time worker, but somehow organizations tend to fill software engineers’ calendars with meetings of dubious value.
\n\nWorking hourly versus salaried. Personally, I enjoy not being salaried. In addition to the clearer boundaries between you and work, hourly wages give you a more tangible view of your labor. Maybe I want to take advantage of a beautiful day and work in the evening. Those hours are still billable. Perhaps I want to work extra hours one day in order to take a half day the next. It does not matter how I allocate my hours as long as I get shit done. There is a clear transactional relationship when doing client work — you do the work, you send an invoice, you get paid. When working full-time at a company, I feel like there is always a lot more “fluff” and blurred boundaries. You might be expected to work late into the evening or on the weekends, but you do not get extra pay for that as a full-time worker. Also, you have to pretend like you believe in a company mission statement, when everyone knows we are all just trying increase profits and shareholder value for the capitalists. Unlike a salaried position, if you don’t work, then you don’t get paid. But salaried workers are often working more than they should have to.
\n\nExposure to many different companies and projects. Finally, the last part of contracting that I truly enjoy is the breadth and diversity of experience you get. Often, in full-time positions at companies, I would get burnt out — not just from work in general, but from working specifically at that company. As a contractor, you are always getting to work with new clients and working on new projects. You get exposed to such a diversity of projects and technologies, not to mention seeing how different companies operate is fascinating. You have to keep learning. Your perspective broadens as you work with different clients, rather than narrows by stagnating within a company. This helps keep me interested and helps prevent me from burning out so quickly.
\n\nIf you made it this far, congrats! I know this post was long and full of new information. If you are considering going independent, I hope this was a good introduction. Stay tuned as I publish more posts in this series in the coming weeks! If there are any topics you want to know more about, please let me know and I will try to include them.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
Whether you are starting a new job or joining a new project, getting oriented in a new iOS codebase can be difficult and overwhelming. It is particularly hard if the codebase is very large, and especially challenging if you are early in your career and do not yet have much experience to draw from.
\n\n", "tags": [ "ios","xcode","debugging", "software-dev" ], "content_html": "Whether you are starting a new job or joining a new project, getting oriented in a new iOS codebase can be difficult and overwhelming. It is particularly hard if the codebase is very large, and especially challenging if you are early in your career and do not yet have much experience to draw from.
\n\n\n\nWhere do you even begin? How do you know where to look? You’re probably familiar enough to know there’s going to be an AppDelegate
somewhere and a bunch of view controllers, but after running the app how do you find out which view controllers correspond to which screens?
You will probably be assigned some starter tasks, small bugs to help you ramp up. Perhaps you have a mentor that can help guide you. While you should definitely ask questions early and often, it is also beneficial to try to discover some things on your own. By trying to figure out where you need to make changes on your own, you’ll learn more, you’ll get oriented in the project more quickly, and you’ll be able to ask better questions to your team.
\n\nA great way to get started in an iOS codebase is by setting a symbolic breakpoint in -[UIViewController viewDidAppear:]
. This is very similar to the breakpoint I previously wrote about. You’ll need to configure the breakpoint with a “Log Message” action with %B
to print the breakpoint name, and a “Debugger Command” action with po $arg1
which will print the instance of the view controller. Finally, tell the debugger to continue after evaluating the actions. I also recommend filtering the debugger out when you build and run.
Enable the breakpoint and run the app. As you navigate to new screens, you’ll see log messages in the console like the following:
\n\n-[UIViewController viewDidAppear:]\n<LaunchScreenViewController: 0x7fc2a8974ba0>\n\n-[UIViewController viewDidAppear:]\n<TimelineViewController: 0x7fc2a884c650>\n\n-[UIViewController viewDidAppear:]\n<ProfileViewController: 0x7fc2a90c5600>\n
Each time a new view appears, you’ll see the name of the corresponding view controller class name! Then you can simply search Xcode for that view controller class and find where you need to make edits. If you are still stuck, don’t worry. You can ask your team for help, but this time you’ll already know roughly where you need to make changes. Also, I recommend that you share this breakpoint to reuse it in other projects.
\n\n\n\n\nNote: unfortunately, I’m not sure what the equivalent strategy would be for this in SwiftUI, if there is one at all. If you have ideas, please let me know and I’ll update this post!
\n
Finally, if you want to explore even further, you should try using FLEX and Chisel.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
I grew up in the 90s and early 2000s. I was a skater kid and when I wasn’t skateboarding I was playing Tony Hawk’s Pro Skater on my Playstation. That scene introduced me to punk music, which eventually led me to hardcore music. By my early teens I was going to shows nearly every week with my friends.
\n\n", "tags": [ "hardcore","music","punk","masculinity", "essays" ], "content_html": "I grew up in the 90s and early 2000s. I was a skater kid and when I wasn’t skateboarding I was playing Tony Hawk’s Pro Skater on my Playstation. That scene introduced me to punk music, which eventually led me to hardcore music. By my early teens I was going to shows nearly every week with my friends.
\n\n\n\nAside from exposing me to passionate music that I still love today, the punk and hardcore scenes introduced me to radical politics, new ways of thinking about the fucked up world we live in, and gave me a warm sense of community and belonging. Hardcore helped me through some very rough years back then. This counterculture is still thriving today, and I go to shows regularly in the Bay Area.
\n\nIf you aren’t familiar with hardcore, you can watch anything on @hate5six’s YouTube channel. But I’d recommend these recent sets by End It, Terror, Zulu, and Trapped Under Ice to get a good taste of this scene and community. (And if you like it, go to a show!) In hardcore, the stage belongs to everyone (if there is a stage at all). So you’ll see kids running across it to stage dive, and jumping for the mic to sing along. And then there’s the mosh pit, with kids two-stepping and slam dancing, throwing elbows and spin kicks. Only at a hardcore show will you see a person in a wheelchair crowd surf and stage dive, with loving assistance from the crowd and the vocalist of the band. Everyone is so alive. The energy, excitement, and enthusiasm for the music is palpable. It is truly a beautiful thing to witness and be a part of.
\n\nDespite the displays of aggression in the pit, an important part of this community’s social contract is that no one gets hurt on purpose. You can dance in the pit as much as you want, as long as you aren’t deliberately trying to hurt people. Whenever people get knocked down or fall, especially in the pit, everyone around picks them up as quickly as possible. If you are standing at the edge of the pit, you might get hit accidentally. If you are up front, you’ll likely have to catch someone jumping off the stage. There’s an implicit consent if you are standing in these “accidents might happen here” zones. If you aren’t up for that, there are safe places you can enjoy the show and opt-out of potential accidents.
\n\nMost of the time, hardcore is a healthy outlet for kids trying to navigate our predatory, oppressive, and excessively violent surveillance state. Hardcore was the best thing that ever happen to me in my youth. Without such a positive influence at a young age, I’m not sure where I’d be now. But unfortunately, like every diverse interest-based subculture, there are bros.
\n\nIn the tech industry, there are lots of rad, unique folks. But then, there are tech bros. I wish it weren’t true, but there are hardcore bros, too. It’s hard to define a “bro”, it’s often a situation where “you know it when you see it”. But generally, I think a good working definition is a dude who is insecure with his own masculinity, who is oblivious to the lived experience of folks with other identities, and who acts with a reckless disregard for the needs and safety of those around him.
\n\nRegrettably, hardcore bros are sometimes too aggressive when dancing and do try to hurt people on purpose. They enter “the accident zone” and then take personal offense to getting nicked by a fist or spin kick from someone minding their own business dancing in the pit. Instead of brushing it off like the benign accident it is (like everyone else), they try to start a fight like a child. Their insecurities prevent them from adhering to the social contract that keeps everyone safe at shows. At a good show, they’ll be confronted and told to stop. Sometimes they’ll be asked to leave or escorted out. At some shows, sadly, the only option is to avoid them. But it’s important to emphasize that these kinds of men — the ones trying to hurt people on purpose — are usually the exception at shows, not the norm. They do not belong here.
\n\nI was at a show a few weeks ago at a small bar/venue in SF and a few bros were acting like assholes. One in particular — literally the biggest guy in the room — was being way too aggressive for how tight the venue space was. He wasn’t just trying to dance, he was trying to hurt people. Unfortunately, his toxic behavior went unchecked. There simply weren’t enough non-toxic large dudes to confront him. Long story short, I left that show with a black eye.
\n\nSure, I was in “the accident zone” at the edge of the pit (where I love to be), but this “accident” was more than the usual small scuff, which I can handle. Most folks, when they are moshing and realize they are approaching the edge of the pit, they back off. But hardcore bros hit harder. They don’t know who they’re hitting and they don’t care, and it all happens so fast. After nearly two decades of going to shows, always close to the pit and sometimes moshing and stage diving, this was the first time I had ever been truly injured. I wanted to confront him after the show, but I knew it wouldn’t lead to anything productive. So I didn’t. But I reminded myself that you get what you put out in this world.
\n\nFlash forward to last night. My black eye is healed. I was at a hardcore show in Oakland. There was great energy in the crowd. Kids were dancing — respectfully — and having fun. The pit was aggressive, but no one was trying to hurt people. They kept it tight and safe for those who wanted to opt-out. There was no toxic bullshit. And then, I saw the guy that gave me a black eye weeks earlier. He, of course, had no idea who I was. I watched him start dancing, again excessively aggressive like before. It was obvious to everyone in the room that this guy was acting like an asshole. He shifted the entire vibe from safety and fun to guarded and concerned.
\n\nAfter a few songs, I suddenly saw this bro leave the pit, urgently walking through the crowd toward the back. His eye was totally busted, blood was pouring down his face. The weakness of his fragile ego manifested in his expression of disbelief. We glanced at each other as he passed me, still having no clue who I was. And I smiled with pure delight as I basked in the sweet taste of retribution. The show continued, and in his absence the positive energy returned like the flick of a light switch. And no one else got hurt, only him.
\n\nKarma is a bitch, isn’t it.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n
For multiplatform projects where I’m using SwiftUI, it certainly makes developing for multiple platforms at once significantly faster. However, each of Apple’s platforms are different enough that eventually your codebase will be littered with #if os()
checks.
For multiplatform projects where I’m using SwiftUI, it certainly makes developing for multiple platforms at once significantly faster. However, each of Apple’s platforms are different enough that eventually your codebase will be littered with #if os()
checks.
I previously wrote about sharing cross-platform code in SwiftUI apps where you need to bridge the differences between UIKit and AppKit for an app that runs on both iOS and macOS. I used the minor differences between UIPasteboard
and NSPasteboard
as an example. But API differences between platforms do not address all scenarios when you might need to make platform-specific changes. For example, UIKit is (partially) available on both watchOS and tvOS. And, SwiftUI is (obviously) available on all platforms.
One of the best examples of platform-specific differences is padding values for UI layout code. David Smith wrote last week about “pixel perfect” design for one of his watch apps. While he was adjusting a UI layout for each watch device size rather than different platforms, the core issues are the same: UI layout code, even when adaptive, cannot always be universally applied.
\n\nWhat you end up with is a lot of code that looks like this:
\n\nvar body: some View {\n MyCustomView()\n #if os(iOS)\n .padding(10)\n #elseif os(watchOS)\n .padding(4)\n #else // macOS\n .padding(24)\n #endif\n}\n
It is difficult to read, increases cognitive load, and Xcode’s default formatting for #if os()
is terribly ugly. We can avoid having to write #if os()
everywhere and make this code much easier to read at the same time with a simple extension.
extension Double {\n init(iOS: Self, watchOS: Self, macOS: Self) {\n #if os(iOS)\n self = iOS\n #elseif os(watchOS)\n self = watchOS\n #else // macOS\n self = macOS\n #endif\n }\n}\n
Then our layout code simplifies to the following:
\n\nvar body: some View {\n MyCustomView()\n .padding(Double(iOS: 10, watchOS: 4, macOS: 24))\n}\n
It immediately becomes clear that we always want some padding on this view and that the values are platform-specific. If you prefer, you could also extend this pattern directly to view modifiers. If we did this for .padding()
, the resulting code would simplify further to:
var body: some View {\n MyCustomView()\n .padding(iOS: 10, watchOS: 4, macOS: 24)\n}\n
We obviously can’t use this pattern in every scenario where there are platform differences, but it does improve many situations. I’ve found this to be a great way to reduce using #if os()
when platforms share the majority of UI code for a particular view. However, you should never hesitate to build entirely unique views for a specific platform when that’s necessary. In those situations, you can extract the #if os()
check and platform differences into a new View
.
struct MyPlatformSpecificView: View {\n\n var body: some View {\n #if os(iOS)\n self.body_iOS\n #else // macOS\n self.body_macOS\n #endif\n }\n\n private var body_iOS: some View {\n // the body for iOS only\n }\n\n private var body_macOS: some View {\n // the body for macOS only\n }\n}\n\nstruct ContentView: View {\n\n var body: some View {\n MyPlatformSpecificView()\n }\n}\n
If you are writing a lot of multiplatform SwiftUI code, I hope these small improvements can make a difference in your codebase too.
\n\n Originally published on jessesquires.com.\n
\n\n\n Hire me for iOS freelance and contracting work.\n
\n\n\n Buy my apps.\n
\n\n\n Sponsor my blog and open source projects.
\n