PodSearch

Under the Radar

212: A Watchsmith Adventure

 

00:00:00 ◼   ► Welcome to Under the Radar, a show about independent iOS app development.

00:00:04 ◼   ► I'm Marco Arment.

00:00:06 ◼   ► And I'm David Smith. Under the Radar is never longer than 30 minutes, so let's get started.

00:00:10 ◼   ► So for today, I want you to just sit down in a comfortable chair,

00:00:14 ◼   ► lean back, relax, close your eyes, and just come along with me on a journey.

00:00:18 ◼   ► This is a David Smith story hour, or half hour,

00:00:22 ◼   ► and we're going to talk about an experience, a journey I've just recently concluded going on,

00:00:28 ◼   ► building a feature out in WatchSmith.

00:00:31 ◼   ► And I feel like a couple, several months ago, I did a similar kind of episode where

00:00:36 ◼   ► I feel like sometimes it's fun to just talk through the process that I go through,

00:00:41 ◼   ► or Marco has done a couple of these too, where it's this funny thing of like,

00:00:45 ◼   ► "How did we actually build this feature and some of the weird things we have to deal with along the way?"

00:00:50 ◼   ► And I just went through one of these things where I thought,

00:00:52 ◼   ► "Oh, this would be a pretty straightforward feature," and turned out, "No, no, no."

00:00:55 ◼   ► It was the opposite of that.

00:00:58 ◼   ► So the feature that I've been working on is I wanted to add photo complications to WatchSmith.

00:01:06 ◼   ► So if you imagine, so in Widgetsmith, the most popular widget by far is the photo widget.

00:01:11 ◼   ► That is what people love to use, and I can understand why,

00:01:14 ◼   ► and it seems like an obvious sort of inclusion for me to add to WatchSmith

00:01:18 ◼   ► as part of this big update I'm working on for WatchSmith.

00:01:21 ◼   ► And so I want to be able to allow the user to, on their phone, pick a picture,

00:01:26 ◼   ► adjust what it looks like, getting it just right, and then essentially hit save

00:01:32 ◼   ► and have that appear as a complication on their watch.

00:01:35 ◼   ► It seems relatively straightforward. It's just a picture.

00:01:37 ◼   ► It's going to be shown at a very small resolution,

00:01:40 ◼   ► but it turned out the process of getting from that idea to actually having it work,

00:01:46 ◼   ► which I finally had it to work yesterday after many, many days of banging my head against the wall

00:01:51 ◼   ► and lots of pain and suffering.

00:01:53 ◼   ► And so that's the journey I'm going to take you on now,

00:01:56 ◼   ► and that's the end goal that we're trying to get to.

00:01:58 ◼   ► We're trying to get to a place where I can pick a picture on a phone and have it show up on a watch.

00:02:03 ◼   ► This is one of those things that programmers so often use the wonderful phrase,

00:02:08 ◼   ► "How hard could it be?"

00:02:10 ◼   ► "How hard could it be?"

00:02:12 ◼   ► "We're about to find out."

00:02:14 ◼   ► And when I'm describing the feature, I'm like, "Well, that doesn't sound like it would be that bad,

00:02:17 ◼   ► especially since you've already done it for WidgetSmith."

00:02:20 ◼   ► Yeah. So first thing you need to know about watch programming

00:02:26 ◼   ► is that everything on the watch is just a little bit harder.

00:02:28 ◼   ► It's just a little bit more sort of challenging.

00:02:31 ◼   ► And so the first thing you have to deal with,

00:02:33 ◼   ► and this is one of those things where it is completely trivial in WidgetSmith

00:02:37 ◼   ► and it is very difficult on the watch,

00:02:39 ◼   ► is how do you get that picture from the phone to the watch?

00:02:44 ◼   ► And on the phone, I just add it to the application container,

00:02:49 ◼   ► and it's in a shared space, and both the widget and the main app can read it.

00:02:54 ◼   ► But for the watch, you're going to have to somehow get it over there.

00:02:58 ◼   ► And so the obvious answer is to use watch connectivity.

00:03:02 ◼   ► Though I will say...

00:03:05 ◼   ► What? What could possibly go wrong with sending files over watch connectivity?

00:03:10 ◼   ► Exactly. What could possibly go wrong?

00:03:12 ◼   ► And these aren't big files. It's probably fair to say, too.

