Make Money Outside the Mac App Store book cover

My book is out: Make Money Outside the Mac App Store.

Own your products and know your customers: sell outside the Mac App Store. In a few hours, you’ll have in-app purchases, a trial mode, and piracy protection all set. The book includes fully functional sample projects and code ready to be copied into your app.

Lifting Into a New Type: My first “Idiomatic” RxSwift Unit Test

I dabble with RxSwift right now. Figuring out how to write tests for pure functions and reactive code, I tried to write an assertion for incoming events:

let expected: [Recorded<Event<URL?>>] = [next(0, nil), next(0, url)]
XCTAssertEqual(observer.events, expected)

Doesn’t compile because equating arrays with Optional<URL> in them won’t. In other words, [Recorded<Event<URL>>] (non-optional) would work.

I tries to figure out how to make this equatable, but then I stopped – why not lift the URL? into its own type? After all, the signal I’m observing is a path settings change. nil is allowed to signify removing the setting, like when you reset to defaults.

Lifting this into its own “either-or” type, also known as encapsulating the information in an enum, I end up with this:

enum URLChange {
    case set(URL)
    case unset
}

Making this equatable is simple. And the tests work, too. For the record, here’s the idiomatic Rx test:

func testArchiveURLChanges_RxVersion() {

    let adapter = SettingsAdapter(defaults: UserDefaults.standard)
    let url = URL(fileURLWithPath: "/a/url/", isDirectory: true)
    
    let disposeBag = DisposeBag()
    let scheduler = TestScheduler(initialClock: 0)
    let observer = scheduler.createObserver(URLChange.self)

    adapter.archiveURLChange()
        .subscribe(observer)
        .addDisposableTo(disposeBag)

    defaults.set(url, forKey: "archiveUrl")

    let expected: [Recorded<Event<URLChange>>] = [
        next(0, .unset),   // Initial value for fresh defaults
        next(0, .set(url))
    ]
    XCTAssertEqual(observer.events, expected)
}

And the traditional, asynchronous version I used before:

func testArchiveURLChanges() {

    let adapter = SettingsAdapter(defaults: UserDefaults.standard)
    let ex = expectation(description: "Reports back result")
    let url = URL(fileURLWithPath: "/a/url/", isDirectory: true)
    let disposeBag = DisposeBag()
    
    adapter.archiveURLChange()
        .subscribe(onNext: { if $0.url == url { ex.fulfill() } })
        .addDisposableTo(disposeBag)

    defaults.set(url, forKey: "archiveUrl")

    waitForExpectations(timeout: 0.2, handler: nil)
}

I’m not too happy with the verbose setup of the Rx test case. But I’m refactoring the setup into – wait for it – the XCTestCase.setUp() method.

For simple assertions, the async variant would work just as well. Recording longer sequences of events with some timing is interesting, though I didn’t need that, yet, to test-drive my operators.

It’s the Worst Time to Go Open Source Because So Many Stupid People Will See It

Junior Bontognali summed up what most people think about most other people – especially after the U.S. presidential election and Dash for iOS going Open Source: people in general are not ready for generous behavior.

Bogdan’s Dash for iOS is on GitHub now. One tweet highlighting an ugly if statement got lots of attention. One twitter user suggested cutting off Bogdan’s fingers. That’s not a real threat, of course. It’s just a stupid joke. But suddenly you’re in 4th grade again and everyone is making stupid jokes because someone pulled down someone else’s pants.

The thing is, Bogdan of Kapeli is too busy to accept pull requests that don’t add features. Maybe because testing these for regressions is pretty time consuming without and end-user benefit. Who knows. So he already turned down a pull request that would fix the aforementioned if-statement – and many others.

Dash probably isn’t ready for Open Source, either. A Swift library as simple as ReSwift took us over a month to port to Swift 3. Maintenance is time-consuming, no matter how big or small the project. Complaining is stupid and childish, but that’s the noise we have to deal with if we don’t want to be stupid and childish ourselves.

I don’t think everyone’s grown-up enough for democracy. I only hear about 3rd wave feminism and presidential elections in the U.S. from YouTube. Junior quoted Evelyn Beatrice Hall, and that applies just nicely to all the hate that’s going ‘round:

I disapprove of what you say, but I will defend to the death your right to say it.

It seems lots of people with the power to act in public space don’t want to defend anything except their own self-loathing and self-interest. (Looking at you, “safe space” discussions.) If you read my previous ramblings about morals, you will notice that I usually point out that bitching doesn’t help. It will only strengthen victim mentality. Only doing will improve the situation. That’s why I won’t stop open-sourcing code and I’m looking forward to getting feedback by amazing people. The majority of not-so-amazing people on the internet have to be filtered out. It’s worth the effort to reach the good folks.

