GeistHaus
log in · sign up

https://jaanus.com/feed.xml

atom
20 posts
Polling state
Status active
Last polled May 19, 2026 02:05 UTC
Next poll May 20, 2026 01:03 UTC
Poll interval 86400s
ETag "68b1688f-37c13"
Last-Modified Fri, 29 Aug 2025 08:45:03 GMT

Posts

Which side are you on, Apple?
Show full content

In about four months, Apple will have its annual WWDC event for developers. Some new technology and products will be announced. Some changes will be good, some will be meh. Apple will release new operating systems with some new features, and developers like me will whine about how they should slow down the pace and fix old bugs instead. It’s all predictable, and routine.

What are you most excited about? What is your biggest question? Is it the new products? Developer tools and APIs? Next steps in Apple Intelligence?

I am a developer in the Apple ecosystem, and this year, I do not care about any of that. At all. Seriously.

This year, I only have one question to Apple:

Which side are you on?

Is it your users and developers?

Or is it the United States federal government?

I ask because I predict that in 2025, the interests of these two groups will collide, and you will need to pick a side.

The Apple privacy story

Privacy. That’s Apple.

Apple has made privacy central in its offering, and claims that it protects user data better than some other big tech companies. It also makes sense business-wise, because Apple’s business is about selling devices and services, not user data.

As a security- and privacy-minded developer in the Apple ecosystem, I can assert that Apple so far has been serious about privacy, and it is reflected in the design and implementation in their technology. Much of the computation happens on the device side, not in the cloud. The data on devices is sandboxed, and apps can’t see each other’s data. iCloud and CloudKit are designed in a way where I as a developer literally do not have access to the user data. Users can opt in to Advanced Data Protection to get end-to-end encryption protection for both Apple and third-party developer app data. The cloud side of Apple Intelligence, Private Cloud Compute, is designed and built in a privacy-conscious way.

To me as a developer, this so far has been a consistent and trustworthy picture. I am building Tact, a messaging app based on Apple platform, which is sort of an experiment of building such an app only with Apple technologies, including on the cloud side. Tact stores its data in iCloud with CloudKit, and I say that it has the same level of privacy as Photos.

iCloud data privacy has so far been with a positive sign for me. In 2025, I am no longer certain of this.

The new political reality

US voters elected Donald Trump as their president in November 2024, which has changed many things about how the federal government operates. One thing we have seen that is relevant to this post, is how the DOGE team goes around US government institutions and collects data, often illegally. I don’t want to speculate on their goals, but the fact is that they are doing this.

I have no doubt that the new US federal government will try to collect data from private companies for political purposes. As a specific example, imagine that you are an iPhone app developer in the space of reproductive health, and you keep your user data in iCloud, because so far you have believed the Apple privacy story, as I have.

It’s not far fetched to imagine that the US government will walk up to Apple and demand data about the users of your app, including the data they have stored with your app.

How will Apple respond?

That is the point of this post. I don’t know. I would like to know.

Apple’s stance in this new political reality

To my knowledge, there haven’t yet been such cases of such conflict. But we have seen other ways of Apple trying to be on the good side of the current US federal government.

Tim Cook donated $1 million to Trump’s inauguration.

The case of Gulf of Mexico/America.

Apple starts advertising on X again. X and Elon Musk are just not a good site and a good person to be associated with.

There is also UK demanding access to Apple users’ encrypted data, including data that is protected with Advanced Data Protection. This is not related to US federal government, but it does illustrate that Apple is operating in an increasingly hostile political environment.

Apple complies with local laws. There is likely a different attitude to data that Apple has to Chinese users’ data in China. I don’t even know exactly, but there is probably less security and privacy there, and the government has more access to the data. Many of us haven’t paid attention, because “China is China”, and that’s just how it is there, while we have remained on the side of freedom, and the government not interfering with companies and user data in this way.

It’s possible that in 2025 the US law will change to compel private companies to furnish data to government for political purposes, or that the government will demand such things outside of law.

All these developments make me uncomfortable as a developer and custodian of my users’ data, and I have less reason to believe that in the face of the political reality of 2025, Apple’s privacy story will remain entirely true.

What’s Apple to do?

What could Apple do to make me as a developer comfortable?

General – give us reassurance about commitment to privacy. I don’t know what this would look like. Perhaps tell us specific stories about how Apple is committed to people’s privacy? At many Apple marketing events, there are now segments about how Apple Watch has saved people’s lives by letting them contact emergency services. Preserving people’s privacy is an equally noble goal, and there could be stories shared around that. It’s more controversial because it might be at odds with what the government wants, but who said this would be easy?

Specific and technical - make it easier to me as a third-party developer to understand how data protection in iCloud works, including Advanced Data Protection, and how sharing plays into this.

I have previously written this technical post about third-party CloudKit apps and data protection, which outlines some rough edges. It would be helpful if Apple provided security audits of CloudKit and ADP, were more clear about how sharing and ADP interact, and provided more tools for developers like me to provide assurances to our users, including assurance about end-to-end encryption.

iCloud data security overview has this paragraph about third-party app data, which I think has been added since I wrote that post. It answers the base technical question of what’s protected, but doesn’t address the parts about sharing and reassuring the users.

Third-party app data stored in iCloud is always encrypted in transit and on server. When you turn on Advanced Data Protection, third-party app data stored in iCloud Backup and CloudKit encrypted fields and assets are end-to-end encrypted.

Will Apple do any of these things? I think it’s unlikely and low on their priority list. I don’t actually expect to get anything out of WWDC this year in this area.

The shareholder view

Note that I left out one group in the list of conflicting parties above: the shareholders.

I think there are Apple shareholders on both sides, and probably more on the side that prefers compliance over privacy. Business means complying with law, and if the law (or environment of de facto law) changes, so be it.

Besides being a developer, I own some Apple stock, and will continue to own it, because it has been a good investment. I have no doubt that Apple will prioritize my interests as an investor over my interests as an Apple ecosystem developer. It’s just that up to this point, my interests as an investor and developer were largely aligned, and I think they will now align less.

Conclusion

There is no conclusion to this story today. The question remains open, and I’ll be looking for signals and evidence through the coming months and years, to inform my career and technology choices as an Apple platforms developer and user.

Which side are you on, Apple?

Which side are you on, Apple? was originally published by Jaanus Kase at Jaanus on February 19, 2025.

https://jaanus.com/which-side-are-you-on-apple
My social media audit for the end of 2024
Show full content

Happy 2025.

I thought I’d do a small audit of my online presence for the end of the year. What sites do I use to publish things about myself? For what purpose? How has this evolved over time?

The trigger for this was me reviewing my personal expenses. For the longest time, I’ve been paying for Flickr Pro to host my photos. I paid once a year and for the past few years, I kept asking myself… why am I doing this? Is it worth it for me? This triggered a bit broader introspection.

For reference, here is a similar post from a very long time ago. Many of the sites mentioned there no longer exist, or I deleted my account on them. But I do keep using a few.

Preface

Before I dig in to specific sites, here’s how I think about Internet and (social) media landscape today, and how I see myself as part of that.

Internet and social media used to be a happier place. In the early days, it felt like we are all in this together, including the people and companies building these new things. We truly thought that we can harness the Internet for good, and create good faith hive minds and digital town squares for the betterment of our communities.

Fast forward to 2024, and (social) media is a far more sinister, cynical genre. Many sites and media channels now don’t hide that their true purpose is force the (political or oligarch) agenda of their owners down the throat of the audience, and not do any journalism or be a neutral platform. Low-quality AI slop pollutes everyone’s feeds and minds. Chinese and russian troll factories use any media to push their poison and propaganda into Western brains. Engagement farming and intentionally raising other people’s anxiety levels are a thing.

Many people mistake all of this to be the general state of the world: indeed, that is what many (social) media companies want you to think. But it is just not so. I remain optimistic about the state of the world and humanity; it’s just that (social) media now provides an increasingly dark and distorted picture of it.

russia’s full scale invasion of Ukraine in February 2022 provides yet another twist on the whole media space. I changed my use of social media shortly after it happened, and posted a statement about the change, which has aged quite well. I do much of my social media usage these days not as my full self regular human being persona, but as a NAFO brain damaged cartoon dog. NAFO is an online movement that fights russian poison and lies, and collects material assistance for defenders of Ukraine.

I continue to use different sites for different purposes and with different personas. This site remains my home base, from where different things branch out.

Here are the sites relevant in this context, categorized into whether my usage of them decreased, remained the same, or increased.

Use less of Twitter (X)

Rebranding Twitter to X must be one of the dumbest rebrands ever. So, it remains Twitter for me. It turned into garbage after Elon Musk bought it. Here is my longer writeup about how Twitter, and staying on Twitter, these days is harmful and amplifies the extremism and hate there.

I continue to use Twitter, but it is no longer a serious place, and my heart is no longer there. I just do my NAFO work, and I fully expect to get banned soon. I have exported all my content from there, and I have nothing to lose. I have been on Twitter since 2007 and I was sort of attached to it in the past, but letting go of things, especially intangible things in your head, is a valuable life skill. It is unhealthy and dangerous to tie yourself and your identity too much to any single social media account.

Facebook, Threads, Instagram

I still have a Facebook account, mostly because of Messenger and some private groups related to my kids’ activities. I find zero value in the public part of Facebook, and I deleted all my public content from there. (Kudos to Facebook for providing a tool that lets you review and delete all your past activity.)

I’m interested in social media as a tool where we make sense of the world and our communities. Facebook, Threads, and Instagram are intentionally designed to work against this purpose, and suppress political content:

“Meta’s decision to artificially limit the reach of political content on Threads, [Instagram, and Facebook] is itself a POLITICAL decision,” Judd Legum posted last year. “It privileges those who benefit from the political status quo by suppressing information that could disrupt the status quo while elevating entertainment, sports and other topics that do not threaten the powerful.”

Meta deems any content that discusses social justice or identity (things like LGBTQ rights, reproductive justice, etc) as “political”. The company says it will restrict any image, video, or text post that “identifies a problem that impacts people and is caused by the action or inaction of others,” which is an incredibly wide swath of content. If you speak about these things on Meta, your reach will be limited and your account will be surfaced to fewer people.

Meta’s undue restrictions on “political” content matter because social media has a profound impact on how people understand their communities and the world.

I tried out Threads, but I did not find any value in it. I just don’t seem to resonate well with Mark Zuckerberg and his universe any more.

Flickr

I have nothing bad to say about Flickr. To the contrary: I give it high praise, and can recommend using it. It is a rare example of an online thing that has not enshittified over many years, and just keeps doing its thing. I had photos on Flickr from 18 (yes, eighteen) years ago. Back in the day, Flickr gave me the code to embed them to my website, and these embeds just remained working for all these years. That is remarkable and worth praising, given that the modern Internet environment is such that no one gives a damn about the longevity of anything, and things break and get modified regularly with little warning.

Having said all that, I asked myself two questions: (1) why am I paying 70€ a year for Flickr Pro? (2) why do I keep all these photo galleries from my past publicly available there?

I simply decided that I don’t need to do either of these things any more. I ended my subscription, and replaced all the Flickr embeds on my blogs here with just regular images that I host myself. It was a lovely trip down the memory lane as I wrote some interesting travelogues in the past years that were now also interesting for myself to read and reflect upon. My Apple Photos albums also had got a bit out of control over the years as I had’t kept them all that tidy, so this prompted me to tidy up my local albums as well.

So, I still very much recommend Flickr if you need a reliable place to host and share your photos online. It’s just that this need disappeared from my own life.

RSS

I am very much a believer in open web, and the original vision of Internet: that anyone, anywhere can host and publish their own content, and that there are no gatekeepers. This blog and my own site is a small part of open web as well. People hosting their own blogs and reading other people’s blogs with RSS is a good example of open web in practice.

I used to read a lot of content via RSS with Feedbin and NetNewsWire, and was actively interested in NNW development. But over the past few years, this just… faded. I found myself not doing this any more because everything else (since 2022, this largely means russia’s invasion of Ukraine) took priority. It’s kind of sad and ironic that my open web and RSS use declined given how much I believe in the theory behind these, but again, letting go of things and not being religiously and fanatically attached to anything is a valuable life skill. So, I just canceled my Feedbin subscription and saved my subscriptions from there in a safe place for possible future use.

Goodbye, Feedbin

Again, I only have good things to say about Feedbin. (And I was one of their early users and was grandfathered into a ridiculously cheap plan until the very end now years later, which was totally unnecessary but very kind of them.) If you are looking for a good “RSS server” which aggregates things for you and provides a nice reading experience and backend for many RSS reader apps, definitely check it out. Same for NNW and many other RSS clients. I hope a day will come when I shift more of my media use back to RSS.

Unchanged LinkedIn

My LinkedIn use is unchanged in the sense that I haven’t been using it all that much to begin with. I have a profile there and I use it for some private messaging and a directory of professional profiles. But I don’t find much value in the content, feed, and social parts of LinkedIn. I very rarely post things there, mainly things that I think would be useful for people to see about me in a professional context. But LinkedIn was one of the early promoters of low-quality “AI slop” and provided AI writing tools for its users even before they took off in mainstream.

I don’t really trust the content on that site. I would never make judgements about anyone based (only) on what their feed and content look like, since it can all be gamed. I’m really not interested in any of the influencers and engagement type of activities going on there.

Use more of Substack

I started a newsletter this year! The initial motivation for this was that I had written some threads on Twitter which I thought are worth preserving and re-publishing in a less toxic environment. So I queued some posts up and got a publication started. It’s like my second blog, focused on the topics around russia and Ukraine. I could also have hosted it on the open web, but I wanted to try a new service and see how it helps with distribution, since I do want this content to actually get in front of people. And I did not want to have this content here on my main site, since I want to keep this site more neutral and professional.

Substack is again VC-funded and at enshittification risk. It seems to be alright so far for me, both for publishing and for some of the long-form content that I subscribe to and read there. Some of my RSS reading has now shifted to Substack.

Bluesky