00:03:15 ◼   ► The ultimate result for a graphic circular complication is 94 pixels by 94 pixels.

00:03:22 ◼   ► And the extra large circular one, which is the biggest image that I can send,

00:03:27 ◼   ► is 264 pixels by 264 pixels.

00:03:30 ◼   ► These are things that an old, original Macintosh image could probably display.

00:03:37 ◼   ► These are not big things, but you think, okay...

00:03:40 ◼   ► I will say, I did have the thought of,

00:03:42 ◼   ► I wonder if I should upload these to a server and then have the watch download them directly

00:03:46 ◼   ► just so I could avoid watch connectivity.

00:03:48 ◼   ► That's how much I have had problems with watch connectivity over the days.

00:03:52 ◼   ► Though in the end, I decided, no, that's silly.

00:03:54 ◼   ► Then there's all kinds of problems with doing it that way.

00:03:56 ◼   ► Is it silly? I mean, that's literally... I just built my entire watch app from scratch

00:04:00 ◼   ► to have its own direct-to-the-network sync engine,

00:04:03 ◼   ► mostly to avoid the problems I've had for years with watch connectivity.

00:04:07 ◼   ► So that way the watch has to talk to the phone as little as possible.

00:04:11 ◼   ► Yes. And I did think about it, and I did kind of...

00:04:16 ◼   ► The problem that I wanted to avoid is that then it requires an internet connection

00:04:21 ◼   ► to set up your complications,

00:04:23 ◼   ► which maybe is a fine assumption to make,

00:04:26 ◼   ► but it seemed like it was a slightly awkward thing.

00:04:29 ◼   ► If you were out... It's like, imagine you have this scenario where you have to be on good networking,

00:04:36 ◼   ► and especially it gets weird because even if you're out and you have a cellular connection on your phone,

00:04:42 ◼   ► the watch proxying that cellular connection through itself is often just as problematic as watch connectivity.

00:04:48 ◼   ► So, anyway, I decided watch connectivity it is.

00:04:52 ◼   ► So I'm going to use watch connectivity to get these teeny tiny files across, I thought.

00:04:56 ◼   ► So the first thing you think, "Oh, these are files, so maybe I should use the transfer file API in watch connectivity."

00:05:02 ◼   ► Nope. It doesn't work. For whatever reason, I find that the transfer file thing...

00:05:07 ◼   ► When it works, it works flawlessly, but most of the time it doesn't work.

00:05:10 ◼   ► And you hit transfer file, and it's just like transferring,

00:05:13 ◼   ► and you have no idea when it's going to happen.

00:05:16 ◼   ► So you think, "Oh, maybe I should use the message data protocol,

00:05:19 ◼   ► which is a different version of watch connectivity.

00:05:22 ◼   ► It's designed for more interactivity, but turns out because I'm initiating these transfers from the phone,

00:05:28 ◼   ► you can't actually use that side of the protocol unless the watch app is active.

00:05:34 ◼   ► Otherwise it just fails. So I can't use that.

00:05:37 ◼   ► So I'm down to user info, which is the third kind of way to do watch connectivity.

00:05:42 ◼   ► And that seems to work okay for what I'm doing.

00:05:46 ◼   ► But initially I started running into all kinds of payload to large errors with user info.

00:05:55 ◼   ► And so it's like, "Okay, so apparently I'm sending images over this API.

00:05:59 ◼   ► It's not necessarily intended for this. It's probably... You're supposed to be sending data dictionaries back and forth.

00:06:04 ◼   ► So there's a limit somewhere. What is the limit? Who knows?"

00:06:07 ◼   ► So you start googling around, and I find a Stack Overflow comment that's like,

00:06:10 ◼   ► "According to some private symbols, the WC payload size limit user info symbol

00:06:15 ◼   ► says that it's 65,000 bytes."

00:06:18 ◼   ► And so that's what they said. And it's like, "Okay, I need to somehow make sure that all of my images

00:06:22 ◼   ► that I send over this need to only be 65,000 bytes, which should be fine in some ways."

00:06:28 ◼   ► And so the first thing I do at this point is I make a test project,

00:06:32 ◼   ► and I progressively make the payload larger and larger until I sort of cross over the threshold where I get this error back.

00:06:41 ◼   ► And it turned out whoever this person was on this random Stack Overflow form, they were exactly right.

