In an
Asset Catalog
, you can simply fiddle with the checkboxes in theDevices
section of theAttributes Inspector
.
But what about resources that cannot be in an Asset Catalog?
In a traditional multiplatform app, with a target for each supported platform, you can use the
Target Membership
section of theFile Inspector
to choose which resources are built into which target.
But what if I’m working on a single-target Catalyst app?
In your target’s setting, the
Build Phases
section will have aCopy Bundle Resources
phase. Its table has aFilters
column you can use to specify that a resource is to be copied only on a specific platform.
But what if the resource, for reasons outside of my control, needs to end up in the exact same place on both platforms?
My specific use case had to do with Web Extensions. They require a manifest.json
file, which hardcodes paths for things like icons and content scripts, with no option to specify platform-specific alternatives.
Well I couldn’t figure it out, but DTS Engineer Ed Ford came up with a clever trick, and I felt like sharing.
Let’s assume you want your target to contain a resource named exactly stuff.data
in a directory named exactly resources
, but you have two separate versions of this file – one for iOS and one for macOS Catalyst.
Before adding anything to your Xcode project, create this directory structure:
all_resources/
├─ iOS/
│ ├─ resources/
│ ├─ stuff.data <- iOS version
├─ macOS
├─ resources/
├─ stuff.data <- macOS version
Now in Xcode, using File
, Add Files to "ProjectName"
, add each of the resources
directories to your project. Make sure to uncheck all targets from the Add to targets:
section. The directories will show up in the project tree as identically named siblings.
Next, in the target’s settings, navigate to Build Phases
and look under Copy Bundle Resources
. Using the +
button, add both resources
directories to the phase. It will not be possible to distinguish them before adding them, but don’t worry: you can’t add the same directory twice, and you will be able to tell them apart once added.
Finally, for each directory in this list, change the value in the Filters
column using the little filter button. Leave only the appropriate platform checked.
Check out my devlog on Mastodon for more app development tips.
]]>I have no interest in competing with others, especially in terms of who can code faster, and even less if that means waking up at 6 AM my time – but I thought I’d have fun, and get the chance to showcase some neat Swift code on my devlog by sharing my solutions.
I’m not sure why “coding challenges” always end up being about algorithm design and computational complexity. I’ve noticed the same thing when I participated in Italy’s IOI, back in high school. These aspects are pretty marginal in day-to-day software development, at least for me.
They’re not my strong suit nor my favorite part of coding – I’m all about making code readable, maintainable, and pretty – but I still enjoy them.
And yet, 17 puzzles later, sunk cost effect and all, I decided to stop playing!
The solutions frequently turned out not to be particularly interesting for the devlog, and trying to keep them compact enough to fit in a screenshot, while being readable enough to be educational, quickly became unmanageable.
But most importantly, I was no longer having fun solving the puzzles themselves; it started to feel like work. And when I asked myself why, I realized it’s the same issue I have with a lot of contemporary —
I really enjoy deductive reasoning, so when a new puzzle game based on that comes along, I’m excited to check it out. But so, so many times I end up dropping out, because:
A lot of puzzle games use spatial reasoning as a difficulty supplement.
A good example of this is Baba Is You, a game in which the rules themselves are puzzle elements that can be moved around and recombined on a 2D map. At first, the challenge is figuring out how to combine the tools you have by thinking outside the box. But as the levels get harder, their maps get bigger. And while the deduction part never goes away, more and more of the challenge ends up being about figuring out how to get from A to B, or how puzzle elements are related spatially – and not logically.
This isn’t a problem per se, and Baba Is You is a great game for sure, but different players enjoy different challenges. If you have aphantasia like me, spatial reasoning can become so hard it’s near impossible, and using it to solve a puzzle is so frustrating it takes all the fun out of it. Which is why I can love action-platformers while hating metroidvanias – they’re similar genres, but the latter requires you to navigate a space, which I can’t do to save my life. You can imagine how I felt about The Messenger and its (spoilers!) twist.
So it’s pretty annoying when a game draws me in with deduction puzzles, but then gradually becomes more and more about spatial reasoning; but that’s exactly what happened with Advent of Code.
Most of the first 10 puzzles required little to no spatial reasoning. But lately, seemingly every time you open one up you are confronted with one of these:
O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#....
Almost all recent puzzles involve a 2D space of some kind. The above is the example “map” which number 14 is based on. It’s the most egregious example because it not only asks you to move objects in a space, but to rotate that entire space as well; which is tantamount to torture for someone like me.
I stumbled on a post by Advent of Code creator and puzzle designer Eric Wastl, where it is implied that the puzzles are supposed to be in increasing difficulty. I had no idea! To me difficulty was all over the place, because, for each puzzle, the spatial reasoning requirements weighed more than all others combined.
The post also talks about how making puzzles is really hard; that’s definitely true, and I sympathize! I’ve been designing a sequel to Agency for a while now, and I know firsthand how meandering an endeavor it can be.
I don’t want to imply that AoC’s puzzles are bad – in fact, a lot of them were a ton of fun even for me and my weird brain, and you should check them out! I’m only explaining why they aren’t for me, while pointing out a trend I’ve noticed in a lot of recent puzzle games.
Injecting spatial reasoning is a popular way to design advanced puzzles, but it’s by no means the only way.
In Return of the Obra Dinn and The Case of the Golden Idol, you reconstruct complex events with deduction alone. In Chants Of Sennaar and 7 Days to End with You, you translate unknown languages from context clues. Fire Emblem ramps up the difficulty by increasing the number of tactical options at your disposal, while reducing the number of mistakes you can afford to make. Sudokus and Nonograms get harder with larger grids, but they remain pure logic puzzles.
These games can get pretty involved, while still being enjoyable by people who dislike spatial reasoning. But how do we find them, tell them apart, when they’re all just “puzzle games”? I have an idea, but we’d have to broaden the discussion to —
I’ve given this a lot of thought, and I believe that, on a fundamental level:
A game can be defined by listing the skills it tests.
When we make games, I think it’s crucial we decide what we want to be on that list, and keep it in mind throughout development.
This approach is similar to Marc “MAHK” LeBlanc’s taxonomy, also known as the “8 kinds of fun”, but the point of view is slightly different.
If a game intends to test one set of skills, but then evolves and ends up testing another, it becomes a different game. The result often feels messy or unfocused, and leaves its players feeling alienated or betrayed.
This holds true even when the final design only adds to its original skill list. It’s tempting to value variety in games, but it comes at a cost: the players who will love a game must love all the ways it tests them.
We can think in these terms when we talk about games, as well. Games that test similar skills can be grouped into categories that are far more meaningful than the genres we’re familiar with, such as “horror” or “adventure”.
If we called Metroidvanias action/navigation/memory
– which incidentally is close to what they are called in Japan – we’d have a much better idea of what their play experience is like.
Start from the skills, and you can easily generate new game types, too. What would a deduction/action
game be like? What does rhythm/tactical
imply? Is that what Crypt of the NecroDancer is?
If Advent of Code used this framework, it would call itself a deduction/spatial reasoning
game, and this article wouldn’t exist :)
If this post resonated with you, you’ll probably enjoy my very intentionally deduction/lateral thinking
game, Agency.
.frame
does NOT set or modify a View
’s dimensions.
It puts a View
inside an invisible container, which we’ll call a “frame”.
.frame
is an invisible container, just like stacks.
All that’s left to figure out is where inside the frame our view will end up.
You know how HStack
s, VStack
s, and Spacer
s work, right?
Let’s position this view:
let content = Text("Position me!")
// This...
HStack(spacing: 0) {
content
Spacer()
}
// ...is the same as
content
.frame(maxWidth: .infinity, alignment: .leading)
// This...
HStack(spacing: 0) {
Spacer()
content
}
// ...is the same as
content
.frame(maxWidth: .infinity, alignment: .trailing)
// This...
HStack(spacing: 0) {
Spacer()
content
Spacer()
}
// ...is the same as
content
.frame(maxWidth: .infinity) // Center alignment is the default!
// This...
VStack(spacing: 0) {
Spacer()
content
}
// ...is the same as
content
.frame(maxHeight: .infinity, alignment: .bottom)
// And this...
VStack(spacing: 0) {
Spacer()
HStack(spacing: 0) {
Spacer()
content
}
}
// ...is the same as
content
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
So, did I do it?
Of course, if you don’t want the frame to expand forever like Spacer
s do, you can give it max dimensions lower than .infinity
. If you don’t want it to expand at all, and want it to have a fixed size instead, use the .frame(width:height:alignment:)
overload.
As a rule of thumb, Spacer
s are useful to create space between views. If you’re using a Spacer
with nothing on one side, you should probably use a frame instead.
Well for one, it has the same name as the frame
property in UIView
s and NSView
s, but very different behavior. So if you’re coming from UIKit or AppKit, you have some unlearning to do.
But I think devs (me absolutely included) might struggle understanding .frame
because it behaves like a container, but it looks like a modifier.
While it is true that, strictly speaking, all SwiftUI modifiers are containers/wrappers, you could argue that most of them conceptually modify the view they are attached to, while .frame
definitely does not.
Consider this alternative API:
Frame(maxWidth: .infinity, alignment: .trailing) {
content
}
Would you consider this more intuitive? …I bet this article wouldn’t exist if this was real.
The documentation says:
Note that most alignment values have no apparent effect when the size of the frame happens to match that of this view.
Notes like these are typically a sign that you have an issue with your API’s design or naming. And guess what, I have a few other gripes with it!
Like:
.fixedSize()
instead “fixes this view at its ideal size”, which is very much not that!If you take the time to decipher the “Discussion” section of the docs, you can understand its behavior and confirm it in practice – in other words, none of these complaints are due to bugs. The API is working as intended. It’s just that its intended behavior is so byzantine it defies expectations.
I’m not sure what went wrong here. I’d say the rest of SwiftUI is really well designed, including stack-based layout. By and large, things have sensible naming and behavior. Maybe this API is trying to do too much. Maybe it had to be like this – because of a Swift limitation that will be lifted in the future, or for performance reasons. Maybe the problem it’s solving is intrinsically very complex and I’m just not smart enough.
But sometimes I miss Auto Layout.
As you can see, once you understand .frame
you can make common layout code less verbose, and more readable – for yourself, at least.
At the time of writing, this modifier has 68 uses in Wipr 2’s codebase (excluding #Preview
s). Of these:
(width:height:)
variety.Which leaves a grand total of 1 call I haven’t prepared you for :) That’s probably good enough for today.
Check out my devlog for more tips. Happy laying out!
]]>So I started a devlog on Mastodon – if you’re interested in iOS development, Swift, SwiftUI, UI/UX design, accessibility, etc. come check it out! It’s way more active than this blog.
Like all Mastodon accounts, it is also available as an RSS feed.
]]>I’m happy to introduce myself: I’m Kaylee!
Same nerd, new name.
Simply put: I’m a woman, and I need a name that matches that.
Being addressed by my deadname is very painful to me, so much so that I’ve gone to great lengths to make sure that it never happens again.
Due to App Store regulations, a developer’s name in the App Store has to be their legal name.
Changing my legal name took close to 3 years. It was really expensive, too!
Since this was taking so long, I even tried to register “Kaylee” as a commercial name, which honestly was a genius move, but turns out you can’t: a small company would have to be called “Kaylee by <deadname>”, which is somehow even worse, and a big company is prohibitively expensive to set up.
So I just had to grit my teeth and wait until today.
Why are all these restrictions in place? Why do I have to jump through a thousand hoops just to live like a normal person? Honestly, I have no idea! I get the message though (stares daggers at society).
Updates to my website, apps, App Store presence, etc. to reflect this change are either out now or will be out soon. Please forgive any inconsistencies while I sort this out.
Other than that, business as usual I guess!
I’ve been through hell with all the gatekeeping, legal nightmares, bureaucracy, traveling, hospitals, and recoveries.
Now that the worst is over, I’m happier and more fulfilled than I’ve been since I was a kid, and I have the time to focus on my work.
For these reasons, I can only see my apps improving from now on… And I might have a few things in the oven, but shh don’t tell anyone!
If you have a problem with transgender people, please just go away. I don’t want your business, and I definitely don’t want your “opinion”.
]]>Samples started as a hacky soundboard with which I would annoy and/or amuse my colleagues by playing back audio memes. How it got to be a decently featured musical instrument, is hard to tell!
My personal interest in this project has declined over the years, and frankly, better sampler apps have been released in the meanwhile. User interest has faded as well.
I loved Swaipu to bits. It had this kind of charm, a personality to it, which the very, very, veeery few people who downloaded it responded to.
Unfortunately, almost no one else seems to care about Swaipu as much as I do. The app has definitely had enough time to take off, but it never did. And now it never will 😅
I strongly believe that making a great app requires its developer to use it a lot, and love it a lot. When either of these conditions isn’t met, it’s time to let go.
]]>I have to implement a long-running, multi-step, cancelable task. For simplicity, let’s assume there are 3 steps. Cancellation might be requested by the user, or by the task itself – to abort early in case an error occurs.
Obviously, this task cannot be run on the main thread, so I’ll have to run it on some kind of background queue.
I generally use Dispatch for this kind of problem, but there’s no support for cancellation and handling that manually sounds… inconvenient.
So I choose OperationQueue (a.k.a. NSOperationQueue
), which supports cancellation. Each step of the task can be modeled as a single Operation
.
OperationQueue
, concurrent at heart, can be made serial by setting its maxConcurrentOperationCount
property to 1
. It, however, makes no guarantee that the operations will be executed in the order in which they are provided to the queue.
To fix that, the docs suggest we create dependencies between the operations to enforce our desired execution order. I create a chain where each step is given a dependency on the previous. This way, the only step that is ready to execute at any given time, as it has no unsatisfied dependencies, is the first one.
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
let operations: [Operation] = [step1, step2, step3]
step3.addDependency(step2)
step2.addDependency(step1)
queue.addOperations(allOperations, waitUntilFinished: true)
And because each of the steps implements its own cancellation correctly, all I need to do to cancel the entire multi-step task is:
queue.cancelAllOperations()
…right?
Rarely, when a step fails and tries to abort the whole task by canceling the queue, this… doesn’t work. All steps are eventually canceled, but canceling during Step 1 sometimes causes Step 3 to start executing for a split second. Sometimes, it’s Step 2 that gets a brief execution window.
This is unacceptable because the dependencies between these operations aren’t just a way to trick OperationQueue
into following our intended order; some steps actually need the data produced by previous steps, so this breaks the task’s internal consistency.
Don’t you just love it when bug descriptions start with “Rarely”?
The docs read:
Canceling the operations does not automatically remove them from the queue or stop those that are currently executing. For operations that are queued and waiting execution, the queue must still attempt to execute the operation before recognizing that it is canceled and moving it to the finished state.
So cancel()
doesn’t magically stop running code, it just marks it as cancelled by setting isCancelled
on the Operation
object. Executing operations should respond to cancel()
by finishing as soon as possible. And the queue still needs to go through each non-executing operation, and determine whether to run or skip it based on the aforementioned flag.
But the docs also say that when the queue encounters a non-executing operation where isCancelled
is true, the operation will be removed from the queue without running its code at all (i.e. without invoking its main()
method). This doesn’t match our observations.
To figure this out, we can try to imagine how cancelAllOperations()
might be implemented. My guess is:
for operation in operations {
operation.cancel()
}
And here lies the issue.
This is the part where you stop reading if you want to figure it out for yourself. If you did, welcome back!
Canceling operations in array order means that the first operation to receive a cancel()
is Step 1. This marks it as complete, instantly satisfying all of Step 2’s dependencies! So technically, before cancel()
is called on it, Step 2 is ready to execute. And when the planets align, it does.
I’d have to see the code to be sure, but I think the issue is exacerbated by OperationQueue
’s internals.
Imagine that Step 1 fails and wants to abort the entire task. To do so, it calls cancelAllOperations()
on the queue (probably indirectly via a delegate callback, but that’s not important right now).
In turn, cancelAllOperations()
starts by calling cancel()
on Step 1. This changes Step 1’s isCancelled
flag, which (judging by the documentation) is observed by the queue via KVO.
In the KVO observer, OperationQueue
can realize that Step 2’s dependencies are satisfied and change its flags to mark it as ready to execute.
This change will in turn be observed by the queue, which can decide to kick off Step 2.
And because all of the above happens on the same underlying DispatchQueue
where Step 1 is running, which is now free since the task is serial and Step 1 is done, the queue might decide that this is a fantastic place to run Step 2 and just start it synchronously – all before the call to cancel()
on Step 1 even returns.
So even if the thread executing cancelAllOperations()
isn’t pre-empted halfway through its loop, the bug could still strike.
Let’s make sure to never accidentally satisfy any step’s dependencies. On any serial, ordered example like the above, all we need to do is cancel the steps in reverse order. At no point during this there will be a step which is not cancelled and has satisfied dependencies (other than the currently executing one, of course).
for operation in queue.operations.reversed() {
operation.cancel()
}
For more complex dependency trees, operations will need to be canceled from the leaves up.
I’m not even sure whether this can be considered a bug in OperationQueue
. Intuitively, it seems like cancelAllOperations()
should mark all operations as cancelled in a semantically atomic way. I definitely did not expect it to start executing anything. But then again, the documentation doesn’t make any promises either way.
Two days ago me sure wished this post existed. Let me know if this helped you!
]]>This only applies to the Mac version of Wipr, so if you only care about iOS you can skip this :)
In today’s filter update for the Mac version, I had to remove all tracking-related filters. Everything else is as it always was. In other words, Wipr will continue to block ads, cookie warnings, and assorted nastiness just as before – but will no longer protect you against tracking.
I had to do this because Wipr’s filter count has surpassed the limit that Safari imposes for a single filter list.
One way to solve this is to have multiple filter lists. This is what I do on iOS (and is why that version has three toggles in Settings, if you were wondering). But on the Mac this would mean having multiple versions of Wipr on the Extension Gallery… which I think is a terrible solution. If the past is any indication, it might also be the slowest solution – it took forever to get the first version of the extension approved.
The other way is to have fewer filters. I could have cut cookie warnings or other smaller things, but that would’ve just bought a little time. Cutting using some other criterion other than filter purpose risks breaking the whole list. So I decided the best course of action was to cut tracking.
Another reason I chose tracking is that Safari now comes with Intelligent Tracking Prevention built in. While it might not be as thorough or aggressive as what Wipr did, it should be a lot better than nothing :) So if you’re running Safari 11, this change might not impact you too much.
I think it’s really important that I communicate this, because the change is invisible. Additionally, the extension’s description in the Extension Gallery is now incorrect.
I’m working on a native macOS version of Wipr. A single native app can host multiple filter lists, just like on iOS. So this version will have all of the filters, including the tracking ones. Just like on iOS!
This native version will be distributed via the Mac App Store, which will hopefully mean I will no longer get emails asking where Wipr is :)
Once the native version is out, I will also release an update to the extension version. This will add a link to the native version, and will allow me to fix the description so it no longer includes claims of tracking blocking.
Update: the native version is out now!
Well for one, the native version isn’t done. It might take a while for it to be released.
Update: the native version is out now!
But maybe most importantly, it will be released as a paid app (likely priced just like the iOS version).
This was always the plan: keep the extension version free, and then provide a nicer, native experience for those who wanted it.
What I didn’t plan on doing was removing functionality from the extension version. This might look like I’m intentionally crippling the free version to push users toward the paid one, and I want to make it super clear that this is not the case. It would be pretty dumb to cripple the free version long before the paid one was available, right? :)
If this change bothers you, and you can stomach alpha-quality apps, email me and I’ll send you the native version of Wipr for Mac in its current state. It’s very ugly and doesn’t have automatic refresh, but it’s otherwise fully functional – I’ve been using it for a while now.
Update: the native version is out now!
Let me know if you have any questions and I’ll be happy to explain!
]]>To decide whether or not to add whitelisting to Wipr, I started collecting feedback from users on why they were asking for that feature. This has been going on for months now, ever since Wipr was released.
Maybe this goes without saying, but this “poll” is closed – the mere existence of this article invalidates any new data.
All replies have fallen in one of two groups (and rarely, both):
Almost everybody is in group 1, way more than I expected.
Let’s examine each group individually and see how to best address their needs.
I don’t think whitelisting is a good solution to this problem.
I think it’s best if those broken websites are reported to the EasyList forums or to me, so we can fix them once for everybody.
It’s worked pretty well so far. Users from this group to whom I’ve proposed this have universally – so far, at least – agreed it’s the best solution.
I should mention that this is a rare occurrence with Wipr: users with this issue typically mention only one website where they ever had an issue, and sometimes it turns out the website was just broken, Wipr or not. But it can happen. The net is vast and infinite.
With no whitelist, I know exactly what set of filters are being applied to a given user’s browsing. This makes it easy to reproduce issues, and consequently to fix them.
The issue of broken sites is also slightly mitigated by the “Reload Without Content Blockers” Safari feature and the “Disable” button in the Mac version.
By the way, I’m not planning on adding any “quick report” feature. I’m afraid I can’t do that. The result would probably be a massive increase in the volume of reports, and a decrease in their quality. Triaging all these reports would take too long, and it wouldn’t make Wipr significantly better. I just don’t have the resources to handle them. This might change, however, if Wipr becomes a paid-only app.
For group 2, whitelisting is the only solution that I can think of. But there are many downsides:
This group is tiny! Even if my unscientific polling was way off, it would still be too small.
This is a key point, and would alone be sufficient to reject whitelisting.
Implementing something just for this tiny group would make the app worse overall – Wipr would become unnecessarily more complex for everybody else.
Users tell me they’re installing Wipr on their not-as-tech-savvy parents’ devices. Would that still happen if the app needed more than two taps to be operated (zero if someone else sets it up for you), and had a built-in way to shoot yourself in the foot?
The existence of this feature would encourage the whitelist to fix behavior that I’m trying to discourage for group 1 users.
I don’t think most users fully understand what whitelisting a domain means. Sure they want to see a website’s ads so they get paid, but they might not be aware of the avalanche of trackers and assorted crap that rides on the same train.
There is no way to only let ads through, either, because most ads contain trackers.
This could be addressed by educating users, but it would likely mean adding an annoying wall of text to the app that would need to be manually dismissed and no one will read anyway. Yuck. The app is supposed to make your worry less about the problems of the web.
There are better ways to support a website financially, more effectively and without compromising your privacy.
When I want to support a website, I do something like buying their products/merchandise/subscriptions, donating, recommending their services to others, and so on.
There’s no need to degrade your experience of a product in order to support it.
Third-party advertising is a numbers game. If a tiny subset of Wipr users – which is already a tiny subset of a website’s traffic – whitelisted some specific websites, they probably wouldn’t even notice.
If Wipr was popular enough that whitelisted domains mattered a lot, I’d reconsider this point. Probably from the comfort of my solid gold yacht.
Very few are unobtrusive and don’t significantly impact page performance (like The Deck’s), but even those collect data without your consent.
And like all features, it’s costly to implement, debug, maintain, document and support.
I don’t think whitelisting is a good idea for Wipr, and now you know why! Let me know what you think. Or if you spotted all of the sci-fi references I’ve sneaked in. One is tricky.
]]>I have been and will be staring at this for a long time, so it features:
Inspiration and palette courtesy of the excellent game Hyper Light Drifter.
Download the theme, place it in ~/Library/Developer/Xcode/UserData/FontAndColorThemes
, and let me know if you like it.