Open Source and the Open, Distributed Mindset

We developers are familiar with the benefits of Open Source-distributed code: we can re-use the stuff other people achieved in our own applications and experiments. Giving back to “the community” feels nice, too. Most of the day-to-day benefits are centered around the free re-use. But the mentality of being part of Open Source is far more than that. I realized how precious the mindset is to me when I thought about how the gym I train at is managed.

Today at the gym I talked to a trainer about the new equipment and if they could’ve said “No” to it, or if the headquarters prescribe what’s happening. My gym is part of a large-ish chain of inexpensive gyms around Germany. HQ is prescribing everything. When during a routine inspection someone found out there was one spinning bike too many, it had to be removed. Micro-management like this makes me wonder: if the number of bikes is fixed, how do they account for differences in room dimensions? (I guess they don’t.) The number of clocks per room is dictated, too. They cannot even run a competition here without unlikely approval by HQ. This is narrow-minded, top-down micro management. It works, but it certainly adds to the feeling of detachment and coldness in the gym.

So what if someone at the gym is tired of 48pt Arial Bold signs saying “Out of order!!!” and gave the process a bit more thought? She could use a font family that matches the corporate identity, spice it up a bit and add brand colors. The gym owner will probably just like how it looks and that’s it. But what if our budding designer is sending the Word document to HQ into a central repository so others can make use of it, too? Some day, someone at HQ might replace the popular sign with a version that truly fits corporate design. Bottom-up improvement accomplished. Staff will feel more involved, too. Change is transparent and possible; it’s a mindset of contribution and involvement.

And that’s what I take for granted when I think about “Open Source.” It’s not just free re-use, although that’s an important part of it; it’s also an attitude of distributed work that goes without saying. You don’t need to manage and coordinate efforts in advance. Everyone can contribute. And thanks to GitHub (and git in general), it’s super easy to propose changes and merge improvements.

I wouldn’t want to work in an environment where real participation is impossible. A distributed, open mindset equals a transparent company. For me, that’s the only future worth bringing into existence.

Dan Counsell’s Mac App Store Wish List

These wishes by Dan I can easily get behind:

  1. Eliminate or relax the sandboxing guidelines
  2. Revamped pricing models
  3. Emphasize quality over quantity
  4. Get rid of in-app purchases
  5. Streamline the approval process

The picture Dan is painting is dim, but it’s also spot-on. Wishes don’t always come true, and you probably know that some people on the web simply complain and write open letters to Apple instead of wishing for a better future.

Dan’s stance is more productive than complaints, though: Because when you complain, you’re adopting the mentality of a deserving victim. Change the status quo instead.

It’s your turn to do what’s right: if you don’t believe in what’s going on, make something better. On the Mac, you have the power to do so. As an indie app developer, you can distribute your software outside the Mac App Store. Or you can try new business models like Setapp is doing (which I presume to be subscription based for unlimited access to a wide range of apps).

Bitching doesn’t help. It doesn’t improve your own mental state, and it doesn’t make the world a better place. Only doing something will bring change.

Drawing from Dan’s list of wishes, a controversial topic is In-App Purchases. Some people say IAP are the future of making business, that paid up front is dead, and that developers should deal with the reality and adapt. Instead of finding out the truth, think about what you want to make, and which future you want to help bring about. Then act accordingly and never falter.

RocketData is a Uni-Directional UI Component Data Source

I watched “Managing Consistency of Immutable Models” by Peter Livesey where Peter shows how RocketData works. Very worth the time!

I’d call RocketData a uni-directional data source. It’s uni-directional because you set up the DataProvider as the source once, wire update notifications to view updates via the delegate property, and you’re done with the setup. Update events include:

  • Overwriting data, as you’d do to push the result of a successful network request to the DataProvider,
  • Request old data from cache,
  • or sync one DataProvider instance with another, maybe across different view controllers (think UISplitViewController!).

It’s all the same to the DataProvider.delegate, which you’ll most likely implement in the view controller. (Or in your Presenter object, if you have any. While we’re at it, the DataProvider behaves like a VIPER Interactor which you wire to a Presenter. If the task is trivial, Interactor, Presenter, and View collapse into a single component that uses RocketData.) The delegate’s dataProviderHasUpdatedData(_:, context:) callback will be triggered.

An example from Peter’s slides:

class MyViewController: UIViewController {
    let dataProvider = DataProvider<PersonModel>()