Here is my Bluesky. I use Bluesky these days mostly with my NAFO persona. It is working well as a Twitter replacement for me, and a real source of news and insights about russia’s invasion of Ukraine and everything around that.

I have my doubts about Bluesky. It is VC-funded, and most such things eventually enshittify. For now, I can say that I haven’t seen Bluesky management or owners inject themselves as forcefully into the discourse as Elon Musk has on Twitter, and moderation seems by and far alright so far.

These things may change, but the migration from Twitter to Bluesky was relatively painless for me and many other friends of Ukraine and NAFO. If Bluesky enshittifies too much, I think we would just move somewhere else again.

Mastodon

My Mastodon account is where I keep my professional and technical content, which these days is a lot about development on Apple and Flutter platforms. There is a happy subsection of Apple developer community that found a new home on Mastodon a few years ago after one of the numerous Twitter exoduses, living on both iosdev.space and other Mastodon instances.

Mastodon is the closest big social media platform to open web vision because it’s not VC-funded, and I think has the least risk of enshittification.

Conclusion

I’m on several platforms with different personas and goals. I don’t think there will be, or should be, a single privately owned social media platform where everyone is present. Our communities, societies, and the discussion networks that power them are too important to be exposed to the whims of any single VC-funded private entrepreneur.

Both Mastodon and Bluesky have a take on “federation”. I think by and large, social media federation is a good idea, and pretty close to the open web vision. In any case, my take from the past few years is that I expect multiple networks to thrive, rather than picking one single winner to tie my whole identity to.

My social media audit for the end of 2024 was originally published by Jaanus Kase at Jaanus on January 04, 2025.

https://jaanus.com/social-media-audit
How to use string catalogs across Swift package modules
Show full content

Apple platforms have always supported localizing your apps. String Catalogs are a new addition to the toolkit that were released at WWDC 23 with Xcode 15. They compile down to good old strings files, but the frontend is nicer.

I was curious how to use the new string catalogs in a modularized project that has all its features, including UI, split into different Swift Package modules, that seems to be a good practice these days. It’s quite straighforward, but there are some nooks and crannies that were not obvious to me, so I did a small experiment around that.

String catalogs are just a nicer frontend to good old string files. While this post is about the new string catalogs, most of the concepts apply to string files as well.

TL;DR

Check out the toy project.

String catalogs toy

Following is a review of some of the concepts and tips around this topic.

Basic API

I will use examples from a SwiftUI app.

In a top-level app target, you simply use this API to display text.

Text("Some string")

If you have a key called Some string in your strings catalog, this will display a localized string.

If your code lives in a SPM module and you have a strings file or catalog in the same module, you’d use this code:

Text("Some string", bundle: .module)

This tells the system to load the string resources from the bundle of the current module.

Using strings from another module

How do you use strings from another module?

The use case for this might be that you are providing all your strings from a single module, both to other modules and the top-level app target.

I didn’t find a standard approach for this. So what I came up with is that you put your strings in a separate module with a strings catalog file, and then have this piece of code in the module:

public struct StringsExporter {
  public static var bundle: Bundle { Bundle.module }
}

What this does is it simply exports the bundle resources of this module to be used by other modules.

At the call site in another module, you will then do this:

Text("ChildHello", bundle: StringsExporter.bundle)

This tells SwiftUI to load the string resources from the specified bundle, and seems to work fine.

Allow mixed localized resources

I set up my demo app project so that it does not have any localized strings or other localized resources in the top level app target. This works fine, but you have to do this one extra thing if you use this approach: you have to set CFBundleAllowMixedLocalizations to true in your top-level app Info.plist.

Mixed localized resources

If you don’t do this, the localizations provided by the modules are filtered by whatever localizations are present in the top level app target. The localizations that are not present in the app target will simply be discarded. If you set this key, this filtering won’t be done, and whatever localizations are provided by modules will be used.

There is a thread in Swift forums that discusses the risks of this. If you are using modules from other vendors, you may end up in situations like “Today is Montag, le 1er décembre.” But if you are controlling all the code, it should be fine.

Conclusion

Xcode strings catalogs work fine across module boundaries if you add the glue to export and import them, and allow mixed resources if your top-level app target does not include all localizations. String catalogs can be used flexibly across modules and string tables. All projects can find an approach that suits their needs and fits their project setup.

How to use string catalogs across Swift package modules was originally published by Jaanus Kase at Jaanus on April 16, 2024.

https://jaanus.com/swift-string-catalogs
How I displeased Maria Zakharova
Show full content

Saadaval ka eesti keeles.

I had a fun weekend in Vilnius with my fellas.

This week, NATO Summit is happening in Vilnius. I was not invited there. The weekend before that, there was NAFO Summit at the same place. I was very much invited there, and took part.

As result of our collective fun and games over the weekend, both the spokesperson of russian imperialist barbarian federation ministry of foreign affairs Maria Zakharova and one of Team Navalny key figures Maria Pevchikh became so upset that they pronounced nonsense and attacks towards NAFO.

Zakharova whine

Pevchikh whine 1

Pevchikh whine 2

There is nothing interesting in the attack content – the usual russian vomit, “russophobia,” playing victim etc. Most interesting is that they did it at all, and moreover, with such similar content. This is a big accomplishment and a weekend well spent for NAFO, and myself personally.

What is NAFO?

NAFO is a collective of people around the world who counter russian lies and propaganda online, and donate and drive donations to Ukraine.

You can find many stories of NAFO in the press. This recent story in CNN is good and explains most of it, I won’t re-explain the basics here.

NAFO Vilnius Summit

A while ago, the idea emerged to organize a real-life NAFO gathering, and Vilnius right before the NATO summit turned out to be a suitable time and place. Over the weekend, about a hundred fellas gathered, and we had a great conference with an interesting program.

There was a public live stream from the talks. Watch the recording here. If you are into topics like information war and russian lies and propaganda, I can recommend it. What you see on the stage, and how people dress, talk and behave is an accurate representation of the NAFO character. The topics are serious, but we talk about them tongue-in-cheek, using jokes and absurd, not taking ourselves too seriously. Still, all remains tasteful and informed by our mission, which always guides all our work.

As expected, some russians were watching the live stream. This is where the events took an interesting turn. On the stage, you can see an inflatable shark with a bunch of other toys. These were never mentioned on stage and they were just silly props, where you can see references to several NAFO memes, for example “Crimea beach party.” You can of course interpret the shark as a reference to the accident with a tourist in Egypt if you want. What’s important to know is that these props never were mentioned in the discussions and we didn’t explicitly bring them up while at the venue.

Sharkgate

russians saw the shark on stage in the video stream. They themselves decided to completely independently and freely to take the bait and get offended. They published a narrative that NAFO is inhumane because it brazenly and shamelessly in a public video stream laughs and cheers at the death of an innocent russian tourist in Egypt.

I was in the room the whole time. I can assure you that the only NAFO contribution to this narrative was placing an inflatable toy shark on the stage. All the rest is added and interpreted by russians.

Back when the shark incident happened, NAFO and Ukrainians spread many memes around it, with many topics like “even the sharks hate russians” and all. Some of them perhaps crossed the line of good taste (and this line is highly debatable in an environment of real and information war). I didn’t spread some of them, but I absolutely will not condemn the people who did. Dark humor helps Ukrainians during the war.

This was already a while ago. The toy shark on the stage brought all this back to russians, and both the government and Navalny banged the drum on this. The only thing that they accomplished, of course, is to point out once again that russians cry harder over one tourist than they do over many thousands of Ukrainians whom their own russian soldiers continue to kill, deport and gang rape every day.

Fotodega säuts

One of these photos caused an uproar of indignation among russians worldwide pic.twitter.com/cC4B1jL0tI

— Kate Levchuk 🇺🇦 (@KateGoesTech) July 10, 2023

My own little contribution is at play here, which justifies the title of this post. All Sunday, we walked around Vilnius and did different activities, with this silly shark staying with us the whole time. On Sunday night, we participated at a rally to support Ukraine membership of NATO, and I happened to take and publish this photo with the shark there. It could as well have been someone else and there wasn’t much planning or thought around this. Just took and published a photo, that’s all. But the photo and russian reaction to the Sharkgate quite well summarizes the nature of NAFO, and the absurdity of russian narrative and crocodile tears. And is good study material to those who still think Navalny is anyone’s friend and savior.

Kaja Kallas and russian embassy

Some other events happened at the NAFO Summit which perhaps contributed to Zakharova’s angst.

Lithuanian Foreign Minister Gabrielius Landsbergis honored the summit with an address in person, and Kaja Kallas sent us a video greeting. This lent credibility to the event and of course upset russians. What silly events do these Baltic ministers go to, and who are they, anyway?

Greetings to #NAFOfellas at the first-ever #NAFOSummit in Vilnius.#NAFO is a living example of how to disarm Russian disinformation with humour, intelligence and enthusiasm. Behind every Fella is a real person who believes in #Ukraine’s victory.

Thank you for your service. pic.twitter.com/VWOkAe3udr

— Kaja Kallas (@kajakallas) July 8, 2023

In her address, Kaja Kallas said these words:

“Behind every fella is a real person who believes in Ukraine’s victory.”

This was not the main point of the address, but it touched people’s hearts and took off. Fellas took this as an invitation to post their real photo next to their avatar. When you look at the stream of these photos, you see that NAFO is diverse. russian trolls of course like to lie that we are a bunch of losers in our parents’ basements. In reality, we are men, women, young old, from all countries and all walks of life. Search “behind every fella” from Twitter to see us. This is a powerful message when fighting with anonymous trolls, because the russian lie factory cannot respond with the same.

Also, we went to troll the russian embassy in Vilnius on Sunday. Well, “trolling” is doing a lot of work there. We just hung out for a little while and took some photos. But I bet the nerves in the embassy were tight before the NATO summit, and 50 people, many in Ukrainian gear, surely got their attention, and a report was sent to Moscow.

NAFO Vilnius

When I look back at the whole weekend and especially Zakharova’s and Pevchikh reactions, I can’t help but think, “if the russians are howling, we did it right.”

Donate to Ukraine

Fun and games aside, the war situation remains serious. Ukrainians continue to defend the freedom of Europe on the battlefield, and they need our help. Me and you cannot buy tanks and airplanes. What we can do is send vehicles, drones, medical equipment, and many other supplies that the defenders continue to need in large quantities.

There are may people in Ukraine, Europe and elsewhere in the world who organize the equipment and assistance projects all day, and very much need our contributions. Unfortunately we have had some scams and scandals where donations have not reached where needed. The full-scale war has lasted for over a year. It may seem that the Ukrainians are already winning, and we can afford to get tired and look away.

No, we can’t. russian invasion continues, and we cannot look away. Please donate to Ukraine and help them in any other way that you can.

Where to donate? My Twitter profile has some links to safe projects endorsed by Ukrainians. There are also local efforts in many countries, but I don’t know them all and don’t wish to unfairly showcase some while not mentioning others. There’s info in social and regular media. Please look it up, verify which ones are trustworthy, and continue to spread the word and donate.

How I displeased Maria Zakharova was originally published by Jaanus Kase at Jaanus on July 12, 2023.

https://jaanus.com/nafo-zakharova
Using Swift snapshot testing with Xcode Cloud
Show full content

swift-snapshot-testing by Point-Free is an excellent modern approach to Swift snapshot (screenshot) testing. It doesn’t work out of the box with Xcode Cloud. This post is about how I made it work correctly with Xcode Cloud.

TL;DR: gist with the code.

Background

snapshot-testing default behavior assumes that the environment running the tests has access to the source repository where the reference snapshots are kept. This is a fine assumption when developing and testing locally and also in many CI environments, but not so in Xcode Cloud. Xcode Cloud is designed so that building and executing the tests are two discrete steps that happen in different environments, and the test runner does not have access to the source code, except the ci_scripts folder that seems to get special treatment.

When you set up snapshot-testing with its minimal default configuration and test locally, everything works. When you push your code and tests to Xcode Cloud and run them, the tests will fail because they do not find reference screenshots. You will see the error about snapshot-testing saving new screenshots, but of course the environment is cleaned with every run, so next run will have the same failure.

Many people have devised clever solutions about hacking the screenshots to be saved into ci_scripts, so that they get transferred across environments. This felt too hacky for me, so I thought about a better way to do this. Turns out that there is one: you can package the screenshots into the test bundle.

Storing snapshots into the test bundle

I asked about this in WWDC 2023 Ask Apple session, and got this helpful comment.

Ask Apple suggestion about bundling the screenshots

I had of course previously used resources bundled in tests, but hadn’t thought too much about it in the context of snapshot testing. It makes sense - the test bundle is cleanly transferred to the test runner in Xcode Cloud, and can use the snapshots that are bundled there.

With this in mind, I set myself the following design goals for my solution.

Minimal deviation from standard snapshot-testing behavior. I like what snapshot-testing does out of the box: for example, storing the snapshots in __Snapshots__ folder adjacent to the test code. Whatever I do for Xcode Cloud should be a thin clean layer on top of this.

Should work with both SPM test targets as well as top-level “xcodeproj” unit test targets. I have tests, including snapshot tests, that are run and built in both of these ways. Both kinds of snapshot tests should run correctly in Xcode Cloud.

Support testing multiple locales. I specifically test with multiple locales to assure that my localization is correct.

Minimal impact to call site. The snapshot testing API from the test functions should change as little as possible. When you build the tests, you shouldn’t need to know about Xcode Cloud.

Easy to understand and maintain. I don’t want to think too much about this. Whatever I do should be resilient and remain working with minimal maintenance. This is why I am doing this blog post and have comments in the code–mostly for my own future self.

So here’s my solution to make swift-snapshot-testing work correctly in Xcode Cloud in three easy steps.

Step 1: get comfortable with snapshot-testing locally

Before talking about Xcode Cloud, youy should have snapshot-testing set up and working locally. My solution builds on its standard behavior of storing snapshots next to your unit test classes in __Snapshots__ folder. It’s fine if you customize it, you’ll just need to accordingly customize the next steps then.