00:06:45 ◼   ► It's about 65,000 bytes. So yay for Stack Overflow, and now I have a goal that I need to work against.

00:06:52 ◼   ► And so now what I need to do is this weird problem where I take the image,

00:06:56 ◼   ► and I progressively use JPEG compression to make it more and more compressed

00:07:02 ◼   ► until it fits in the space that I have available to me.

00:07:06 ◼   ► So I start off with it being good and high quality, and if for some reason...

00:07:09 ◼   ► I've actually done this same logic on my servers for years for thumbnailing.

00:07:14 ◼   ► I have the exact same problem, because if you send an image that's too large to iOS devices,

00:07:21 ◼   ► they might crash while decoding it or trying to hold it in memory.

00:07:25 ◼   ► And so I try to put as much data on the server side as possible, and my thumbnailer actually has different sizes,

00:07:31 ◼   ► and it has different byte-sized thresholds for each size, and it will attempt to maintain 100% quality,

00:07:38 ◼   ► like first with ping, and then if it can't, it will go to high quality JPEGs and then do the same thing,

00:07:42 ◼   ► slowly lower the quality level until it is below that threshold.

00:07:46 ◼   ► However, I have right away, because I have done that process,

00:07:52 ◼   ► I know of a gotcha that might be getting you, which might be a future part of the story that I don't necessarily want to ruin,

00:07:58 ◼   ► but were you removing things like color profiles from the images first,

00:08:03 ◼   ► because those alone can balloon it past that threshold.

00:08:07 ◼   ► And you can keep the image compressing further and further down to quality zero,

00:08:11 ◼   ► and it will still be like 85k, and you're like, "Why is it this big? What could possibly be taking up all that space?"

00:08:17 ◼   ► And it's basically crap in the EXIF data, like large blobs in the EXIF data.

00:08:21 ◼   ► So I haven't run into that yet, though I am writing that down as something that inevitably will come up and bite me at some point,

00:08:28 ◼   ► and I'll need to deal with.

00:08:30 ◼   ► So I think, though, because of the way that I've, of something else that I'm doing,

00:08:35 ◼   ► I think I will be fine, because ultimately the images that I'm, I don't take the raw image and try and compress that down,

00:08:41 ◼   ► I have to resize it first, and I think because I'm resizing it, there's no profile,

00:08:46 ◼   ► because the version that I'm making is just like a very simple basic UI image renderer image.

00:08:52 ◼   ► I don't think it has any of that overhead to it, but I will investigate.

00:08:57 ◼   ► As long as it can't carry forward any of the EXIF data from the original, then you should be okay.

00:09:01 ◼   ► Yeah, because I think it's fine because I'm rendering, you know, I'm doing a UI image rendering context,

00:09:07 ◼   ► and so it should, ultimately I'm just moving the pixels back and forth, so I think I should be fine,

00:09:12 ◼   ► but I will, that is a good thing to investigate.

00:09:15 ◼   ► But anyway, so I have this method now that I can get these images,

00:09:19 ◼   ► because essentially, progressively compressed down until they are small enough that watch connectivity will move them,

00:09:26 ◼   ► so now I have the ability to add to this test project a watch companion app, and I can send these over the watch connectivity,

00:09:34 ◼   ► and then whenever it receives one, it just throws it into a complication.

00:09:37 ◼   ► And it actually worked.

00:09:39 ◼   ► So this was the point of this project where I was like, okay, this is viable.

00:09:43 ◼   ► This is something that theoretically should be possible now, that I've gone through the proof of concept phase,

00:09:49 ◼   ► because for a while I was like, I don't even know if this is possible, that there may be some weird limitation,

00:09:53 ◼   ► and before I went too far down the road with this, I didn't want to invest all the time,

00:09:58 ◼   ► and it turns out I can't even move the images between my phone and my watch.

00:10:01 ◼   ► That just wasn't possible.

00:10:03 ◼   ► But now I know it is possible. It's just tricky.

00:10:06 ◼   ► So now comes the question of, I need to get the images,

00:10:11 ◼   ► and I need to allow you to be able to orient and adjust the images so that they make sense in a tiny little complication.

00:10:17 ◼   ► Because you're talking about, you know, you may have a big, you take a nice image with your iPhone,

00:10:21 ◼   ► and you have this beautiful, what is it, 12 megapixel image,

