9 years of Apple textual content editor solo dev
⏳ 19 min learn
In 2015 I was a daily full-stack internet developer (and nonetheless am to at the present time). I’ve owned a Mac. I’ve constructed a tiny iOS app as soon as. That was concerning the extent of my proximity to the world of Apple dev on the time.
Having spent a while behind a Mac, I’ve grown keen on its quirky and vibrant ecosystem of indie apps. Sooner or later, after stumbling upon a quite simple and stylish Markdown editor referred to as iA Writer, for one purpose or the opposite, I made a decision to make one thing comparable.
Armed with enthusiasm, I began studying methods to make a local textual content editor for the Mac. Xcode, AppKit, Goal-C — all of it was new to me and never one thing I’d ever discover use for at my day job. I needed to study a very completely different tech stack that might reside inside my head parallel to all the net data.
Sooner or later, I began calling the app Paper as a result of, in pursuit of final minimalism, I’ve decreased the editor to nothing greater than a clean rectangle. To high it off, I’ve made the corners 90° as an alternative of the everyday rounded ones. Foolish? Possibly… But it surely was my app so I might do no matter I wished. 😈
In January 2017, 2 years after ranging from floor zero, I launched the Mac app on the Mac App Retailer. The iOS app adopted in 2019.
Now, it’s not daily {that a} random, unknown internet developer decides to construct an app in a ridiculously crowded class, in a tech stack they don’t have any expertise with, after which really does it, carrying on for the subsequent 7 years. “There’s gotta be some good materials right here” — I assumed to myself.
And so — here’s a mind dump of all of the bizarre, weird, and sometimes good ways in which I’ve organized my dev course of, app structure, and product philosophy, coming from an online developer who has not earned a cent working at a job as a Mac or an iOS developer however has earned fairly a number of of them promoting a local textual content editor to the customers of Apple gadgets.
Why native?
You might make the argument that an Electron app would work as properly. Why undergo the effort of studying a model new tech stack particularly when my major job is web-related? I might have reused the talents, saved time, and supported extra platforms all on the similar time.
Properly, my purpose was to ship the perfect expertise potential. I used to be attempting to compete with extremely polished writing apps, thus my app needed to be gentle and quick to start with. Along with that, there are merely extra methods to mess with the app on the native degree — to make it distinctive (particularly relating to textual content). I used to be not attempting to succeed in the utmost variety of customers nor to chop down the event time. I had on a regular basis on the planet. I used to be attempting to craft an expertise that begins with lightning-fast obtain time and carries on right into a native-feeling UI and UX.
I wished the perfect and I used to be prepared to pay the penalty.
Why Goal-C?
In 2015 Swift was simply getting began. I made a decision to make a take a look at. I’ve compiled an empty Xcode mission in Goal-C and one other one in Swift after which examined the respective .app
packages. To my shock, the Swift one had the complete Swift runtime embedded into it — about 5MB
, whereas the Goal-C one was tremendous gentle — tens or possibly 100KB
in whole. That was sufficient to persuade me to go along with Goal-C.
Once more — I wished the perfect and I used to be prepared to pay the penalty of a harder-to-learn, soon-to-be outdated language to get a slimmer distributable.
To be honest, when you run this experiment right this moment the distinction is not going to be that dramatic. Swift has come a good distance and my guess is Apple has both embedded it into their platforms or added some fancy tree shaking for the binary.
Third-party dependencies
Paper doesn’t have third-party dependencies.
I had little belief in my means to choose the correct dependencies from an ecosystem that I used to be not aware of. Plus by constructing every little thing alone I might tailor it to my wants, gaining a slight benefit over opponents who typically depend on exterior dependencies even for core components of their apps.
For instance, the Markdown parsing engine in Paper is bespoke. Why is {that a} good factor? As a result of Paper helps much less Markdown syntax than the normal fully-fledged Markdown editor. I can code in simply the correct quantity of parsing logic and nothing extra. Along with that, I can parse it with the correct degree of metadata granularity which makes implementing options equivalent to highlighting and textual content transformations less complicated and extra environment friendly.
An analogous however even stricter strategy applies to UI parts. Paper makes use of solely native UI components from AppKit and UIKit since they’ve the bottom upkeep overhead: auto-updated by Apple, adjustable to varied traits, backward appropriate, and assured to work on each system. To not point out that to the typical consumer, it’s the most acquainted UI — from the way in which it really works to the bounciness of animations.
In case not a single built-in UI element is appropriate to implement the specified characteristic, then I merely don’t add the characteristic. As an illustration, the NSPopover
is an efficient candidate for bubbles that trace at stuff within the Mac app. An iOS counterpart sadly doesn’t exist, so no bubbles within the iOS app. 🤷🏼♂️
Imaginative and prescient
The preliminary imaginative and prescient for Paper was easy — construct one thing that has the core methods of iA Author, however in a package deal that feels much more elegant and minimal. To realize the specified impact I went all-in on reducing down distractions:
- Not having a single additional button contained in the app window.
- Not having an ordinary Preferences window, however as an alternative packing every little thing into menu gadgets and menu widgets and hiding not often used menu gadgets below the ⌥ key.
- Slimming down the scroll bar to a 2px line that runs on the sting of the window and matches the 2px caret.
- Centering characters throughout the line so that there’s an equal quantity of caret above and beneath the character.
- Scrolling away the title bar and lengthening the textual content space into the title bar (and making the remaining whitespace across the editor draggable to account for the truth that the standard draggable title bar space may be taken by the editor).
Have folks observed the hassle? Likely not… however some have. A lot in order that at one level Paper acquired a wonderfully succinct assessment that I take advantage of as a tagline to at the present time.
This can be a super-clean writing area with quite a lot of configurability that stays out of sight if you don’t want it.
As time went on I began creating a sense for the way the market of minimal Apple textual content editors seems to be like and what could possibly be Paper’s place in it.
To my observations, minimalist writing apps normally comply with 2 paths:
- Turn into in style and begin slowly drifting away from their minimalist roots to fulfill the ever-growing calls for of mainstream customers.
- Stay too easy and area of interest to ultimately be deserted by their creators.
Paper just isn’t [1.] but it surely could possibly be on the trail to [2.].
My plan is to eternally maintain the app as minimal because it was when it launched — to withstand including any visible muddle. For a sure group of individuals, this can be a very important requirement that different apps (apparently) fail to handle.
Please don’t make Paper extra advanced! There are loads and loads of ‘full featured’ editors on the market, and they don’t match the invoice for targeted writing.
On the similar time by having a sluggish and predictable cadence of small updates (extra on that at the end) I can slowly add options to the fringes of the app whereas protecting the default path tremendous clear. Making Paper extra helpful however not bloated.
The sluggish tempo and the general restricted variety of options permit me to deal with constructing a greater basis, to raised perceive how issues work collectively, and to keep away from including options that convey instability and a excessive upkeep burden sooner or later.
Closed-sourced native UI is a fragile place in comparison with the predictable JavaScript runtime of the browser. Should you don’t make investments substantial sources into refactoring your app and eliminating bugs — it’s dying by a thousand crashes. And that is what I’m banking on with regard to [1.]. The bloat, complexity, and bugs that [1.] accumulate from their choice to go mainstream current good alternatives to seize among the disenchanted customers that ultimately go away them.
This nevertheless is probably not sufficient to make Paper right into a viable product. There are merely not sufficient (reachable) individuals who want these sorts of ultra-simple writing apps (not to mention pay for one). Energy customers are those who pay the payments as a result of they want energy instruments to earn cash that they’ll then justify spending on these instruments. And whereas [1.] and [2.] are the bulk, there positively are instances of easy writing apps that stay easy and/or which can be nonetheless supported by obsessive single gamers like me. There would possibly simply by no means be a large enough hole available in the market for Paper. 🤷🏼♂️
Structure
I discover it handy to consider Paper’s code as consisting of two scopes.
-
Utility scope
-
International stuff that exists as a single occasion or is utilized to the app as an entire.
- macOS menu
- iOS standing bar
- Contact bar
- App icon
- Darkish mode
- Enter language
- International app config
- International app logic
- Single occasion views
-
-
Doc scope
- A set of views and logic that reside inside a view controller representing a single doc.
- A brand new doc scope spawns into life the second the doc is opened and dies when it’s closed.
- In contrast to a singular utility scope, zero or extra doc scopes can exist on the similar time.
The fact is a little more nuanced as there are additionally scenes on iOS that subdivide the worldwide scope, however the psychological mannequin holds kind of.
For each scope, I outline a storyboard that serves 2 capabilities:
- It describes varied views and widgets which can be used throughout the scope.
- It acts as a dependency injection container that glues collectively all modules throughout the scope.
Modules (to not be confused with .modulemap
stuff — I simply occur to make use of the identical title) in Paper are plain Goal-C courses that take duty for a bit of performance throughout the app. It’s a strategy to group performance associated to a specific characteristic as an alternative of spreading it throughout a number of locations.
Modules have a well-defined lifecycle:
-
They’re created by the storyboard when the major factor throughout the storyboard will get created.
-
Utility scope
- The major factor is the app itself, so all modules are created on startup.
-
Doc scope
- The major factor is the view controller that holds the editor.
-
-
They’ve a setup technique that will get referred to as after the dependencies have been injected, however earlier than the major factor turns into seen.
-
Utility scope
-
Referred to as on
ApplicationDidFinishLaunchingNotification
UISceneWillConnectNotification
for scenes
-
-
Doc scope
- Referred to as on
didMoveToWindow
of the principle view.
- Referred to as on
-
-
They’ve a tear-down technique that will get referred to as earlier than the major factor is destroyed.
-
Utility scope
- No want. Modules die with the app.
-
Doc scope
- Referred to as on
willMoveToWindow
of the principle view when thenewWindow
argument isnil
.
- Referred to as on
-
Modules declare their dependencies — views and different modules.
@interface TvTextAttributeModule()
@property IBOutlet NSTextView *textView;
@property IBOutlet TvTextContentModule *tvTextContentModule;
@property IBOutlet TvLayoutAttributeModule *tvLayoutAttributeModule;
@property IBOutlet TvCaretModule *tvCaretModule;
@property IBOutlet TvTypewriterModeModule *tvTypewriterModeModule;
@finish
I resolve them manually in Xcode.
Dependencies are then injected by the storyboard at runtime when the view controller is instantiated.
UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Document"
bundle:nil];
UINavigationController *ctr = [sb instantiateInitialViewController];
If a document-scoped module wants one thing from the applying scope it could get it from the worldwide variable…
[self.viewController.presentingViewController dismiss_:^{
[self.docNavigationBarModule documentViewControllerDidDismiss];
[Application.get.viewController.storeReviewModule tryAsk];
}];
…or from the basis of the view hierarchy for scenes.
BOOL unread = self.rootViewController.supportChatModule.unread > 0;
UIImage *icon = [UIImage systemImageNamed:@"gear"];
UIImage *unreadIcon = [UIImage systemImageNamed:@"gear.badge"];
self.rightBarButtonItem.picture = unread ? unreadIcon : icon;
Modules subscribe to notifications (pub-sub occasions between courses) throughout the setup part of the lifecycle and unsubscribe throughout the tear-down part.
- (void)addNotificationObservers {
[self addObserver:@selector(windowDidResize)
:NSWindowDidResizeNotification
:self.window];
[self addObserver:@selector(textViewDidChangeSelection)
:NSTextViewDidChangeSelectionNotification
:self.textView];
[self addObserver:@selector(colorsDidChange)
:Colors.didChangeNotification];
}
Modules can let different modules know that one thing has occurred. That is performed via direct technique calls quite than notifications to maintain issues less complicated, IDE navigable, and quicker.
UIEdgeInsets contentInset = self.calculateContentInset;
UIEdgeInsets indicatorInsets = self.calculateScrollIndicatorInsets;
self.textView.contentInset = contentInset;
self.textView.verticalScrollIndicatorInsets = indicatorInsets;
[self.tvTypewriterModeModule textViewDidChangeInsets];
Modules can ask different modules to do or to calculate one thing that these different modules are liable for.
- (void)textViewDidAnimateCaretFirstAppearance {
if (self.shouldCancel) {
return;
}
[self.label centerIn_:self.docFrameModule.visibleFrame];
[self animateIn:self.label :^{
[Delay_ by:1.0 :^{
[self animateOut:self.label :^{
[self.label removeFromSuperview];
self.label = nil;
}];
}];
}];
self.label.hidden = NO;
}
This leaves the delegates, view controllers, and views as proxies that notify modules about stuff that’s happening within the app and delegate every little thing to them.
- (void)insertText:(id)string replacementRange:(NSRange)vary {
[self.tvAutocompleteModule textViewWillInsertText:string];
[self.tvTypewriterModeModule textViewWillInsertText:string];
if ([self.tvTextAttributeModule textViewWillInsertText:string]) {
[super insertText:string replacementRange:range];
}
[self.tvTextAttributeModule textViewDidInsertText];
[self.tvTypewriterModeModule textViewDidInsertText];
}
All in all, the mixture of modules and dependency injection via storyboards offers a pleasant mechanism to decouple and subdivide what would have been a large doc view controller and/or textual content view. Making use of the identical strategy to the applying scope offers the entire app a uniform construction that may be a pleasure to work with.
Cross-platform code
AppKit and UIKit are each fairly comparable and annoyingly completely different in lots of locations.
I make use of 2 Goal-C options to work across the variations:
- Macros (this one is inherited from C)
- Classes
For instance, in lots of instances, the distinction comes right down to NS
vs UI
prefix which I mitigate with this macro.
#if TARGET_OS_OSX
#outline KIT(image) NS##image
#else
#outline KIT(image) UI##image
#endif
Now, each time I have to declare a view in a shared module I can merely kind KIT(View)
as an alternative of NSView
or UIView
.
- (KIT(View) *)cloneCaretView {
KIT(View) *view = [self createCaretView:self.caretFrame];
[view.layer setNeedsDisplay];
return view;
}
In addition to that, I can use these #if
statements to shortly add platform-specific code in shared courses. To make such code extra readable Xcode even has a cool trick that deemphasizes the code that doesn’t apply to the at the moment chosen platform.
Classes in Goal-C are a manner so as to add new strategies to any present class, together with framework courses (classes can be used to interchange strategies, which is each highly effective and scary 😵). I take advantage of them to harmonize the API. So if a technique in UITextField
known as textual content
and in NSTextField
it’s referred to as stringValue
I can add a stringValue
technique to UITextField
that calls textual content
(or vice versa). Now I can at all times check with the stringValue
technique in my KIT(TextField)
variable and it’ll compile on each platforms.
I collect all these little patches to an adapter header within the shared library.
@interface UITextField(SharedApplicationAdapter)
@property (nonatomic) NSString *stringValue;
@property (nonatomic) NSString *placeholderString;
@finish
After which implement this header in respective app initiatives.
@implementation UITextField(SharedApplicationAdapter)
- (NSString *)stringValue {
return self.textual content;
}
- (void)setStringValue:(NSString *)stringValue {
self.textual content = stringValue;
}
- (NSString *)placeholderString {
return self.placeholder;
}
- (void)setPlaceholderString:(NSString *)placeholderString {
self.placeholder = placeholderString;
}
@finish
By the way in which, I additionally use classes to shorten lengthy framework strategies.
@implementation UIViewController(Mobile_)
- (void)dismiss_ {
[self dismissViewControllerAnimated:YES completion:nil];
}
@finish
The underscore on the finish helps to keep away from clashes with public or non-public strategies that Apple would possibly resolve so as to add sooner or later. Apple typically prefixes non-public strategies with an underscore, so having it on the finish makes extra sense.
Debugging
All through my profession I’ve at all times realized the quickest and essentially the most a few explicit third-party dependency by studying the code quite than going via the docs. Documentation is ok to get a common image, however the particulars matter particularly when stuff doesn’t work the way in which it ought to both by (undocumented) design or on account of errors of the authors.
Now within the case of Apple frameworks, documentation is unfortunately the one readable possibility you will have. A far much less readable one is to place a breakpoint in your code and study the compiled stack hint of the framework.
It seems to be like gibberish, however the necessary factor is that the tactic names are seen. Analyzing the movement of technique calls is sufficient to grasp what’s going on. And if it’s essential examine one thing that’s exterior the stack ending in your supply code you’ll be able to at all times add a symbolic breakpoint to cease contained in the framework. As an illustration, beneath it stops at recognizedFlickDirection
which is seen on the high of the compiled code above.
As a aspect word, going via a 30-year-old codebase you sometimes uncover gems equivalent to if
statements adjusting the logic of the framework relying on the (Apple) app that runs the framework, or situations of artistic naming. 🙃
Paid options
Again in 2015–17, subscriptions weren’t a widespread factor but within the App Retailer. Pay-to-download was (and is) frequent, however I didn’t suppose that somebody would pay for an unknown app with out attempting it, so the one-time fee freemium appeared like the one possibility. On the similar time, I didn’t need to simply paywall the options or implement a customized time-based trial (solely subscriptions have built-in trials within the App Retailer). I wished one thing extraordinarily user-friendly. One thing that appears like you’ll be able to push all of the buttons and regulate all of the toggles of the paid providing with out limits, while not having to explicitly decide to a trial of arbitrary size.
My resolution was to supply solely beauty upgrades as a part of the Professional providing — visible modifications quite than purposeful options (e.g. file syncing or PDF exports).
I’d then make them trialable for an infinite period of time. Customers might take a look at drive the options to see how they give the impression of being and work after which purchase the Professional providing in the event that they thought that the options have been price it.
After all, there wanted to be a measure that prevented folks from utilizing the options indefinitely with out paying. At first, it was a 60-second timer that might nag them to purchase if a minimum of one of many Professional options was lively. This proved to be too annoying, so I tied the nagging to characters written as an alternative. It made extra sense — attempt every little thing for as a lot time as you need with out distractions however as quickly as you begin utilizing Paper to jot down then each couple of hundred characters you’ll see a popup.
Why does this work just for beauty upgrades? As a result of, not like purposeful options whose worth is tied to a particular momentary motion, beauty upgrades improve your writing expertise on a relentless foundation. You can not put a purposeful characteristic behind the identical frictionless trial described above because the worth of the characteristic is absolutely realized the second the motion is carried out. Then again, letting the customers apply beauty modifications accompanied by a nagging mechanism offers them a style of the improved writing expertise but limits the extended worth of this enhancement. Forcing the acquisition, but not limiting the power to completely discover every little thing on their phrases earlier than spending the cash.
What do customers give it some thought? Right here is an excerpt from a current assessment.
…the truth that it enables you to fiddle with and take a look at professional options as an alternative of locking them behind a paywall (a purpose I don’t like quite a lot of apps. They don’t allow you to attempt the options till you begin a subscription or trial you could very properly neglect about, if it even offers you a trial) is extraordinarily good. […] You don’t even must essentially belief me on it: bounce in and take a look at it your self. It enables you to take a look at each single characteristic it has.
Pricing
Paper began with 2 one-time funds of $5 every for two units of Professional Options. At this time, after many experiments, it’s $10 per 30 days or $100 lifetime for a single set.
First, earlier than plunging into the world of subscriptions, I experimented with one-time funds. I began steadily doubling the value and observing if the full sum of money earned would enhance or lower. My App Retailer visitors was fairly secure, so there have been at all times new folks coming via the door who didn’t know something about earlier costs. That’s once I first found that individuals have been prepared to pay as much as $100 for an app from an unknown developer. I attempted going as much as $200. I believe I’ve gotten like 1 or 2 gross sales and a few quantity of complaints over a number of months so I figured that $200 might be the ache level. Thus the market has determined that $100 is an sufficient worth for Paper.
Subsequent after quite a lot of hesitation, I made a decision to introduce subscriptions. “Nobody would pay a subscription for such a easy app” — was my conviction. However I used to be fallacious. They labored and right this moment Paper has a wholesome mixture of month-to-month, annual, and lifelong funds with subscriptions being the default, promoted fee possibility (since SaaS is all the trend, proper?).
Even now I nonetheless experiment with costs. Not too long ago I doubled the month-to-month worth for brand new subscribers in some international locations whereas protecting the annual worth unchanged. That is to incentivize the annual possibility and to scale back churn. Preliminary outcomes present that individuals proceed subscribing although in smaller numbers. Nonetheless — this could possibly be a constructive change bearing in mind the upper month-to-month worth and annual upgrades. We’ll have to attend and see…
What’s additionally nice concerning the App Retailer is that you would be able to simply take a look at reducing costs for international locations with decrease incomes. Since Apple doesn’t cost the everyday ¢50 per transaction (e.g. like Paddle) you’ll be able to go as little as you need with out these charges consuming into your margins. At present, I’m testing to see if reducing the value has any impact or if folks in these international locations merely don’t pay for software program (or textual content editors 🤷🏼♂️) it doesn’t matter what.
Gnarly bits
Textual content editors are exhausting…
Textual content is a type of issues that at all times will get issues bolted onto it. Copy-paste, drag and drop, undo, caret interactions, right-to-left languages, non-alphabetic languages, dictation, spoken textual content, scanned textual content, non-text objects in textual content, information format recognizers, tap-and-hold hyperlink previews, textual content search overlay, spell examine, autocorrection, autocomplete, AI ideas — it by no means stops. You’re at all times on the mercy of the subsequent OS replace including new methods to insert, replace, and work together with editable textual content.
Determining the maths behind the textual content editor rectangle is especially difficult on iOS with varied components just like the Dynamic Island (or the notch), the Dwelling Bar, and the dynamically showing software program keyboard, instances 2 orientations, getting in the way in which.
Enter languages and fonts are a headache as properly. Alphabetic languages are comparatively simple to cope with as most main fonts assist even quirky glyphs like umlauts and Cyrillics. The non-alphabetic languages nevertheless want particular fonts to show their glyphs. Fortunately Apple’s programs include a minimum of a type of fonts preinstalled per each non-alphabetic enter language. The one drawback is that no API maps an enter language to supported fonts — so I needed to brute power it. I’ve put in each enter language on my Mac and wrote some textual content in every of them in TextEdit to see what font TextEdit falls again to when it sees the typed glyphs. The next swap
is the results of it (I’m fairly positive I’ve tousled the names of some languages or nationalities — don’t choose me too exhausting, I’m only a developer 😩).
swap (inputLanguage) {
case InputLanguage_Alphabetic:
swap (fontName) {
case FontName_Courier:
return @"Courier";
case FontName_CourierPrime:
return @"CourierPrime";
case FontName_CourierPrimeSans:
return @"CourierPrimeSans-Common";
case FontName_Menlo:
return @"Menlo-Common";
case FontName_Helvetica:
return @"Helvetica-Gentle";
case FontName_Avenir:
return @"AvenirNext-Common";
case FontName_NewYork:
return @"NewYork-Common";
case FontName_Georgia:
return @"Georgia";
case FontName_TimesNewRoman:
return @"TimesNewRomanPSMT";
case FontName_Palatino:
return @"Palatino-Roman";
case FontName_BrushScript:
return @"BrushScriptMT";
case FontName_Charter:
return @"Constitution-Roman";
}
case InputLanguage_Japanese:
case InputLanguage_Cantonese:
return @"HiraginoSans-W2";
case InputLanguage_ChineseSimplified:
return @"PingFangSC-Gentle";
case InputLanguage_ChineseTraditional:
return @"PingFangTC-Gentle";
case InputLanguage_Korean:
return @"AppleSDGothicNeo-Common";
case InputLanguage_Arabic:
return @"SFArabic-Common";
case InputLanguage_Thai:
return @"Thonburi-Gentle";
case InputLanguage_Hebrew:
return @"SFHebrew-Common";
case InputLanguage_Hindi:
return @"KohinoorDevanagari-Common";
case InputLanguage_Bengali:
return @"KohinoorBangla-Common";
case InputLanguage_Malayalam:
return @"MalayalamSangamMN";
case InputLanguage_Burmese:
return @"MyanmarSangamMN";
case InputLanguage_Gujarati:
return @"KohinoorGujarati-Common";
case InputLanguage_Kannada:
return @"NotoSansKannada-Common";
case InputLanguage_Oriya:
return @"NotoSansOriya";
case InputLanguage_Telugu:
return @"KohinoorTelugu-Common";
case InputLanguage_Gurmukhi:
return @"GurmukhiSangamMN";
case InputLanguage_Sinhala:
return @"SinhalaSangamMN";
case InputLanguage_Khmer:
return @"KhmerSangamMN";
case InputLanguage_Tibetan:
return @"Kailasa";
case InputLanguage_Armenian:
return @"SFArmenian-Common";
case InputLanguage_Georgian:
return @"SFGeorgian-Common";
case InputLanguage_Tamil:
return @"TamilSangamMN";
case InputLanguage_Amharic:
return @"Kefa-Common";
case InputLanguage_Syriac:
return @"NotoSansSyriac-Common";
case InputLanguage_Cherokee:
return @"Galvji";
case InputLanguage_Dhivehi:
return @"NotoSansThaana-Common";
case InputLanguage_Adlam:
return @"NotoSansAdlam-Common";
case InputLanguage_Hmong:
return @"NotoSansPahawhHmong-Common";
case InputLanguage_Inuktitut:
return @"EuphemiaUCAS";
case InputLanguage_Lao:
return @"LaoSangamMN";
case InputLanguage_Mandaic:
return @"NotoSansMandaic-Common";
case InputLanguage_MeeteiMayek:
return @"NotoSansMeeteiMayek-Common";
case InputLanguage_NKo:
return @"NotoSansNKo-Common";
case InputLanguage_Osage:
return @"NotoSansOsage-Common";
case InputLanguage_Rejang:
return @"NotoSansRejang-Common";
case InputLanguage_HanifiRohingya:
return @"NotoSansHanifiRohingya-Common";
case InputLanguage_OlChiki:
return @"NotoSansOlChiki-Common";
case InputLanguage_Tifinagh:
return @"NotoSansTifinagh-Common";
case InputLanguage_Wancho:
return @"NotoSansWancho-Common";
case InputLanguage_Punjabi:
return @"MuktaMahee-Common";
}
I then detect the chosen enter language and set the font accordingly.
Yet another factor price mentioning is the fiddly logic of batched textual content updates e.g. shortcuts that toggle Markdown formatting on a specific piece of textual content. A naive resolution is easy, however coding for all the sting instances to keep away from breaking the Markdown generally is a ache.
Gimmicks
A factor I’ve picked up from the superb Things app is the pleasant resize bounce.
After quite a lot of trial and error, that is the damping logic that I’ve landed on.
NSRect body = self.window;
if (self.isMinWidthReached) {
if (NSWidth(body) > self.window.minSize.width) {
self.minWidthReached = NO;
}
} else if (NSWidth(body) == self.window.minSize.width) {
self.minWidthReached = YES;
self.minWidthFrame = body;
self.minWidthMouse = NSEvent.mouseLocation;
}
if (self.isMinHeightReached) {
if (NSHeight(body) > self.window.minSize.top) {
self.minHeightReached = NO;
}
} else if (NSHeight(body) == self.window.minSize.top) {
self.minHeightReached = YES;
self.minHeightFrame = body;
self.minHeightMouse = NSEvent.mouseLocation;
}
CGFloat (^damp)(CGFloat) = ^(CGFloat delta) {
return signal(delta) * 3.0 * log(pow(fabs(delta) / 30.0 + 1.0, 5.0));
};
CGFloat mouseDeltaX = NSEvent.mouseLocation.x - self.minWidthMouse.x;
CGFloat mouseDeltaY = NSEvent.mouseLocation.y - self.minHeightMouse.y;
CGFloat minWidthFrameX = NSMinX(self.minWidthFrame);
CGFloat minHeightFrameY = NSMinY(self.minHeightFrame);
CGFloat dampedX = minWidthFrameX + self.axes.x * damp(mouseDeltaX);
CGFloat dampedY = minHeightFrameY + self.axes.y * damp(mouseDeltaY);
self.isMinWidthReached ? dampedX : NSMinX(frame),
self.isMinHeightReached ? dampedY : NSMinY(frame),
NSWidth(frame),
NSHeight(frame)
) display:YES];
The difficult half was determining the mixture of math capabilities that yield a practical rubber band impact. 🤓
One other enjoyable mathy characteristic in Paper is the rotate-to-undo gesture.
A whole lot of calculations are concerned to make it occur, however on the core of it’s this logic.
self.wheelView.frameCenterRotation = self.wheelViewRotation;
for (NSUInteger i = 0; i < self.wheelView.subviews.rely; i++) {
CircleView* undoItemView = self.wheelView.subviews[i];
CGFloat scale = [self bottomPointPercent:i :-35.0 :180.0];
undoItemView.alpha = [self bottomPointPercent:i :5.0 :20.0];
undoItemView.layer.affineTransform = CGAffineTransformMakeScale(
scale,
scale
);
if ([self shouldWheelItemBeFilled:i]) {
undoItemView.fillColor = [self wheelItemFillColor:i];
} else {
undoItemView.fillColor = KIT(Colour).clearColor;
}
}
- (CGFloat)bottomPointPercent:(CGFloat)wheelItemIndex
:(CGFloat)zeroRange
:(CGFloat)maxRange {
CGFloat rotation = self.wheelViewRotation + wheelItemIndex * 15.0;
whereas (rotation < 0.0) {
rotation += 360.0;
}
return fmin(
fmax(fabs(fmin(360.0 - rotation, rotation)) - zeroRange, 0.0),
maxRange
) / maxRange;
}
- (BOOL)shouldWheelItemBeFilled:(NSUInteger)wheelItemIndex {
return [self undoItemIndex:wheelItemIndex] >= 0
&&
[self undoItemIndex:wheelItemIndex] <= self.undoRedoCount;
}
- (KIT(Colour) *)wheelItemFillColor:(NSUInteger)wheelItemIndex {
if ([self undoItemIndex:wheelItemIndex] == self.redoCount) {
return Colours.theme.floatingElementHighlight;
}
return Colours.theme.floatingElement;
}
- (NSInteger)undoItemIndex:(NSUInteger)wheelItemIndex {
NSUInteger wheelItemCount = self.wheelView.subviews.rely;
NSUInteger highlightedIndex = self.redoCount % wheelItemCount;
CGFloat fullWheelTurns = ground(self.redoCount / wheelItemCount);
return ground((fullWheelTurns - 0.5) * wheelItemCount)
+
(wheelItemIndex < highlightedIndex ? wheelItemCount : 0)
+
wheelItemIndex;
}
I do not know if this subsequent one has any impact on gross sales, however in any case — I present the full variety of Professional customers, the constructive delta because the final time the consumer noticed this quantity, and the annual plan financial savings (which can differ per nation or change on-the-fly on account of pricing experiments).
The delta quantity is taken into account seen when it turns into seen on the display. So it could sit there, unseen for a very long time, after which show an enormous delta as soon as the consumer has scrolled to it.
Transferring on to the tremendous gimmicky territory — this animation is triggered when the display goes to sleep whereas Paper is within the foreground. 💤
Lastly — one thing I’ve observed on the planet of Mac apps is that regardless of the eye to element within the app itself the About window virtually at all times will get zero love. Why not make it a little bit bit extra enjoyable? 👨🏻🎨
(roughly within the dimensions of a enterprise card)
Suggestions
Again within the day, Paper was utilizing an app lifecycle administration platform referred to as HockeyApp. The Mac model of HockeyApp got here with a hosted assist chat that you could possibly simply launch from inside your app. As a result of this chat was so accessible, folks have been leaving helpful suggestions in vastly better numbers in comparison with e mail.
Then got here Microsoft, purchased HockeyApp and turned it into AppCenter, sunsetting this characteristic within the course of.
After going again to the desert that’s suggestions through e mail I made a decision to construct the assist chat myself for each platforms and combine it to be a phenomenal a part of the app expertise quite than one thing tacked on as an afterthought.
I attempted mimicking the aesthetic of the Messages app on respective platforms right down to these little curvy tips about chat bubbles.
In contrast to HockeyApp which required an e mail to get began, I made a decision to decrease the friction to zero to maximise the suggestions potential. I used to be high quality to commerce decrease entry limitations for extra spam.
For the backend, I wished one thing hosted and free so I ended up saving conversations to UUID.json
information on Dropbox.
I edit these information in a textual content editor and it syncs again to Dropbox. Trying again, I ought to have merely constructed it on high of a Slack workspace. Nonetheless, Dropbox has labored properly and nonetheless works nice for me.
With time I even noticed a number of clear patterns and coded the logic that appears for key phrases each within the unsent message field and within the final despatched message to indicate auto-replies as quickly because the consumer has typed the wanted key phrase. Now I not often get these questions — they’re answered earlier than the ship button is pressed.
Oh, and naturally I can’t reply at evening. 😴
In the long run, constructing a elegant, frictionless assist chat was the most effective choices that I’ve ever made for Paper. Not solely did it differentiate the app from the remainder (not like the net, reside chats in native apps aren’t a factor particularly in tiny apps like Paper), but additionally resulted in happier (and sometimes positively shocked) customers, quicker suggestions in case of bugs, and quite a lot of nice concepts. In actual fact, sooner or later, 90% of my roadmap began being knowledgeable by concepts from the chat.
Releases
So I’ve gathered and applied the suggestions from the chat. How are the options then propagated to the customers?
About as soon as a month I launch one flagship characteristic that goes into launch notes. Typically I put together a number of options throughout a improvement streak after which drip them out one after the other over the subsequent months. This provides me the best likelihood that somebody would really learn the only sentence that’s current within the launch notes each month.
Bug fixes, tweaks, and smaller options go into releases normally with out being talked about. As a developer, you would possibly really feel higher about placing “We’ve fastened some bugs and made a number of efficiency enhancements” into your launch notes, however the common consumer of a distinct segment, unknown app like Paper has no endurance to care.
If an pressing repair is required — I merely copy-paste the earlier launch notes to maintain the flagship characteristic commercial going. I don’t point out the repair in any respect.
For the model quantity, I take advantage of a single monotonically rising quantity. It’s the only factor so why complicate it with 2+ numbers separated by dots?
And right here is the template that I take advantage of for the discharge notes.
Pricey Consumer,One sentence. Two–three in uncommon instances.
Nice replace,
Your Paper
A bit cringey, however I kinda prefer it. 🫠 It’s like each month the app sends you a letter explaining what’s new. Some customers have even talked about this as a factor they look ahead to.
Having one thing to launch each month be it small or massive is what greases the flywheel of App Retailer distribution.
It reveals the present customers that their subscription is price it.
It alerts to potential customers that the app has been not too long ago up to date.
It tells the App Retailer algorithm that the app just isn’t deserted.
And eventually, this sluggish and regular tempo is what permits me to maintain this factor going for years to return.
Acknowledgment
Early on in Paper’s life, an artist and a fan of Paper reached out to supply his providers.
Essentially the most distinguished results of our collaboration is Paper’s palette of accent colours. Particularly the signature Sepia colour that was impressed by sepia ink.
Hats off to Ben Marder for his assist. 🎩
PS — I like sweating the main points. Should you suppose I is perhaps helpful to you → reach out. 😉