If you now run your tests in Xcode Cloud, you should see snapshot-tests failing with “no snapshot found, storing a new one.”

Step 2: bundle your snapshots into the test bundle

This step will vary depending on if you are working with SPM targets or xcodeproj app target.

For SPM, you just add the copy step into Package.swift in your test target:

.testTarget(
  name: "MyViewTests",
  path: "If/There/Is/Custom/Path",
  resources: [
    .copy("__Snapshots__")
  ]
)

For xcodeproj app target, you should add the folder containing the snapshots into the test target: make sure the “Target membership” has the test target checked for the folder.

You may first attempt to add multiple __Snapshots__ folders from different tests into the test bundle. When you do this, Xcode will return this kind of build error.

Error for duplicate Snapshots folder

It doesn’t like multiple __Snapshots__ folders being added into the same test target. I thought that it would be smart and consolidate the contents of those folders into a single top-level __Snapshots__, but it doesn’t do that.

The solution is to not add the __Snapshots__ folders to the test target, but add the folders under them, one level down. The names of those folders should match the names of your test classes and are hopefully unique across your project. I did this by removing references to the __Snapshots__ folder from my Xcode project, and dragging the lower-level folders with snapshots into the project instead, which I added as members of the test target.

At the end of this step, everything looks like before: your snapshot tests continue to run locally and fail in Xcode Cloud. However, if you inspect your xctest bundles in DerivedData, you should see the screenshots bundled in them for both SPM targets and xcodeproj targets. In the first case, screenshots are in __Snapshots__ folder, and in the second, they are directly contained in the test resources.

Here’s what the folders look like for one of my SPM targets. The xctest target looks like this.

SPM xctest bundle contents

If you inspect the Library_SettingsTest.bundle that contains the resources, you should see the screenshots.

SPM xctest bundle resourcrs

It will look similar for the app target, except that the xctest is inside your app target Plugins, and there are no intermediate Library_something.bundle and __Snapshots__ folders.

By this point, you should see that the screenshots are present in your test bundle. Let’s now make the snapshot testing library pick them up, which will make the tests to run correctly in Xcode Cloud.

Step 3: instruct snapshot-testing to use screenshots from the test bundle

Here is my test function that implements this.

The core idea is that the test runner checks whether a screenshot is present the test bundle. When true (which is the case for Xcode Cloud when everything is set up correctly), the runner uses screenshots inside the bundle. When false (which may be the case if you have written a test locally but not yet recorded snapshots), the runner falls back to snapshot-testing default behavior of using a folder adjacent to the test code, auto-recording the snapshot if needed.

When calling this function from SPM tests, use this:

assertSnapshot(
  view: someSwiftUIView,
  testBundleResourceURL: Bundle.module.resourceURL!
)

When calling in top-level xcodeproj app target tests, use this:

assertSnapshot(
  view: sut,
  testBundleResourceURL: Bundle(for: type(of: self)).resourceURL!
)

The testBundleResourceURL parameter is a bit unwiedly, but I couldn’t think of a better way to pass the information cleanly and correctly over to the function from both contexts.

I’ve written it up as a gist rather than a SPM package, because the requirements may vary, and this is currently implemented for only one device and fixed view size. If you are comfortable with snapshot-testing, you can surely adjust this to your own needs.

One other thing you need to do when using my solution is to adjust your Xcode workflow to use just “iPhone 14 Pro” instead of “Recommended iPhones”.

If you have done everything above correctly and followed the above steps and I didn’t mistype anything, you should now see your snapshot tests passing in Xcode Cloud. 🎉

Using Swift snapshot testing with Xcode Cloud was originally published by Jaanus Kase at Jaanus on June 20, 2023.

https://jaanus.com/snapshot-testing-xcode-cloud
Canopy: write better, testable CloudKit apps
Show full content

I released Canopy: a library that helps you isolate CloudKit dependency and write testable code using CloudKit.

Canopy is the portion of Tact code that interacts with CloudKit, to transfer the content to and from the cloud. I’ve been working on Tact using CloudKit for a few years now, and accumulated a lot of insights and opinions on the way.

A while ago, I had the idea to extract the CloudKit part of Tact and release it separately like this. I had a few goals in mind.

First, I believe that it does provide real value to developers who use CloudKit, and want to have cleanly testable code and isolate all dependencies. I have been working with The Composable Architecture a lot lately, and it certainly has influenced my thinking about isolating dependencies and testable code, which you see reflected in Canopy. You can use Canopy with or without TCA and swift-dependencies.

Beyond the functionality, Canopy is also an expression of myself as a developer, and a public showcase project. I don’t have other recent examples of code online, and wanted to have something that I can show if needed. That’s why I took the time to make Canopy a complete project: it includes not only the library itself, but a documentation site and example app.

I had several “firsts” in this project. For example, the documentation site was the first site I have built with DocC. There were third-party tools available to generate documentation, but I find more and more that I make my own life easier if I stick with first-party technology that comes straight from Apple. DocC is one such example, and I had a pretty good time with it in this project.

I greatly enjoyed putting the whole package together. For better or worse, it reflects my shape as a maker of things on Apple platforms as of early 2023. In the spirit of testable code, Canopy itself is well tested, and the important parts have 100% or near-100% test coverage. I’m pretty happy to see this coverage.

Canopy test coverage

Shipping things naturally includes compromises. I carefully thought through the scope of the project, and wrote it up on the project motivation page. One compromise I chose to make is that Canopy produces a number of warnings with Xcode 14.3. Halfway working through Canopy, Apple shipped Xcode 14.3 with Swift 5.8 that enables Sendable warnings for many Apple system types, including CloudKit types like CKRecord. Canopy does indeed ship these types across actor boundaries, which currently produces warnings like this.

Canopy warnings

It annoys me a lot, but I chose to ship with these and not rethink my whole approach halfway through. I anticipate that WWDC 23 will bring updates to Apple technology platform and direction regarding the continued migration towards Sendable types, and this will inform future work on Canopy. My goal is to make Canopy safe and warning-free with clean code and without any tricks.

I don’t have much feedback about Canopy, other than it has over 100 stars on GitHub as of June 2023. I have no idea if anybody actually uses it. There’s no issues filed, which tells me that either it works fine for people, or nobody else besides me uses it. The latter is fine–I did release Canopy mainly as a showcase project, and am not chasing numbers as the immediate goal.

I intend to keep working on Canopy and maintaining it, first and foremost to support the needs of Tact, but also for the benefit of anyone else who might be using it. If you have any thoughts or feedback, please do reach out.

Canopy: write better, testable CloudKit apps was originally published by Jaanus Kase at Jaanus on June 02, 2023.

https://jaanus.com/canopy
How to set up monitoring for your Mastodon instance with Prometheus and Grafana
Show full content

I’m not much of a “server person”. But there’s no escaping servers and cloud if you are serious about working in technology. Applications and data live in the cloud, and it’s always good to have understanding of how it works, even if you mostly work on the client side.

Recent surge of interest in Mastodon took me to setting up some instances and helping to operate them–some on my own, and some as part of a group. Such projects are a useful vehicle to learn new things in the context of a real application, and one of the areas I was interested in, is monitoring.

I went through the process of setting up monitoring for some Mastodon instances with Prometheus and Grafana. I haven’t worked with these systems before, so it was a basic crash course to me, which I think I somewhat passed, as I understand the basic concepts now. This post is mainly a set of notes and copypastable instructions for my future self, but there’s nothing secret here, and it may be of value to others too.

This material is largely based on the excellent series of blog posts by IPng Networks: part 1, part 2, part 3. That series assumes a bit more background knowledge and doesn’t cover the basics, like installing Grafana and Prometheus. So my post fills in some gaps in those.

Okay. Let’s dig in

Mental model of Grafana and Prometheus

Here is a naive explanation of what these are. Prometheus is a tool for collecting metrics, and Grafana displays those metrics on dashboards.

Here’s how I think about them.

Diagram about Prometheus and Grafana

Prometheus has the concept of exporters - they export metrics about a particular application or subsystem from a particular host. One key exporter provided by Prometheus is node exporter. It has nothing to do with Node.js - it means “node” as in a physical machine instance (which may be really physical or a VM).

The exporters all run a small web server which exports metrics in specific text format. There is a list of canonical port allocations for exporters.

Next, there is a central “prometheus” agent, which collects and stores metrics from the individual exporters, and exposes yet another small web server. Prometheus also has its own web UI to visualize the metrics, but I won’t look at that since I’ll use Grafana.

Grafana can consume data from many sources, with Prometheus being one (and in this post, the only) source. It visualizes the data as dashboards. It can do many other thigns like generate alerts, which is not covered in this post.

This was a very quick and naive explanation to help establish a basic mental model. These systems have a lot more depth which is beyond the scope of this basic post.

So with the basic mental model in place, let’s install some software.

Starting point

I assume the following starting point.

  • I have a Ubuntu host running Mastodon.
  • I have superuser access on the host, and know how to run some basic Linux configuration and sudo. (If I already got Mastodon up and running, I am more than covered here.)
  • I will install all monitoring on the same host.
Install Grafana

Let’s get an empty Grafana up and running before we fill it with data. Grafana is present in many APT repositories, but the version of Grafana that comes from my hosting provider is very old. Fortunately, Grafana provides its own APT repository. Follow the instructions here. I chose to install the open source version.

Now, you follow the instructions, you start the daemon, the server is running… or is it? How do you know?

Here is one important command you should use throughout this setup process: sudo lsof -nP | grep LISTEN. This shows all applications that are listening on some ports. I mentioned above that we’ll be installing a bunch of small web servers, and this is a good way to confirm they are actually running.

When you get to this point, and look for grafana in the list of running servers, you won’t see it at first. What is going on?

By default, both Grafana and Mastodon listen at port 3000. Since Mastodon is already running on that port, Grafana couldn’t, and fails to run.

To confirm this, here is another useful command: sudo journalctl -u grafana-server (replace “grafana-server” with whatever daemon you are interested in). You will likely see something like this.

Jan 11 08:01:35 example grafana-server[584534]: logger=server t=2023-01-11T08:01:35.790401765Z level=error msg="Stopped background service" service=*api.HTTPServer reason="failed to open listener on address 0.0.0.0:3000: listen tcp 0.0.0.0:3000: bind: address already in use"
Jan 11 08:01:35 example grafana-server[584534]: logger=secret.migration t=2023-01-11T08:01:35.796920608Z level=error msg="Stopped secret migration service" service=*migrations.DataSourceSecretMigrationService reason="context canceled"
Jan 11 08:01:35 example grafana-server[584534]: logger=infra.lockservice t=2023-01-11T08:01:35.797571767Z level=error msg="Failed to release the lock" error="context canceled"
Jan 11 08:01:35 example grafana-server[584534]: logger=server t=2023-01-11T08:01:35.798076811Z level=error msg="Server shutdown" error="*api.HTTPServer run error: failed to open listener on address 0.0.0.0:3000: listen tcp 0.0.0.0:3000: bind: address already in use"
Jan 11 08:01:35 example grafana-server[584534]: *api.HTTPServer run error: failed to open listener on address 0.0.0.0:3000: listen tcp 0.0.0.0:3000: bind: address already in use
Jan 11 08:01:35 example systemd[1]: grafana-server.service: Main process exited, code=exited, status=1/FAILURE
Jan 11 08:01:35 example systemd[1]: grafana-server.service: Failed with result 'exit-code'.
Jan 11 08:01:36 example systemd[1]: grafana-server.service: Scheduled restart job, restart counter is at 5.
Jan 11 08:01:36 example systemd[1]: Stopped Grafana instance.
Jan 11 08:01:36 example systemd[1]: grafana-server.service: Start request repeated too quickly.
Jan 11 08:01:36 example systemd[1]: grafana-server.service: Failed with result 'exit-code'.
Jan 11 08:01:36 example systemd[1]: Failed to start Grafana instance.

To fix this, edit /etc/grafana/grafana.ini. Add some port where there isn’t anything running already, for example 3100:

# The http port  to use
http_port = 3100

Restart the server with sudo systemctl restart grafana-server. You should now see it in the list of servers:

sudo lsof -nP | grep LISTEN

…
grafana-s 584598                           grafana    9u     IPv6            5587781       0t0        TCP *:3100 (LISTEN)
grafana-s 584598 584607 grafana-s          grafana    9u     IPv6            5587781       0t0        TCP *:3100 (LISTEN)
grafana-s 584598 584608 grafana-s          grafana    9u     IPv6            5587781       0t0        TCP *:3100 (LISTEN)
grafana-s 584598 584609 grafana-s          grafana    9u     IPv6            5587781       0t0        TCP *:3100 (LISTEN)
grafana-s 584598 584610 grafana-s          grafana    9u     IPv6            5587781       0t0        TCP *:3100 (LISTEN)
grafana-s 584598 584611 grafana-s          grafana    9u     IPv6            5587781       0t0        TCP *:3100 (LISTEN)
grafana-s 584598 584612 grafana-s          grafana    9u     IPv6            5587781       0t0        TCP *:3100 (LISTEN)
grafana-s 584598 584613 grafana-s          grafana    9u     IPv6            5587781       0t0        TCP *:3100 (LISTEN)
grafana-s 584598 584614 grafana-s          grafana    9u     IPv6            5587781       0t0        TCP *:3100 (LISTEN)
grafana-s 584598 584615 grafana-s          grafana    9u     IPv6            5587781       0t0        TCP *:3100 (LISTEN)
grafana-s 584598 584616 grafana-s          grafana    9u     IPv6            5587781       0t0        TCP *:3100 (LISTEN)
Install nginx frontend for Grafana

Your Grafana isn’t available from the public Internet. Or at least shouldn’t be. Check that you can’t publicly access http://grafana.example.com:3100. If you can, use some firewall or network configuration on your host or hosting provider to limit access to this port from the public Internet.

A common practice for public servers is to have nginx frontend which does HTTPS termination and reverse-proxies to the actual application. This is also what Mastodon itself does. We will now do this for Grafana.