00:10:24 ◼   ► and you're compressing it down into something that's like 48 pixels, or 96 pixels by 96 pixels.

00:10:30 ◼   ► So I don't want to just smush it down, because that would be pointless.

00:10:33 ◼   ► You wouldn't be able to see anything. It wouldn't make sense.

00:10:36 ◼   ► So I need you to be able to sort of zoom and pan inside of an image.

00:10:41 ◼   ► And this is one of those things where at first, it's like, okay, how do I do this in a way that makes sense?

00:10:48 ◼   ► And this is where using SwiftUI for this application definitely came back to bite me a little bit,

00:10:53 ◼   ► because I feel like in UIKit, this would have been a much, much easier thing,

00:10:57 ◼   ► because the gesture recognizers inside of UIKit are so good.

00:11:02 ◼   ► So that's a little bit of foreshadowing, but I start going down the road of doing this using the SwiftUI gestures,

00:11:08 ◼   ► and I get it vaguely to work, like in terms of, you know, you pick a picture,

00:11:14 ◼   ► which as a side note is using the UIImagePickerController API just wrapped in a SwiftUI view,

00:11:23 ◼   ► because that's the only way that I have found to be able to do this in a reasonable way.

00:11:28 ◼   ► And I love using UIImagePicker, because then I don't have the photo kit,

00:11:33 ◼   ► photo authorization issues, and having to ask people for permission to see all of their pictures,

00:11:38 ◼   ► which I don't want to get into. So I just want to use an image picker, and it works.

00:11:42 ◼   ► But I get the image, and it's giant, and so I need to zoom in and pan around,

00:11:46 ◼   ► and I get it working in SwiftUI. But it's one of those funny things,

00:11:52 ◼   ► and this is just an interesting note that I've been discovering in my work with SwiftUI and from UIKit,

00:11:59 ◼   ► is that there's a lot of these things in UIKit that they're just, like,

00:12:03 ◼   ► I imagine ultimately things like these do these little coefficients that are built into things like UIScrollView

00:12:09 ◼   ► and the way the UIPinchGestureRecognizer works and things, where I just could never get,

00:12:14 ◼   ► with a magnification gesture and the pan gesture inside of SwiftUI, things to behave like you would expect them to.

00:12:21 ◼   ► And it was hard to explain, because it was working correctly, in the sense of,

00:12:26 ◼   ► when I move my fingers out, it changes size, but it didn't change the right speed

00:12:32 ◼   ► or handle the ramp up and the ramp down. All these things just felt off.

00:12:38 ◼   ► And so, in the end, what I do is I just ended up having to get rid of all the native SwiftUI stuff,

00:12:43 ◼   ► and I just ended up wrapping some UIGestureRecognizers inside of a SwiftUI view

00:12:49 ◼   ► and end up using that as a way to use it inside of SwiftUI.

00:12:53 ◼   ► And that worked, except for all kinds of weird, just sort of like,

00:12:58 ◼   ► getting the state management of that working correctly just drove me insane.

00:13:02 ◼   ► There was probably a good four-hour period where everything seemed like it was working,

00:13:06 ◼   ► but nothing would update correctly.

00:13:08 ◼   ► And it turned out it was one of these weird things that had happened to me many times in SwiftUI,

00:13:12 ◼   ► where the wrong part of my view hierarchy's state data was getting updated.

00:13:19 ◼   ► And so, some of the parts were getting these change events.

00:13:24 ◼   ► So when I'm saying, "Move the image to the left, move the image to the right,"

00:13:27 ◼   ► some parts of it were getting it and some parts weren't.

00:13:30 ◼   ► And so it was just breaking in weird ways.

00:13:32 ◼   ► And so a little pro tip that I came up with with this, which is how I ultimately saved my sanity,

00:13:37 ◼   ► was I added an extension to color that you could just ask it for a random color.

00:13:43 ◼   ► So I just say color.random, and it just gives me a random color every time.

00:13:46 ◼   ► And I would wrap a bunch of my components at different levels of the view hierarchy

00:13:50 ◼   ► with .border, color.random.

00:13:53 ◼   ► So essentially, all the elements have these random colors, and if everything's changing correctly,

00:13:58 ◼   ► as I swipe my finger across, everything should go crazy and there should be these rainbow colors everywhere.

00:14:04 ◼   ► Every time it updates, it gets a new color.