    func viewDidLoad() {
        super.viewDidLoad()
        dataProvider.fetchDataFromCache(cacheKey: self.id) { (_, _) in
            self.refreshView()
        }
        MyNetworkManager.fetchPerson(id: self.id) { (model, error) in
            if let model = model {
                self.dataProvider.setData(model)
                self.refreshView()
            }
        }
    }
}

Here you see that the DataProvider queries the cache (which is supposed to quickly find results on a background thread from a local key–value-store) and updates the view when finished, and that a network request is fired to fetch new data and push changes to the view, too.

A specialized CollectionDataProvider, your go-to table view data source, will uses a different delegate that passes change information along, essentially:

enum CollectionChangeInformation {
    case update(index: Int)
    case delete(index: Int)
    case insert(index: Int)
}

So it’s uni-directional because DataProvider updates are pushed to the delegate (your view component). In order to update the user interface, you have to push model changes either to the DataProvider directly or to a synced DataProvider instance. Models are designed to be immutable, so you cannot change data under the feet of your UI.

RocketData is a per-view component data source. The underlying principles are very similar to what I got to know from ReSwift. Bringing RocketData and ReSwift into context, you can say ReSwift is a DataProvider for all of your app. RocketData focuses on keeping single components consistent; ReSwift focuses on keeping all of your app’s state consistent.

I’d love to see their cache implementation, too. The combination of a key–value-cache and RocketData can be very powerful if your app relies on data from the network a lot.

Debugging NSBeep Error Sound in NSResponder Method Calls

I utilize NSResponder actions in TableFlip to move the selection around. Naturally, neither a standard NSTableView nor a custom NSResponder implement default behavior for (most of) the methods I need to support arrow key movement, tab movement, and the usual shortcuts, like Cmd-Left to jump to the first cell in the current row. Or Alt + arrow keys to insert a row or column next to the selection. So I implemented this in a custom NSResponder subclass.

Curiously, the app beeped when I hit Alt-Up or Alt-Down, the actions to insert a row above or below the current selection. I implemented moveToBeginningOfParagraph(_:) to consume these events but still the system “input error” sound was audible while the key combo itself performed the action just fine.

How do you find out the source of problems like this? Well, this is how I dug in.

My setup:

  • The custom NSTableView subclass overrides keyDown(_:) with a call to nextResponder?.interpretKeyEvents(_:) – but not delegating to super, thus consuming the event, to disable standard NSTableView shortcuts.
  • Note that keyUp(_:) would in general produce a similar effect but probably beep for every shortcut; keyDown(_:) has a special meaning in all this, so make sure to override this method.
  • The next responder implements moveToBeginningOfParagraph(_:). This method is called just fine thanks to interpretKeyEvents(_:) being called. This method maps common shortcuts to NSResponder method calls.

If you have a custom NSView subclass, you can call self.interpretKeyEvents(_:), too, instead of pushing it to the next responder; it’s just that I want to bypass the table view in this particular case. NSTableView.interpretKeyEvents results in unwanted behavior.

This is where the beep happened even though the Alt-Up/Alt-Down NSResponder methods were present. The input error sound usually plays when you don’t implement a shortcut anywhere in the responder chain.

According to lore and the docs, performKeyEquivalent(with:) -> Bool should be useful: you return true to indicate that the receiver took care of the event selector passed to it. In theory, this should prevent the beep sound even when you don’t implement the NSResponder method – but that didn’t work at all for me.

screenshot of Xcode breakpoint
Symbolic breakpoint in Xcode

These are the steps I found useful to hunt for the source:

  1. Add a symbolic breakpoint in Xcode for symbol “NSBeep” in module “AppKit”. When you hit enter or run the program for the first time, the breakpoint should obtain a child item; that indicates the symbol is recognized. Run the program, hit the key combo, and Xcode should stop execution. Look at the stack trace to see from where the key interpretation reaches nirvana. – I found out that shortcut execution indeed reaches interpretKeyEvents(_:) but seemingly bypasses the rest of the responder chain which should handle the expected moveToBeginningOfParagraph(_:) call. (At least when it beeps. Since the action is performed by the program, I know the method is invoked.)
  2. Add doCommandBySelector(_:) in your target responder. Insert a manual breakpoint. Look at the value of the selector parameter for every call.

The first step revealed that some methods were never called when the beep happened. The second step told me which selector was actually invoked for a single hit of Alt-Up:

  1. moveBackward(_:)
  2. moveToBeginningOfParagraph(_:)