I will walk through the nginx host setup in typical steps.

First, update your DNS to point grafana.example.com to the IP of this server.

Next, add grafana into /etc/nginx/sites-available, with the following content:

server {
  listen 80;
  listen [::]:80;
  root /var/www/html;
  server_name grafana.example.com;
  index index.html index.htm index.nginx-debian.html;
  location / {
    # First attempt to serve request as file, then
    # as directory, then fall back to displaying a 404.
    try_files $uri $uri/ =404;
  }
}

Link it to /etc/nginx/sites-enabled. Test your configuration: sudo nginx -t. If it looks OK, restart nginx. sudo nginx -s reload. You should now see a default site when you go to http://grafana.example.com.

Now, add HTTPS to the site. Easiest is to use Let’s Encrypt, and have their certbot do all the work. Run sudo certbot and follow the prompts. This sets up HTTPS certificates for grafana, as well as the automatic cetificate rotation (Let’s Encrypt certificates are short-lived and must be automatically rotated). When you did this correctly, you can now go to https://grafana.example.com and see a default site, served correctly over HTTPS without warnings.

Finally, set up the reverse proxy from nginx. Here’s what I use, and what your /etc/nginx/sites-available/grafana should look like in the end.

server {

  server_name grafana.example.com;

  location / {
    proxy_pass http://localhost:3100;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }

  listen [::]:443 ssl ipv6only=on; # managed by Certbot
  listen 443 ssl; # managed by Certbot
  ssl_certificate /etc/letsencrypt/live/grafana.example.com/fullchain.pem; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/grafana.example.com/privkey.pem; # managed by Certbot
  include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

server {
    if ($host = grafana.example.com) {
      return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    listen [::]:80;

    server_name grafana.example.com;
    return 404; # managed by Certbot
}

Test your configuration: sudo nginx -t. Restart nginx: sudo nginx -s reload.

Launch Grafana, set up admin password and users

Finally you can start to see something in your browser. Go to https://grafana.example.com. You should see Grafana UI.

Grafana

The first login is with user “admin” and password “admin”. It will prompt you to set a secure password. Do that. You can then continue using Grafana as admin, or set up another admin user for yourself. You can also invite more people. We haven’t set up e-mail sending in Grafana by this point, so e-mail invitations won’t work. Just copy the invitation links from the web UI and share those yourself.

Install Prometheus and node-exporter

Install Prometheus from latest official binary following these instructions. Ignore the part about “firewall rules opened for accessing Prometheus port 9090” because we won’t access Prometheus from public Internet.

You should now see that Prometheus is listening on your server:

sudo lsof -nP | grep LISTEN

…
prometheu 585995                        prometheus    7u     IPv6            5612824       0t0        TCP *:9090 (LISTEN)
prometheu 585995 585996 prometheu       prometheus    7u     IPv6            5612824       0t0        TCP *:9090 (LISTEN)
prometheu 585995 585997 prometheu       prometheus    7u     IPv6            5612824       0t0        TCP *:9090 (LISTEN)
prometheu 585995 585998 prometheu       prometheus    7u     IPv6            5612824       0t0        TCP *:9090 (LISTEN)
prometheu 585995 585999 prometheu       prometheus    7u     IPv6            5612824       0t0        TCP *:9090 (LISTEN)
prometheu 585995 586003 prometheu       prometheus    7u     IPv6            5612824       0t0        TCP *:9090 (LISTEN)

Install node_exporter from the official downloads page. (Replace the URL with the latest version.)

curl -OL https://github.com/prometheus/node_exporter/releases/download/v1.5.0/node_exporter-1.5.0.linux-amd64.tar.gz
tar xzvf node_exporter-1.5.0.linux-amd64.tar.gz
sudo cp node_exporter-1.5.0.linux-amd64/node_exporter /usr/local/bin

Create /etc/systemd/system/node_exporter.service with the following content:

[Unit]
Description=Node Exporter
After=network.target

[Service]
User=prometheus
Group=prometheus
Type=simple
ExecStart=/usr/local/bin/node_exporter

[Install]
WantedBy=multi-user.target

Add the service and check its status. All should look good:

sudo systemctl daemon-reload
sudo systemctl start node_exporter
sudo systemctl status node_exporter

You should now see the node metrics being exported. You can manually query the URL with curl and you’ll see a ton of metrics.

$ curl http://localhost:9100/metrics
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
go_gc_duration_seconds_sum 0
go_gc_duration_seconds_count 0
# HELP go_goroutines Number of goroutines that currently exist.
…

Add the newly added node_exporter to prometheus configuration. Edit /etc/prometheus/prometheus.yml and add this into scrape_configs:

- job_name: node
    # If prometheus-node-exporter is installed, grab stats about the local
    # machine by default.
    static_configs:
      - targets: ['localhost:9100']

Restart prometheus for the new configuration to take effect: sudo systemctl restart prometheus

Add your first Grafana dashboard

We have done a lot of work but not yet seeing any dashboards. Let’s add our first dashboard to Grafana which exposes metrics from the node exporter that you just set up.

Go to https://grafana.example.com. Add Prometheus as data source. The only thing you need to enter here is the URL.

Add Prometheus as Grafana data source

Add the dashboard. Select “Import dashboard” and add 1860 as the dashboard ID. This is the official full node_exporter dashboard.

Import dashboard to Grafana

Click “Load”. In the next screen, select Prometheus as data source. Congratulations, you should now see your first dashboard.

Node exporter dashboard in Grafana

Add Mastodon statsd exporter and dashboard

Let’s now add the Mastodon-specific dashboard that IPng Networks describes in part 3 of their Mastodon blog series.

Download a recent binary version from the package releases page.

curl -OL https://github.com/prometheus/statsd_exporter/releases/download/v0.23.0/statsd_exporter-0.23.0.linux-amd64.tar.gz
tar xzvf statsd_exporter-0.23.0.linux-amd64.tar.gz
sudo cp statsd_exporter-0.23.0.linux-amd64/statsd_exporter /usr/local/bin

Install the statsd mapping file provided by IPng Networks:

curl -OL https://ipng.ch/assets/mastodon/statsd-mapping.yaml
sudo cp statsd-mapping.yaml /etc/prometheus

Create /etc/default/statsd_exporter with this content:

ARGS="--statsd.mapping-config=/etc/prometheus/statsd-mapping.yaml"

Create statsd_exporter.service in /etc/systemd/system/ with this content:

[Unit]
Description=Statsd exporter
After=network.target

[Service]
Restart=always
User=prometheus
EnvironmentFile=/etc/default/statsd_exporter
ExecStart=/usr/local/bin/statsd_exporter $ARGS
ExecReload=/bin/kill -HUP $MAINPID
TimeoutStopSec=20s
SendSIGKILL=no

[Install]
WantedBy=multi-user.target

Add this to /etc/prometheus/prometheus.yml:

  - job_name: statsd_exporter
    static_configs:
    - targets: ['localhost:9102']

Add this to your Mastodon .env.production:

STATSD_ADDR=localhost:9125

Restart the daemons:

sudo systemctl daemon-reload
sudo systemctl start statsd_exporter
sudo systemctl restart prometheus
sudo systemctl restart mastodon-sidekiq
sudo systemctl restart mastodon-streaming
sudo systemctl restart mastodon-web

Verify that you see some output from the exporter:

$ curl http://localhost:9102/metrics

# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 1.5989e-05
go_gc_duration_seconds{quantile="0.25"} 3.2811e-05
go_gc_duration_seconds{quantile="0.5"} 5.1658e-05
go_gc_duration_seconds{quantile="0.75"} 6.6586e-05
go_gc_duration_seconds{quantile="1"} 0.000104236
go_gc_duration_seconds_sum 0.000498889
go_gc_duration_seconds_count 9
…

Import the dashboard to Grafana. Dashboard ID is 17492.

Grafana Mastodon dashboard

Add more exporters

You can now add more exporters, such as for PostgreSQL, Redis, nginx, and ElasticSearch if you have ES enabled. I am not going to provide the details for each exporter here, but you saw the basic pattern above, and it’s the same for all exporters:

  • Install the exporter software
  • Set up configuration for it in /etc/default
  • Set up a system service for it in /etc/systemd/system (if you install it yourself) or make sure it exists in /lib/systemd/system (if installed by package manager). Difference explained here.
  • Add any other needed configuration and permissions. See the specific exporter docs. E.g for PostgreSQL, you will need to set up a special database user to read the database stats.
  • Add a link to the exporter to prometheus.yml to make Prometheus aware of it
  • Restart the services, check manually that the exporter is indeed exporting
  • Add a dashboard to Grafana
Where to go from here

This was a naive post that only covered the basics of monitoring Mastodon with Grafana and Prometheus. You can do many more things with these systems that I did not cover, like build your own dashboards, set up alerts, monitor things across hosts etc.

In more advanced devops environments, many of the things I do here manually are automated, templatized, containerized, scripted etc. This is just a basic manual set up with one host.

For more inspiration about Mastodon monitoring (which I may further dig into myself one day and improve my own dashboards) see lots of info from Hachyderm. For example:

How to set up monitoring for your Mastodon instance with Prometheus and Grafana was originally published by Jaanus Kase at Jaanus on January 11, 2023.

https://jaanus.com/mastodon-monitoring-prometheus-grafana
The end, and the beginning, as Tact enters App Store
Show full content

Tact is now available in the iOS and macOS App Stores. Read our official announcement. Get it here.

Tact app store version on macOS and iOS

With this release, one chapter in the history of Tact ends, and another one begins. It goes from a silly little idea, to a real app in the store. While still being quite small, and possibly silly.

Looking back

I’ve written quite a bit about Tact over the past few years. About why we made it, and how it has evolved.

February 2021. Initial announcement

December 2021. Public beta announcement and my reflection on it

I just re-read these. Everything that I wrote before still holds true today.

Here is a key thought from my public beta reflection:

We designed Tact to be a safe environment, where you can be free from unwanted intrusion. The Internet can be unwelcoming and hostile. We can’t fix all of it, but we can design our own little corner of it, where you only hear from those who you want to hear from.

As I work on Tact, I keep going back to this idea, and it still very much keeps guiding how we think about the project. Today, I also add the aspects of sustainability and predictability. We have been running the project for a few years, and can continue running it in its current form for many more. We won’t need to “pull the rug” and surprise you with abrupt changes to our business or product. It will keep evolving, but in a fashion that extends, rather than disrupts.

Looking forward

I ask you to do three things.

Try

Just get it from the App Store, and try starting a chat with someone. It’s free and easy to get started, and you don’t have to pay anything if you have three or fewer chats.

If you don’t have anybody to chat with, here are the links for myself, Priidu, and our big Everyone joinable chat.

Buy

If you end up having more than three chats in Tact, it tells me you find it valuable and useful. Tact then asks you to buy a monthly or yearly subscription to create and join unlimited chats. You can also choose to buy the subscription even before you hit the chat limit.

The future development speed of Tact depends on how many people buy it. This does not concern operating Tact in its current form, or doing ongoing maintenance. There are very few ongoing operating costs, and we can keep the lights on basically forever in its current “side project” form, even if nobody buys it. We would very much like to spend more time and effort on Tact, but it will directly depend on how many people find it valuable and are willing to pay for it.

You may look at the current offering and say “Tact is expensive, and there are more feature-rich products out there for free.” And you’d be right. I humbly ask you to think of this partly as crowdfunding, and consider not only the current form of the product, but also where we can take the Tact vision further with your contribution.

Say

Don’t be a stranger. After trying Tact, and especially after paying for it, you have every bit of right to say what should happen next.

Tact is not “done” by any means. It has bugs like any software, and many features we chose to postpone, some of which you could even say are “essential” for this kind of chat app. What should we do next, and in what order?

Please let us know, and let’s keep building it together. I won’t promise that we will immediately build every idea that you think of, but we do value and consider all your thoughts and discussion.

Speak with us:

Chat in Tact: myself, Priidu, and a big “Everyone” joinable group

Bugs and discussions: our public community on GitHub

Email: support@justtact.com

Twitter: @justtact

Mastodon: @tact

The end, and the beginning, as Tact enters App Store was originally published by Jaanus Kase at Jaanus on November 28, 2022.

https://jaanus.com/tact-app-store
My Twitter editorial statement for the Russian aggression war in Ukraine
Show full content

Hi, new followers.

I’m Jaanus. Welcome to my personal social media accounts. These days, I’m mainly active on Twitter.

My goal with this editorial statement is to outline who I am, why I post what I post, and what you can and can’t expect when following me.

Russia attacked Ukraine on Feb 24, 2022 (which also happens to be Estonian Independence Day). Russia’s war of aggression is also being waged in the information space and in all of our minds.

Why

Standing against the Russian invasion in Ukraine is the greatest cause of our time. Previously I’d tweet about all sorts of random trivia, technology and work stuff. But none of this matters now as much as beating Russia in Ukraine. I decided to convert my account over to information warfare, and join in.

My greatest motivation is signal boosting. I want to amplify the signals that I think need to be amplified, to help Ukraine win. Ukraine is doing very well in the information space, but Putin’s strategic corruption in the West is deep, and our work is not done until there is no more talk of “appeasement” and “reset” and such. Russia does not belong in the family of civilized nations. Our goal right now, beyond beating them militarily on the territory of Ukraine, is complete economic, military, cultural and mental blockade and isolation.

We don’t exactly know how Twitter algorithm works, but all likes and retweets help. I hope that all my likes, retweets and posts will help surface the material that needs to reach wide audiences. The content of my own account, what you see when you follow me, is almost a side effect and not my main goal.

Some other motivations:

I’m Estonian. I was born in Soviet Union, in occupied Estonia. Today, I live in free Estonia and European Union. We know what is at stake.

I’m Ukrainian. I don’t mean spiritually, like “we are all Ukrainians now in our hearts.” I mean it literally. One of my grandmothers was born in Ukraine. Thus, I’m 1/4 Ukrainian. I haven’t shared it too widely, and due to various life circumstances, I haven’t been in touch with that side of my family all that much. Perhaps that will change now. But I do all the more feel it viscerally. This is my war. It is my people, my kids and grandmas, whom Russian orcs are raping, torturing and killing right now.

Principles

I follow these principles when selecting what to like, retweet and especially, what to post on my own.

I’m a free man. I have no connections with any government agency or international organizations. I work in technology, my work and customers are completely unrelated to the Russian aggression war. This means I don’t have any inside info about anything, but it also means I’m not accountable to anybody, and can post what I like. I am only accountable to my own conscience.

I’m not an expert. You should not trust me, as you shouldn’t trust most things you see on the Internet. I am a layperson about most economic, military and political matters. I am curious, though, and try to keep myself informed and educated.

I cross boundaries. I mediate the world and Ukrainian affairs to my Estonian followers, and vice versa.

I work across languages. Twitter official app and website have a fantastic “Translate tweet” feature. I share tweets originally written in Ukrainian, Russian, German, Estonian, English and many other languages, posting my commentary in either Estonian or English. The machine translation is sometimes lossy and weird, but I understand the gist of the message in most cases, and often rephrase it in my own terms.

I follow the New York Times rule. Especially in my own posts and commentary, I stand by everything that I post.

A balance of rational and emotional. Humans operate as a perfect union between the brain and the heart. My posts also cover both sides. So I don’t only do facts: emotions matter as well, if not more, than the cold hard facts.

Culture and stories. I take great interest in stories, culture, music, as they relate to the war. I try to share the ones that matter to me.

Lossy and sparse. There is a lot going on, and I don’t aim to cover everything. I have some time to spend on Twitter every day, but I have my limits and I myself have unfollowed accounts simply because they post too much. I try to share less, but more relevant things, and mostly when they speak to me directly in light of all the other principles.

Humor and clarity. I often quote-tweet stories that strike me, with my own comment. I try to be light-hearted, and on the humor side. Often, I rephrase technical language into something that’s more human.

More love, less fear. I believe that we as humans have two main motivators for anything we do: love or fear. I try to live my life geared more towards love. Thus, my posts ultimately also are about love of life and freedom for Ukraine, Estonia, Europe, and all of the free world. I don’t fear or hate Russians: I simply wish they would leave Ukraine, stop bothering other nations, and figure out for themselves who or why they are.

Cover image

I want to talk about a photo that I used as my Twitter cover image for a while. It appeared during the first weeks of the war, and deeply struck me. There are many iconic photos from the war, and surely there will be more. This one has stayed the most deeply with me so far.

I have since replaced the cover image with my NAFO fella, but if you look carefully enough, you can find a reference to this photo on my cover image.

A striking picture. And a metaphor for the whole war. She is Ukraine. 🇺🇦 https://t.co/vkKCIg6X3S

— Jaanus Kase 🌻💙💛 (@jaanus) March 11, 2022

Here is the primary source. Here is another Facebook post from the same author explaining the background of the photo. The girl is the photographer’s 9-year-old daughter, the photo was taken on Feb 22 two days before the invasion started, and the shotgun was of course unloaded.

The photo speaks to me on so many levels. Most obviously the behavior of Russian invader orcs towards girls, women, boys, toddlers, infants, and really anyone. Raping, killing, torture. She is ready to confront it. On a more abstract level, she also is a metaphor for Ukraine, Europe, and the whole free world. There is the duality of her being ready, and the whole situation being so unfair. She is a child who should be in school and not carrying a gun. She did nothing, Ukraine did nothing, to deserve this. She is not guilty of anything. Our grandparents in Ukraine, Estonia and the whole Eastern Europe gave no cause to the fascist invaders of 1939 and 2022 to come rape and kill us. This photo has it all.

My Twitter editorial statement for the Russian aggression war in Ukraine was originally published by Jaanus Kase at Jaanus on April 17, 2022.

https://jaanus.com/twitter-editorial-statement
My visit to Chernobyl
Show full content

I visited Kyiv with some friends a few years ago. The most remarkable part of the trip for me was our day tour to Chernobyl. I wasn’t aware of this, but turns out you can visit the area, and it is perfectly safe if you follow the instructions and precautions. There are organized day trips which we took.

Now with the war, currently there is no tourism to Ukraine, and the status of the Chernobyl area is unknown. Russian orc troops were there, and it’s not clear how much they destroyed or what happened (other than some of them getting radiated). I hope tourism to Ukraine resumes soon, and you can visit Chernobyl again too.

I previously shared this series on Instagram a few years ago. I’ve since closed my Instagram account, and, well, the Russian invasion to Ukraine happened. Feels like a good occasion to re-post the memories.

Abandoned kindergarten.

Abandoned kindergarten in Chernobyl area

Abandoned kindergarten in Chernobyl area

Abandoned kindergarten in Chernobyl area

Abandoned kindergarten in Chernobyl area

Abandoned kindergarten in Chernobyl area

The ginormous Duga radar installation. 150m high, 800m long. Secret Soviet military complex in the Chernobyl region. Not strictly part of the nuclear power plant area, just happens to be nearby. Though the radar did use so much power that it used a significant part of the power plant output.

Duga radar installation

Duga radar installation

Duga radar installation

Duga radar installation

Pripyat Amusement Park. It was scheduled to be opened on May 1, 1986. On April 26, disaster struck. A few days later, the city was evacuated. No kids have ever been on these rides.

Pripyat amusement park

Pripyat amusement park

Pripyat amusement park

Pripyat amusement park

Pripyat amusement park

What’s so interesting about this average-looking bunch of trees? Well… 30 years ago, this used to be the football stadium of Pripyat. Incredible how quickly and completely nature returns.

Pripyat football field

Pripyat football field

Pripyat ghost town.

Pripyat ghost town

Pripyat ghost town

Pripyat ghost town

Pripyat ghost town

Pripyat ghost town

Pripyat ghost town

Pripyat ghost town

Pripyat ghost town

Ground Zero. Inside this dome is Chernobyl Reactor 4 that exploded on April 26, 1986. How can I be so close to it? It’s perfectly safe these days. For many years, we were all at risk because the hastily constructed first sarcophagus was crumbling and threatened to release more contamination. Fortunately, before that happened, the New Safe Confinement structure was installed on top of the whole thing a few years ago. Inside it, work can safely continue to dismantle, decommission, research, without risk to the outside world.

Chernobyl Ground Zero

Chernobyl Ground Zero

Chernobyl Ground Zero

My visit to Chernobyl was originally published by Jaanus Kase at Jaanus on April 09, 2022.

https://jaanus.com/chernobyl
Modernizing a 5-year-old UIKit app
Show full content

I just finished modernizing a 5-year-old UIKit app, bringing it into the SwiftUI world and making other adjustments. It’s a simple content blocker app for Estonian media, and the new version is now available in the App Store.

There’s not that much visible change to the user. There’s a UI refresh, but otherwise, everything behaves pretty much the same. Internally and maintenance-wise, though, it’s a different story. I basically rewrote the whole app. Which is not that big of a deal as it sounds—it’s a very simple app. I always use such opportunities not only to work with the outwardly visible features, but also to test some new ideas and patterns and modern platform API. Following are my notes from this work, both as a reminder to myself, and perhaps useful to someone else too.

Old to new architecture

The starting point was a fairly typical UIKit app from a junior/mid level iOS developer (that would be myself, in 2015). This was the time way before SwiftUI, so it was all UIKit, with the UI implemented in a mix of storyboards and code. There was the usual assortment of app delegate, view controllers, model-object-like things. There were a few tests for the purchasing and receipt validation, but no tests for most of the app model and logic.

cloc reports this for the old state of the app.

     151 text files.
     148 unique files.                                          
      12 files ignored.

github.com/AlDanial/cloc v 1.90  T=0.12 s (1138.2 files/s, 376313.8 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
C/C++ Header                    85           4706           9716          25965
XML                             28              0             11           3034
Swift                           20            562            493           1445
JSON                             4              1              0            588
HTML                             2              1              0             71
Markdown                         2              7              0             17
-------------------------------------------------------------------------------
SUM:                           141           5277          10220          31120
-------------------------------------------------------------------------------

Whoa. 25K lines of C/C++? What is that?

That would be OpenSSL headers. In the old world and old StoreKit, I need to do custom validation of the transaction receipts to ensure their integrity, and one way to do this is to build custom OpenSSL and bundle it with your app, to parse the needed data structures. Ugh. Either that, or use an external library or service. With StoreKit 2, I don’t need to do this—the needed verifications are part of the Apple SDK. I can still do it externally if needed, but in this app, I don’t have any reason to do that.

Okay. So that was the old app. How about the new one, after modernizing?

      43 text files.
      43 unique files.                              
       6 files ignored.

github.com/AlDanial/cloc v 1.90  T=0.03 s (1328.4 files/s, 120006.0 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
XML                             15              0              0           1454
Swift                           16            289            192            911
JSON                             3              2              0            472
HTML                             2              4              0             85
Markdown                         2              7              0             17
-------------------------------------------------------------------------------
SUM:                            38            302            192           2939
-------------------------------------------------------------------------------

Overall reduction from 31K lines to 2.9K, over 90%. The big percentage is of course cheating since it previously was largely OpenSSL, but still—it was code living in the project that I needed to maintain.

XML has been greatly reduced. A big part of XML was storyboards that are now gone. The remaining XML is Xcode project files and configuration that cloc for some reason reads as XML.

Swift has gone from 1400 lines to 911, with 114 of those being tests. Previously there were also some tests, but only for the purchasing and receipt validation part that’s now mostly handled by the system, nothing for the app itself.

Now, let’s look at the app entry point, which has greatly improved in clarity. I really like how SwiftUI nudges me to be clear about the app architecture and model. Here is the new app entry point in its entirety.

import SwiftUI

@main
struct PrillikiviApp: App {
    
    @StateObject var buying = Buying()
    @StateObject var filters = Filters()
    @StateObject var preferences = Preferences()
    @Environment(\.scenePhase) var scenePhase
    
    var body: some Scene {
        WindowGroup {
            ContentView(buying: buying, filters: filters, preferences: preferences)
        }
        .onChange(of: scenePhase) { scenePhase in
            switch scenePhase {
            case .active:
                // When app becomes active, trigger loading new info
                filters.loadInfotAtApplicationDidBecomeActive()
            default:
                break
            }
        }
    }
}

I get a lot of insight in these 20 lines of code. I see that there are three model objects. They are separated and have no dependencies on one another. The view part of the app depends on all of them. The app checks for some kind of new information every time it becomes active.

I find this to be way more digestible than a mix of declarative storyboard structure and imperative UIKit code. There is no weird code and connections scattered around in view controllers, app delegate, and who knows where else. Sure, you could have this kind of clarity in the old world if you were disciplined. I freely admit that I’m not. I found it easy to be sloppy. The new tooling nudges me greatly towards discipline.

Making StateObjects aware of each other’s state

In reality, of course, there is a lot more nuance than shown above. I separated the model objects to be clear about their different responsibilities. But they have dependencies. Filters deals with actually loading and applying the content filters that are the heart of my app. Buying deals with everything related to the app purchasing and subscription. Filters obviously needs to know what is the state of the purchase and whether the user has a valid subscription to the app, because the available filters depend on that. How would you model this?

In the old world, I’d consider solutions like putting everything into one big model object, or Buying broadcasting notifications and Filter listening to those, or making the models be aware of entire other model objects by making them weakly-held properties on one another, or using the delegate pattern, or many other tools.

In this project, I reached out to Combine, which is already implicitly used when you use SwiftUI, and you can further leverage for your own benefit. This StackOverflow question showed me idea.

Using AnyPublisher decouples the idea of having a specific types for either side of the equation, so it would be just as easy to connect ViewModel4 to ViewModel1, etc.

Here is my recipe of making Filters aware of the purchased state of Buying.

@MainActor
class Buying: ObservableObject {
    @Published private(set) var currentPurchaseExpiration: Date?
}

@MainActor
class Filters: ObservableObject {
    private var purchaseCancellable: AnyCancellable?
    func connectToPurchased(_ publisher: AnyPublisher<Date?, Never>) {    
        purchaseCancellable = publisher
            .removeDuplicates()
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { newDate in
                // Do something with the received date (note it’s Optional)
            })
    }
}

struct ContentView: View {
    var body: some View {
        WhateverContentView()
        .onAppear {
            filters.connectToPurchased(buying.$currentPurchaseExpiration.eraseToAnyPublisher())
        }
    }
}

Buying isn’t aware of any connections. It just exposes a regular @Published property for SwiftUI. But this contains a whole CurrentValueSubject Combine subject inside, which we will use.

Filters creates a Combine subscriber to the purchased date, but isn’t aware of where exactly it comes from.

ContentView connects the two sides, by grabbing the published property from Buying, and passing it to Filters. Since the published property contains a CurrentValueSubject, the initial value is also sent immediately upon making the connection. You could argue that it shouldn’t be a view making this connection, but rather another model-like thing or the top-level app structure, and you’d probably be right.

This setup affords great testability. So let’s say we want to test some behavior of Filters that depends on some value of the purchased date being received. Here’s how.

func testPurchasedFilters() {
    let filters = Filters()    
    filters.connectToPurchased(Just(Date().advanced(by: 86400)).eraseToAnyPublisher())
    let expectation = XCTestExpectation(description: "Wait for filters connection")
    Task {
        let categories = filters.categories
        let expected: Set<FilterCategory> = … some expected value
        XCTAssertEqual(categories, expected)
        expectation.fulfill()
    }
    wait(for: [expectation], timeout: 1.0)
}

In a test, we use Just to create an immediate publisher from a simple value, without any Buying object being present. From Filters perspective, everything works all the same, it just receives a value. I am creating a Task for asynchronous execution, since all this value propagation and publishing doesn’t all seem to happen immediately on the same thread.

I feel that with this setup, there is “just enough” amount of connection between the objects. They can get the initial values of specific properties, and be aware of changes to those, without having to know anything else about each other’s internal structure, and without having to create extra protocols or any other glue.

Running only one task at a time

Another interesting problem I dealt with was, how to run only one task of a given type at a time? In my app, it would be something like downloading filter content from iCloud, which I use to distribute the filters. You see above how some info is loaded every time the app becomes active. The user may repeatedly do this, and I do not want to do this if there already is such work in progress.

Here’s an article by John Sundell that covers the basics of how this works in the new Swift concurrency world. Although most of the article is about actors and data isolation, towards the end it also provides a recipe of how to set up the tasks so that only task of a given kind is running at a time. I just took that code, simplified, and ended up with this.

@MainActor
class Filters: ObservableObject {
    private var downloaderTask: Task<FilterDownloadResult, Never>?
    private func downloadNewFiltersFromCloudKit() async -> FilterDownloadResult {
        if let existingDownloaderTask = downloaderTask {
            return await existingDownloaderTask.value
        }
        let newDownloaderTask = Task<FilterDownloadResult, Never> {
            guard someCheck else {
                downloaderTask = nil
                return .error
            }
            // Do work to get the result
            downloaderTask = nil
            return .someResult
        }   
        downloaderTask = newDownloaderTask
        return await newDownloaderTask.value
    }
}

I can now call downloadNewFiltersFromCloudKit many times, and there’s only up to one instance of the task ever running.

It lets me quite neatly express what I want to do. The one weak point visible here is that at each site of returning a result, I need to remember to nil out the reference to the task. I also haven’t thought through how error handling and throwing works together with tasks. In this task, I handle all the errors inline and just return a different result to indicate an error. But I could also use native Swift error handling that throws the errors, and I haven’t yet examined how that works with tasks.

Summary

I took an old project and re-wrote it with some modern technologies (SwiftUI, Combine, modern concurrency, StoreKit 2). Next up: perhaps another modernization in another 5 years’ time.

Modernizing a 5-year-old UIKit app was originally published by Jaanus Kase at Jaanus on January 26, 2022.

https://jaanus.com/modernizing-a-5-year-old-uikit-app
A fairy tale about Apple code signing
Show full content

It is the year 2035.

We all use Xcode 25 (VR) on our Apple Glasses and iPad to develop new Magic Reality experiences and ship them to the Magic Store for billions of people to download.

Everything is smooth and shiny.

Except once in a while, when preparing your app, you get the error:

Code signing error > Requires provisioning profile

Nobody knows what this means exactly, or why it appears. Nobody remembers the time when “computers” and “command line” were still a thing. Except a few old men with long beards, called Provisioning Oracles. They live in a cave. The journey there is long and treacherous, yet all Apple platform developers must undertake it once in a while, lest they be forever stuck with this error.

The old men still master the long lost art of command line, Apple developer portal navigation and messing around in Keychain Assistant.

You sigh and take the journey.

“Another one?” They shake their head. But it’s not with contempt. It is with great compassion and understanding. “Come forward, young one. Let’s look at it together. We have seen many like you.”

You sign in to your developer account together. You then step away from the keyboard, and the bearded wise man starts his incantations. You watch with amazement as screenfuls of stuff flash by, as he does things you do not even begin to comprehend.

“There. I fixed your keys and profiles for you. It builds again.”

As you prepare to leave and start to offer your thanks and perhaps some payment, and say goodbye, the old bearded man shakes their head.

“Do not thank me. Thank Apple in their infinite wisdom to give us this great system that is beyond the comprehension of mortals, yet enables us to build magical things. Your greatest thanks to me is that you go forth and build more magical things.”

“Now, leave. The day is ahead of you.”

“And perhaps this is a goodbye, but not farewell. I know you will be back.”

A fairy tale about Apple code signing was originally published by Jaanus Kase at Jaanus on January 10, 2022.

https://jaanus.com/a-fairy-tale-about-apple-code-signing
Toying with Apple link previews and SwiftUI
Show full content

I took a closer look at LPLinkMetadata and LPLinkView. These are part of Apple’s official SDK on all Apple platforms. They let us fetch web content metadata and use an Apple-provided UI, or draw our own UI.

I wanted to understand what kind of metadata is available, and understand the tradeoffs of using Apple’s own view, vs drawing something custom in SwiftUI. To explore all these things, I built a small SwiftUI app, available in GitHub. The app shows you metadata downloaded for a given URL, as well as how it looks with the Apple-provided UI.

LinkPreviewToy on iOS

LinkPreviewToy on macOS

SwiftUI views as function of state

Before diving into link previews, I’ll point out a SwiftUI pattern that I’ve found useful: view as function of state.

“Wait”, you say. “This is not original. This is the main point of SwiftUI. What’s the pattern and your contribution here?”

My contribution is that the other day, I started thinking of this. View as function of state. What would a pure implementation of this look like? Perhaps literally have a state variable driving the view. Since the states of a view are mutually exlusive—a view can only be in one state at any given time—an enum is a natural data type to model this.

Besides the state, views also need data, but the data is often different for each state. What’s the closest that the state and data can be to each other? They can live right in the same data type. Each value of the state enum can have a different set of associated variables, tailored for that particular state. And that can be the only thing that the view cares about: give me the state, and its associated variables.

This means that the main control flow of a view can be a switch statement. This is really neat, because it forces you to think through all the possible cases.

For regular app use, view model publishes the state as a read-only property. For something like SwiftUI previews or testing, there can be a special initializer for the view model which just takes a state and makes it the current state, without doing any other work. This means that you can very neatly make and see previews of all the possible states of your view. I think it’s quite handy.

View as function of state

Okay. On to the actual link previews.

Drilling deeper into Twitter previews

There isn’t really much to say. The code works as advertised. LPLinkMetadata gives you a bunch of metadata, which you can use to draw the UI yourself, or use the Apple-provided LPLinkView view, to get a decent UI.

Something was off with Twitter previews, though. Take a look at this example.

Preview of a tweet

The top part contains all the info that we get from LPLinkMetadata. The bottom part is the LPLinkView rendering of the same metadata. Both should, in theory, contain the same info. Yet… notice how the tweet text is there in the bottom part, and not visible in the top part.

This tells me one of two things. Either LPLinkMetadata contains more info than it publicly exposes, or LPLinkView does some extra work to fetch the missing data.

The answer is not very far. Turns out it’s the first one, and there’s more info in the metadata object than is exposed publicly. Just examine a variable of this type in the Xcode debugger to see it all.

More metadata properties

Especially the summary field looks what we’re looking for. It’s a shame there isn’t a public property for it. If anyone at Apple is reading, I’ve filed FB9836598 on this.

Toying with Apple link previews and SwiftUI was originally published by Jaanus Kase at Jaanus on January 07, 2022.

https://jaanus.com/toying-with-apple-link-previews
My thoughts on Tact public beta
Show full content

My hobby project Tact is now available as public beta.

tl;dr

Tact on macOS and iOS

Tell me more

This was all the “official” info and links you need to get started with Tact. I’d like to add some color with fresh thoughts, as they have crystallized through this past year of working on Tact.

Sometimes it’s easier to say what something isn’t. This reflects the design and focus of Tact, where we intentionally said “no” to a lot of things at the project outset.

No

No personal data. We feel very strongly about this, and think Tact shines here. We do not have access to your communications, and do not want or ask for any personal data. It’s common for mobile communication tools, even the ones that claim to be “private”, “end-to-end encrypted” and all that, to start the relation by asking for your phone number, or siphon in your whole address book to do “contact matching” or whatever they call it. We’re not saying they all misuse the data, but the most private data access is always no access at all. Tact does not ask for your phone number, e-mail address, address book access, or anything else. The only thing that we ask at the outset is your name. It does not have to be real: use your first name, pseudonym, nickname, or anything else. You can optionally also set a picture (again, no requirements for what it represents), and that’s it.

No final form. What you see today in Tact public beta is the first pass. A sketch. A question. A provocation. Does this make sense? Should we continue? You’ll see bugs and rough edges. Priidu and I are our own biggest critics. It pains us to not build everything we imagined, and not build everything to perfection in the first pass, but, “great artists ship”.

No hyperbole. The world and specifically the technology industry are full of big empty words, a lot of crazy fast change, and just the sense of things rushing by and leaving you behind. We’re tired of this, and make Tact “for the rest of us”. We don’t claim to “change the world”. We just make a chat app.

No discovery. There is no way to discover other people who use Tact, and connect directly with them. You connect with other people in Tact by directly sharing invitation links. Sure, you can publish it to your website if you choose, but you can also just directly share it with a loved one and no one else. You won’t have any unwanted “connection requests”.

No Android. For many technical, business and philosophical reasons, we built Tact for Apple platforms, and this remains so for the foreseeable future. It’s what Priidu & I care about the most, and where we want to focus.

No funding. We took Tact to this point without external funding. Priidu and I put countless hours into it over the past months, and we’ve used our own savings to hire a few great Apple platform engineers to help us build. We’ll need a new plan in the new year, be it shipping a paid product, obtaining external funding, shutting the project down, or any other number of avenues. Time will tell.

No calling. No other fancy realtime features either. We chose to limit the scope of this initial version to what we can build and maintain ourselves. Calling certainly fits into the Tact general vision, but we’d need to find our own unique angle. Until then, FaceTime works great, and you can share FaceTime links in Tact for a fairly seamless experience.

No ads. We can’t exactly say what the future of Tact looks like, but we can sure as hell say that it will not involve ads as the primary means to fund our business. Ads have their place in the app business, but Tact is not that place.

Okay. That was a lot of noes. What are the yesses? What do we stand for?

Yes

Craft and technical excellence. There’s always more work to do, but we’re pretty happy with the current technical state of Tact. It’s small, it has design intention behind it, it starts fast, it’s reliable, it doesn’t use a whole lot of CPU or memory. It’s just a decent iOS and macOS app—fully native, of course, on all platforms where it runs.

Children and family. All of us have our lives, and shipping Tact public beta has taken a while because sometimes we’ve needed to live our lives instead of working. I’ve made it a very explicit rule of the project from the beginning, for both myself and everyone else: life comes first, and Tact comes second. What we may lose on a single day because we need to take care of a sick kid, we make up with long-term patience and persistence.

Communication. If you were to look at Tact with an inquisitive lens, hunting for bugs and faults, you’d find many. We’re not done yet, and we needed to heavily focus, optimize and cut scope. What we feel good about, though, is looking beyond any single problem, and seeing Tact as a means to actually chat with each other. Tact has been a part of our lives through various small groups for many months now. It’s been with us at home, for team calls, at child daycare, at the gym, on hiking trails and ski slopes, at hospital beds, in grocery stores, on road trips and travels, in pubs and bars, and anywhere else where we’ve lived and worked. We share our daily joy and misery, culture and memes, daily news, silly jokes, family photos, work material, and anything else that forms the digital fabric of our lives. At that, Tact does a pretty fine job.

Self-expression. Priidu and I think of Tact as a project of self-expression, almost an art performance, or an expensive hobby, rather than “work”, “software” or “investment”. We just like to mess around in Figma and Xcode and put some flesh on the bones of the ideas around communication that we’ve kicked around. We work on Tact because we’re convinced that the world needs it and it will be good business in the end, but we don’t look for a quick return and quick growth at all costs.

Desktop app. The sad state of macOS desktop chat apps in today’s mobile-focused world was a big factor in starting Tact in the first place. We design, build and ship Tact’s native macOS desktop app in lockstep with the iOS version. We are creative professionals who spend considerable time at our computers, and macOS will always be a native first-class target for Tact.

Kindness. We can’t and won’t police what content you have in your private chats. We can still say, though, that in this age of hyperbole and frenzy, we’d like Tact to stand for kindness, dignity, peace of mind, and a bit nicer messages that we share with one another. That has certainly been the case within the Tact team itself: we look after one another and have a lot of camaraderie, despite some of us having never met.

Payment. Tact will be a paid product. We don’t intend to compete on price, because there are many free apps there. We haven‘t yet locked the exact pricing and model: if you have a family group chat, it would be silly to force everyone there to pay. We’ll come up with something smart.

iCloud. You access Tact with your iCloud account, and all Tact content goes from your device straight into iCloud. That’s why there’s no separate “user account” or “registration” in Tact, and why are confident in our privacy claims: we don’t do anything with your chats because we simply don’t have access to them.

Safety. We designed Tact to be a safe environment, where you can be free from unwanted intrusion. The Internet can be unwelcoming and hostile. We can’t fix all of it, but we can design our own little corner of it, where you only hear from those who you want to hear from. You don’t get any “connection requests” from strangers, unless you have chosen to share your personal Tact invitation link with them.

Love. We work on Tact with Priidu and the team because we just want to have this app for ourselves and our friends, and we love working on it. We’ll likely continue in some shape or form for quite a while. We wanted to ship the public beta to gauge the reaction of our extended networks and the wider world. So, here we are.

So, that’s Tact, as of December 2021.

No personal data.
No final form.
No hyperbole.
No discovery.
No Android.
No funding.
No calling.
No ads.

Craft and technical excellence.
Children and family.
Communication.
Self-expression.
Desktop app.
Kindness.
Payment.
iCloud.
Safety.
Love.

Join our journey

My thoughts on Tact public beta was originally published by Jaanus Kase at Jaanus on December 21, 2021.

https://jaanus.com/tact-public-beta
The state of Universal Links as of August 2021
Show full content

I did an investigation of Universal Links. They’re supposedly this great technology that allows HTTPS links to be opened automatically in your iOS or macOS app if all the conditions are right, and thus to provide deep links to your content straight in your app.

TL;DR: I could not get two key pieces of the technology to work: the developer mode, and onOpenURL when using macOS with SwiftUI. Although Apple keeps telling us that custom URL schemes are deprecated and we should switch to Universal Links, the latter is a dealbreaker with SwiftUI app lifecycle. You can use Universal Links, but be prepared for them not being reliable.

Let’s dig in.

Learning about Universal Links

Universal Links have been around for quite a while already. In WWDC19 and WWDC20, there were two sessions with the identical name “What’s new in Universal Links”. Go watch these to learn of important updates. Much of both sessions is dedicated to the nuances of handling links and directing which links are handled by your apps and which aren’t.

Before I dug into handling different links differently, I wanted to get the basics working. I ran into some trouble with that, and the rest of this post will investigate these troubles.

apple-app-site-association file, CDN, and developer mode

Universal Links require that you host a file on your web site at the location .well-known/apple-app-site-association (henceforth I will call it AASA). The simplest content of this file can be like this:

{
    "applinks": {
        "details": [
            {
                "appIDs": [ "ABCD1234.com.example.YourAppId" ]
            }
        ]
    }
}

This tells the system that this website authorizes the indicated app ID to handle its links as Universal Links. If you wish, you can have more info in this file to say which links should and shouldn’t be handled by the app. Watch the WWDC sessions for more info about that. But this simple file is sufficient for a demo that causes all of the links to be handled as Universal Links by the app.

WWDC20 and the platforms of 2020 brought us an important update: AASA is no longer downloaded by the clients directly from my website, but is instead cached by Apple’s CDN in the production situation. Since their CDN cannot access internal development servers, there is a new “developer” mode that you need to enable both on each client device where you develop, as well as specifying ?mode=developer in your Associated Domains applinks entitlement.

Just as a reference for myself, you enable the developer mode on macOS with this command: swcutil developer-mode -e true On iOS, there is a setting “Associated Domains Development”.

WWDC20 video claims that you can use “any valid certificate” to secure the HTTPS server that is serving the association file in developer mode, as opposed to “System-trusted root certificate“ required in production configuration.

I tried a bunch of certificates for developer mode: both self-signed, and a certificate issued by a custom CA added to the system trust root store, as I describe in this post. None of it worked.

The method of debugging this is to connect to your iOS device console with Xcode, and filter it for swcd. I saw a line Trust evaluate failure: [root AnchorTrusted] which was present in case of a certificate signed by custom CA trusted in the root store, and was not present when using a truly trusted certificate (e.g public server whose certificate is issued by LetsEncrypt).

root AnchorTrusted error

In case of a truly self-signed cert, you get different errors: Trust evaluate failure: [leaf AnchorTrusted SSLHostname ServerAuthEKU], and another message saying Failed to verify server trust <private> for task AASA-3619F364-FD45-41DE-AB59-4D4F120377AD { domain: 19….16….0.10?mode=developer, bytes: 0, route: .wk }: Error Domain=SWCErrorDomain Code=100 "Disallowed trust result type." UserInfo={Line=167, Function=-[SWCSecurityGuard verifyTrust:allowInstalledRootCertificates:error:], NSDebugDescription=Disallowed trust result type., TrustResultType=5}

So, one of the following two statements is true. Either the developer mode is simply not working correctly, or I have misunderstood what “any valid certificate” means in this context. There doesn’t seem to be any more info or documentation available about this.

A while ago, I looked quite deeply about HTTPS certificate validation, and how custom root CAs work in modern macOS and iOS: again, I refer you to this post on the topic. If I have a certificate that works for other Apple Transport Security purposes, but does not work for AASA development mode, I question what’s different and special about this mode and what kind of certificates it requires.

Long story short: AASA developer mode works only on public servers whose certificate is issued by trusted root CAs. Custom trusted roots don’t seem to work. Filed Feedback ID for Apple: FB9551780.

Okay. So this was not actually a showstopper for me, since I had access to a development server with a trusted certificate where I could play around with my AASA.

onOpenURL is not called on macOS SwiftUI app when opening Universal Links

The other bug I ran into, tested on both Big Sur and Monterey, involves how the app receives Universal Links.

In SwiftUI, the way to handle incoming Universal Links is very straightforward. You just place a onOpenURL modifier on any view that you’d like to handle the link, perhaps like this:

ContentView()
    .onOpenURL { url in
        print("Got url in content view: \(url)")
    }

Instead of printing the URL, you’d of course want to parse it and do something with it. This is just for the demo.

If you figure out the certificate stuff discussed above, then on iOS everything works pretty much as it should, on both simulator and devices. The code above does what it should, and is reliably called when opening Universal Links.

On macOS, although the system correctly switches to my app upon opening an Universal Link, onOpenURL is never called for me, no matter what I try. This yields an especially unfortunate situation, where Universal Links seemingly work and switch to my app instead of opening the website, but give my app no indication of the link, and so I cannot provide the correct experience for my user since I have no access to the link they clicked on.

I did a test also with AppKit lifecycle, where Universal Links works as expected. For an incoming universal link, I consistently receive the much-longer-named user activity continuation method called in my NSApplicationDelegate class. So it works as expected with AppKit. I’d expect it to work equally well in SwiftUI lifecycle: there’s no sign in documentation that it wouldn’t or shouldn’t. For simpler apps, SwiftUI multiplatform app lifecycle is great. I wish Universal Links worked as well with the SwiftUI lifecycle as they do on iOS.

If anyone at Apple is reading this, I’ve filed FB9544271.

Deprecating custom app schemes for Universal Links

The WWDC videos about Universal Links from both 2019 and 2020 tell us in no uncertain temrs that custom URL schemes for apps are deprecated, and we should instead switch to Universal Links. I found the above two problems quite disheartening towards that end. Custom schemes have their warts, but not these kinds, they work very reliably on all platforms for my practical purposes. So whatever deep-linking solution I end up with, it will probably involve a combination of Universal Links and custom URL scheme with additional fallbacks for cases like the macOS one, where the Universal Link looks like it works, but really doesn’t, so I must provide additional escape hatches and workarounds if the link is really critical for my app.

The state of Universal Links as of August 2021 was originally published by Jaanus Kase at Jaanus on August 22, 2021.

https://jaanus.com/universal-links
Reflections after WWDC21
Show full content

Apple’s annual developer conference WWDC happened this week. The conference keynote session is aimed at the public. The rest is a developer-oriented event where the Apple developer community learns of Apple software platform changes and updates for the coming year.

WWDC21

I don’t have much to say about the content that’s not already been said elsewhere. I posted my notes for Keynote and Platforms State of the Union on the Tact blog.

This WWDC was a fully virtual event for the second time in the row. As I went through the conference experience, it struck me that there is a very curious and diverse mix of channels this year that I use to access the event. Unlike last year where Apple had to scramble to make the event online (COVID happened just a few months before the event), this year they had proper time to prepare. I think the whole mix of channels is interesting to study for other future virtual events, and provides some intriguing clues about Apple’s thinking.

Truth be told, I haven’t been to many virtual conferences (ever, or over the past year) so I can’t compare the experience with other similar events. I have been to a few WWDC-s in person, though, and can definitely make a comparison between the in-person and online WWDC flavors.

Let’s dive right in and go through the different ways to participate in WWDC.

Sessions

Sessions are the heart of WWDC. They are focused presentations about a specific subject, often complemented by code snippets and example projects. You watch the sessions either on the web, or in the dedicated Developer app which exists for macOS and iOS (and maybe tvOS too? I don’t know about tvOS).

WWDC sessions on the web

WWDC sessions in Developer app

One WWDC session in Developer app

During the in-person conferences, the sessions were presented live on stage (and recorded for video distribution in more recent years). They were divided into a daily schedule of roughly hour-long blocks. A big part of live WWDC was moving around between the different rooms and sessions (and often standing in line to get into the more popular ones, with some of the rooms even being full sometimes, overflow rooms sometimes used etc).

The modern video sessions have a high production value, are all released at the same time on a given day, and have varying lengths according to the actual content needs. I join those who find the video experience to be much more usable, as you are not dependent on a strict daily schedule and can watch them at your own pace. You don’t have worries like “these two things I’d like to see are happening at the same time”, “can I fit in the room”, “none of the sessions during this hour apply to me so it’s almost an hour of the conference wasted”.

The Developer app is useful for watching the sessions, as you can manage your own playlist across all year’s WWDC-s, and each session includes a transcript and direct links to extra resources.

Digital Lounges

Digital Lounges were a new and exciting thing this year. Essentially, a heavily moderated Slack community with a packed program including trivia quizes, coding exercises, “watch a session together” events, “meet the presenter”, and more.

Most of the time, you couldn’t speak freely in channels. To ask a question, you submitted it to a form, and it was then posted with an answer in the public channel (or sometimes replied to privately, e.g to direct you to a similar question already answered before). Discussion was open in many threads though.

The lounges were open during Californian business hours because those were formally the hours of the conference, and I suppose that’s when the staff manning the channels was working. There was massive staff participation. You could indeed see many presenters or experts answer questions and actively participate in thread discussions.

It was curious to note that the profiles of all Apple staff were Memoji. This syncs with the opening of the keynote, where Tim Cook walked into a theater full of emoji audience, which I suppose was all the Apple staff participating in the event. I didn’t check if the emoji in that opening scene were random or actually corresponded to Apple staff, but I wouldn’t be surprised if the latter was the case.

It would be great to post a screenshot of the Lounges, but I think the Lounges terms prohibit us from doing that, so I won’t. The information was freely moving though. As one of the staff members said, “The Digital Lounges are not confidential spaces, and you may share the information you’ve learned with others.” Not much to see on the screenshot though—it looked and worked just like regular Slack.

Participants were encouraged to take any notes and screenshots for their own reference, since the lounges would close after the event. Many participants thought the closing was a shame, as there was lots of great knowledge shared there by Apple staff, some of it quite unique, and it would be useful to have this as a searchable resource. On the other hand, I also understand treating it as a real “event lounge“ which is an experience limited to a specific time and place.

It’s curious to see Apple embracing Slack so much. Employees discussing its internal use on Twitter. Some #WWDC21 events in Slack. Xcode Cloud posts to Slack. Slack Slack Slack. I guess they think it is the state of the art and the best on the market in its class?

— Jaanus Kase 🌻💙💛 (@jaanus) June 8, 2021

I cannot help but note that Apple seems to be endorsing Slack big time. They don’t do frequent integrations with third parties in this fashion. So for example, the fact that Xcode Cloud can natively post notifications to Slack has quite a lot of weight and meaning. I suppose Apple has decided that Slack simply is the state of the art in professional enterprise business chat space. Yet, it’s not so important that they’d want to have it in house (yet?), as they do with their core technologies, at least at this time.

Labs

Labs have made a successful transition from physical to virtual space. I’ve participated in both kinds, and the virtual format has worked better for me.

For those who haven’t been in the labs – you book an appointment on a given topic, and you get assigned a call slot of half an hour. At that time, you call in, and have a regular video call, possibly with screen sharing, with Apple engineers who are experts in that area. Myself and many other lab participants can say that these have always been useful, and the all the engineers I have spoken with are knowledgeable, helpful, and kind.

A thought struck me about labs, though. Essentially, they’re customer support for developers. Can you think of another product where customer support is available only once a year for one week? I’m exaggerating, of course, as there are many other ways for developers to get support besides the labs.

Developer forums

Apple developer forums

The sessions refer to developer forums that are a public platform for discussing Apple technology, with varying level of participation from the staff. All WWDC sessions are numbered and have their own corresponding tag in the forums, with varying levels of activity. Somehow I didn’t feel the same excitement with forums as with other channels, where there seemed to be a larger amount of more consistent participation by the staff.

Swift forums

Swift forums

Although not formally a WWDC channel, Swift forums are another valuable source of developer information, related to Swift language and ecosystem. This WWDC brought us a particularly contentious issue of concurrency in Swift. While it was anticipated to be released with Swift 5.5 and announced at WWDC, it was a surprise and disappointment to many developers that it wasn’t backported to previous platforms. As of this writing, Apple has said that it isn’t backported, but they are considering doing it. This is a highly informative thread on the matter that accurately presents all information and viewpoints, and also modes of speaking and behavior.

Employee Twitter

A curious thing has happened over the past year or so. More and more Apple staff is making an appearance on Twitter. The company hasn’t acknowledged this in any of its official channels, but the staff presence on Twitter is surely sanctioned, if not encouraged, by the company.

Unlike Digital Lounges where all of the staff used the same style of Memoji as their avatar and the language was somewhat scripted and sugar-coated, the staff Twitter often feature their true photos and authentic selves. Although much of the talk is about work, there’s often personal reflection that would have been unthinkable to publicly see from someone working at Apple just a few years ago.

There’s no directory or list of Apple people on Twitter. Well, sure, there are many Twitter lists curated by different people, but there’s no single official or authoritative one. Twitter’s discovery works quite well for me: I follow interesting people in the Apple developer community, and through mentions and Twitter recommendations, I find more.

Employee Twitter augmented the WWDC experience really nicely. The staff encouraged audience to join labs, highlighted interesting things from the sessions, and followed up on questions.

The Swift labs are underbooked, so bring some of your questions to us! Got some generic SwiftUI code that has cryptic compiler errors? A view body that takes forever to build? Need help understanding property wrappers or result builders? I’m in Swift open hours on Friday 🙂 https://t.co/HFKxAOdgEo

— Holly Borla (@hollyborla) June 10, 2021

I think you will enjoy the talk! 😉 https://t.co/ScMRuPkqG6

— @numist@xoxo.zone (@numist) June 11, 2021
Social and community events

Above were all the channels and events tied to Apple company and staff. There is a surrounding universe of community events too numerous to list here. Many Apple educators, journalists and podcasters provide live stream and commentary, and there are social events.

The one event not replicated in virtual WWDC is the official Beer Bash, which was a social after-hours event for WWDC members with food, drinks, and music. I wonder what a virtual Beer Bash would look like? Throughout the COVID isolation times, I had quite a few small-scale beer bashes with various circles of friends using various calling tools. Some tools are interesting, like spatial.chat, which lets you sort of recreate a party atmosphere where you move around between groups. I wonder what would that be like for thousands of WWDC participants?

Live near WWDC is an institution that I was lucky enough to see live a few times, and that now made the transition to the online world. I had to miss the current one because timezones.

Summary

What does all this mean? Where are the Apple community and WWDC going?

Two trends stand out to me: openness, and diversity+inclusion (D&I). Which, perhaps, are not too far from each other.

It was not too long ago when the whole WWDC was under lock and key. The content was available strictly to developer program members, forums were under a confidentiality agreement, and you could not discuss anything about the beta publicly. Fast forward to today, where all of the content is online and freely distributed, sharing is encouraged, and we have real Apple people on Twitter providing nuance and direct unfiltered thoughts on it all.

Perhaps touching both openness and D&I, virtual WWDC is surely available and accessible to more people around the world. The amount of real-event tickets was limited and distributed with a lottery during the more recent years, and the cost of attendance was prohibitive to many. (Do you remember WWDC used to cost $1500+ just for attendance? Not to mention travel, lodging etc.)

I love to see how Apple is steadfast committed to D&I. This is pervasive through all aspects of WWDC: the showcased software features, the large share of accessibility sessions, the composition of session presenters, and events like Diversity in Swift Community Meetup. I truly believe they are showing the way to other companies and communities around the world who have not yet decided to fully embrace D&I.

What does the future hold? I don’t know, but I’m looking forward to it. It feels fitting to end this with the newest James Dempsey song, first performed at the Live near WWDC event this year.

We debuted a new song at last night’s LIVE near WWDC show.

It’s called Futureproof and it holds a lot of meaning for me.

For a short break from all the great #WWDC21 sessions—I’d love it if you’d give it a listen.

I hope you enjoy it and pass it along.https://t.co/OZa2d5tBwP

— James Dempsey @jamesdempsey@mastodon.social (@jamesdempsey) June 10, 2021

Reflections after WWDC21 was originally published by Jaanus Kase at Jaanus on June 13, 2021.

https://jaanus.com/reflections-after-wwdc21
Obra Dinn
Show full content

I recently finished playing Return of the Obra Dinn.

I don’t post about games very often any more. First of all, I don’t get to play them all that much. And even if I do, there’s not much interesting to say about most of them.

Obra Dinn is an incredible independent game, designed and developed by just one person, Lucas Pope (with of course help for various aspects of the production, but the concept and design is his alone.) I do think such independent efforts deserve praise and recognition more than multimillion-dollar studio productions. Plus, Obra Dinn just is a very unique and enjoyable game.

I wish I could say I finished it without cheating. I didn’t. I cheated. Well, not entirely. I did go through all the content there is to go in the game, and I honestly solved a few fates all by myself. But that was it. Just a few. For going through most of the game, I relied on this great wiki that explains (and spoils!) everything about the game.

Even though I’m sad that I resorted to cheating, I’m glad I went to that wiki and read all there is to read. I missed tons of subtlety and nuance and lots of the story just by looking at the game. I just didn’t understand what was happening much of the time. Maybe I’ve been too accustomed to high-budget games and their high production values. Or maybe my eyes are aging. Or maybe my brain is aging and I’m getting dumb or lazy. Going through the explainer content on the wiki only made me appreciate the whole game, story and content more than I could gain just from the game experience itself.

I try to play games as a form of escapism with great “immersion factor” - that really place you into a different time, place, and character. Obra Dinn is brilliant at that. The combination of mechanics, 1-pixel graphics, and sounds is just perfect. Even though I didn’t succeed at solving most of the game myself (and didn’t try too hard either), I much enjoyed going through the whole experience just for this immersion aspect.

Here’s a more complete video (with some spoilers) that shows and explains the whole premise in more detail.

Obra Dinn was originally published by Jaanus Kase at Jaanus on April 22, 2021.

https://jaanus.com/obra-dinn
My hobby project Tact, and how you can come along to the journey
Show full content

Dear Internet,

I would like to introduce you to a hobby project that me and Priidu Zilmer have been working on for a while, and I invite you along to the journey.

Tact is a simple chat app for families, friends and small circles, designed for Apple platforms.

tl;dr: I’m looking for these kinds of people:

Apple platform engineers. Come help me build. Details in this post.

Private beta testers. If you’re not afraid of unfinished software, consider joining the test.

Investors, advisors, and hustlers. Tact doesn’t yet have external funding. I’m working on Tact because I believe it can be a sound and sustainable business. I haven’t plotted the exact path to get there. I’m interested in your views.

I’d be happy to have a call with anybody about these areas. Contact me to set one up.

You’ll find a lot of info about Tact on our placeholder site, and I encourage you to go through that. Here are some more personal bits and pieces.

Backstory

For the longest time, Priidu, myself and some other friends have kept in touch since the good old Skype times. We previously used Wire because that’s what we built, but more recently Wire has shifted its focus away from consumers and towards business. Nothing wrong with that. It means, though, that there’s less of a focus on our kind of consumer scenario that I’m interested in.

So for quite a while, we had this joke, “we should make our own chat app”. Of course. That’s all it was, just a joke. But the thought kept nagging me until I did a thought experiment. “What if… it was not a joke?” What would it take? Knowing that I had no time and no money to put into it, what kind of chat app would and could I make? The thought experiment very quickly led me to iCloud that provides two vital services–users and backend.

For past many months, we’ve had a functional technical experiment of Tact trying to answer the question, is it possible to build Tact on iCloud like this? To date, I haven’t seen any evidence that it wouldn’t be possible. Seems to be fine. There is lots of work remaining to do, but I haven’t seen any fundamental reason why it wouldn’t work.

Tact is informed by developments in the external environment. In the Apple world, SwiftUI was released, letting me build efficiently for two platforms together. (I started out with separate AppKit and UIKit apps already before SwiftUI, but that was no fun.) On the privacy and economics side, we continue to see the struggles of ad-funded platforms. All these signals are feeding into how I think about Tact.

Future

In 2021, our goal is to build a functional basic enjoyable chat app and make sure it is a solid experience to private beta participants. We’ll then make it available in paid form to validate our hypothesis that there are enough people out there who share our principles and are willing to pay for something like Tact.

Beyond that, Tact could go in many directions. This is just a rough outline of ideas that we’ve been kicking around. We may end up building all, some, or none of these, and this list will evolve over time.

Realtime features (calling, screen sharing). This fits well into the Tact vision, but is more expensive to build and maintain than we can afford in our current hobby mode. We postponed this whole area until we have a more solid business case.

Chat and content experience. What is basic chatting like? Does it always need to have bubbles? (There are no bubbles in Tact.) How do files, photos, links, other media work? Should recent and older content behave differently? We believe the whole messaging field is still nascent and ripe for innovation. We haven’t yet executed many ideas here, and Tact today does not yet break much new ground. We would like to play with these directions.

On-device integrations. Apple devices are incredibly powerful machines, loaded with hardware sensors and other apps. How might we involve content flowing freely to and from hardware and other apps? Can on-device integrations act as bots and provide useful services for both personal and business uses?

Social group dynamics. What should group dynamics look like in 2021? What are the potential risks of harm and abuse? How can we build the Tact software and network in a way that promotes kind and discourages harmful behavior?

Publishing. Tact starts out with closed groups, but doesn’t always need to remain like that. Beyond strictly closed groups, should Tact have some publicly visible outlet, more publicly accessible groups?

More client platforms. Tact is currently on iOS and macOS. It’s technically possible to build a web experience for Tact. Would it make sense? What other platforms could Tact live on? What would Tact be on watchOS or tvOS?

The path for 2021

Priidu and myself will continue to build Tact in hobby mode and do three things through the year: work on the product, engage with our beta community, and continue consulting with potential investors and advisers. We will not do the latter actively and won’t explicitly look for funding, but will definitely talk with interested people.

Through all these actions, I expect to reach one of these outcomes for Tact in 2021.

There is no point. We fail to generate sufficient community interest and private beta tester engagement. We have fundamentally got our vision wrong, our product design is poor and unusable, or our build quality is too low. The world just does not have a place for Tact. In this case, we may still continue to operate Tact indefinitely on life support since the ongoing operating expense is low, but there is no business here. It is not the end of the world. I just need to do something else with my life, and Tact won’t be the main thing that I work on.

A viable small business. Apple announced their Small Business Program in 2020 and revealed that a “vast majority” of app developers are in “under 1M of revenue” category. Tact would be just fine in this category. Arriving at a sustainable business model to support ongoing operating costs and a few people’s work seems like a completely reasonable and achievable goal, and is the one I am working towards.

Something big. We work on Tact because we believe that our principles and vision have potential to be way bigger than the current feature set. This requires a lot more - of people, investment, and other resources. We are not actively working towards this goal, but it may turn out to be the future anyway, whether in 2021 or a later time.

My hobby project Tact, and how you can come along to the journey was originally published by Jaanus Kase at Jaanus on February 04, 2021.

https://jaanus.com/my-hobby-project-tact
Registering for remote notifications appears broken in Big Sur
Show full content

Updated Nov 8: I did further testing and realized that registering for remote notifications works in production environment on Big Sur, but not in development environment. The original version of this post stated that it doesn’t work at all on Big Sur, which would be a big deal. Not working in development environment is obviously annoying to developers, but has no real user impact. I’ve edited this post to reflect this learning.

TL;DR: registering for remote notifications in development environment appears to be broken in Big Sur. This code works for me as expected in Catalina with both development and production configurations. On Big Sur, it works in production context, but not during development. I’ve been looking for an explanation and solution for a while, but no luck so far.

macOS Big Sur is about to be released publicly soon. All macOS users will get a major design update with this release. Us developers have been looking at it for a while already, and I quite like working with it. There’s one area where I am having trouble with though, and haven’t found a solution so far. Hence, I’m writing this up publicly, in the hope that someone can point towards what I am doing wrong.

Remote notifications are generally a pretty stable part of the Apple SDK. It first shipped on iOS many versions ago, and was later added to macOS. To my knowledge, there weren’t any API and SDK changes in Big Sur in this area. The code I had working in Catalina should just remain working in Big Sur.

Alas, that is not the case for me. I have made a small example app that you can just build and run. It first asks the user for authorization to send notifications. If the user grants it, the app then registers to receive remote notifications. It obtains a device token that I can later use to send notifications to the device.

Everything works as expected in Catalina. You see these lines in the log.

2020-11-07 14:19:41.581802+0200 BigSurRemoteNotifications[59054:1699865] [App delegate] Notification authorization ok, registering for remote notifications
2020-11-07 14:19:44.930732+0200 BigSurRemoteNotifications[59054:1699373] [App delegate] Did register for remote notifications. Token length: 32

On Big Sur, it’s a different story. Neither the didRegisterForRemoteNotificationsWithDeviceToken nor didFailToRegisterForRemoteNotificationsWithError callbacks are called. One of them should always be called. Here’s what I see in the log on Big Sur when running in development context, such as building and running straight from Xcode.

2020-11-07 10:58:47.851135+0200 BigSurRemoteNotifications[2823:36378] [App delegate] Notification authorization ok, registering for remote notifications

The registration just appears to hang forever.

I have never seen this work in Big Sur in development environment. It has been broken like this since the first beta. I’ve checked with each subsequent beta release for any behavior changes, but nothing so far. And now we have Big Sur Release Candidate out where this is similarly not working.

This thread in Apple developer forum suggests that it might be due to my network setup. Someone from Apple claims that they had connectivity problems in APNs developer environment at some point. Fair enough. This is also why I use os_log for logging: I can build the app with production configuration and then see the logs in system console.

When I build the app on Big Sur and then export a notarized and signed copy of it which is built against production APNS environment, it appears to be working as expected. I stream the app log and can see the callback being called.

Notification registration callback on Big Sur

To sum it up, it works on Catalina with both development and production configurations, and on Big Sur only with production configuration. I am using exactly the same iMac and network environment with the same code, suggesting to me that it really is something about the operating system.

So that’s where I am. Usually in a situation like this, when something is not working as expected, one of these two is true. (Sometimes both. But usually one.)

  1. I am doing something wrong.
  2. There is something wrong with the underlying system and platform.

I would happily fix my mistake if I knew what I was doing wrong. I just fail to see what it is, or what and where to look for. The notification registration API and system behavior hasn’t changed. There aren’t any warnings or deprecations in Xcode. The network is fine. It really is just working in Catalina and not working in Big Sur.

I have filed FB8731513 to Apple about this a while ago. It doesn’t have any reactions from Apple, but status remains “Open”.

If you happen to be a developer on Big Sur, perhaps you could test this and see if you get similar results? Let me know on Twitter or such. I’ll update this post once I find out more.

Registering for remote notifications appears broken in Big Sur was originally published by Jaanus Kase at Jaanus on November 07, 2020.

https://jaanus.com/big-sur-remote-notifications
How Design Makes The World
Show full content

Today, I‘d like to recommend a design book to you—even (especially!) if you don’t identify as a “designer.”

The book is “How Design Makes The World” by Scott Berkun. And it has a cool trailer.

It will give you a new, human-centered lens to look at the world around us, including your own work and daily life. Everything we make and do impacts somebody else, and the book gives you many design-inspired frames to question, critique and improve your own and other people’s decisions.

Design has always co-existed with other disciplines and been informed by real-world constraints and tradeoffs. The book does talk a bit about software and the digital world, but only marginally. It shows us how design as an intentional activity has been part of the human domain since forever, talking a lot about physical products and spaces.

These days, I work in the niche domain of Anti-Money Laundering and the fun world of financial institutions. The part in the book that touches my work directly is the bit about constraints and tradeoffs. I deal with a different frame and reference system compared to, say, consumer mobile apps. There are many regulations, stakeholders, institutions, decision-making structures and, ultimately, roles and people involved. But ultimately, the design checklist still applies:

  • What am I trying to improve?
  • Who am I trying to improve it for?
  • How do I ensure I am successful?
  • Who might be hurt by my work, now or in the future?

The book is a light, entertaining read. There are many sources, but not so much academics and “science”. It’s mostly real-world stories and history with some light analysis.

How Design Makes The World was originally published by Jaanus Kase at Jaanus on May 14, 2020.

https://jaanus.com/how-design-makes-the-world