00:14:07 ◼   ► And that would let me identify which part of the view hierarchy wasn't getting updated,

00:14:10 ◼   ► because its border would stay the same and wouldn't have the rainbow party.

00:14:15 ◼   ► So pro tip, if you ever need to debug SwiftView object refresh stuff, that's a great way to do it.

00:14:25 ◼   ► I wish I had thought of it earlier in the process rather than just banging my head against the wall for a long time.

00:14:32 ◼   ► But in the end, I was able to get it to work.

00:14:36 ◼   ► I was able to get the pan and zoom system to work reasonably well, and I ended up having to fall back to UIKit,

00:14:47 ◼   ► which I feel kind of sad about. There's part of me that wishes I didn't have to go back to UIKit, but I did, and so it's fine.

00:14:52 ◼   ► And in general, it worked reasonably well. There are a few areas that it was a bit funny,

00:14:58 ◼   ► because the number of different coordinate systems that I'm having to deal with in this app right now is too many.

00:15:05 ◼   ► It's a short reality, because I'm dealing with the UI views, coordinates and coordinatespace,

00:15:12 ◼   ► and then I need to translate that into the images coordinatespace, which is slightly different than the SwiftUI imagespace,

00:15:21 ◼   ► because then I need to ultimately, and this is the one that really, really hurt my head,

00:15:26 ◼   ► and this is where it made me laugh a little bit, because ultimately I have this image, I have it in a SwiftUI view,

00:15:32 ◼   ► and I can scale it up and down and I can move it around, so I can orient it onto a different part of the image.

00:15:36 ◼   ► And then I need to ultimately render that into a smaller, essentially the cropped version of that image,

00:15:42 ◼   ► is what I'm ultimately needing to render. But trying to get the coordinate system transform and the things like,

00:15:49 ◼   ► I need to essentially take the image and I need to actually be trying to render it at an origin that is outside of the frame that you can see.

00:15:56 ◼   ► So if you imagine you zoom in on a picture, the actual origin of that picture that I'm rendering is way off to the top left, conceptually.

00:16:04 ◼   ► And that way you can see just the little part in the middle in the window that you actually can see.

00:16:09 ◼   ► And trying to get that logic right, it made me laugh because it was essentially like machine learning programming,

00:16:14 ◼   ► is what I felt like I was doing, where I had no idea the right combination of negatives and positives,

00:16:21 ◼   ► and adding offsets and subtracting offsets, and all the things that I needed to do,

00:16:25 ◼   ► and when I need to multiply by the difference in aspect ratio between the two things, and all this.

00:16:30 ◼   ► I had no idea what I was doing at some point, and so I just kept trying. I just tried every possible version,

00:16:35 ◼   ► and then just like, once I got whichever version seemed slightly better, I would sort of like dial down into that,

00:16:41 ◼   ► and it's like this genetic algorithm I'm running where I just keep trying every possible solution of plus and minus,

00:16:46 ◼   ► and add and subtract, and divide and multiply, until it worked.

00:16:49 ◼   ► And eventually it worked, and it sort of is not a very viable way to do programming,

00:16:54 ◼   ► but in this particular case it actually sort of worked.

00:16:57 ◼   ► And that was just one of those funny, like, okay, once it worked, I'm like, great, it worked.

00:17:03 ◼   ► I don't actually know all the math that I did to get there.

00:17:07 ◼   ► I don't really understand why that's the particular math that I need to do, but I can do it.

00:17:10 ◼   ► I can take the giant image, you've moved it around with your pan and zoom gesture, and I can render it now into an image.

00:17:17 ◼   ► Hooray. It worked. But that's not the end of the story.

00:17:21 ◼   ► Well, we'll hear the shocking conclusion next, but first we are brought to you this week by Pingdom.

00:17:28 ◼   ► Do you have a website? Does your website have things like a shopping cart, or registration forms, or contact pages?

00:17:34 ◼   ► If you answered yes to any of these questions, you need Pingdom.

00:17:37 ◼   ► Nobody wants their critical website transactions to fail, because that means bad experiences for your users,

00:17:42 ◼   ► and could mean lost business for you.

00:17:44 ◼   ► Pingdom can monitor these transactions, in addition to monitoring your website uptime in general.

00:17:50 ◼   ► So, transaction monitoring will alert you when things like cart checkout, or form submissions, or login pages fail,