Alt-Down produces a similar combo:

  1. moveForward(_:)
  2. moveToEndOfParagraph(_:)

Aha!

Once I added an empty implementation of moveBackward(_:) and moveForward(_:), the beeps went away.

NSResponder debugging isn’t much fun most of the time because of all the implicit behavior. Then again, you get a lot of useful stuff for free. Sheesh. At least now I know that doCommandBySelector(_:) is a great opportunity to see what’s done behind the scenes because it’s a bottleneck every call has to pass.

BundleHunt Experiment

I participate in this year’s BundleHunt “Holiday Bundle” (running until January). I submitted both the Word Counter and TableFlip – mostly to see what happens.

The Bundle Experiment

I’ll disclose the details of my calculation later. This is an experiment: does presence in a bundle with so many hundreds of thousands of subscribers affect my regular sales and visibility? Being part of the bundle is like advertising, only I don’t really pay anything, I just make a lot less money when somebody buys the app from the bundle ($0.60 per license sold, or 97% discount for an app priced at $19.99). If the bundle sells 5000 times and 50% of bundles include one of my apps, that’s about $47,000 I will thus have “spent” on marketing. Imagine that. What a crazy number!

“These 2500 people could’ve paid you $20 instead and you’d be rich!” – Not quite.

My tools are targeted at users with very specific needs. The bundle is mostly targeted at folks wanting to make a deal. It’s people I may usually not attract at all. So even if I sell 2500 licenses through the bundle, I bet most of the customers will be people who wouldn’t have bought the apps without the bundle. I don’t really lose money if that’s true.

Part of the experiment is to find out what kind of people are going to buy the bundle.

Bundle Customer Ethics

I said this sometime in the past already: if you care about 1 app in a bundle of 10 and don’t want or need the other 9, contact the developer directly and ask for a discount. If MyDreamApp costs $50 and is part of a bundle for $20, ask the developer to buy the app directly for $20. This way they will make 10x more than from the bundle alone (ignoring bundle fees). That’s infinitely more % than not having you as a customer at all. It’s a good deal for everybody.

If you really do want to buy the BundleHunt Holiday Bundle because of the great offer, use this link and I’ll get $5 extra as an affiliate. Merci beaucoup!

Exploring Mac App Development Strategies” 4th Extended Edition Now Available

4th edition book cover
Extended 4th Edition Now Available

I’ve just finished updating the book “Exploring Mac App Development Strategies” for Swift 3.

Looking at my Word Counter, this 4th edition sports about 15000 new words in the book and code combined: I re-wrote a lot of chapters and source files. I also expanded a few sections to add more context and incorporate things I learned in the past 2 years.

There’s another book in the making about universal software architecture and ReSwift, with a macOS and iOS sample app. If you want to beta test the book and get early access, drop me a line: hi@christiantietze.de

Buy on Leanpub
Buy on iBooks Store

If you already own an earlier version of the book, you should be able to download an update today.

Resolving NSTreeController’s “Ambiguous use of ‘children’” in Swift 3

I’m converting code for my first book to Swift 3. It uses Cocoa Bindings a lot, including NSTreeController.

Now the compiler changed; and one of the issues I faced was working with the NSTreeController.arrangedObjects. The compiler assumed a wrong type of the children property (see the docs) and reported “Ambiguous use of ‘children’”.

Thanks to StackOverflow, I found out that specific casting is supposed to help. The disambiguation didn’t work for me though.

My resulting code looks like this:

let itemsController: NSTreeController = ...
let treeNodes: [NSTreeNode] = itemsController.arrangedObjects as AnyObject).children!!

To make this work, I had to move my custom tree node type to a different .swift source file. That type contains a children property to comply to NSTreeNode’s requirements. Thus the compiler assumed the unknown arrangedObjects as AnyObject cast really meant my own custom type. (That’s my explanation at least.)

For reference, this is the type I’m talking about:

@objc public protocol TreeNode {
    var title: String { get set }
    var count: UInt { get set }
    var children: [TreeNode] { get }
    var isLeaf: Bool { get }
    weak var eventHandler: HandlesItemListChanges? { get }
    func resetTitleToDefaultTitle()
}

So if your Swift 3 conversion doesn’t work as expected and your NSTreeController/NSArrayController/NSPageController is causing trouble …

  1. move any type with a children property to a different file,
  2. ensure you are specifying that you expect the children to be of type [NSTreeNode] – either through a cast (.children as [NSTreeNode]?), through specifying a helper function’s return type, or though specifying the expected variable type.

Help your compiler infer what’s going on in this still-very-weird casting situation.


→ Blog Archive