{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "mbehan.com",
  "home_page_url": "https://mbehan.com",
  "feed_url": "https://mbehan.com/feeds/feed.json",
  "description": "Sporadic postings of a computer enthusiast, Apple user, software engineering leader, husband / dad",
  "authors": [
    {
      "name": "Michael Behan",
      "url": "https://mbehan.com"
    }
  ],
  "language": "en-IE",
  "items": [
    {
      "id": "https://mbehan.com/index.html?post=20260612081039087",
      "url": "https://mbehan.com/index.html?post=20260612081039087",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Totally missed that an excellent AOE2: Definitive Edition macOS port came out last month. Deleting Parallels / Windows from my Mac feels so good</p>\n</article>",
      "summary": "Totally missed that an excellent AOE2: Definitive Edition macOS port came out last month. Deleting Parallels / Windows from my Mac feels so good",
      "date_published": "2026-06-12T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260610074302517",
      "url": "https://mbehan.com/index.html?post=20260610074302517",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Being signed into the App Store with my US account (but still signed into my main / Irish account in settings) is enough to get Siri AI in the EU, on the developer beta at least</p>\n</article>",
      "summary": "Being signed into the App Store with my US account (but still signed into my main / Irish account in settings) is enough to get Siri AI in the EU, on the developer beta at least",
      "date_published": "2026-06-10T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260609202310189",
      "url": "https://mbehan.com/index.html?post=20260609202310189",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>I didn’t plan to watch 2 1997 volcano thrillers in one day, but it happened. <br>\n  Dante’s Peak: ⭐️ ⭐️ ⭐️ ⭐️ <br>\n  Volcano: ⭐️ ⭐️</p>\n</article>",
      "summary": "I didn’t plan to watch 2 1997 volcano thrillers in one day, but it happened. \nDante’s Peak: ⭐️ ⭐️ ⭐️ ⭐️ \nVolcano: ⭐️ ⭐️",
      "date_published": "2026-06-09T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260608210614015",
      "url": "https://mbehan.com/index.html?post=20260608210614015",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Resizable iPhone mirroring, an Origami demo app, shirts so new you can still see the creases … are you getting it?</p>\n</article>",
      "summary": "Resizable iPhone mirroring, an Origami demo app, shirts so new you can still see the creases … are you getting it?",
      "date_published": "2026-06-08T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260608201202716",
      "url": "https://mbehan.com/index.html?post=20260608201202716",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>I get distracted watching WWDC videos for the same reason I get distracted watching Shrinking, thinking about how none of these clothes have ever been worn before</p>\n</article>",
      "summary": "I get distracted watching WWDC videos for the same reason I get distracted watching Shrinking, thinking about how none of these clothes have ever been worn before",
      "date_published": "2026-06-08T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260608161022720",
      "url": "https://mbehan.com/index.html?post=20260608161022720",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Relatedly, my enthusiasm for the technology and the products I've worked on has always been something I've used to my advantage as a manager, but it turns out forcing it when it just isn't there can be exhausting and isn't always worth it</p>\n  <p>Thread: <a href=\"https://mbehan.com/index.html?post=20260608160751691\">After 2 years as a certified AWS cloud practitioner, I'm no longer a certified AWS cloud practitioner</a></p>\n</article>",
      "summary": "Relatedly, my enthusiasm for the technology and the products I've worked on has always been something I've used to my advantage as a manager, but it turns out forcing it when it just isn't there can be exhausting and isn't always worth it",
      "date_published": "2026-06-08T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260608160751691",
      "url": "https://mbehan.com/index.html?post=20260608160751691",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>After 2 years as a certified AWS cloud practitioner, I'm no longer a certified AWS cloud practitioner</p>\n  <p><img src=\"https://mbehan.com/assets/posts/20260608160751691-20260608160751691-1.jpeg\" alt=\"Nic Cage feels good, man\"></p>\n  <p>Thread: <a href=\"https://mbehan.com/index.html?post=20260608161022720\">Relatedly, my enthusiasm for the technology and the products I've worked on has always been something I've used to my advantage as a manager, but it turns out forcing it when it just isn't there can be exhausting and isn't always worth it</a></p>\n</article>",
      "summary": "After 2 years as a certified AWS cloud practitioner, I'm no longer a certified AWS cloud practitioner",
      "date_published": "2026-06-08T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260608130044949",
      "url": "https://mbehan.com/index.html?post=20260608130044949",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p><img src=\"https://mbehan.com/assets/posts/20260608130044949-20260608130044949-1.jpeg\" alt=\"light house at Balbriggan harbour on a calm and sunny day\"></p>\n</article>",
      "summary": "light house at Balbriggan harbour on a calm and sunny day",
      "date_published": "2026-06-08T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260605092031093",
      "url": "https://mbehan.com/index.html?post=20260605092031093",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>All done with my AC chemo. I started weekly Paclitaxel yesterday and I'm really hoping the stories of if it being easier turn out to be true.</p>\n  <p>Cycles 3 and 4 were progressively more awful - the pattern was the same as earlier cycles but the side effects generally ramped up. I was fairly melted onto my bed for a good 10 days in round 4 with no interest in doing anything on the computer or even watching anything.</p>\n  <p>I was really banking on having a few good days at the end but they never quite arrived, by the time I had some energy the terrible taste and mouth sores were making me feel just as bad. There were a few days during the cycle that were definitely the most depressing and negative-thinking days I've ever had.</p>\n  <p>I never found a perfect response to the mouth issues, but regular salt/bicarb rinses for the ulcers and chewing gum to mask the taste helped. The one food I could reliably enjoy was McCambridge stone ground whole wheat bread with butter, the texture of the bread feels good on my weirdly smooth tongue but without being crispy/crunchy enough to bother my gums and butter is the one taste that seems to break through without much change. I was prescribed \"Magic Mouth Wash - BMX\": for the mouth ulcer pain but the pain had mostly gone by the time I got it.</p>\n  <p>There were 2 new side effects for cycle 4. Severe gassiness for a week or so that thankfully for me, but perhaps unthankfully for the family was not at all trapped. I also developed a bad looking rash all over my face, but it didn't actually cause any pain, I just look diseased.</p>\n  <p>My hair is still clinging on to most of my body besides the head. Not having to shave is definitely the one upside, my neck has never been happier. Chest has thinned out quite a bit but no budge at all on my shoulder hair, I might still have to give them their annual summer shave!</p>\n  <p>Just one day into 12 weeks of Taxol now but I feel better than I did yesterday so thats helping me feel a bit more hopeful.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/20260605092031093-20260605092031093-1.jpeg\" alt=\"Homer shaving his shoulders (getting it all shaved off)\"></p>\n</article>",
      "summary": "All done with my AC chemo. I started weekly Paclitaxel yesterday and I'm really hoping the stories of if it being easier turn out to be true.",
      "date_published": "2026-06-05T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260528083823150",
      "url": "https://mbehan.com/index.html?post=20260528083823150",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>A massive care package arrived from work, lots of great stuff inside from the great people at SiriusXM</p>\n  <p><img src=\"https://mbehan.com/assets/posts/20260528083823150-20260528083823150-1.jpeg\" alt=\"Huge box being delivered, as seen by the doorbell cam\"></p>\n</article>",
      "summary": "A massive care package arrived from work, lots of great stuff inside from the great people at SiriusXM",
      "date_published": "2026-05-28T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260515210721206",
      "url": "https://mbehan.com/index.html?post=20260515210721206",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Screenshot of my step count from Pedometer++ captures how my current chemo cycle is going, hopefully back in the green tomorrow</p>\n  <p><img src=\"https://mbehan.com/assets/posts/20260515210721206-20260515210721206-1.jpeg\" alt=\"\"></p>\n</article>",
      "summary": "Screenshot of my step count from Pedometer++ captures how my current chemo cycle is going, hopefully back in the green tomorrow",
      "date_published": "2026-05-15T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=apple-50-fave-5",
      "url": "https://mbehan.com/index.html?post=apple-50-fave-5",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>A lot of best / favourite Apple product lists going around during Apple's 50th anniversary year, here's my faves:</p>\n  <p>5. The iPhone 5C. The best material to make a phone from remains unapologetic plastic, I would love them see them have another go at it. If Crocs can have a comeback, so can those holey cases!</p>\n  <p>4. The Original Smart Cover, for iPad 2. Magnets and cloth have never combined so well, every subsequent iteration has been frustratingly worse. Was a big part of why the iPad 2 seemed so much cooler the original iPad for me.</p>\n  <p>3. The original Mac mini. It hasn't always been the case but the original base model was an actually cheap (even in Ireland, even for a student) way to get into the Mac with a decent spec. I plugged it into my Dell monitor, keyboard and mouse and never looked back!</p>\n  <p>2. iPhone X. Back when I used to get a new iPhone every year the ten lived up to the name by feeling three better than the seven it replaced! Still the best looking phone there's been.</p>\n  <p>1. M4 MacBook Air (or any of the current M2-M5 design). Best computer I've owned by a mile, including the better-spec-wise current MacBook Pros. Can throw it around the place, use it on the arm of the couch, never runs out of battery, crazy powerful.</p>\n  <p>Honorable mention: iPod Socks. My iPhones 3G to 4 lived in them, better than various more recent strap and case efforts.</p>\n</article>",
      "summary": "A lot of best / favourite Apple product lists going around during Apple's 50th anniversary year, here's my faves:",
      "date_published": "2026-05-13T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=any-austin-night-club",
      "url": "https://mbehan.com/index.html?post=any-austin-night-club",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>The Goldeneye 007 nightclub on Wii captures my favourite part of being a programmer - finding creative ways to make the computer do more than you think it should be able to. Also, probably my fave Any Austin video so far.</p>\n  <p><a href=\"https://www.youtube.com/watch?v=2IyjDtp7sJQ\">The Best Nightclub in Video Game History</a></p>\n</article>",
      "summary": "The Goldeneye 007 nightclub on Wii captures my favourite part of being a programmer - finding creative ways to make the computer do more than you think it should be able to. Also, probably my fave Any Austin video so far.",
      "date_published": "2026-05-13T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260507181403052",
      "url": "https://mbehan.com/index.html?post=20260507181403052",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Balbriggan beach shots from the weekend</p>\n  <p><img src=\"https://mbehan.com/assets/posts/20260507181403052-20260507181403052-1.jpeg\" alt=\"A cormorant drying its wings on the rocks\"></p>\n  <p><img src=\"https://mbehan.com/assets/posts/20260507181403052-20260507181403052-2.jpeg\" alt=\"Seagulls and a cormorant hanging out\"></p>\n  <p><img src=\"https://mbehan.com/assets/posts/20260507181403052-20260507181403052-3.jpeg\" alt=\"Seagulls swimming in the evening sun\"></p>\n  <p><img src=\"https://mbehan.com/assets/posts/20260507181403052-20260507181403052-4.jpeg\" alt=\"Seaweed covered rocks at low tide\"></p>\n  <p><img src=\"https://mbehan.com/assets/posts/20260507181403052-20260507181403052-5.jpeg\" alt=\"Rockabill in the distance\"></p>\n</article>",
      "summary": "Balbriggan beach shots from the weekend",
      "date_published": "2026-05-07T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=20260507070438050",
      "url": "https://mbehan.com/index.html?post=20260507070438050",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>I loved Sierra adventure games as a kid, this is a neat project.</p>\n  <p><a href=\"https://youtu.be/z9Yp9S23ICo?is=odfrJ03zV6j9i9KH\">Upscaling classic Sierra adventure games</a></p>\n</article>",
      "summary": "I loved Sierra adventure games as a kid, this is a neat project.",
      "date_published": "2026-05-07T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=ac-chemo-second-cycle",
      "url": "https://mbehan.com/index.html?post=ac-chemo-second-cycle",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Done with cycle 2 of AC. It followed a very similar path to the first, maybe slightly worse side effects overall but no ambulance ride this time so I'm calling it a win.</p>\n  <p>I mostly wanted to (and did) stay in bed for the first whole 9 or 10 days. First time around I made an effort to get out for a 30 minute walk on those days but I didn't this time. Any spells of feeling a bit better was given to messing around on the computer instead. I was still able to go sit with the family for dinner or maybe a bit of TV time most of those days too (Brooklyn 99 has been our go to if I'm up for it).</p>\n  <p>For the first 5 or so of these days nausea was the worst complaint, I needed to reach for the antiemetics more often than the first time around but they did their job again. For the rest it was more the tiredness and the on-edge / jittery / out of it feeling I reported last time. Perhaps-related, my Apple Watch informed me that my average resting heart rate has increased from low 50s to low 70s since I started the chemo (I told one of the oncology nurses and he said that was still an ok number).</p>\n  <p>I don't think it varied much through the cycle but my taste is significantly reduced now, water tastes and feels gross, most everything else is muted. I had a sore on the inside of my mouth that got bad for a couple of days, but I added salt and bicarb rinses a few times a day and kept away from anything crunchy or spicy and thankfully it didn't become a big problem. I've lost some weight, but I'm probably eating exactly what I should be, I'm avoiding the free office snacks and late night stress eating that is my usual routine when I'm working.</p>\n  <p>The hair was the biggest difference this time - it's gone! Started shedding 2 days into the cycle, it all went dry/stiff and it hurt to wash or touch. Came out slowly at first but after a couple of days it was coming out in big clumps anytime I touched it. When it started looking like a bad haircut I buzzed the rest off. Still hair remaining elsewhere on my body, curious to see what remains after the next cycle. The full hair status is:</p>\n  <p>👨‍🦲 ........ Head<br>\n  🐻 ........ Shoulders<br>\n  🐻 ........ Chest<br>\n  🐻 ........ Back<br>\n  👨‍🦲👨‍🦲 ... Downstairs</p>\n  <p>Things gradually improved from there and for the last couple of days (day 13 &amp; 14) I felt back to myself again. I was lucky it coincided with a bank-holiday weekend and some nice weather so I got to enjoy some time with the family.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/ac-chemo-second-cycle-shine-o-ball-o-png.webp\" alt=\"Homer next to the SHINE-O BALL-O\"></p>\n</article>",
      "summary": "Done with cycle 2 of AC. It followed a very similar path to the first, maybe slightly worse side effects overall but no ambulance ride this time so I'm calling it a win.",
      "date_published": "2026-05-06T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=54c23f36-2380-4b19-99d7-3d940cf0d550",
      "url": "https://mbehan.com/index.html?post=54c23f36-2380-4b19-99d7-3d940cf0d550",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Finally got around to watching Elio, loved it! ⭐️⭐️⭐️⭐️⭐️</p>\n</article>",
      "summary": "Finally got around to watching Elio, loved it! ⭐️⭐️⭐️⭐️⭐️",
      "date_published": "2026-05-02T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=side-project-enlightenment",
      "url": "https://mbehan.com/index.html?post=side-project-enlightenment",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>I've reached side-project enlightenment (or, what I find fun has changed)</p>\n  <p><img src=\"https://mbehan.com/assets/posts/side-project-enlightenment-side-projects.png\" alt=\"The beginner / mid / expert dev graph meme going from abandons half finished ideas, to must ship complete well engineered projects, to just do the fun parts.\"></p>\n</article>",
      "summary": "I've reached side-project enlightenment (or, what I find fun has changed)",
      "date_published": "2026-04-28T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=geordi-holodeck",
      "url": "https://mbehan.com/index.html?post=geordi-holodeck",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>I keep coming back to this scene of Geordi in the holodeck working a problem with the computer. With the latest Codex and Claude Code it feels like we're almost there.</p>\n  <p><a href=\"https://www.youtube.com/watch?v=4Faiu360W7Q\">Geordi solves a mystery on the Holodeck (Star Trek TNG)</a></p>\n</article>",
      "summary": "I keep coming back to this scene of Geordi in the holodeck working a problem with the computer. With the latest Codex and Claude Code it feels like we're almost there.",
      "date_published": "2026-04-24T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=tim-peeks",
      "url": "https://mbehan.com/index.html?post=tim-peeks",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Starting to notice the hair coming  out now, Tim Peeks here I come</p>\n  <p><a href=\"http://www.timpeeks.com\">Hello</a></p>\n</article>",
      "summary": "Starting to notice the hair coming  out now, Tim Peeks here I come",
      "date_published": "2026-04-23T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=bullet-memo",
      "url": "https://mbehan.com/index.html?post=bullet-memo",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Signed up for Bullet Memo. Fits my way of organising tasks but mostly I'm there for the insipiration boost I get using nice, thoughtful, opinionated software interfaces when I'm working on my own.</p>\n  <p><a href=\"https://www.bulletmemo.com\">Bullet Memo - The Software Developer's Superpower</a></p>\n</article>",
      "summary": "Signed up for Bullet Memo. Fits my way of organising tasks but mostly I'm there for the insipiration boost I get using nice, thoughtful, opinionated software interfaces when I'm working on my own.",
      "date_published": "2026-04-22T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=ed-fee",
      "url": "https://mbehan.com/index.html?post=ed-fee",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>TIL / remembered you get billed €100 for visiting the emergency department. I think it's to encourage people to go to their GP first but feels like they could carve out an exception for folks that have been ambulanced in after collapsing.</p>\n</article>",
      "summary": "TIL / remembered you get billed €100 for visiting the emergency department. I think it's to encourage people to go to their GP first but feels like they could carve out an exception for folks that have been ambulanced in after collapsing.",
      "date_published": "2026-04-22T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=open-moji",
      "url": "https://mbehan.com/index.html?post=open-moji",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>I used OpenMoji for all the emoji and icons here. Style is just what I wanted + Creative Commons license = can't go wrong.</p>\n  <p><a href=\"https://openmoji.org/\">OpenMoji</a></p>\n</article>",
      "summary": "I used OpenMoji for all the emoji and icons here. Style is just what I wanted + Creative Commons license = can't go wrong.",
      "date_published": "2026-04-22T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=new-site",
      "url": "https://mbehan.com/index.html?post=new-site",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Built this new website today. Wanted something that looked like a social feed but just for my posts and none of that following and retooting business. I like it!</p>\n</article>",
      "summary": "Built this new website today. Wanted something that looked like a social feed but just for my posts and none of that following and retooting business. I like it!",
      "date_published": "2026-04-22T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=mlb",
      "url": "https://mbehan.com/index.html?post=mlb",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>I've been leaning into my middle initial more lately</p>\n  <p><img src=\"https://mbehan.com/assets/posts/major-league-behan.jpg\" alt=\"Michael, in MLB logo form\"></p>\n</article>",
      "summary": "I've been leaning into my middle initial more lately",
      "date_published": "2026-04-21T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=frame-blaster-and-luminator",
      "url": "https://mbehan.com/index.html?post=frame-blaster-and-luminator",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Rewrote a couple of my old Objective-C tools / sample projects as Swift packages. Gave them snazzy new names while I was at it.</p>\n  <p><a href=\"https://github.com/mbehan/FrameBlaster\">GitHub - mbehan/FrameBlaster: For playing long image sequences without the memory spike you get with the UIImageView animationImages approach</a></p>\n  <p><a href=\"https://github.com/mbehan/Luminator\">GitHub - mbehan/Luminator: Simple dynamic lighting effects for iOS image views.</a></p>\n</article>",
      "summary": "Rewrote a couple of my old Objective-C tools / sample projects as Swift packages. Gave them snazzy new names while I was at it.",
      "date_published": "2026-04-21T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=ac-chemo-first-cycle",
      "url": "https://mbehan.com/index.html?post=ac-chemo-first-cycle",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>I'm just about at the end of my first 2-week cycle on AC (doxorubicin / cyclophosphamide) chemotherapy. I've read a lot of people's experiences with it on r/breastcancer but they're mostly women, and most seem to have handled it really well, so I wanted to share a different perspective.</p>\n  <p>I've felt really rough the majority of the time. Thankfully I had already decided to take the entire 8 weeks of this treatment off. I had been hoping I might be able to return to work during the 12 weeks of Taxol (paclitaxel) chemotherapy that will follow, but I can't imagine working until I'm completely done and recovered now. Most stories say Taxol isn't as bad, but the tiredness is cumulative and I'm already so tired.</p>\n  <p>The best way I can describe my baseline feeling is that it's like back in college if I'd been up all night coding and downing red bulls, a sort of shaky, on-edge feeling. On top of that it's like somebody turned up the gravity, my arms and legs feel heavier than they should, getting out of bed is a chore. I feel kinda detached from reality, like I'm watching a video of life going by rather than being in it. Nausea has been present much of the time, but only once or twice did it feel like I might barf, thankfully I didn't. I took all the recommended meds (Emend, Dexamethasone and Olanzapine) and only felt the need to reach for the additional antiemetics (Maxalon and Valoid) a couple of times.</p>\n  <p>My mouth feels crap and I can barely taste most foods. No full on sores or bleeding but theres a general unpleasantness. I'm swishing (as best one can swish 1ml of anything) Mycostatin 4 times a day and using Corsodyl's chlorohexidine mouthwash. The Corsodyl was making my toungue sting so I've started diluting it, which helps a lot.</p>\n  <p>I'm on the 2 week dose-dense schedule so I'm injecting Neulasta to boost my white-cell count but thankfully I've avoided the bone pain that is a common side effect of that.</p>\n  <p>On day 6 I fainted. My wife called an ambulance but I was conscious and feeling ok by the time they arrived. ECG, bloods, and vitals were all OK in the emergency department so they sent me home. Followed up at the oncology ward the next day with a CT scan of the brain which was clear and they had me wear a heart monitor for 48 hours which showed nothing, so the consensus is let's not worry about it unless it happens again. It was scary, for my wife more so than me (\"you looked dead\").</p>\n  <p>Maybe I overdid it that day, I had been going for a 30 minute walk each day and I was feeling better that morning so went a bit farther, I hadn't eaten much either, maybe that contributed. I've resolved to take it easier for the first week of cycle 2, now that I know there is a point where it starts to feel much better.</p>\n  <p>That point was around day 9, I've been gradually feeling more myself since then, it's day 13 now and I feel pretty much back to myself (taste aside), able to stay out of bed all day and have been doing some work on some coding projects.</p>\n  <p>No idea how the next cycle will go, but if it's about the same minus the fainting, I'll take it. No sign of my hair going yet, I've heard it should be any day now, I keep running my hands through it, looking at my empty hand, weirdly disappointed, waiting on that movie \"guy with cancer notices hair falling out\" moment!</p>\n  <p><img src=\"https://mbehan.com/assets/posts/walter-white-chemo.png\" alt=\"Screen grab from Breaking Bad - Walter White in his chemo chair\"></p>\n</article>",
      "summary": "I'm just about at the end of my first 2-week cycle on AC (doxorubicin / cyclophosphamide) chemotherapy. I've read a lot of people's experiences with it on r/breastcancer but they're mostly women, and most seem to have handled it really well, so I wanted to share a different perspective.",
      "date_published": "2026-04-20T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=backlog",
      "url": "https://mbehan.com/index.html?post=backlog",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Adding it to the backlog</p>\n  <p><img src=\"https://mbehan.com/assets/posts/chief-wiggum-invisible-typewriter.gif\" alt=\"Chief Wiggum typing it up on his invisible typewriter\"></p>\n</article>",
      "summary": "Adding it to the backlog",
      "date_published": "2026-04-12T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=blush-neo-midnight-air",
      "url": "https://mbehan.com/index.html?post=blush-neo-midnight-air",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Blush Neo next to the midnight Air</p>\n  <p><img src=\"https://mbehan.com/assets/posts/IMG_7824.jpeg\" alt=\"Blush Macbook Neo next to a Midnight Macbook Air\"></p>\n</article>",
      "summary": "Blush Neo next to the midnight Air",
      "date_published": "2026-04-02T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=lump-timeline",
      "url": "https://mbehan.com/index.html?post=lump-timeline",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Turns out I'm one of about 20 or 30 men who will be diagnosed with breast cancer in Ireland this year. I haven't found many men's stories, so I thought I'd share how it's gone for me so far. From finding a lump to diagnosis, surgery, and chemo within a couple of months.</p>\n  <p>I found a lump by chance during the first week of January, awkwardly taking off my oversized fleece hoodie that I was wearing with my hands inside more like a blanket. I noticed a hard lump under the surface next to my left nipple. I knew it wasn't good right away. A hard, round mass, about the size of one of those little rubber bouncy balls we always seem to end up with from kids' birthday party bags. Feeling around some more, I could tell it wasn't smooth like one of those, it had a rough texture, maybe more like a golf ball. It was late Saturday evening and I was up by myself. I decided I'd go to bed and make a plan in the morning.</p>\n  <p>The next morning I checked the lump was still there. I could feel its presence without touching it, and it was just about visible now that I knew it was there. I also noticed the nipple was slightly inverted. I didn't tell my wife right away. I planned to go to the GP during work the next day, hoping they'd say it was nothing, but the possibility of cancer was already top of my mind.</p>\n  <p>The next day was the first day back at work after Christmas. I split the morning between onboarding a new hire and calling around to get a GP appointment nearby. I was relieved when I got one who could squeeze me in at the end of their day on short notice. I gather I got a good one, given how seriously they took my concern and got things in motion. The GP had a look and feel around the lump, the nipple, and under my arm and said it needed to be checked out immediately. I was referred to the Triple Assessment Clinic at the Mater Private Hospital, and they called the next day with an appointment two days later.</p>\n  <p>The triple refers to having a mammogram, ultrasound, and a consultation with a surgeon in the same appointment. First up was the mammogram. I've always had enough man boob to be self-conscious at a swimming pool, but still it seemed like not quite enough to easily manipulate into the specialist X-ray machine, but we got there in the end. Several scans were taken on both sides and the technician was happy with the quality. That inverted nipple warranted an extra set of high-res shots.</p>\n  <p>Next up was the ultrasound. They were very thorough, really pushing the scanner deep into the tissue, especially in the underarm area. Later I'd realise this was because they were already seeing strong indications of malignancy and were looking for spread to the lymph nodes. I was told a biopsy was needed and they were going to proceed right there and then. I was given local anaesthetic and a large needle-like device emerged, which, with two loud thunks, took two samples through the core of the lump. They patched me up and sent me across the road to see the surgeon.</p>\n  <p>The surgeon described what they had seen in the imaging: a 23mm lump that needed further investigation by biopsy. It would take about two weeks for results, but he laid out the three possibilities clearly. The first was that the sample was normal tissue. He said if this was the result he'd order a new biopsy because something had clearly gone wrong. The second was atypical or benign. If so, he'd likely do an excision biopsy to confirm. The third was where the bets were being placed: that it was cancer, which would require mastectomy surgery, a lymph node biopsy, and potentially chemotherapy. Radiation looked unlikely. He set the appointment for the biopsy results and recommended that my wife come with me.</p>\n  <p>Waiting for the results was no fun. Some sleepless nights and consultations with Dr GPT went alongside pain and bruising from the biopsy, but work and kids and everything else carried on as normal. It was a bit less than two weeks later that we got the results, and option three was confirmed: invasive ductal carcinoma. Surgery was scheduled for two weeks' time. They would take the lump, the nipple, and the breast tissue from the left side, along with two sentinel nodes to test, as they'd be the first place the cancer would likely spread. They said three weeks after surgery we'd meet again to go over the findings and decide the rest of the treatment plan, but endocrine treatment was already likely given my tumour was oestrogen-receptor positive. After talking it through with the surgeon, the breast care nurse took us to another room to give us more time to ask questions and provide more info. It was funny that it was clear she was used to the man being there for moral support rather than being the patient. We were given a pink breast cancer booklet, but I'd already read up on everything in the weeks before, so there wasn't much to learn. It was all just actually happening for real now.</p>\n  <p>Surgery was a little scary. It was my first time under general anaesthetic, but everything went according to plan and I was able to go home the next morning. I emerged with a plastic tube coming from below the incision, with a bulb attached collecting fluid from where the breast tissue had been. Looking after the drain was probably the worst part of recovery. There was some pain, but it was easily managed with painkillers. There were restrictions in moving my left arm, but the physio exercises did the job and within a few weeks everything felt fairly back to normal. The drain could come out when it was draining less than 30ml a day, which took just over a week. I was back to work after two weeks. I'd have taken longer, but two weeks was all that was going to be paid.</p>\n  <p>The three-week wait after surgery was easier than the two after the biopsy. I had a lot more information now. From what I'd read, my cancer was very standard and the path ahead fairly well understood. If there was spread to the nodes that would be different, but everything so far indicated there wasn't. My appointment for the results was moved up. I got a call in work asking if I was free that day, so I headed over to the surgeon's office on my own. They said it was good news and didn't want to keep me waiting. Margins were clear, nodes were negative, and the tumour measured closer to 3cm than the original 23mm from imaging, but that wasn't a concern. There was one test left before we'd know what was next: the Oncotype test. That was ordered, my tumour sent to a lab in America, and I had another two weeks of waiting before seeing the oncologist once the results were in. This is the test that decides whether chemotherapy is needed, and in about 25% of cases like mine it is, so it means a lot of people can avoid it unnecessarily.</p>\n  <p>I didn't get warned to bring someone to the next appointment, so I went alone, but I could tell from the greeting at the door that it was probably bad news. The news was that my Oncotype score means chemotherapy should be effective at reducing my chance of recurrence. So it's not bad per se, but I haven't been able to avoid chemo. I'm dreading it more than having cancer. The cancer is gone for the most part, but now I have five months of treatment planned to clear up the stragglers. The oncologist explained everything clearly and I had the same extra time with the nurse as with the original diagnosis. They're well practiced at explaining how everything is going to go. Two months of AC followed by three months of Paclitaxel. The AC is usually harder. My hair will go. I should take off the entire time from work if possible and cancel my summer holidays. The only other prep the oncologist recommended was to tell the kids.</p>\n</article>",
      "summary": "Turns out I'm one of about 20 or 30 men who will be diagnosed with breast cancer in Ireland this year. I haven't found many men's stories, so I thought I'd share how it's gone for me so far. From finding a lump to diagnosis, surgery, and chemo within a couple of months.",
      "date_published": "2026-03-24T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=beach-sun",
      "url": "https://mbehan.com/index.html?post=beach-sun",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Lovely morning on Balbriggan beach</p>\n  <p><img src=\"https://mbehan.com/assets/posts/IMG_7830.jpeg\" alt=\"Bright sun over sea on a clear morning\"></p>\n</article>",
      "summary": "Lovely morning on Balbriggan beach",
      "date_published": "2026-03-19T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=birthday-party",
      "url": "https://mbehan.com/index.html?post=birthday-party",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>After much deliberation on gaming busses and party venues, our newly-9 year old went for an old school house party</p>\n  <p><img src=\"https://mbehan.com/assets/posts/IMG_7922.JPG\" alt=\"Musical chairs!\"></p>\n  <p><img src=\"https://mbehan.com/assets/posts/IMG_7923.JPG\" alt=\"Whacking the piñata!\"></p>\n</article>",
      "summary": "After much deliberation on gaming busses and party venues, our newly-9 year old went for an old school house party",
      "date_published": "2026-02-20T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=balbriggan-harbour-waves",
      "url": "https://mbehan.com/index.html?post=balbriggan-harbour-waves",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>The Irish Sea at Balbriggan harbour giving Atlantic coast vibes today</p>\n  <p><a href=\"https://mbehan.com/assets/posts/IMG_7596.mp4\">Big waves at Balbriggan harbour</a></p>\n</article>",
      "summary": "The Irish Sea at Balbriggan harbour giving Atlantic coast vibes today",
      "date_published": "2026-01-23T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=car-park-pillar",
      "url": "https://mbehan.com/index.html?post=car-park-pillar",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Got a new record in my morning work carpark challenge</p>\n  <p><img src=\"https://mbehan.com/assets/posts/IMG_7602.jpeg\" alt=\"Car parked next to carpark pillar with mere millimeters to spare\"></p>\n</article>",
      "summary": "Got a new record in my morning work carpark challenge",
      "date_published": "2025-12-29T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=rugger-with-the-fam",
      "url": "https://mbehan.com/index.html?post=rugger-with-the-fam",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>At the rugger with the fam</p>\n  <p><img src=\"https://mbehan.com/assets/posts/IMG_7400.jpeg\" alt=\"Sitting in the crowd at the Aviva stadium\"></p>\n</article>",
      "summary": "At the rugger with the fam",
      "date_published": "2025-11-08T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=fan-oven-bomb-defused",
      "url": "https://mbehan.com/index.html?post=fan-oven-bomb-defused",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Defused a bomb / replaced the fan oven heating element</p>\n  <p><img src=\"https://mbehan.com/assets/posts/IMG_7387.jpeg\" alt=\"Back of a fan oven, mid heating element replacement\"></p>\n</article>",
      "summary": "Defused a bomb / replaced the fan oven heating element",
      "date_published": "2025-10-31T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=forty",
      "url": "https://mbehan.com/index.html?post=forty",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p><img src=\"https://mbehan.com/assets/posts/IMG_6609.jpeg\" alt=\"My 40th birthday pint glass\"></p>\n</article>",
      "summary": "My 40th birthday pint glass",
      "date_published": "2025-06-28T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=nbsp-in-the-wild",
      "url": "https://mbehan.com/index.html?post=nbsp-in-the-wild",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Always love to see a &amp;nbsp; in the wild</p>\n  <p><img src=\"https://mbehan.com/assets/posts/IMG_5741.gif\" alt=\"An animated GIF showing a DART real time information board showing &amp;nbsp reaptedly\"></p>\n</article>",
      "summary": "Always love to see a &nbsp; in the wild",
      "date_published": "2025-06-15T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2022-01-31-being-right-on-the-internet",
      "url": "https://mbehan.com/index.html?post=2022-01-31-being-right-on-the-internet",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>I’m self-isolating with Covid. I don’t have the energy to complete any of my half-done side projects, but I have just enough to start some news ones!</p>\n  <p><img src=\"https://mbehan.com/assets/posts/futureproof.png\" alt=\"A text box that says Hello, World! and a button to Generate Proof, a hash is shown below\"></p>\n  <p>I've long had the notion that it would be fun to have an app to quickly generate and share hashes of predictions, so you could publicly declare a prediction that can be proven later without contaminating the timeline, so I started putting together that app today.</p>\n</article>",
      "summary": "I’m self-isolating with Covid. I don’t have the energy to complete any of my half-done side projects, but I have just enough to start some news ones!",
      "date_published": "2022-01-31T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2021-12-07-xcode-release-sizes",
      "url": "https://mbehan.com/index.html?post=2021-12-07-xcode-release-sizes",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Selected Xcode versions and their download size</p>\n  <p> 4.6: <code>1.59GB</code><br>\n   5.1: <code>2.11GB</code><br>\n   6.4: <code>2.61GB</code><br>\n   7.3: <code>4.7GB</code><br>\n   8.3: <code>4.20GB</code><br>\n   9.4: <code>4.91GB</code><br>\n  10.3: <code>5.6GB</code><br>\n  11.7: <code>7.59GB</code><br>\n  12.5: <code>10.98GB</code><br>\n  13.0: <code>9.98GB</code></p>\n</article>",
      "summary": "Selected Xcode versions and their download size",
      "date_published": "2021-12-07T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2021-06-28-swiftui-views-in-uikit",
      "url": "https://mbehan.com/index.html?post=2021-06-28-swiftui-views-in-uikit",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Incorporating SwiftUI Views in a UIKit Layout</p>\n  <p>Mixing some SwiftUI in with your UIKit app is a great way to start using it for real without having to commit to fundamentally changing how you build your app. Doing so requires some boilerplate and when what you want to start with is a single view that only makes up a small part of a screen it might seem overly onerous.</p>\n  <p>This almost put me off, but I saw it through and turned the boilerplate into an extension on UIViewController which will add your SwiftUI View on top of a placeholder UIView that has been laid out with constraints (in my use case, from a storyboard).</p>\n  <pre><code class=\"language-swift\">extension UIViewController {\n\t\tfunc addSwiftUIView&lt;Content&gt;(_ swiftUIView: Content, for placeholder: UIView) where Content: View {\n\t\t\t\tlet swiftUIController = UIHostingController(rootView: swiftUIView)\n\t\t\t\tself.addChild(swiftUIController)\n\n\t\t\t\tguard let swiftUIView = swiftUIController.view else { return }\n\t\t\t\tswiftUIView.translatesAutoresizingMaskIntoConstraints = false\n\t\t\t\tplaceholder.addSubview(swiftUIView)\n\t\t\t\tswiftUIController.didMove(toParent: self)\n\t\t\t\tswiftUIView.constrain(to: placeholder)\n\t\t}\n}</code></pre>\n</article>",
      "summary": "Incorporating SwiftUI Views in a UIKit Layout",
      "date_published": "2021-06-28T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2021-05-18-my-swift-extension-pattern",
      "url": "https://mbehan.com/index.html?post=2021-05-18-my-swift-extension-pattern",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>My Swift Extension Pattern</p>\n  <p>I’ve become aware of how frequently I create extensions in Swift. I previously viewed them as something to use sparingly, like when there was something missing from a built-in type that was needed frequently - but now I regularly write them for once-off use.</p>\n  <p>This is the example that got me thinking about it today:</p>\n  <pre><code class=\"language-swift\">extension UInt8 {\n\t\tstatic func entireRange() -&gt; ClosedRange&lt;UInt8&gt; {\n\t\t\t\treturn UInt8.min...UInt8.max\n\t\t}\n}</code></pre>\n  <p>Which exists only to be used in this other extension:</p>\n  <pre><code class=\"language-swift\">extension Data {\n\t\tstatic func randomBytes(size: Int) -&gt; Data {\n\t\t\t\tvar byteArray = [UInt8]()\n\n\t\t\t\tfor _ in 0..&lt;size {\n\t\t\t\t\t\tbyteArray.append(UInt8.random(in: UInt8.entireRange()))\n\t\t\t\t}\n\n\t\t\t\treturn Data(byteArray)\n\t\t}\n}</code></pre>\n  <p>Which exists to be called once in some test code that will eventually be replaced when working with the actual data.</p>\n  <p>I leave the extensions in context in the files where they’re being used rather than in a specific file for the extension, or a collection of helpers and I find the resulting code reads wonderfully.</p>\n</article>",
      "summary": "My Swift Extension Pattern",
      "date_published": "2021-05-18T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2021-04-14-new-2fa-app",
      "url": "https://mbehan.com/index.html?post=2021-04-14-new-2fa-app",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Every authenticator app I’ve used has annoyed me in some way so I’ve started making my own.</p>\n  <p>The first annoyance I’m working on removing is having to wait around to get the next code when the timer’s running down.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/thisonetime.gif\" alt=\"Screen capture: Swiping left and right to reveal copy and copy next options\"></p>\n</article>",
      "summary": "Every authenticator app I’ve used has annoyed me in some way so I’ve started making my own.",
      "date_published": "2021-04-14T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2020-03-30-explaining-obvious-things",
      "url": "https://mbehan.com/index.html?post=2020-03-30-explaining-obvious-things",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>You'd Be Surprised What People Don't Know</p>\n  <p>Have you ever thought about trying to explain something, but it's so obvious that you assume everyone already knows?</p>\n  <p>Have you ever thought that maybe there are lots of things you've never even though about explaining, because, well they're even more obvious than that?</p>\n  <p>I've been thinking about this lately and I know for sure I can answer yes to the first question, and almost certainly even more so to the second. I've avoided unnecessary explanations - success! Except I've come to realise that a great many of the explanations I avoided, code comments I didn't leave, blog posts I didn't write, were missed opportunities.</p>\n  <p>As Daniel Jalkut put it in a recent episode of Core Intuition, sometimes <a href=\"https://coreint.org/2020/02/episode-409-we-know-rome-too-well/\" target=\"_blank\" rel=\"noopener noreferrer\">we know Rome too well</a>. In his metaphor a lifelong resident of a city is less suited to writing a guide for visitors than a more recent arrival whose freshly acquired knowledge is less ingrained. He was talking about marketing his product, but it immediately resonated with me for a slightly different reason: bringing new developers on to an old codebase. The one or two people who've been working on the app for the last 5 years won't know what a new developer is going to struggle with, they may not spot that the fact that you have to do x,y and z before you call some particular function is non obvious.</p>\n  <p>With <a href=\"https://en.wikipedia.org/wiki/2019–20_coronavirus_pandemic\" target=\"_blank\" rel=\"noopener noreferrer\">recent events</a> there has been lots of sharing and explaining about a couple of things that I thought were pretty obvious. Hand washing, and exponential growth. It turns out a lot of people don't know that soap is the thing that cleans your hands and that you don't have to double things very many times before the numbers get huge. I'm really glad that people have been sharing because it wasn't until I really thought about it that it made sense that not everyone knows this already and that I just happened to know them too well. It turns out my obsession with computers and taking chemistry in secondary school with a teacher that now that I think about it was way too excited about soap than is normal made me uncommonly equipped for current events.</p>\n  <p>I've learned that it's important to examine how we've come to know something, and to consider if it's reasonable to assume others will have had similar opportunity to come by this knowledge when deciding if something is worth explaining, commenting on or sharing. I've resolved to document earlier, comment better, and in general share more. I'm also planning to make newcomers central to my efforts to fill in documentation deficits and improve developer onboarding on older apps.</p>\n  <p>Hopefully you didn't already know all this.</p>\n</article>",
      "summary": "You'd Be Surprised What People Don't Know",
      "date_published": "2020-03-30T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2019-11-23-app-background-notifications-combime",
      "url": "https://mbehan.com/index.html?post=2019-11-23-app-background-notifications-combime",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Detecting When Your App Gets Backgrounded using Combine</p>\n  <p>The introduction of the <a href=\"https://developer.apple.com/documentation/combine\" target=\"_blank\" rel=\"noopener noreferrer\">Combine Framework</a> provides a new (reactive) way to respond to system events such as your app entering the background.</p>\n  <p>In this example I have a <code>CALayer</code> subclass running some <code>CABasicAnimation</code>s that need to be paused when the app gets backgrounded and resumed when the app becomes active again. In the layer's <code>init</code> we attach a subscriber to the default notification centre's publisher for the particualr events that we're interested in. When attaching the subscriber we provide a closure that will be executed every time a new event arrives.</p>\n  <pre><code class=\"language-swift\">let bgSubscriber = NotificationCenter.default\n   .publisher(for: UIApplication.willResignActiveNotification)\n   .sink {\n      _ in\n\n      self.pauseAnimation()\n   }\n\nlet foregroundSubscriber = NotificationCenter.default\n   .publisher(for: UIApplication.didBecomeActiveNotification)\n   .sink {\n      _ in\n\n      self.resumeAnimation()\n   }\n}</code></pre>\n  <p>Let's dig in a little to what's going on here because it may not be obvious if you're not familiar with Combine or NotificationCenter.</p>\n  <p>NotificationCenter predates Combine as a mechanism for broadcasting information (notifications) to interested observers. Every app gets a default notification centre which is used to broadcast system events such as those in the example here. Without Combine you can register to observe notifications and provide a selector to define what function to call when a notification is received.</p>\n  <p>With the introduction of Combine however, NotificationCenter added the <code>publisher(for:object:)</code> method which returns a <code>Publisher</code> that will emit values when notifications are broadcast.</p>\n  <p>A Publisher is how Combine represents a sequence of values over time that can be subscribed to. By calling <code>sink(receiveValue:)</code> we create a subscriber that will receive every value from the publisher.</p>\n  <p>The true power and utility of Combine isn't seen in this example, but even still I don't think it's overkill to use Combine to provide a modern Swifty replacement for old fashioned notification observers and selectors.</p>\n  <p>To complete this specific example there's a little more work to do. In my app I create many of these custom <code>CALayer</code> instances and at various points they get removed and new ones created in their place. With the code above as it is we're preventing the layers from being deallocated when they're no longer being used (because we captured <code>self</code> in the closure passed in to the <code>sink</code> call). To fix this problem we can keep a reference to the subscribers</p>\n  <pre><code class=\"language-swift\">\nprivate var appEventSubscribers =  [AnyCancellable]()\n\ninit(){\n\t\n   ...\n\t\n   appEventSubscribers = [foregroundSubscriber, bgSubscriber]\n\n}</code></pre>\n  <p>so that we can cancel them when the layer gets removed from its parent.</p>\n  <pre><code class=\"language-swift\">override func removeFromSuperlayer() {\n        appEventSubscribers.forEach {\n            $0.cancel()\n        }\n\n        super.removeFromSuperlayer()\n    }\n}</code></pre>\n</article>",
      "summary": "Detecting When Your App Gets Backgrounded using Combine",
      "date_published": "2019-11-23T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2019-07-15-ipad-os-names-are-important",
      "url": "https://mbehan.com/index.html?post=2019-07-15-ipad-os-names-are-important",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>iPad OS or, Names Are Important</p>\n  <p>When I attended WWDC for the first time in 2010 the iPad was still brand new and not yet available in Ireland. Many had dismissed it as just a big iPhone, others (me, at least) had hyped it as A BIG IPHONE.</p>\n  <p>It ran iPhone OS version 3.2.</p>\n  <p>As we queued up before the Monday morning keynote we were all expecting the next iPhone and the next version of iPhone OS and we speculated about what new features we'd see. There was one seemingly minor announcement though, that nobody was expecting: the next version of the operating system would be called iOS 4, dropping the 'iPhone' to accommodate iPad. It made sense at the time and initially my only reaction was delight at how they managed to do it mid presentation, with nobody slipping up and using the new name before the announcement and nobody using the old one afterwards. In the years that followed though, as the 'just a big iPhone' opinions persisted and many developers put forth minimal effort in making their iPhone apps work on iPad, I came to think that not creating a separate identity for the  iPad's operating system was a missed opportunity, one that they finally rectified this year by renaming iPad's operating system to iPad OS.</p>\n  <p>Perhaps making it incredibly clear that iPhone developers could make iPad apps made the previous OS naming scheme worthwhile up to a point but I believe making it clearer that iPad has its own OS, with its own interface, its own set of interactions and idioms that are distinct from iPhone will be a bigger benefit to software on the platform. OS naming is now consistent across all of Apple's mobile devices - they are all based off iOS but Apple Watch runs watch OS, Apple TV runs tv OS and iPad runs iPad OS. These operating systems are not distinct in terms of technology or how we develop for them or who can develop for them, the distinction rather is in how the user interacts with them. Hopefully (and I believe it will) for iPad, this will result in more differentiated iPad software that takes advantage of its unique and powerful features.</p>\n</article>",
      "summary": "iPad OS or, Names Are Important",
      "date_published": "2019-07-15T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2018-11-01-a-swiftier-looking-button",
      "url": "https://mbehan.com/index.html?post=2018-11-01-a-swiftier-looking-button",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Passing a closure to a UIButton</p>\n  <p>I'm tired of <code>@objc</code> <code>#selector (nonsense: )</code> muddying up my Swift code. This most commonly rears its ugly head when dealing with buttons. Why can't we just provide a button with a closure to execute when someone taps it? *</p>\n  <pre><code class=\"language-swift\">// 😩\nbutton.addTarget(self, action: #selector(myButtonHandler), for: .touchUpInside)\n\n// 😍\nbutton.on(.touchUpInside){\n   ...\n}</code></pre>\n  <p>Well now you can! I made a small UIButton subclass that provides a swifty facade for adding target/actions for events and otherwise behaves exactly like a regular old UIButton (to be clear, selectors are still doing the business under the hood.)</p>\n  <p><a href=\"https://gist.github.com/mbehan/708b67552162aceff89679bdcf3728e6\" target=\"_blank\" rel=\"noopener noreferrer\">Full source on gist.</a></p>\n  <p>* The reasons are simple and boring and because we're still working with Objective-C frameworks from the 1890s (that for the record, I still love), but that wasn't the point of this post.</p>\n</article>",
      "summary": "Passing a closure to a UIButton",
      "date_published": "2018-11-01T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2017-11-10-apple-does-not-share-your-face-id-data",
      "url": "https://mbehan.com/index.html?post=2017-11-10-apple-does-not-share-your-face-id-data",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>No, Apple Does Not Share Your FaceID Data</p>\n  <p>The notch full of sensors on iPhone X enables Face ID to capture</p>\n  <blockquote>accurate face data by projecting and analyzing over 30,000 invisible dots to create a depth map of your face and also captures an infrared image of your face. A portion of the A11 Bionic chip's neural engine — protected within the Secure Enclave — transforms the depth map and infrared image into a mathematical representation and compares that representation to the enrolled facial data.</blockquote>\n  <p>Meanwhile, from the same notch, third party developers can access</p>\n  <blockquote>a coarse 3D mesh geometry matching the size, shape, topology, and current facial expression of the user’s face.</blockquote>\n  <p>These are 2 different things.</p>\n  <p>For more see <a href=\"https://support.apple.com/en-us/HT208108\" target=\"_blank\" rel=\"noopener noreferrer\">Apple's support article on Face ID</a> and their <a href=\"https://developer.apple.com/documentation/arkit/creating_face_based_ar_experiences\" target=\"_blank\" rel=\"noopener noreferrer\">developer documentation on ARKit Face Tracking</a>.</p>\n</article>",
      "summary": "No, Apple Does Not Share Your FaceID Data",
      "date_published": "2017-11-10T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2017-11-06-develop-apple-watch-apps",
      "url": "https://mbehan.com/index.html?post=2017-11-06-develop-apple-watch-apps",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>The Apple Watch's Platform Problem</p>\n  <p>I've a phone in my pocket most of the time, Alexa is always waiting for me in the kitchen and I spend hours every day in front of an old fashioned PC but my watch is with me all day long wherever I go. Sometimes it's the only computation available and almost always it's the least intrusive.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/AppleWatch.png\" alt=\"Apple Watch, The Computer That&#x27;s Always There\"></p>\n  <p>As good as Apple Watch is though, it has so far failed as an app platform. Apple Watch is built on the same technology that runs iPhone, and the same tools that developers use to make iPhone apps are used to make Apple Watch apps, so why are there so few Apple Watch apps, and why are so many really bad?</p>\n  <p>The original watch hardware was very limited, and app support even more so. Apps actually ran on your phone and were sort of beamed onto the watch's screen. If you managed to find the apps on the terrible honeycomb grid they loaded really slowly and performed terribly. A lot of developers and users were instantly turned off of third party apps, but the watch got by with the excellent built in notifications and fitness tracking functionality.</p>\n  <p>Each release of watchOS and every hardware revision has seen incremental improvements to third party app support: apps actually running natively on the watch, custom watch face complications, new capabilities, better performance, and a better way of launching apps (a list!). But the great new app platform imagined when the watch was first announced has still to arrive and many apps on your Apple Watch today likely still date back to the original release.</p>\n  <p>I don't know what it will take for the Apple Watch platform to become as successful as the Apple Watch but I don't think  the capability of the device or the OS is holding it back at this stage (though WatchKit does leave a lot to be desired.) I'd like to see watch apps completely decoupled from iPhone apps (they run on the watch now, but are still delivered as extensions of iPhone apps) and they need to have more ways to integrate with or at least appear on the watch face. At least then we might finally be able to rule out finding, installing and launching watch apps as the reason for there not being very many good ones.</p>\n</article>",
      "summary": "The Apple Watch's Platform Problem",
      "date_published": "2017-11-06T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2017-10-31-free-unlimited-icloud-storage",
      "url": "https://mbehan.com/index.html?post=2017-10-31-free-unlimited-icloud-storage",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Why Can't We Just Pay for Free Unlimited iCloud Storage?</p>\n  <p>Over the past few years Apple has proven that they're willing to try charging higher prices for iPhone. Just a couple of years ago the 6S plus was priced from $749, a year later the 7 plus was available from $769 and now the 8 plus is on sale from $799. Meanwhile, the market has shown it's happy to pay those prices and I suspect it will prove so once more with the impending $999 iPhone X.</p>\n  <p>What I'd like to see next year is for Apple to charge us <em>even more</em> money for phones that don't cost them anything extra to produce, and here's why:</p>\n  <p>The experience of figuring out that you might need an iCloud subscription, figuring out how much space you might need, paying for it, dealing with the inevitable failures to renew when your card expires or your balance is low, and getting warnings about backups failing is awful. I'd love to see Apple try to figure out the cost of providing all new iPhone users with unlimited (with an asterisk that says there's actually some limits) iCloud storage and build it into the price of the phone.</p>\n  <p>I pay Apple $35.88 for iCloud storage each year, I'd happily pay $99 more for the phone instead.</p>\n</article>",
      "summary": "Why Can't We Just Pay for Free Unlimited iCloud Storage?",
      "date_published": "2017-10-31T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2017-05-10-avaudioplayer-smart-speed",
      "url": "https://mbehan.com/index.html?post=2017-05-10-avaudioplayer-smart-speed",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Audio Degapinator - The Poor Dev’s Smart Speed</p>\n  <p>I've been listening to podcasts with <a href=\"http://www.overcast.fm\" target=\"_blank\" rel=\"noopener noreferrer\">Overcast</a>'s Smart Speed feature turned on for long enough to have saved 55 hours of not listening to the silences between every podcast host's thoughts.</p>\n  <p>I decided to spend 1 of those hours today making my own simple, but surprisingly effective AVAudioPlayer version of that feature. I'll explain below how it works, but you can check out the full Swift iOS source (there's not much to it) on GitHub: <a href=\"https://github.com/mbehan/audio-degapinator\" target=\"_blank\" rel=\"noopener noreferrer\">Audio Degapinator on GitHub</a>.</p>\n  <p>AVAudioPlayer offers features for audio level metering:</p>\n  <pre><code class=\"language-swift\">/* metering */\n    \nopen var isMeteringEnabled: Bool /* turns level metering on or off. default is off. */\n\nopen func updateMeters() /* call to refresh meter values */\n\nopen func peakPower(forChannel channelNumber: Int) -&gt; Float /* returns peak power in decibels for a given channel */\n\nopen func averagePower(forChannel channelNumber: Int) -&gt; Float /* returns average power in decibels for a given channel */</code></pre>\n  <p>And for adjusting playback, including:</p>\n  <pre><code class=\"language-swift\">open var rate: Float /* See enableRate. The playback rate for the sound. 1.0 is normal, 0.5 is half speed, 2.0 is double speed. */</code></pre>\n  <p>My code then:</p>\n  <p>• turns metering on<br>\n  • updates meters with a timer<br>\n  • checks if there is currently silence playing using averagePower<br>\n  • increases the playback rate 2x until the silence ends</p>\n  <p>I tested using the latest episode of <a href=\"http://atp.fm/episodes/220\" target=\"_blank\" rel=\"noopener noreferrer\">ATP</a> and <a href=\"http://www.imore.com/debug-49-siracusa-round-2\" target=\"_blank\" rel=\"noopener noreferrer\">Debug episode 49</a>. In both cases the silences were noticeably reduced and, to my ear, sounded completely natural. I listened to the entire episode of Debug and it had shaved off a little over 3 minutes by the end.</p>\n  <p>This was a fun little project, it's the first time I've looked at anything related to audio playback on iOS in quite a while and it was super interesting ... I fear I may just have to write my own podcast app now.</p>\n</article>",
      "summary": "Audio Degapinator - The Poor Dev’s Smart Speed",
      "date_published": "2017-05-10T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2016-11-09-simulating-universal-gravitation-with-spritekit",
      "url": "https://mbehan.com/index.html?post=2016-11-09-simulating-universal-gravitation-with-spritekit",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Simulating Universal Gravitation with SpriteKit</p>\n  <p>Gravity in SpriteKit is a <em>single planet</em> sort of gravity. By that I mean that it applies a single force to all bodies in the simulation - basically everything falls down. But what if you wanted a <em>mutliple planet</em> sort of gravity, can that be achieved in SpriteKit?</p>\n  <p>The answer is yes, and it turns out it's it doesn't take a whole lot of code to get some fun and quite realistic results.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/fgmmr.gif\" alt=\"Screen capture. Small circular nodes orbit a larger central node like planets around a star.\"></p>\n  <p>In this SpriteKit app, dragging on the screen creates a new 'planet'.</p>\n  <p>First we turn off gravity as it normally applies in a SpriteKit scene and then on every tick we apply Newton's law of universal gravitation to all the nodes in the physics simulation.</p>\n  <p><img src=\"https://upload.wikimedia.org/wikipedia/commons/0/0e/NewtonsLawOfUniversalGravitation.svg\" alt=\"F=G m1.m2/r^2\"></p>\n  <p>That is for every pair of nodes, apply a force to each one that is equal to the product of their masses (and the universal gravitational constant), divided by the distance between them, squared.</p>\n  <p>There are some tweaks to the above formula to make the numbers a bit easier to deal with (i.e. smaller) and to make creating stable systems a bit easier, but sticking exactly to the formula above and plugging in some realistic numbers things work pretty much as you'd expect.</p>\n  <p>In addition to simulating gravity, I'm also combining planets that pass close to each other and adding trails to trace their paths and giving new planets random colour. It all results in a surprisingly fun and addictive little toy so even if you're not interested in the code just build and run it on your iPhone (or watch, or Mac) and enjoy!</p>\n  <p>Get the code at <a href=\"https://github.com/mbehan/fgmmr\" target=\"_blank\" rel=\"noopener noreferrer\">github.com/mbehan/fgmmr</a>.</p>\n</article>",
      "summary": "Simulating Universal Gravitation with SpriteKit",
      "date_published": "2016-11-09T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2016-10-18-detect-which-complication-launched-watchkit-app",
      "url": "https://mbehan.com/index.html?post=2016-10-18-detect-which-complication-launched-watchkit-app",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Detecting Which Complication Launched Your WatchKit App</p>\n  <p>One of the joys of working with watchOS, much like it was working with iPhone OS many years ago, is the enforced simplicity. Free from worrying about about the unending device combinations and configurations and the unlistible features and extension points of modern iOS the constraints of a limited SDK focus your creativity. Simple, robust, yet still delightful interfaces flow from your fingertips, designers designs are readily translated to working product.</p>\n  <p>Sadly though, we're not content for long. Just like in the early days of iPhone OS, you soon find yourself wanting to do just a tiny bit more than Apple has made available, and so focus and delight makes way to our more common friend, the ugly hack. Today's feature that just couldn't wait for a proper API is: detecting which watch face complication launched my app.</p>\n  <p>When your app is launched in response to the user tapping a complication, the <code>handleUserActivity</code> method of your <code>WKExtensionDelegate</code> is called. You're given a <code>userInfo</code> dictionary, and this is where we'd hope to find the details of which complication had launched us. Sadly though there's no <em>CLKComplicationFamilyKey</em> to let you know the user tapped the circular small rather than the utilitarian large to launch the app, but there is something we can use, the <code>CLKLaunchedTimelineEntryDateKey</code>. This gives us the exact date and time that the complication was created at. By remembering exactly when we created which complication then we can figure out which complication resulted in the app being launched and acting accordingly.</p>\n  <pre><code class=\"language-swift\">// 1.\nclass ComplicationTimeKeeper{\n    \n    static let shared = ComplicationTimeKeeper()\n   \n    var utilitarianLarge : Date?\n    var utilitarianSmall : Date?\n    var circularSmall : Date?\n    var modularLarge : Date?\n    var modularSmall : Date?\n}\n\n// 2. in your CLKComplicationDataSource\nfunc getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping ((CLKComplicationTimelineEntry?) -&gt; Void)) { \n     \n     // Call the handler with the current timeline entry\n     switch complication.family {\n     case .utilitarianLarge:\n            \n         let date = Date()\n         ComplicationTimeKeeper.shared.utilitarianLarge = date\n            \n         let template = CLKComplicationTemplateUtilitarianLargeFlat()\n         template.textProvider = CLKSimpleTextProvider(text:\"Something\")\n            \n         let timelineEntry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)\n         handler(timelineEntry)\n            \n     default: handler(nil)\n     }\n}\n\n// 3. in your WKExtensionDelegate\nfunc handleUserActivity(_ userInfo: [AnyHashable : Any]?) {\n    \n    guard let userInfo = userInfo, let timelineDate = userInfo[CLKLaunchedTimelineEntryDateKey] as? Date else{\n        return\n    }\n    \n    if let utilLarge = ComplicationTimeKeeper.shared.utilitarianLarge, timelineDate.compare(utilLarge) == .orderedSame {\n         WKExtension.shared().rootInterfaceController?.pushController(withName: \"SomeController\", context: nil)\n    }\n}</code></pre>\n  <p>In 1, we create a singleton (no shameful hack is complete wihtout one) to track when our various complications were made.</p>\n  <p>In 2, we setup the utilitarian large complication and store the creation date, just add more cases to the switch statement for other complication families that you are supporting.</p>\n  <p>Finally in 3 we check what time the complication that launched the app was created and check which one it was and launch the relavant interface controller.</p>\n  <p>The code above has a couple of limitations that you may need to work around. First it doesn't take Time Travel into account so if your app supports that each complication may have more than one corresponding datetime. Secondly (though in practice I haven't seen this be an issue) I don't see why two complications couldn't have clashing datetimes, for that you could add a method to <code>ComplicationTimeKeeper</code> that returns the next unique date.</p>\n  <p>It's Time For Complications</p>\n  <p>Apple made much of the value of complications at this years WWDC. Having originally not allowed you to make your own in watchOS 1, to allowing you but telling you its only if you really have something super important that gets updates throughout the day in watchOS 2, now this year they told us we really need to have a complication even if its just an icon to launch your app. It seems they've noticed, as anyone who has worn Apple Watch for any reasonable amount of time will tell you, that complications are the best way to access the functionality of an app. But everything they talked about at WWDC was about having a complication, singular. You can support multiple complication families, but you can only have one of each and they are treated as different views of a single feature, showing more data when you've the room, but not really doing anything different.</p>\n  <p>Ideally, we'd have the ability to provide multiple complications for each complication family. If that was the case you could have a watch face with each complication slot filled by the same application, each showing something else (the built in world clock complication can already do this, but nothing else) and crucially each performing a different function of your app when they're tapped. I wouldn't be surprised if this is something that is eventually supported in WatchKit, but for now at least we can ugly hack our way to using different complication families to provide different functionality.</p>\n</article>",
      "summary": "Detecting Which Complication Launched Your WatchKit App",
      "date_published": "2016-10-18T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2016-09-24-deprecate-uilongpressgesturerecognizer",
      "url": "https://mbehan.com/index.html?post=2016-09-24-deprecate-uilongpressgesturerecognizer",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Should Apple Deprecate UILongPressGestureRecognizer?</p>\n  <p>The answer is <em>yes</em>.</p>\n  <p>• For anywhere you currently require a long press, move to 3D touch.<br>\n  • For anywhere you have different actions for both, make the long press action an option when 3D touching. (For example organising icons on the home screen.)<br>\n  • Make an accessibility preference that makes a long press behave as a progressively more forcefull 3D touch.</p>\n</article>",
      "summary": "Should Apple Deprecate UILongPressGestureRecognizer?",
      "date_published": "2016-09-24T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2015-07-03-cheating-on-swift-substrings",
      "url": "https://mbehan.com/index.html?post=2015-07-03-cheating-on-swift-substrings",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Cheating on Swift Substrings</p>\n  <p>If you found yourself needing to get a substring of a String in Swift before you got around to the relavant chapter of <a href=\"https://itunes.apple.com/ie/book/swift-programming-language/id881256329?mt=11\" target=\"_blank\" rel=\"noopener noreferrer\">the book</a> you were probably left scratching your head for a bit. How could something so conceptually simple be so akward to perform?</p>\n  <p>Here's a great article explaining <a href=\"http://natashatherobot.com/swift-string-substringinrange/\" target=\"_blank\" rel=\"noopener noreferrer\">how to find a substring in Swift</a>, from <a href=\"https://twitter.com/NatashaTheRobot\" target=\"_blank\" rel=\"noopener noreferrer\">Natasha The Robot</a>.</p>\n  <p>It turns out that Swift's strings are much cooler than your old fashioned strings from other languages, and Swift Ranges are even cooler still. But unless you're using them frequently, I find that</p>\n  <pre><code class=\"language-swift\">str.substringWithRange(Range(start: (advance(str.endIndex, -1)), end: str.endIndex))</code></pre>\n  <p>doesn't exactly roll off the tongue.</p>\n  <p>So here's my cheat, which is to not use <code>String</code> at all. Arrays in Swift can be chopped up using plain integer ranges, and a <code>String</code> is just an <code>Array</code>. Swift even lets you iterate over the contents of a <code>String</code> and access each <code>Character</code> in turn, but it doesnt give you string subscripting.</p>\n  <p>So theres a couple of cheat options, implement subscript on <code>String</code> yourself, or what I preferred, extend <code>String</code> to give you quick access to an <code>Array</code> representation of the string.</p>\n  <pre><code class=\"language-swift\">extension String {\n    func asArray() -&gt; [Character] {\n        \n        var array : [Character] = []\n        for char in self {\n            array.append(char)\n        }\n        return array\n    }\n}</code></pre>\n  <p>You can then do fun stuff like this, which for me, reads very nicely.</p>\n  <pre><code class=\"language-swift\">let str = \"Coma inducing corporate bollocks\"\nstr.asArray().last // \"s\"\nstr.asArray()[10] // \"i\"\nString(str.asArray()[2..&lt;7]) // \"ma in\"</code></pre>\n  <p>You don't need to break out the big O notation to see this isn't going to perform great, you're iterating over the entire string everytime you want to get a piece of it, then the array methods are going to go do it again, so use with caution!</p>\n</article>",
      "summary": "Cheating on Swift Substrings",
      "date_published": "2015-07-03T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2015-04-12-death-by-date-format-string",
      "url": "https://mbehan.com/index.html?post=2015-04-12-death-by-date-format-string",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Death By Date Format String</p>\n  <p>Recently I learned that you probably always want <em>\"yyyy\"</em> and not <em>\"YYYY\"</em>.</p>\n  <pre><code class=\"language-swift\">let dateFormatter = NSDateFormatter()\ndateFormatter.dateFormat = \"YYYY-MM-dd\"\n\nprintln(dateFormatter.stringFromDate(\n   dateFormatter.dateFromString(\"2015-12-26\")!))</code></pre>\n  <p>This prints <em>2015-12-26</em>. Obviously. So what about</p>\n  <pre><code class=\"language-swift\">println(dateFormatter.stringFromDate(\n   dateFormatter.dateFromString(\"2015-12-27\")!))</code></pre>\n  <p>It prints <em>2016-12-27</em>.</p>\n  <p>Note that the year is <em>2016</em>.</p>\n  <p>I was fortunte enough to get assinged a production crash bug this week that after a long day of head scratching, turned out to be caused by this.</p>\n  <p>Interestingly, the <code>NSDate</code> created with the format is the date I expected, it represents 27 December 2015 and it's only getting a string from the date with that format that gives you the 'wrong' year. Similarly an <code>NSDate</code> constructed in any other way that represents 27 December 2015 will behave the same.</p>\n  <p>The <code>NSDateFormatter</code> docs point you at <a href=\"http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns\" target=\"_blank\" rel=\"noopener noreferrer\">Unicode Technical Standard Number 35</a> for the definitions of date format strings. I've looked at this before and I expect I'm not alone in having paid more attention to the day and month parts of the format. They're usually what we're interested in because the year is always the year, at most we might prefer 2 or 4 digits but thats about as interesting as it gets. I suspect what happens fairly often (and what probably happened with our bug) was that the developer guessed at YYYY as the year format, and when it appeared to work just fine, assumed it was correct.</p>\n  <p>The relavant part of that standard states that y is the year, but Y is</p>\n  <blockquote>Year (in \"Week of Year\" based calendars) ... May not always be the same value as calendar year.</blockquote>\n  <p>And the problem is that, as far as I can tell, it almost always is the same as the calendar year. The last few days of the year are the only ones I've seen causing problems. If it was more different, it would be spotted easier and perhaps I would have already known that YYYY was wrong and spotted that as the error right away.</p>\n</article>",
      "summary": "Death By Date Format String",
      "date_published": "2015-04-12T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2015-04-02-swift-optional-optionals",
      "url": "https://mbehan.com/index.html?post=2015-04-02-swift-optional-optionals",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Optional Optionals</p>\n  <p>So here's a confusing sentence.</p>\n  <p>With Swift functions you can have optional parameters, you can also have parameters that are optionals, and you can have optional parameters that are optionals.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/donald-rumsfeld.png\" alt=\"A rather confused looking Donald Rumsfeld\"></p>\n  <p>Not taking the time to think about the 3 different levels of optionality in function parameters had me scratching my head for a few minutes today, but all the options (sorry) are useful and it's not all that confusing once you remember them.</p>\n  <p>My scenario was that I created a function that does some stuff and then executes a closure supplied by the caller. Something like</p>\n  <pre><code class=\"language-swift\">func doSomeStuff(thenDoThis:()-&gt;())</code></pre>\n  <p>Which a caller would call like</p>\n  <pre><code class=\"language-swift\">doSomeStuff {\n\t// and then do this stuff\n}</code></pre>\n  <p>But I want to let the caller decide whether they want to supply the closure or not, so if they like they could just call the function and be done.</p>\n  <pre><code class=\"language-swift\">doSomeStuff()</code></pre>\n  <p>So let's make the closure optional. Easy, as with any type in Swift, we can mark it optional by including a <em>?</em></p>\n  <pre><code class=\"language-swift\">func doSomeStuff(thenDoThis:(()-&gt;())?)</code></pre>\n  <p>So then if we go ahead and call</p>\n  <pre><code class=\"language-swift\">doSomeStuff() // Error: Missing argument for parameter #1 in call</code></pre>\n  <p>But it was optional, so why the error? Well it wasn't optional in the sense that I could leave it out, it's just that it was an optional type which we are still expected to provide every time. As our type is an optional closure with no parameters and no return, we have to supply a closure with no parameters and no return or nil. So we'd actually have to call</p>\n  <pre><code class=\"language-swift\">doSomeStuff(nil) // this works fine but isn't what we want</code></pre>\n  <p>So how do you create an optional parameter, one that a caller can decide to leave out? To do that you provide a default value to be used for that parameter, right in the function declaration.</p>\n  <pre><code class=\"language-swift\">func doSomeStuff(thenDoThis:()-&gt;() = defaultClosure)</code></pre>\n  <p>This means that if the caller doesn't supply a value for thenDoThis we'll use defaultClosure instead (assuming <em>defaultClosure</em> is defined elsewhere as a <em>()-&gt;()</em>.) We can now happily call the following if we don't want to supply a closure.</p>\n  <pre><code class=\"language-swift\">doSomeStuff() // yay!</code></pre>\n  <p>The behaviour I was interested in though was that if I didn't supply a closure, that there would be no closure executed at all, not that some other one I had to define would be called instead. Well, I could just make <em>defaultClosure</em> do nothing, or just have the default value be <em>{}</em> like</p>\n  <pre><code class=\"language-swift\">func doSomeStuff(thenDoThis:()-&gt;() = {})</code></pre>\n  <p>Which is fine, and maybe even the preferred way, but you can also have an optional optional parameter, and have it's default value be <em>nil</em>.</p>\n  <pre><code class=\"language-swift\">func doSomeStuff(thenDoThis:(()-&gt;())? = nil)</code></pre>\n  <p>Now if the caller omits the closure, <em>thenDoThis</em> will be <em>nil</em>, which makes more sense to me in this situation.</p>\n</article>",
      "summary": "Optional Optionals",
      "date_published": "2015-04-02T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2014-12-18-orm-on-fmdb-sqlite-ios",
      "url": "https://mbehan.com/index.html?post=2014-12-18-orm-on-fmdb-sqlite-ios",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Basic ORM on top of FMDB and SQLite with Objective-C</p>\n  <p>The <a href=\"https://github.com/ccgus/fmdb\" target=\"_blank\" rel=\"noopener noreferrer\">FMDB</a> wrapper simplifies using SQLite it from Objective-C. A typical scenario is that you'll execute a query and get back a lovely <code>FMResultSet</code> object and you can extract values from that using your database column names - nice.</p>\n  <p>What would be slightly nicer thought is automatically mapping that result set onto a model object. So lets make that a thing.</p>\n  <p>Example</p>\n  <p>We have a table in our database called People with the following fields:</p>\n  <p>• personId<br>\n  • firstName<br>\n  • lastName<br>\n  • address<br>\n  • favouriteSimpsonsReference</p>\n  <p>And it makes sense for us to have a Person class in our app because maybe we'll want to maintain a table of people and be able view the detail of a person by passing a Person from the table view to the detail view. The Person class will be defined something like this:</p>\n  <pre><code class=\"language-swift\">@interface Person : NSObject\n\n@property(nonatomic) NSInteger personId;\n@property(nonatomic, copy) NSString *firstName;\n@property(nonatomic, copy) NSString *lastName;\n@property(nonatomic, copy) NSString *address;\n@property(nonatomic, copy) NSString *favouriteSimpsonsReference;\n\n@end</code></pre>\n  <p>So to create some Person objects we could alloc init a bunch of them and set their properties based on what we get back from the database, alternatively we could create a custom initialiser method to take a <code>FMResultSet</code> and set them all that way. All of which is perfectly fine until you find yourself repeating it over and over again.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/homer-making-oj.png\" alt=\"Homer Simpson making OJ the old fashioned way\"></p>\n  <p>For simple situations like this though, there is a better way.</p>\n  <p>Here's the class that I use as a base class for all my model objects, it provides an initialiser that takes a result set as a parameter and looks for columns in that result set with the same name as its properties.</p>\n  <pre><code class=\"language-swift\">@interface MBSimpleDBMappedObject : NSObject\n\n-(instancetype)initWithDatabaseResultSet:(FMResultSet *)resultSet;\n\n@end</code></pre>\n  <pre><code class=\"language-swift\">-(instancetype)initWithDatabaseResultSet:(FMResultSet *)resultSet\n{\n    self = [super init];\n    if(self)\n    {\n        unsigned int propertyCount = 0;\n        objc_property_t * properties = class_copyPropertyList([self class], &amp;propertyCount);\n        \n        for (unsigned int i = 0; i &lt; propertyCount; ++i)\n        {\n            objc_property_t property = properties[i];\n            NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];\n            \n            [self setValue:[resultSet objectForColumnName:propertyName] forKey:propertyName];\n        }\n        free(properties);\n    }\n    \n    return self;\n}\n\n@end</code></pre>\n  <p>What we're doing here is quite simple, but it's enabled by a couple of powerful Objective C features. Firstly, at runtime we can dynamically retrieve the names of a loaded classes properties, then we can simply set the values of properties using key value coding.</p>\n  <p>Those are the only 2 things happening here, get a list of the properties, then for each property set its value to the one from the result set with a matching column name.</p>\n  <p>This means all our model subclasses have to do is declare a bunch of properties, so all there is to those classes is the interface I described before, just subclassing MBSimpleDBMappedObject instead of NSObject like so.</p>\n  <pre><code class=\"language-swift\">@interface Person : MBSimpleDBMappedObject\n\n@property(nonatomic, readonly) NSInteger personId;\n@property(nonatomic, readonly, copy) NSString *firstName;\n@property(nonatomic, readonly, copy) NSString *lastName;\n@property(nonatomic, readonly, copy) NSString *address;\n@property(nonatomic, readonly, copy) NSString *favouriteSimpsonsReference;\n\n@end</code></pre>\n  <p>I've marked the properties read only, because all I'm interested in is a copy of what's in the database, changing the values of those properties won't update the database, though I do plan to add that functionality in the future. If this is all you need then you're done, your Person implementation can be left blank.</p>\n  <p>If you're familiar with SQLite and FMDB you'll know they don't really do dates, but you'll probably find yourself wanting to keep track of some dates in the database. FMResultSet's objectForColumnName will gladly give you a number or a string, but it doesn't do NSDate's. Here's how I deal with that.</p>\n  <p>Let's change our People table a bit to make it a bit more useful, so our list of fields looks like:</p>\n  <p>• personId<br>\n  • firstName<br>\n  • lastName<br>\n  • address<br>\n  • dateOfBirthTimestamp</p>\n  <p>and update our Person interface too</p>\n  <pre><code class=\"language-swift\">@interface Person : MBSimpleDBMappedObject\n\n@property(nonatomic, readonly) NSInteger personId;\n@property(nonatomic, readonly, copy) NSString *firstName;\n@property(nonatomic, readonly, copy) NSString *lastName;\n@property(nonatomic, readonly, copy) NSString *address;\n@property(nonatomic, readonly)NSTimeInterval dateOfBirthTimestamp;\n@property(nonatomic, readonly, strong)NSDate *dateOfBirth;\n\n@end</code></pre>\n  <p>With no other changes the dateOfBirthTimestamp property will be set correctly which may be enough, but you'd probably have to make an NSDate with it anytime you wanted to do anything useful with it. We've added an NSDate property, but as there is no corresponding column name, it will remain nil. That is until we override the initialiser as follows.</p>\n  <pre><code class=\"language-swift\">-(instancetype)initWithDatabaseResultSet:(FMResultSet *)resultSet\n    {\n        self = [super initWithDatabaseResultSet:resultSet];\n        if(self)\n        {\n            _dateOfBirth = [NSDate dateWithTimeIntervalSince1970:self.dateOfBirthTimestamp];\n        }\n   return self;\n\n}\n\n@end</code></pre>\n  <p>The base class will still map all the other properties, we just construct the NSDate.</p>\n</article>",
      "summary": "Basic ORM on top of FMDB and SQLite with Objective-C",
      "date_published": "2014-12-18T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2014-04-24-upload-xcode-bot-builds-testflight-launchd",
      "url": "https://mbehan.com/index.html?post=2014-04-24-upload-xcode-bot-builds-testflight-launchd",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Uploading Xcode Bot Builds to Testflight, with launchd</p>\n  <p><a href=\"https://developer.apple.com/library/ios/documentation/IDEs/Conceptual/xcode_guide-continuous_integration/000-About_Continuous_Integration/about_continuous_integration.html#//apple_ref/doc/uid/TP40013292-CH1-SW1\" target=\"_blank\" rel=\"noopener noreferrer\">Continuous integration with Xcode</a> is super easy to set up and does the basics of continuous integration really well. With almost no effort you'll have nightly builds, test suites doing there thing, email alerts to committers, lovely graphs and even a cool dashboard thing for your big screen. I won't go through setting that all up here, the Apple docs are excellent and there are plenty of other people who've already explained it better than I will.</p>\n  <p>Where things are less than straightforward is when you want to use the IPA file produced–to send it to your testers via TestFlights, or to your remote teammates, your client or whoever.</p>\n  <p>The server executes an Xcode scheme, which defines your targets, build configuration, and tests. In the scheme there's an opportunity to include custom scripts that run at various points, pre and post each of the schemes actions, so you can run a script pre-build or post-archive etc.</p>\n  <p>This post-archive step is the last place we can do some work, so it's the obvious place to go upload our build to TestFlight, right? Well it would be except the IPA file never exists at this point. The IPA file is generated some time after this. The process is:</p>\n  <p>• Archive<br>\n  • Post archive scripts<br>\n  • ???<br>\n  • Generate IPA file</p>\n  <p>So if you want to upload to TestFlight what can you do? Well the solution offered by anyone I've seen blogging about it is to go make your own IPA using xcrun. That doesn't sound so bad until you end up with code signing and keychain issues and it's all to do something that is about to happen as soon as you're done anyway.</p>\n  <p>My solution was to just wait until the IPA file was made. My initial naive attempts were to schedule the upload from the post-archive script using <a href=\"https://keith.github.io/xcode-man-pages/at.1.html\" target=\"_blank\" rel=\"noopener noreferrer\">at</a> or simply adding a <a href=\"https://keith.github.io/xcode-man-pages/sleep.1.html\" target=\"_blank\" rel=\"noopener noreferrer\">delay</a> for some amount of time while the IPA file didn't exist. What I should have realised though is that the Bot will wait as long as I'm waiting and only when my script finishes will it continue and make the IPA file.</p>\n  <p>launchd to the rescue.</p>\n  <p>What I've ended up with, and which is working nicely for us, is a scheduled job on the build server which will notice any IPA files built by an Xcode bot, and upload them. I wasn't familiar with launchd prior to this and was excepting to use cron, but it turns out this is the modern OSX way for scheduling jobs. There's a great site showing you <a href=\"http://launchd.info\" target=\"_blank\" rel=\"noopener noreferrer\">how to use launchd</a> but I'll show you what I have anyway.</p>\n  <p>What I have:</p>\n  <p>1. A plist for launchd 2. Plists for each project that explain where to send the build 3. A shell script that looks for IPA files and sends them to TestFlight or FTP using the information from 2.</p>\n  <p>2. The launchd plist</p>\n  <p>This is placed in /Library/LaunchDaemons and simply tells launchd that we want to run our script every 600 seconds. You could schedule it to run once a day or any other interval, I left it at 10 minutes so any bots that are run on commit or are started manually will have their builds uploaded right away rather than at the end of the day.</p>\n  <pre><code class=\"language-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\n&lt;plist version=\"1.0\"&gt;\n&lt;dict&gt;\n\t&lt;key&gt;Label&lt;/key&gt;\n\t&lt;string&gt;com.mbehan.upload-builds&lt;/string&gt;\n\t&lt;key&gt;ProgramArguments&lt;/key&gt;\n\t&lt;array&gt;\n\t\t&lt;string&gt;/ci_scripts/build-uploader.sh\n\t\t\t&lt;key&gt;StartInterval&lt;/key&gt;\n\t\t\t&lt;integer&gt;600&lt;/integer&gt;\n\t\t\t&lt;key&gt;StandardOutPath&lt;/key&gt;\n\t\t\t&lt;string&gt;/tmp/build-uploads.log&lt;/string&gt;\n\t\t\t&lt;key&gt;StandardErrorPath&lt;/key&gt;\n\t\t\t&lt;string&gt;/tmp/build-uploads.log&lt;/string&gt;\n\t\t&lt;/dict&gt;\n\t&lt;/plist&gt;\n&lt;/key&gt;</code></pre>\n  <p>3. Per project plist</p>\n  <p>If we want the build to be uploaded automatically, it needs a plist telling it where to go. We share builds with one of our clients via FTP so there is a method key for that and a different set of keys are required if it's value is to FTP rather than TestFlight. I keep these plists in the same directory as the script.</p>\n  <pre><code class=\"language-xml\">&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\n&lt;plist version=\"1.0\"&gt;\n&lt;dict&gt;\n\t&lt;key&gt;Method\n\t\t&lt;string&gt;TestFlight&lt;/string&gt;\n\t\t&lt;key&gt;ProductName&lt;/key&gt;\n\t\t&lt;string&gt;Some App.ipa\n\t\t\t&lt;key&gt;APIToken&lt;/key&gt;\n\t\t\t&lt;string&gt;GET THIS FROM TESTFLIGHT\n\t\t\t\t&lt;key&gt;TeamToken&lt;/key&gt;\n\t\t\t\t&lt;string&gt;AND THIS&lt;/string&gt;\n\t\t\t&lt;/dict&gt;\n\t\t&lt;/plist&gt;\n\t&lt;/key&gt;\n&lt;/key&gt;</code></pre>\n  <p>4. Checking for IPA files, uploading</p>\n  <p>We're using find with the -mtime option here to find recently created files with the name specified in the plist. If we find a file we then either use curl to upload to TestFlight or we send it via FTP depending on the method indicated in the plist.</p>\n  <p>You can remove the stuff for FTP if you only care about TestFlight, and you might want to add extra detail to the plist such as distribution lists.</p>\n  <pre><code class=\"language-sh\">#!/bin/bash\n\nfiles=/ci_scripts/*.plist\n\nfor f in $files\ndo\n\techo Processing $f \"...\"\n\tproductName=\"$(/usr/libexec/plistbuddy -c Print:ProductName: \"$f\")\"\n\t\n\techo $productName\n\tipaPath=$(find /Library/Server/Xcode/Data/BotRuns/*/output/\"$productName\" -mtime -15m | head -1)\n\t\n\tif [ ${#ipaPath} -gt 0 ]; then\n\t\techo \"Have IPA FILE: \" $ipaPath\n\t\t\n\t\tmethod=\"$(/usr/libexec/plistbuddy -c Print:Method: \"$f\")\"\n\t\t\n\t\tif [ $method == \"FTP\" ]; then\n\t\t\techo \"Attempting FTP ...\"\n\t\t\t\n\t\t\thost=\"$(/usr/libexec/plistbuddy -c Print:FTPHost: \"$f\")\"\n\t\t\tuser=\"$(/usr/libexec/plistbuddy -c Print:UserName: \"$f\")\"\n\t\t\tpass=\"$(/usr/libexec/plistbuddy -c Print:Password: \"$f\")\"\n\t\t\tfilename=\"$(/usr/libexec/plistbuddy -c Print:FileNameOnServer: \"$f\")\"\n\t\t\thostDir=\"$(/usr/libexec/plistbuddy -c Print:DirectoryOnServer: \"$f\")\"\n\n\t\t\tdate=`date +%y-%m-%d`\n\n\t\t\tftp -inv $host &lt;&lt;-ENDFTP\n\t\t\tuser $user $pass\n\t\t\tcd $hostDir\n\t\t\tmkdir $date\n\t\t\tcd $date\n\t\t\tbinary\n\t\t\tput \"$ipaPath\" \"$filename\"\n\t\t\tbye\n\t\t\tENDFTP\n\t\t\t\n\t\telif [ $method == \"TestFlight\" ]; then\n\t\t\techo \"Attempting TestFlight ...\"\n\t\t\t\n\t\t\tapiToken=\"$(/usr/libexec/plistbuddy -c Print:APIToken: \"$f\")\"\n\t\t\tteamToken=\"$(/usr/libexec/plistbuddy -c Print:TeamToken: \"$f\")\"\n\t\t\t\n\t\t\t/usr/bin/curl \"http://testflightapp.com/api/builds.json\" \\\n\t\t\t  -F file=@\"$ipaPath\" \\\n\t\t\t  -F api_token=\"$apiToken\" \\\n\t\t\t  -F team_token=\"$teamToken\" \\\n\t\t\t  -F notes=\"Automated Build\"\n\t\tfi\n\tfi\ndone</code></pre>\n  <p>This all assumes you've set your provisioning profile and code signing identity up correctly for the build configuration used by your Xcode scheme. Make sure the configuration used in the archive step (Release by default) will make a build the people you want to share builds with will be able to install.</p>\n</article>",
      "summary": "Uploading Xcode Bot Builds to Testflight, with launchd",
      "date_published": "2014-04-24T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2014-03-05-dynamic-image-lighting-coreimage",
      "url": "https://mbehan.com/index.html?post=2014-03-05-dynamic-image-lighting-coreimage",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Dynamic Image Lighting with CoreImage</p>\n  <p>With the kind of apps I usually make, I often end up doing a lot of gamey looking things right inside of UIKit. The addition of UIDynamics made one of those jobs, gravity, super easy. I wanted the same kind of simplicity for lights.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/luminator-demo.gif\" alt=\"Animated figure being dynamically lit by 3 moving coloured lights\"></p>\n  <p>It only works on image views for now, but it works well and frame rates are good (much better than the gif lets on) for all but very large images on older devices. You can <a href=\"https://github.com/mbehan/image-view-lighting\" target=\"_blank\" rel=\"noopener noreferrer\">get all the code on github</a> and using it should be pretty simple.</p>\n  <p>You just create a lighting controller, add some light fixtures and image views you want to be lit to the controller, and let it know when you need to update the lighting (when we're moving the lights in the example above). Here's the interface for the <em>MBLightingController</em>:</p>\n  <pre><code class=\"language-swift\">@interface MBLightingController : NSObject\n\n@property(nonatomic) BOOL lightsConstantlyUpdating;\n\n-(void)addLightFixture:(id&lt;MBLightFixture&gt;)light;\n-(void)addLitView:(MBLitAnimationView *)litView;\n-(void)setNeedsLightingUpdate;\n\n@end</code></pre>\n  <p>Only set <code>lightsConstantlyUpdating</code> if the lighting is always changing (this came about because I was playing around with adding a light to a rope with UIDynamics, which you can see in the project on github.)</p>\n  <p>So, there are a couple of things there that you won't know what they are, the <code>MBLightFixture</code> protocol, and <code>MBLitAnimationView</code>.</p>\n  <p>Anything can be a light, so long as it implements the protocol, which means it needs a position, intensity, range and color. I've just been using a UIView subclass but maybe your light will be a <code>CAEmitterLayer</code> or something.</p>\n  <p><code>MBLitAnimationView</code> can be used everywhere you'd use a UIImageView, it just adds the ability to be lit, and <a href=\"http://mbehan.com/post/78399605333/uiimageview-animation-but-less-crashy\" target=\"_blank\" rel=\"noopener noreferrer\">makes working with animation easier</a>.</p>\n  <p>Your view controller's viewDidLoad might include something like this:</p>\n  <pre><code class=\"language-swift\">//create the ligthing controller\nself.lightingController = [[MBLightingController alloc] init];\n    \n//add an image to be lit\nMBLitAnimationView *bg = [[MBLitAnimationView alloc] initWithFrame:self.view.bounds];\nbg.ambientLightLevel = 0.1; // very dark\n[bg setImage:[UIImage imageNamed:@\"wall\"]];\n[self.view addSubview:bg];\n[_lightingController addLitView:bg];\n    \n//add a light\nSimpleLightView *lightView = [[SimpleLightView alloc] initWithFrame:CGRectMake(200, 200, 25, 25)];\nlightView.intensity = @0.8;\nlightView.tintColor = [UIColor whiteColor];\nlightView.range = @250.0;\n    \n[self.view addSubview:lightView];\n[_lightingController addLightFixture:lightView];</code></pre>\n  <p>The light effect is achieved using CoreImage filters and everything happens in the <code>applyLights</code> method of <code>MBLitAnimationView</code>.</p>\n  <p>I experimented with a bunch of <a href=\"https://developer.apple.com/library/ios/documentation/graphicsimaging/reference/CoreImageFilterReference/Reference/reference.html\" target=\"_blank\" rel=\"noopener noreferrer\">different filters</a> trying to get the right effect, and there were several that worked so just try switching out the filters if you want something a little different.</p>\n  <p>Multiple filters are chained together, first up we need to darken the image using <code>CIColorControls</code>:</p>\n  <pre><code class=\"language-swift\">CIFilter *darkenFilter = [CIFilter filterWithName:@\"CIColorControls\"\n                                           keysAndValues:\n                                 @\"inputImage\", currentFrameStartImage,\n                                 @\"inputSaturation\", @1.0,\n                                 @\"inputContrast\", @1.0,\n                                 @\"inputBrightness\", @(0-(1-_ambientLightLevel)), nil];</code></pre>\n  <p>Then, for every light that we have, we create a <code>CIRadialGradient</code>:</p>\n  <pre><code class=\"language-swift\">CIFilter *gradientFilter = [CIFilter filterWithName:@\"CIRadialGradient\"\n                                              keysAndValues:\n                                    @\"inputRadius0\", [light constantIntensityOverRange] ? [light range] : @0.0,\n                                    @\"inputRadius1\", [light range],\n                                    @\"inputCenter\", [CIVector vectorWithCGPoint:inputPoint0],\n                                    @\"inputColor0\", color0,\n                                    @\"inputColor1\", color1, nil];</code></pre>\n  <p>Then we composite the gradients with the darkened image using <code>CIAdditionCompositing</code>:</p>\n  <pre><code class=\"language-swift\">lightFilter = [CIFilter filterWithName:@\"CIAdditionCompositing\"\n                                     keysAndValues:\n                           @\"inputImage\", gradients[i],\n                           @\"inputBackgroundImage\",[lightFilter outputImage],nil];</code></pre>\n  <p>Finally, we mask the image to the shape of the original image:</p>\n  <pre><code class=\"language-swift\">CIFilter *maskFilter = [CIFilter filterWithName:@\"CISourceInCompositing\"\n                                      keysAndValues:\n                            @\"inputImage\", [lightFilter outputImage],\n                            @\"inputBackgroundImage\",currentFrameStartImage,nil];</code></pre>\n  <p>Just  set the image view's image property to a UIImage created from the final filter's output and we're done!</p>\n  <pre><code class=\"language-swift\">CGImageRef cgimg = [coreImageContext createCGImage:[maskFilter outputImage]\n                                                  fromRect:[currentFrameStartImage extent]];\n        \nUIImage *newImage = [UIImage imageWithCGImage:cgimg];\nimageView.image = newImage;\n        \nCGImageRelease(cgimg);</code></pre>\n  <p>Playing with CoreImage was fun so I think I'll revisit the code at some point in the future, I'd like to try it out with SpriteKit's <code>SKEffectNode</code> where it really makes more sense for using with games. Or I might keep working with UIKit and get it working for any view–shiny / shadowy interfaces might be interesting.</p>\n</article>",
      "summary": "Dynamic Image Lighting with CoreImage",
      "date_published": "2014-03-05T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2014-03-02-uiimageview-animation-performance",
      "url": "https://mbehan.com/index.html?post=2014-03-02-uiimageview-animation-performance",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Higher Performance UIImageView Animation</p>\n  <p>Animation with <code>UIImageView</code> is super simple and for basic animations it is just what you need. Just throw an array of images at your image view and tell it to go, and it will go. For animations of more than a few frames though its simplicity is also its failing–an array of <code>UIImage</code> s is handy to put together but if you want large images or a reasonable number of frames then that array could take up a serious chunk of memory. If you've tried any large animations with <code>UIImageView</code> you'll know things get crashy very quickly.</p>\n  <p>There are also a few features, like being able to know what frame is currently being displayed and setting a completion block that you regularly find yourself wanting when dealing with animations, so I've created <a href=\"https://github.com/mbehan/MBAnimationView\" target=\"_blank\" rel=\"noopener noreferrer\">MBAnimationView</a> to provide those, and to overcome the crash inducing memory problems.</p>\n  <p>The premise for the memory improvements is the fact that image data is compressed, and loading it into a <code>UIImage</code> decompresses it. So, instead of having an array of <code>UIImage</code> objects (the decompressed image data), we're going to work with an array of <code>NSData</code> objects (the compressed image data). Of course, in order to ever see the image, it will have to be decompressed at some point, but what we're going to do is create a <code>UIImage</code> on demand for the frame we want to display next, and let it go away when we're done displaying it.</p>\n  <p>So the <code>MBAniamtionView</code> has a <code>UIImageView</code>, it creates an array of <code>NSData</code> objects and then on a timer creates the frame images from the data, and sets the image view's image to it, it's that simple.</p>\n  <p>Check out <a href=\"https://github.com/mbehan/MBAnimationView\" target=\"_blank\" rel=\"noopener noreferrer\">MBAnimationView on github</a></p>\n</article>",
      "summary": "Higher Performance UIImageView Animation",
      "date_published": "2014-03-02T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2014-02-26-create-rope-with-uidynamics",
      "url": "https://mbehan.com/index.html?post=2014-02-26-create-rope-with-uidynamics",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Creating a Rope with UIDynamics</p>\n  <p>I've made rope simulations for games with Box2D before but I wanted to see if I could make a rope that could be used easily with UIKit elements, and without having to use Box2D directly. Below is the result, a highly practical user interface, I'm sure you'll agree!</p>\n  <p><img src=\"https://mbehan.com/assets/posts/rope.gif\" alt=\"A UIButton dangling on the end of a swaying rope\"></p>\n  <p>There are 2 distinct problems to consider when create a rope:</p>\n  <p>1. The physics joint that connects two elements together as though they were connected with a rope 2. Drawing the rope</p>\n  <p>Box2D has a bunch of different joints for connecting physics bodies and <em>b2RopeJoint</em> is just what you need to solve problem 1. UIDynamics though, only exposes 1 joint for us to join dynamic bodies through <em>UIAttachmentBehavior</em>. Fortunately the joint it appears to be using under the hood is <em>b2DistanceJoint</em> which, with the right amount of parameter fiddling, can be made into a <em>b2RopeJoint</em>.</p>\n  <p>So thats problem 1 sorted, right? Just draw the rope with the help of some <a href=\"http://en.wikipedia.org/wiki/Verlet_integration\" target=\"_blank\" rel=\"noopener noreferrer\">verlet integration</a> and you're done? Well I could be done, but I wanted to try something different and a bit more light weight, something that didn't involve wikipedia pages full of equations to understand fully.</p>\n  <p>More Chain Than Rope</p>\n  <p>By simply connecting a series of small views by their ends with <em>UIAttachmentBehavior</em> you get a chain, with enough links in that chain, and with the right attachment parameters you can get something that behaves pretty rope like. You can attach one view to another like so:</p>\n  <pre><code class=\"language-swift\">UIAttachmentBehavior *chainAttachment = [[UIAttachmentBehavior alloc] initWithItem:view1 attachedToItem:view2];</code></pre>\n  <p>I just do this in a loop, joining together a whole bunch of views. It ends up looking like this not very ropey looking thing.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/rope-2.png\" alt=\"Connected boxes make up the rope segments\"></p>\n  <p>But there's no reason why, just because we're using the views to create the rope like joint, that we have to look at the views. Instead I draw a path connecting their centres</p>\n  <pre><code class=\"language-swift\">[path moveToPoint:[links[0] center]];\nfor(int i = 1; i &lt; links.count; i++)\n{\n   [path addLineToPoint:[links[i]center]];\n}</code></pre>\n  <p>and we end up with what you see up top.</p>\n  <p><a href=\"https://github.com/mbehan/MBRope\" target=\"_blank\" rel=\"noopener noreferrer\">All the code is on github</a>. It still needs a bit of work but you can get a simple rope up and running with just a couple of lines of code. Import the header and do something like this:</p>\n  <pre><code class=\"language-swift\">MBRope *rope = [[MBRope alloc] initWithFrame:CGRectMake(350, 180, 5, 200) numSegments:15];\n[self.view addSubview:rope];\n[rope addRopeToAnimator:animator];</code></pre>\n  <p>To attach something, like the button in the example, you can get the last view by calling <em>attachmentView</em> on the rope and attach your other view with your own <em>UIAttachmentBehavior</em>. The top of your rope will be fixed to the origin of the rect supplied when you init the rope, but it wouldn't take much to change it so you can attach your own stuff to both ends.</p>\n</article>",
      "summary": "Creating a Rope with UIDynamics",
      "date_published": "2014-02-26T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2014-02-19-storyboards-multiple-developers-git",
      "url": "https://mbehan.com/index.html?post=2014-02-19-storyboards-multiple-developers-git",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Storyboards, Multiple Developers and Git.</p>\n  <p>Storyboards are great. You can get the flow of your app set up in a few minutes without writing a line of code, you can initialise your navigation controllers and tabs ridiculously easily and zoom out a bit and you get a lovely picture of your entire app on a single screen, with lines and boxes and everything.</p>\n  <p>But storyboards can be cruel if you're not careful. Git pulls become nervy affairs, a slip merging by hand can render your storyboard unreadable by Xcode and not knowing when to stop using them can turn your lovely lines and boxes into a maintenance nightmare.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/mcclane-before.jpg\" alt=\"John McClane, crawling through some ducting, wishing he still used nibs\"></p>\n  <p>\"Come to the coast, we'll get together, have a few segues\"</p>\n  <p>I've worked on a bunch of apps of all shapes and sizes using storyboards over the last year or so as part of a team and we've been working on perfecting our use of them. I've been investigating and testing storyboard best practice and this is what I've learned so far.</p>\n  <p>The Precap</p>\n  <p><a href=\"http://en.wiktionary.org/wiki/precap\" target=\"_blank\" rel=\"noopener noreferrer\">(Wiktionary says it's a word)</a></p>\n  <p>• Everyone on the same build of Xcode<br>\n  • Multiple storyboards<br>\n  • Use Nibs for custom views<br>\n  • One person owns the storyboard setup / decides granularity<br>\n  • Think about which storyboards are involved when assigning tasks<br>\n  • Merge storyboards often<br>\n  • Xcode is your git client</p>\n  <p>Xcode</p>\n  <p>We've had hassle sharing storyboards across even minor versions of Xcode, storyboards created in one will do crazy stuff in another, or just plain won't work. Don't let anyone sneak ahead to the latest developer preview unless they're doing a separate installation.</p>\n  <p>Multiple storyboards</p>\n  <p>But what about the lovely whole app view, all those lovely lines and boxes all perfectly arranged? That was never what storyboards were about, and you've still got your whiteboard for that. Divide and conquer is your mantra for everything else you do so it should be for storyboards too. It's easier to reason about storyboards with a single purpose and devs are less likely to trip each other up if they're not working on the same storyboards at the same time.</p>\n  <p>So how do you break it up?</p>\n  <p>Per user story is a decent approach, but that can be too granular at times. A separate storyboard for login and one for viewing an account makes sense, but maybe you should keep the lost password flow in with your login - If all you've got in each storyboard is a single view controller you might as well be using Nibs, the beauty of storyboards is making connections between view controllers.</p>\n  <p>But you said to keep using Nibs?</p>\n  <p>Yep, for custom views, table view cells and the like. A view can't exist outside a view controller in a storyboard so if you don't need a view controller for it you really shouldn't be adding it to a storyboard.</p>\n  <p>So what about single view controllers in Nibs then?</p>\n  <p>We've a project in which we've used some Nibs from an existing project in conjunction with a storyboard and it hasn't been a problem, but if I was making those components again now they'd be in a storyboard. Having a single view controller in a storyboard will make sense at times, and when you end up wanting to add additional screens to your account details say and all you have to do is drag a new view controller in beside the existing one and hook up a segue you'll be happy.</p>\n  <p>Multiple devs</p>\n  <p>Even with all your concerns perfectly separated, and making sure everyone's on the same page you're going to end up working on the same storyboard as someone else at the same time. Having to wait for someone to give you their changes before you can do something is no fun so I wanted to see just how careful you really have to be.</p>\n  <p>I created a simple Xcode project with a single storyboard and set out with my new git friend to put together a few screens.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-1.png\" alt=\"\"></p>\n  <p>Test 1: Adding to the same empty view controller</p>\n  <p>I started off simply adding a label and having Testy add an empty Image View.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-2.png\" alt=\"\"></p>\n  <p>We're working on the same view controller right away so I expected there to be a conflict to sort out and indeed there was.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-3.png\" alt=\"\"></p>\n  <p>The XML is clearly not intended to be parsed by human eyes but this looks straight forward enough, I can see two separate additions so accepting one followed by the other should work fine. This looks just like the kind of conflict that comes up on .xcodeproj files when two devs are adding files.</p>\n  <p>It worked, I had to look at some XML but nothing blew up, for extremely simple changes to the same view controller we don't have to worry too much.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-4.png\" alt=\"\"></p>\n  <p>Test 2: Editing different view controllers</p>\n  <p>I added a lovely purple view to one view controller and had Testy add a view to a different view controller. We really shouldn't have any problems here</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-5.png\" alt=\"\"></p>\n  <p>And we don't. It seems editing the same storyboard is fine so long as we keep to different view controllers. But, sometimes we might edit another view controller without meaning to, so I looked at some more scenarios …</p>\n  <p>Test 3: Rearranging parts of the storyboard that someone else changed</p>\n  <p>Here Testy has changed one of my purple boxes to green, and I've just been fiddling with the layout a bit, swapping the order of the 2 view controllers to the right.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-6.png\" alt=\"\"></p>\n  <p>This auto merged and left us looking good, it chose my layout.</p>\n  <p>Test 4: Adding modified views to a Navigation Controller</p>\n  <p>When you're inferring screen elements such as the nav bar adding a navigation controller can affect a bunch of view controllers that someone else might have been working on. Here I've added a navigation controller while Testy's been changing some colours.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-7.png\" alt=\"\"></p>\n  <p>To my surprise this auto merged just fine, the views that Testy was working on got the nav bar. It makes sense if you take a look at the XML, no nav bar is added as a child of the view controllers, the inferred setting is in there and Xcode knows what to do with that.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-8.png\" alt=\"\"></p>\n  <p>Test 5: Making changes to a slightly more filled out view controller</p>\n  <p>Things have gone ok so far, so lets revisit editing the same view controller, this time making it a bit more realistic.</p>\n  <p>We both started off with this</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-9.png\" alt=\"\"></p>\n  <p>All I did was enclose the label in a scroll view, Testy had a few more bits and pieces to do, he changed that label from an attributed label to a regular label, moved it, changed its text and changed the background colour of the view for good measure. We know we're going to be looking at XML here but that wasn't a problem before.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-10.png\" alt=\"\"></p>\n  <p>And <em>some</em> of the XML here isn't so bad either.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-11.png\" alt=\"\"></p>\n  <p>But it's clear that as soon as you make more than 1 simple change you've got a problem, and you could easily waste a lot of time dealing with it.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/storyboards-git-12.png\" alt=\"\"></p>\n  <p>The order of of XML has changed between the versions significantly, and it doesn't seem to be too smart at highlighting which parts are the same. For example I didn't touch the table view in either revision but Xcode highlighted it being removed on one side and added in again in the middle of a bunch of other stuff later on.</p>\n  <p>It ended up only taking a few minutes to figure this example out and getting to a  version that made sense including both sets of changes but I was hand editing the XML and that is dangerous. It's clear that if you make more than a few changes and keep them for yourself for too long, you could end up in a bad way pretty quickly.</p>\n  <p>Xcode as git client you say?</p>\n  <p>This might be more of a personal preference, and if you want to rebase rather than merge this is a not an option (for now at least) but it seems screwing up the storyboard XML is likely to happen less frequently if you don't let anything other than Xcode touch it.</p>\n  <p>A conclusion, for now</p>\n  <p>After experimenting a little with this test project I'm happy that we can edit our storyboards simultaneously when we absolutely have to, but that shouldn't stop us planning things out so that it doesn't happen, and we'll be sticking to this list, same one as above:</p>\n  <p>• Everyone on the same build of Xcode<br>\n  • Multiple storyboards<br>\n  • Use Nibs for custom views<br>\n  • One person owns the storyboard setup / decides granularity<br>\n  • Think about which storyboards are involved when assigning tasks<br>\n  • Merge storyboards often</p>\n  <p><img src=\"https://mbehan.com/assets/posts/mcclane-after.jpg\" alt=\"Who has two thumbs and loves storyboards now? John McClane\"></p>\n</article>",
      "summary": "Storyboards, Multiple Developers and Git.",
      "date_published": "2014-02-19T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2014-02-09-drawing-physics-spritekit",
      "url": "https://mbehan.com/index.html?post=2014-02-09-drawing-physics-spritekit",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Drawing Physics with SpriteKit</p>\n  <p>There are plenty of games out there with this basic mechanic already but I wanted to see if it could be done easily using SpriteKit, spoiler alert: it can.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/spritekit-physics.gif\" alt=\"Shapes being drawn and then becoming part of a physics simulation\"></p>\n  <p>The code is on github, <a href=\"https://github.com/mbehan/drop-shape\" target=\"_blank\" rel=\"noopener noreferrer\">knock yourself out</a></p>\n  <p>I make use of some handy dandy categories on UIBezierPath made by other people, they are:</p>\n  <p>• Creating a UIImage from a path, taken from <a href=\"http://stackoverflow.com/a/17408397/61698\" target=\"_blank\" rel=\"noopener noreferrer\">this stackoverflow answer</a><br>\n  • A couple from the <a href=\"https://github.com/erica/iOS-6-Cookbook\" target=\"_blank\" rel=\"noopener noreferrer\">iOS6 cookbook</a> that make dealing with the points that make up a path much easier</p>\n  <p>We’re combining UIKit and SpriteKit here so we’re layering a transparent <code>UIView</code> on top of an <code>SKView</code>.</p>\n  <p>The <code>SKView</code> presents a single scene, it will contain our shapes and has a bounding static physics body to stop them escaping. The view controller sets up the scene in standard fashion.</p>\n  <pre><code class=\"language-swift\">- (void)viewWillLayoutSubviews\n{\n    scene = [[DropShapeScene alloc] initWithSize:self.view.bounds.size];\n    scene.scaleMode = SKSceneScaleModeAspectFill;\n    SKView *spriteView = (SKView *) self.view;\n    [spriteView presentScene: scene];\n}</code></pre>\n  <p>We have a very simple <code>UIView</code> subclass that sits on top providing very basic drawing functionality - it will handle drawing a single path, once the drawing ends it passes the path to it’s delegate and forgets about it. The drawing is done similar to <a href=\"http://mbehan.com/post/75703618266/a-bit-of-fun-with-uibezierpath-and-cashapelayer\" target=\"_blank\" rel=\"noopener noreferrer\">my previous post</a>, here’s the delegate protocol.</p>\n  <pre><code class=\"language-swift\">@protocol SimplePathDrawingDelegate &lt;nsobject&gt;\n-(void)drawingViewCreatedPath:(UIBezierPath *)path;\n@end</code></pre>\n  <p>We’ll let the view controller be the delegate, and thats where we do the interesting stuff, once it gets the drawn path.</p>\n  <pre><code class=\"language-swift\">-(void)drawingViewCreatedPath:(UIBezierPath *)path\n{\n    CGRect pathBounds = CGPathGetPathBoundingBox(path.CGPath);\n    \n    UIImage *image = [path strokeImageWithColor:[UIColor greenColor]];\n    SKTexture *shapeTexture = [SKTexture textureWithImage:image];\n    SKSpriteNode *shapeSprite = [SKSpriteNode spriteNodeWithTexture:shapeTexture size:pathBounds.size];\n    \n    shapeSprite.position = CGPointMake(pathBounds.origin.x + (pathBounds.size.width/2.0), scene.frame.size.height - pathBounds.origin.y - (pathBounds.size.height/2.0));\n    \n    shapeSprite.physicsBody = [SKPhysicsBody bodyWithConvexHullFromPath:path];\n    shapeSprite.physicsBody.dynamic = YES;\n    [scene addChild:shapeSprite];\n}</code></pre>\n  <p>We take the drawn line on a journey from path, to image, to a texture that is applied to a sprite. That part is pretty straightforward, more tricky is using that path to create a physics body.</p>\n  <p>SKPhysicsBody gives us a number of options for creating physics bodies, they are:</p>\n  <pre><code class=\"language-swift\">+ bodyWithCircleOfRadius:\n+ bodyWithRectangleOfSize:\n+ bodyWithPolygonFromPath:\n+ bodyWithEdgeLoopFromRect:\n+ bodyWithEdgeFromPoint:toPoint:\n+ bodyWithEdgeLoopFromPath:\n+ bodyWithEdgeChainFromPath:</code></pre>\n  <p>There are a few there that will take a path and give us a body, perfect, right? Except on closer inspection only 1 of them will create a path that can be dynamic, and that one <code>bodyWithPolygonFromPath:</code> has the caveat</p>\n  <blockquote>A convex polygonal path with counterclockwise winding and no self intersections.</blockquote>\n  <p>Sadly any realistic user isn’t going to like having to draw nothing but convex polygonal counterclockwise paths with no intersections.</p>\n  <p>Additionally, SpriteKit only lets us have bodies with 12 or fewer sides!</p>\n  <p>There are a few approaches we could take for getting by these restrictions: multiple joined physics bodies, using Box2D directly to get around the limit on body vertices, but we’ll use a <a href=\"http://en.wikipedia.org/wiki/Convex_hul\" target=\"_blank\" rel=\"noopener noreferrer\">convex hull</a> from the points that make up the path and make an <code>SKPhysicsBody</code> category to do it for us.</p>\n  <p>I won’t list the code here, you can download the project to have a look but here’s what it does. <em>(I use some existing categories on UIBezierPath to help out here and got a convex hull implementation online too, they’re all included in the project.)</em></p>\n  <p>• Get the points from the path<br>\n  • Order the points for the convex hull algorithm<br>\n  • Get the convex hull<br>\n  • While there are too many points in the hull, smooth it using increasing tolerance (removing points that make the smallest angles).</p>\n  <p>And that's all there is to it. The results are pretty nice for most shapes, if you wanted to get started on a physics drawing game you wouldn’t need much more than the <code>SKPhysicsBody (ConvexHull)</code> category.</p>\n</article>",
      "summary": "Drawing Physics with SpriteKit",
      "date_published": "2014-02-09T12:00:00Z"
    },
    {
      "id": "https://mbehan.com/index.html?post=2014-01-30-uibezierpath-cashapelayer-fun",
      "url": "https://mbehan.com/index.html?post=2014-01-30-uibezierpath-cashapelayer-fun",
      "authors": [
        {
          "name": "Michael Behan",
          "url": "https://mbehan.com"
        }
      ],
      "content_html": "<article>\n  <p>Fun with UIBezierPath and CAShapeLayer</p>\n  <p>This is a quick prototype for a fun drawing tool - as you drag your finger across the canvas the line grows branches which sprout leaves. The branches are randomly generated within certain parameters and animate on while you draw the main line.</p>\n  <p><img src=\"https://mbehan.com/assets/posts/vine-line.gif\" alt=\"A line is drawn, with branches automatically being added along its path\"></p>\n  <p>Yes, the leaves are very realistic looking, thank you.</p>\n  <p>The code is all on on <a href=\"https://github.com/mbehan/vine-line\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub</a>, feel free to use and improve!</p>\n  <p>It’s not about the line drawing</p>\n  <p>The line drawing is very basic - simply adding points to a <code>UIBezierPath</code>. I keep an array of the curves and draw them all in <code>drawRect:</code>. I don’t care about smooth curves or different textures or performance but I’m sure this will work with more sophisticated drawing code too. Most of the drawing code I’ve shipped has been OpenGL based, so it was nice to see how good the results are when keeping things super simple with UIKit / CoreGraphics.</p>\n  <p>How it Works</p>\n  <p>Let’s start with the basic line and layer on the other bits. It starts with a pan gesture recogniser in our UIView subclass.</p>\n  <pre><code class=\"language-swift\">UIPanGestureRecognizer *pgr = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];\n\n[self addGestureRecognizer:pgr];</code></pre>\n  <p>Now in the action selector we create new paths when a pan begins, and add to the current path when a pan changes.</p>\n  <pre><code class=\"language-swift\">-(void)handlePan:(UIPanGestureRecognizer *)gestureRecognizer\n{\n    if(gestureRecognizer.state == UIGestureRecognizerStateBegan)\n    {\n        UIBezierPath *newVineLine = [[UIBezierPath alloc] init];\n        [newVineLine moveToPoint:[gestureRecognizer locationInView:self]];\n        [vineLines addObject:newVineLine];\n    }\n    else if(gestureRecognizer.state == UIGestureRecognizerStateChanged)\n    {\n        UIBezierPath *currentLine = [vineLines lastObject];\n        [currentLine addLineToPoint:[gestureRecognizer locationInView:self]];\n    }\n\n    [self setNeedsDisplay];\n}</code></pre>\n  <p>You can see we’ve a mutable array called <code>vineLines</code> that we’re holding our paths in. This means to draw our paths we can simply iterate over that like so:</p>\n  <pre><code class=\"language-swift\">- (void)drawRect:(CGRect)rect\n{\n    for(VineLine *vineLine in vineLines)\n    {\n        [vineLine stroke];\n    }\n}</code></pre>\n  <p>That’s the basic line drawing done, now let’s add some branches. Again they’re <code>UIBezierPath</code>s. Every so often we want to generate a random path, the branch, and add it to the user drawn path, the vine. There are a couple of options for this, we could let the view / view controller keep track of the branches and when to draw them but let’s encapsulate all that in a VineLine, a subclass of UIBezierPath. (In the snippet above just swap out <code>UIBezierPath</code> with our new subclass.</p>\n  <pre><code class=\"language-swift\">@interface VineLine : UIBezierPath\n\n@property(nonatomic, retain, readonly)NSMutableArray *branchLines;\n\n@end</code></pre>\n  <p>Rather than just subclassing NSObject and having a property for our path we’re subclassing UIBezierPath and overriding <code>addLineToPoint:</code>, adding functionality to the existing method to decide when to create our branch and add it to the branchLines array. Note that VineBranch is just another UIBezierPath subclass that can create random paths with leaves on the end. All we’re doing here is checking if the point we’re adding is far enough away from the last branch (or beginning of the line) to create a branch and if it is, creating a new random branch and storing it in an array of branches.</p>\n  <pre><code class=\"language-swift\">-(void)addLineToPoint:(CGPoint)point\n{\n    [super addLineToPoint:point];\n    \n    float distanceFromPrevious;\n    \n    if([_branchLines count] == 0)\n    {\n        distanceFromPrevious = hypotf(point.x - firstPoint.x, point.y - firstPoint.y);\n    }\n    else\n    {\n        distanceFromPrevious = hypotf(point.x - lastBranchPosition.x, point.y - lastBranchPosition.y);\n    }\n    \n    if(distanceFromPrevious &gt; _minBranchSeperation)\n    {\n        VineBranch *newBranch = [[VineBranch alloc] initWithRandomPathFromPoint:point maxLength:_maxBranchLength leafSize:_leafSize];\n        newBranch.lineWidth = self.lineWidth / 2.0;\n        \n        [_branchLines addObject:newBranch];\n        lastBranchPosition = point;\n    }\n}</code></pre>\n  <p>If we modify our <code>drawRect:</code> from before we can now draw the branches and leaves as well as the main line.</p>\n  <pre><code class=\"language-swift\">- (void)drawRect:(CGRect)rect\n{\n    [vineColor setStroke];\n    \n    for(VineLine *vineLine in vineLines)\n    {\n        [vineLine stroke];\n\n        for(UIBezierPath *branchLine in vineLine.branchLines)\n        {\n            [branchLine stroke];\n        }\n    }\n}</code></pre>\n  <p>And we’re done!</p>\n  <p>Animating The Branches</p>\n  <p>That's where <code>CAShapeLayer</code> comes in. <code>CAShapeLayer</code> has a number of animatable properties, and animating <code>strokeEnd</code> is great for drawing a path to the screen. So we can remove the code to iterate through the list of branches and stroke them instead, every time a branch is created we create a layer for it and animate the stroke.</p>\n  <pre><code class=\"language-swift\">-(void)vineLineDidCreateBranch:(VineBranch *)branchPath\n{\n    CAShapeLayer *branchShape = [CAShapeLayer layer];\n    branchShape.path = branchPath.CGPath;\n    branchShape.fillColor = [UIColor clearColor].CGColor;\n    branchShape.strokeColor = vineColor.CGColor;\n    branchShape.lineWidth = branchPath.lineWidth;\n    \n    [self.layer addSublayer:branchShape];\n    \n    CABasicAnimation *branchGrowAnimation = [CABasicAnimation animationWithKeyPath:@\"strokeEnd\"];\n    branchGrowAnimation.duration = 1.0;\n    branchGrowAnimation.fromValue = [NSNumber numberWithFloat:0.0];\n    branchGrowAnimation.toValue = [NSNumber numberWithFloat:1.0];\n    [branchShape addAnimation:branchGrowAnimation forKey:@\"strokeEnd\"];\n}</code></pre>\n  <p>We can make our view the VineLine’s delegate and add a call to the delegate notifying it of a new branch in our <code>addLineToPoint:</code> method from above.</p>\n  <p>Random Paths</p>\n  <p>Initially I tried to be clever and see what way the line was curving and attach curves that seemed natural but that wasn’t looking too good. Eventually I just started throwing random numbers at it and things started looking better (this probably should have been obvious to me). So what we’re doing here is getting a random point close to the main line (as defined by <code>_maxLength</code>) and adding a curve to that point, control points are picked near that end point so we don’t end up with curves that are too crazy. Finally, we add the leaf, which for now is just a circle.</p>\n  <pre><code class=\"language-swift\">-(id)initWithRandomPathFromPoint:(CGPoint)startPoint maxLength:(float)maxLength leafSize:(float)leafSize\n{\n    self = [super init];\n    if(self)\n    {\n        [self moveToPoint:startPoint];\n        \n        CGPoint branchEnd = CGPointMake(startPoint.x + arc4random_uniform(maxLength * 2) - maxLength,startPoint.y + arc4random_uniform(maxLength * 2) - maxLength);\n        CGPoint brachControl1 = CGPointMake(branchEnd.x + arc4random_uniform(maxLength) - maxLength / 2,branchEnd.y + arc4random_uniform(maxLength) - maxLength / 2);\n        CGPoint branchControl2 = CGPointMake(branchEnd.x + arc4random_uniform(maxLength / 2) - maxLength / 4,branchEnd.y + arc4random_uniform(maxLength / 2) - maxLength / 4);\n        \n        [self addCurveToPoint:branchEnd controlPoint1:brachControl1 controlPoint2:branchControl2];\n        \n        UIBezierPath* leafPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(branchEnd.x - leafSize/2.0, branchEnd.y - leafSize/2.0, leafSize, leafSize)];\n        \n        [self appendPath:leafPath];\n    }\n    return self;\n}</code></pre>\n</article>",
      "summary": "Fun with UIBezierPath and CAShapeLayer",
      "date_published": "2014-01-30T12:00:00Z"
    }
  ]
}