00:17:56 ◼   ► before they affect too many of your customers and therefore your business.

00:18:00 ◼   ► Pingdom will let you know the moment any of these fail in whatever way is best for you.

00:18:04 ◼   ► You can customize how you're alerted, who's alerted, depending on the outages, conditions, and severity.

00:18:09 ◼   ► And Pingdom cares about your users having the smoothest site experience possible.

00:18:14 ◼   ► If disaster strikes, you will be the first person to know, so you can go fix it.

00:18:19 ◼   ► It is super easy to get started on Pingdom. I personally have used Pingdom for a long time,

00:18:23 ◼   ► and I'm using it like literally right now, this morning.

00:18:26 ◼   ► I keep checking Pingdom because I'm having a network connectivity issue with some of my servers,

00:18:30 ◼   ► and I can look at Pingdom and I can see latencies of all the checks from all around the world that they're checking it from.

00:18:36 ◼   ► So I can see like, "Hey, this server's okay when it's checked from North America,

00:18:39 ◼   ► but right now when it's being checked from Europe, it's seeing, you know, unusual latency."

00:18:43 ◼   ► And I can actually go be notified of stuff like that and go fix it.

00:18:46 ◼   ► It is great. Go to Pingdom.com/RelayFM right now for a 30-day free trial with no credit card required.

00:18:55 ◼   ► Pingdom.com/RelayFM. When you sign up, use code RADAR at checkout to get a huge 30% off your first invoice.

00:19:03 ◼   ► Once again, Pingdom.com/RelayFM, code RADAR for 30% off your first invoice.

00:19:08 ◼   ► Thank you to Pingdom from SolarWinds for their support of this show and RelayFM.

00:19:12 ◼   ► Okay, so now that we left our hero, he had just finally gotten the basics of the system to work.

00:19:19 ◼   ► And so what does he do? Does he celebrate? Does he say, "Good job, Dave," and move on?

00:19:23 ◼   ► No. He decides, "Let's make this problem 10 times harder."

00:19:26 ◼   ► So what I just had the thought of is like most people, I think, who are going to use this feature

00:19:31 ◼   ► are going to take a picture probably of someone they care about and they want to show that person on their watch face.

00:19:37 ◼   ► So what do I think they do? Oh, I'm going to use the vision framework to identify faces in those images

00:19:43 ◼   ► and have the ability for you to zoom in automatically to the person.

00:19:48 ◼   ► If I detect people in a picture, I want you to be able to automatically zoom into those people's faces

00:19:53 ◼   ► and have that be the center point of the complication.

00:20:01 ◼   ► Seems like that should be reasonable, right? Like there's a whole framework for this.

00:20:04 ◼   ► Every year Apple talks about how amazing their machine learning and stuff is, and I've never had a reason to use it.

00:20:10 ◼   ► And I was like, "I finally have a reason to use machine learning kind of stuff in my application, so let's try it."

00:20:16 ◼   ► First thing I discovered, which broke my heart, was that the vision frameworks don't work in the simulator,

00:20:21 ◼   ► which doesn't make any sense to me because I have an M1 Mac, so I don't understand why they don't work,

00:20:27 ◼   ► but apparently they just don't. And so it's Apple Silicon. I don't know why these don't work, but they don't.

00:20:34 ◼   ► So that is always just a bit discouraging because a lot of this stuff, when I'm doing this iteration,

00:20:39 ◼   ► when I'm working kind of rapidly and trying to just work through stuff, and as we'll get into,

00:20:43 ◼   ► there's a whole other round of my machine learning-based programming where I'm just flailing wildly trying to get something to work.

00:20:49 ◼   ► It's so much nicer when it's in the simulator and not on the device, but that's not the case.

00:20:54 ◼   ► So I get on the device, and the nice thing, the vision framework is actually, it's relatively,

00:20:58 ◼   ► from what I'm trying to do with it, it's relatively straightforward. You give it an image,

00:21:01 ◼   ► it has this really weird call structure where you create a request, and then the request has to be passed to a request processor,

00:21:10 ◼   ► and there's all kinds of weird error states, but once I worked through that, I could get it so I could reliably say,

00:21:14 ◼   ► "Here's a UI image. Give me the rectangles where there's a face, if you see one, or nothing, if there was no face,

00:21:22 ◼   ► if it's a picture of a non-face, anyway." But the strange thing there is, of course,

00:21:28 ◼   ► I have to add in one more different coordinate system. So the vision framework does something that I've never seen anywhere else,

00:21:35 ◼   ► completely broke my brain for many hours, is it gives all of the rectangles in a normalized version,

00:21:43 ◼   ► where all of the values go from zero to one, and it's proportional inside of the width and the height of the image.

00:21:52 ◼   ► I don't know why, but that's what it does. And so everything you do has to be denormalized back into image space

00:22:01 ◼   ► before you can deal with it. And in a weird way, the y-axis seemed like it was flipped in a weird way,

00:22:11 ◼   ► and so even just trying to know if I was using it, which always drives me crazy when I'm working with something like this,

00:22:17 ◼   ► I wanted to see if I'm using it correctly. And so I give it an image, and I get back a rectangle,

00:22:23 ◼   ► and the rectangle is from 0.268, 0.563, and then it's 0.1 and 0.1. And it's like,

00:22:32 ◼   ► "What do I do with this? How do I know if that number is correct, if I'm using the framework correctly,

00:22:36 ◼   ► if the zigs are good?" And it took me forever to be able to reconvert that back into something that I could then pass to SwiftUI,

00:22:42 ◼   ► to then overlay an image, and then move that image inside of its geometry reader so that it was in the right place.

00:22:48 ◼   ► And eventually I got there. Eventually I didn't lose my mind, but there were a few touch-and-go moments there

00:22:55 ◼   ► where trying to get to that point turned out to just be so tricky. And I get why, in some ways, they're doing it that way,

00:23:03 ◼   ► but at a certain level, I'm like, "Why are you doing this? Why are you inventing this totally new..."

00:23:07 ◼   ► I've never seen any other UI framework in all of iOS, and I've been doing this for a long time,

00:23:12 ◼   ► where it's all normalized. It's a CG rect, but it isn't actually a CG rect, because it doesn't represent x and y coordinates.

00:23:20 ◼   ► Essentially it represents percentages of the image. But anyway, I finally got down there, and it was one of those things.

00:23:28 ◼   ► You know it's a good feature when the first time you do it, it just works perfectly, and it just does exactly what you want,

00:23:34 ◼   ► where I load up a picture of me and my family, and immediately it just zooms right in on our faces,

00:23:42 ◼   ► and I have a little preview window that shows how it would look on the watch, and it's just like, "Yep, that's perfect."

00:23:47 ◼   ► It's really cool when it happens, where the idea is relatively straightforward, but I didn't need to do it.

00:23:54 ◼   ► Once I've done it, it's like any other version of this is completely insufficient, because trying to get it exactly zoomed in

00:24:01 ◼   ► and tanned around and look just right, if I can do it automatically, that's so much better, and it makes the feature so much better.

00:24:08 ◼   ► Other than having to make it so that I zoom out slightly, because the funny thing about the vision framework is it's face detection,

00:24:15 ◼   ► not head detection, and so it gives me the rectangle that is essentially eyebrow to just below your lips, mid-chin to eyebrows,

00:24:25 ◼   ► and so the first version zoomed to there, and so everyone looks a little bit crazy, but I could zoom out a little bit,

00:24:32 ◼   ► and I just had a buffer to it, and finally it works, and then I had to convert that into zoom and pan calls,

00:24:39 ◼   ► as though it had happened to the pan and zoom thing, so that the UI pinch recognizer, which is deeply wrapped way down inside of a SwiftUI view,

00:24:47 ◼   ► can have the correct starting point when it starts to move again, but it all worked, and that was the end of this fun,

00:24:54 ◼   ► and it was interesting because, and at this point I'm still working entirely inside of a test project,

00:25:01 ◼   ► because I will say I highly recommend using test projects for this kind of thing, where I didn't worry about all the overhead that you have to do

00:25:10 ◼   ► if you actually incorporate this into your main app, because the number of times I just had to keep build and run, build and run,

00:25:17 ◼   ► because it was only building essentially one file, the build process was super fast, the run process was super fast,

00:25:24 ◼   ► it was easy to change the entry point into the application, so that it was always exactly what I was trying to test at a particular time,

00:25:30 ◼   ► and now I'm just going through the process of actually backporting essentially this test project into the main WatchSmith app,

00:25:36 ◼   ► and that so far has been going reasonably well, I've worked out most of the problems,

00:25:42 ◼   ► and now comes the only things that are going to be slightly annoying, or is always the thing when you're prototyping something,

00:25:49 ◼   ► you make a lot of assumptions, you make a lot of guesses, things that you don't necessarily have to worry about,

00:25:54 ◼   ► where in this version I need to make sure that I'm not creating files, sending them over to the watch,

00:26:00 ◼   ► and then never deleting them if they get replaced, for example, so I have to do that kind of bookkeeping and cache management

00:26:06 ◼   ► to make sure that I'm not just slowly gobbling up all the user space over time, and things like that which I didn't have to worry about in the test project,

00:26:13 ◼   ► but overall, that's where we are, a few more gray hairs as a result of this feature, but I finally got there.

00:26:19 ◼   ► Wow. Yeah, watch connectivity and trying to build apps on the watch is such that I'm kind of surprised that you have any non-gray hairs left,

00:26:30 ◼   ► given your career choices of focusing so much on the watch. I feel like this is a great example of, A, something that seems like it should be fairly straightforward,

00:26:42 ◼   ► and then realizing doing it at all, let alone doing it well, has some surprising pitfalls that you might have thought back in the beginning when you think,

00:26:52 ◼   ► "How hard could it be?" But also, I think this is a good example of tackling somewhat hard to do correctly things like this is a pretty good business model to attempt,

00:27:06 ◼   ► because here's something that, it seems easy, and it seems like a lot of people want this, so there's demand for this,

00:27:14 ◼   ► but doing it right is a little bit beyond what most iOS programmers are willing to tackle or able to tackle.

00:27:22 ◼   ► And if you make a career out of doing, you know, mostly easy enough things so you can handle it as an indie,

00:27:31 ◼   ► but occasionally tackling something that's kind of tricky like this and doing a really good job with it,

00:27:36 ◼   ► you can build a business on that, because there will be way less competition that is willing and able to go through all this hassle and get this right,

00:27:45 ◼   ► compared to, you know, you being willing, just wanting this to exist so badly that you're willing to go through all these challenges to get there.

00:27:52 ◼   ► And so, that actually, as long as the market wants whatever you're doing, that's actually a pretty reasonable business strategy.

00:27:59 ◼   ► Yeah, and I think it's also, I feel like it's one of these opportunities to grow as a developer. Like, I really,

00:28:06 ◼   ► as much as I joke about how sort of like crazy making it was, I'm really glad that I know how the vision framework works now.

00:28:14 ◼   ► Like, I've never used that before, and it's really cool. And I do think it is kind of one of these funny things where I do think it puts a moat around my app,

00:28:20 ◼   ► because there are so many steps that you would have to go through in order to recreate this feature that it just makes it that much harder.

00:28:27 ◼   ► Not that I think anyone else is doing this, because I think in general, with complications,

00:28:34 ◼   ► I'm pushing the complications way beyond what I think it was intended for with WatchSmith.

00:28:41 ◼   ► But from a business perspective, I do think it is a good thing. And hopefully it's a good thing.

00:28:45 ◼   ► Hopefully people like this feature. I really have enjoyed having pictures of my kids on my watch, and hopefully the other people like it too.

00:28:51 ◼   ► But the process of putting it there, it turns out to be really uncomfortable of a journey to make it happen.

00:28:58 ◼   ► Which is, hopefully, yeah, it's a business move, and it makes me think of a lot of the things where you think of some of the choices you make in Overcast,

00:29:07 ◼   ► with your voice processing and the way that you've kind of held the line on, there's been several points that there was the easy way,

00:29:14 ◼   ► and then there was the right way of building the feature. And you could have decided long ago,

00:29:18 ◼   ► "Oh, I'm not going to do any voice processing of things on the watch." And it's like, you decide to know,

00:29:22 ◼   ► everywhere you listen in Overcast, you want it to sound good. And as a result, that makes your life harder, but it makes the app better.

00:29:30 ◼   ► And I feel like that balance between the hard thing and the right thing, in this case, is definitely something that is not an easy choice always,

00:29:39 ◼   ► but it's usually a rewarding choice, and so I can certainly recommend it nevertheless.

00:29:43 ◼   ► Thanks for listening everybody, and we'll talk to you in two weeks.

00:29:46 ◼   ► Bye.

00:29:47 ◼   ► [ Silence ]