<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title><![CDATA[Atelier Clockwork]]></title>
        <description><![CDATA[Atelier Clockwork]]></description>
        <link>https://atelierclockwork.net</link>
        <generator>RSS for Node</generator>
        <lastBuildDate>Mon, 05 Aug 2024 08:48:14 GMT</lastBuildDate>
        <atom:link href="https://atelierclockwork.net/feed.xml" rel="self" type="application/rss+xml"/>
        <copyright><![CDATA[©2024 Michael Skiba]]></copyright>
        <managingEditor><![CDATA[Michael Skiba]]></managingEditor>
        <webMaster><![CDATA[Michael Skiba]]></webMaster>
        <item>
            <title><![CDATA[WWDC24 Week 6]]></title>
            <description><![CDATA[<p>I managed to wrap up the videos early so I don't need to think about this project during my vacation, which will be nice. Overall I feel like there were fewer sessions that were totally outside of my area of interest this year. Part of it may be because I've now done enough work with ML that the sessions there weren't completely inscrutable.</p><p>Now that I've done all the video watching, my next plan on deck is to get my WWDC session progress app into a state I'm happy with and can use so that it's ready to got for next year.</p><h3 id="bring-your-machine-learning-and-ai-models-to-apple-silicon">Bring your machine learning and AI models to Apple silicon</h3><p>This session is entirely focused on bringing existing ML models to Apple Silicon, and covers the new features for quantizing models, as well as support for stateful models, and multifunction models.</p><h3 id="optimize-for-the-spatial-web">Optimize for the spatial web</h3><p>This session covers how web technologies can be used on visionOS. In particular is talks about how to use content ships to highlight interactive elements cleanly when the user looks at them, how to display spatial photos, and how to use WebSpeech, WebAudio and WebXR.</p><h3 id="whats-new-in-quick-look-for-visionos">What’s new in Quick Look for visionOS</h3><p>Quick Look in visionOS is interestingly different from other platforms as it’s tied into other applications and lets applications present content with it. This year is adds the ability to display a collection of objects, supports enough data that an application can keep track of if the preview for an item has been closed, and Quick Look supports configurations within a file on all platforms now, so that one asset can represent multiple colors or other configurations of a model.</p><h3 id="deploy-machine-learning-and-ai-models-on-device-with-core-ml">Deploy machine learning and AI models on-device with Core ML</h3><p>This session follows on from the technologies discussed in “Bring your machine learning and AI models to Apple silicon”, and shows how to deploy models with these features using CoreM, and how to measure performance.</p><h3 id="build-a-spatial-drawing-app-with-realitykit">Build a spatial drawing app with RealityKit</h3><p>This session walks through a lot of the features for implementing a 3D drawing app, and covers a lot of useful low level details for working in RealityKit. In particular this covers how to work with different vertex buffer formats, low level mesh formats, and how to create a low level texture driven by a Metal shader.</p><h3 id="support-real-time-ml-inference-on-the-cpu">Support real-time ML inference on the CPU</h3><p>This is an in-depth dive into using BNNS Graph to execute ML model in a real time context, such as processing audio, and walks through importing the model, setting up the project, and implementing the BNNS graph from the audio.</p><h3 id="render-metal-with-passthrough-in-visionos">Render Metal with passthrough in visionOS</h3><p>This I a detailed talk about that it takes to create a renderer that reads the transforms for the user’s viewpoint and other trackable anchors, and then renders the scene correctly for the user. It also explains the different requirements in the passthrough mode compared to full immersion.</p><h3 id="build-a-great-lock-screen-camera-capture-experience">Build a great Lock Screen camera capture experience</h3><p>Locked camera capture allows 3rd party cameras to be used while the device is locked, and this session explains how to build an extension to do this, what the limitations are when running in that extension, and how data can be sent from your extension to the photo library or your application.</p><h3 id="optimize-your-3d-assets-for-spatial-computing">Optimize your 3D assets for spatial computing</h3><p>This session walks through the practical concerns for how to optimize a scene for rendering on visionOS. This includes texture optimizations, baking lighting, when to use different texture types, and splitting up geometry so that objects not in view can be culled.</p><h3 id="break-into-the-realitykit-debugger">Break into the RealityKit debugger</h3><p>This session walks through using the RealityKit debugger to spot some common issues, and talks through what could have caused each issue, and how to address it.</p><h3 id="explore-app-store-server-apis-for-in-app-purchase">Explore App Store server APIs for In-App Purchase</h3><p>This session covers enhancements to the App Store server APIs, including the ability to receive notifications for all purchases, as well as fetching the full transaction history for a user. It also covers how to create various types of offers such as win back offers.</p><h3 id="build-immersive-web-experiences-with-webxr">Build immersive web experiences with WebXR</h3><p>This session covers the practical concerns for setting up a WebXR experience, talks about some available frameworks to make it easier to work with, and then goes into detail about how the different input types work.</p><h3 id="discover-area-mode-for-object-capture">Discover area mode for Object Capture</h3><p>Area mode allows you to capture objects in contexts where the object can’t be easily isolated. Object capture also added the ability to export quad meshes with square polygons to make them easier to work with for 3d artists, higher quality export options, and easier ways to add extra data to object capture samples.</p><h3 id="design-interactive-experiences-for-visionos">Design interactive experiences for visionOS</h3><p>This session walks through some of the design choices from the Encounter Dinosaurs app, and primarily focuses on how to design interactions to help guide a user in immersive experiences.</p><h3 id="train-your-machine-learning-and-ai-models-on-apple-gpus">Train your machine learning and AI models on Apple GPUs</h3><p>This session is aimed at developers who are used to using python to train ML models, and want to take advantage of hardware acceleration on Apple Silicon. It covers the frameworks that have Apple Silicon support, and how to enable support for both model training and model export.</p><h3 id="bring-your-ios-or-ipados-game-to-visionos">Bring your iOS or iPadOS game to visionOS</h3><p>This session focuses on how to add visionOS specific features to an existing game, such as dynamic backgrounds, supporting parallax rendering, and head tracking.</p><h3 id="whats-new-in-device-management">What’s new in device management</h3><p>Device management is now supported on visionOS, and there is a long list of new convenience features. The most important one is likely that managed devices can now have activation lock disabled by the MDM software.</p><h3 id="discover-swift-enhancements-in-the-vision-framework">Discover Swift enhancements in the Vision framework</h3><p>The Vision framework now supports asynchronous/await, which will make integrating with Vision much cleaner than it was with the old completion handler based API. There are also some new types of requests, the most interesting one to me is assessing image aesthetics.</p><h3 id="implement-app-store-offers">Implement App Store Offers</h3><p>This session covers how to implement win back offers for subscriptions in detail, including configuring eligibility conditions, how to choose the best offer for a customer in app, handing off purchases of these offers from the App Store, and how to test them in Xcode.</p><h3 id="accelerate-machine-learning-with-metal">Accelerate machine learning with Metal</h3><p>This walks through some of the new optimizations available in Metal Performance Shaders, and the debugging tools. If I hadn’t spent a while doing ML work this year, I don’t think any of this would have made any sense to me.</p><h3 id="customize-spatial-persona-templates-in-shareplay">Customize spatial Persona templates in SharePlay</h3><p>This session walks through the things to consider when working with a spatial Persona template. It covers how to set up seating, how to reserve seats for particular roles, considerations for transitions, and affordances to be made for users on other platforms or who aren’t using Spatial Personas.</p><h2 id="modeling-progress">Modeling Progress</h2><p>I primed, painted, and weathered the first few sprues in the kit, and did some experimenting to get a glow effect on the eyes rather than using the clear plastic part as-is and I think it looks good when viewing it from a distance.</p><p>I then got a second set of sprues primed and have the base coat in place on those, so I just have weathering left there.</p><p><img alt="Acerby, weathered" src="https://atelierclockwork.net/Images/WWDC24/Week_6/1.jpg"><img alt="Acerby, face small" src="https://atelierclockwork.net/Images/WWDC24/Week_6/2.jpg"><img alt="Acerby, face large" src="https://atelierclockwork.net/Images/WWDC24/Week_6/3.jpg"><img alt="Acerby, based" src="https://atelierclockwork.net/Images/WWDC24/Week_6/4.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2024/07/16/wwdc24-week-6</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2024/07/16/wwdc24-week-6</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Tue, 16 Jul 2024 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC24 Week 5]]></title>
            <description><![CDATA[<p>I just checked, and I have exactly 21 videos left, so if I stick to three videos a day I'll finish up next Friday. As I have some vacation planned at the tail end of the week, I won't be doing 3 videos a day, but I'll see how ambitious I am and I either will double up on videos a few days, or take another week to finish this out.</p><h3 id="enhance-ad-experiences-with-hls-interstitials">Enhance ad experiences with HLS interstitials</h3><p>Interstitials now support more integration into the primary timeline, and can be shared across a SharePlay session, so that things like recaps, previews, and other ad content can cleanly be used in interstitials on a stream.</p><h3 id="meet-the-next-generation-of-carplay-architecture">Meet the next generation of CarPlay architecture</h3><p>The new CarPlay system will include the ability for phone rendered elements, on car rendered elements, and passthrough elements like video feeds from the car to all co-exist and be combined by a compositor that keeps animations and such in sync. It looks interesting, and it will be very interesting to see what happens when cars with this tech ship.</p><h3 id="use-hdr-for-dynamic-image-experiences-in-your-app">Use HDR for dynamic image experiences in your app</h3><p>This session goes into detail on how HDR images are structured, and the implications for those formats when working with the files in an image editor. HDR content is often saved as a SDR image plus extra data that describes the brightness of pixels outside of the standard SDR range.</p><h3 id="enhance-the-immersion-of-media-viewing-in-custom-environments">Enhance the immersion of media viewing in custom environments</h3><p>This session covers how to set up mounting points in a custom environment, and how to set up reflections, environment probes, and tints on the passthrough video for a custom environment.</p><h3 id="explore-game-input-in-visionos">Explore game input in visionOS</h3><p>This session covers both gesture based and controller based game input on visionOS. Controller support is exactly the same as on the other operating systems, and the gesture support section covered both how to use system gestures, and work with hand tracking.</p><h3 id="whats-new-in-privacy">What’s new in privacy</h3><p>This session is a high level overview of the changes in privacy, most of the content in it has dedicated sessions explaining the details of how to work with those changes. Getting an overview of the direction behind all of the privacy sessions was particularly informative, and it’s nice seeing that the focus is on both privacy and user friendliness.</p><h3 id="whats-new-in-dockkit">What’s new in DockKit</h3><p>DockKit adds improved tracking, support for buttons on devices, battery information, and the ability for an app to interact with the stream of focus interest data from the device. Most of this was done to add support for gimbal mounds, and to allow DockKit to be used for photography and panorama.</p><h3 id="keep-colors-consistent-across-captures">Keep colors consistent across captures</h3><p>Having spent time doing product photography and attempting to get the color grading correct from capture to display, it’s absolutely fascinating hearing about how this was implemented. The system captures an image with and without flash, and uses how the known light source from the flash changes the brightness of pixels to correct the color on images.</p><h3 id="whats-new-in-usd-and-materialx">What’s new in USD and MaterialX</h3><p>This is a session on new features in the content creation ecosystem that can be used across Apple platforms. Of particular interest is some of the new tools being added for things like reducing the file size of USDZ files, better quicklook support, and some Apple shaders that can be used in MaterialX workflows.</p><h3 id="port-advanced-games-to-apple-platforms">Port advanced games to Apple platforms</h3><p>This is an overview of all of the tools that Apple has made available for porting games to Apple platforms. Given that Apple was ambivalent at best about gaming for a long time, it’s nice to see they’ve both built up a toolchain for porting games to their platforms, and that they continue to add features showing the project is important.</p><h3 id="build-compelling-spatial-photo-and-video-experiences">Build compelling spatial photo and video experiences</h3><p>This session walks through how to work with both the display and capture of spatial video and photos. It covers a range from simple things like how to display spatial video in a video player, to the math involved in using a dual camera setup for spatial video capture and how to make sure the image planes are properly aligned for a good 3d viewing experience.</p><h3 id="discover-realitykit-apis-for-ios-macos-and-visionos">Discover RealityKit APIs for iOS, macOS, and visionOS</h3><p>This session walks through building a simple game, and handling things like physics, portals, and controls in a visionOS project, and then walks through how those features can all be enabled on other platforms.</p><h3 id="whats-new-in-app-store-connect">What’s new in App Store Connect</h3><p>The biggest change here is the ability to send notes on future feature releases to App Store editorial and they can then collaborate with you to feature them on launch. There’s also the ability to deep link into an app after the install via a custom project page, improvements to tester onboarding in TestFlight, and the ability to create marketing images.</p><h3 id="create-enhanced-spatial-computing-experiences-with-arkit">Create enhanced spatial computing experiences with ARKit</h3><p>This session covers some of the new features coming to ARKIit, the headline items are the ability to track a room, tracking angled planes, and improved hand tracking options.</p><h3 id="explore-object-tracking-for-visionos">Explore object tracking for visionOS`</h3><p>This session walks through the process of setting up object tracking in visionOS, which starts with training a ML model to recognize a known object from a reference model, and then you can use that to find the object and track it. The reference model created for training also is useful to create an occlusion object so that content rendered by the OS can be blocked by the tracked object.</p><h3 id="explore-machine-learning-on-apple-platforms">Explore machine learning on Apple platforms</h3><p>This is a high level overview of all of the new machine learning features that came out this year. IT was interesting to see the focus on research and open source contributions alongside the regular SDK work.</p><h3 id="meet-tabletopkit-for-visionos">Meet TabletopKit for visionOS</h3><p>TabletopKit is a new API for creating either single player or multiplayer board gaming experiences on vsionOS. IT allows you to create a table, game objects and a game board,, and easily set up multiplayer games using SharePlay. It will be really interesting to see what developers do with it.</p><h3 id="whats-new-in-create-ml">What’s new in Create ML</h3><p>This session is an overview of new features in Create ML this year.The headline feature is object tracking, but there are also some nice improvements to the tools for visualizing your data sets to be able to make sure the data is good before using it to train a ML model.</p><h3 id="compose-interactive-3d-content-in-reality-composer-pro">Compose interactive 3D content in Reality Composer Pro</h3><p>Reality Composer Pro keeps adding new features, this session covers how to work with animations, inverse kinematics, skeletal poses, and blend shapes. With these updates, it looks like the built in tooling for the system supports a lot of what used to be in highly specialized tools.</p><h3 id="enhance-your-spatial-computing-app-with-realitykit-audio">Enhance your spatial computing app with RealityKit audio</h3><p>This session covers how to work with audio in RealityKit. It walks through adding sound effects to an object and controlling directionality and volume on those, how to set up sounds for collisions both in an environment, and with real world objects, and how to add music and ambient sounds to an environment.</p><h3 id="get-started-with-healthkit-in-visionos">Get started with HealthKit in visionOS</h3><p>This session covers how to work with HealthKit in visionOS. The current APIs are available with feature parity to other platforms, so the only change is it has special handling for guest mode, and will not allow data to be saved or permissions to be changed when a guest is using the device.</p><h2 id="modeling-progress">Modeling Progress</h2><p>At the end of fall last year, I had to stop work on a kit that was about 75% done because it got too cold to open the window to vent outside while painting with lacquers. So I dug that out and finished the paint work and did a quick pass of panel liners on it. I really can't say enough good things about the lacquers in terms of how thin they go on, and how durable they are. The shine on the high gloss finish is just great in person. At some point in the near future, I'll probably put together some sort of simple base for it to be able to pose it more dynamically.</p><p>After that I started in on one of the kits I picked up in Japan. I managed to finish cutting out and sanding all of the parts from the first few sprues, and will be priming those next week.</p><p><img alt="Aerial, Completed" src="https://atelierclockwork.net/Images/WWDC24/Week_5/1.jpg"><img alt="Acerby, Parts" src="https://atelierclockwork.net/Images/WWDC24/Week_5/2.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2024/07/12/wwdc24-week-5</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2024/07/12/wwdc24-week-5</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Fri, 12 Jul 2024 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC24 Week 4]]></title>
            <description><![CDATA[<p>I'm just a bit pas halfway through all of the videos, and still finding some really interesting stuff as I go through everything. I think the session I'd recommend to most people so far is now “Add personality to your app through UX writing”, as it does a good job of talking about how to get into the right mindset to write in a preferred tone for an app or other project.</p><h3 id="meet-financekit">Meet FinanceKit</h3><p>This is a new framework that gives access to a user’s Apple Card, and Apple Pay Cash balances. It will be interesting to see if Apple eventually extends it so that other cards in Apple Pay can also be exposed to this API.</p><h3 id="meet-adattributionkit">Meet AdAttributionKit</h3><p>This session explains how AdAttributionKit works, and how it allows tracking of impressions, but with a focus on user privacy. That means that data is delivery with some randomization to time values, and also the granularity of data is affected by the size of the data set.</p><h3 id="bring-your-live-activity-to-apple-watch">Bring your Live Activity to Apple Watch</h3><p>This session covers code changes to be made to make a live activity work well for the watch, and also covers some of the new behaviors, especially how live activities are budgeted on the watch, and how to support the always on watch modes.</p><h3 id="customize-feature-discovery-with-tipkit">Customize feature discovery with TipKit</h3><p>This session covers how to make more efficient use of TipKit by creating custom tip identifiers, creating tip groups, and syncing the tip state between devices so users only see the tip once.</p><h3 id="whats-new-in-wallet-and-apple-pay">What’s new in Wallet and Apple Pay</h3><p>This session is a return to the WWDC of old where there’s a grab bag of sort of related content. It covers how to support 3rd party browsers with Apple Pay on the web, as well as updates to how passes are handled in wallet.</p><h3 id="build-custom-swimming-workouts-with-workoutkit">Build custom swimming workouts with WorkoutKit</h3><p>This session covers updates to custom workouts, including both the ability to name workout phases, allow the user to select indoors or outdoors for a given workout, and build time / distance workouts.</p><h3 id="explore-wellbeing-apis-in-healthkit">Explore wellbeing APIs in HealthKit</h3><p>This session explains the science between the wellbeing APIs in HealthKit, as well as how to long data that API, and how to interpret the data you read from the API.</p><h3 id="enhanced-suggestions-for-your-journaling-app">Enhanced suggestions for your journaling app</h3><p>This session covers visual update to the journaling suggestions, new data that will be available, how this data relates to the wellbeing APIs, and how to update an existing app to take advantage of these new features.</p><h3 id="meet-the-translation-api">Meet the Translation API</h3><p>This session explains how to the use translation API in many different circumstances. At a high level you can either have a popover view that translates the text, or you can have a translation session that will translate strings that you pass into it. It also covers how to configure the language settings if you want to manually specify languages to translate.</p><h3 id="introducing-enterprise-apis-for-visionos">Introducing enterprise APIs for visionOS</h3><p>This session covers the new enterprise only APIs available for visionOS. These include being able to capture the pass through environment in screen sharing, recognize bar codes from the camera feed, access the neural engine for running ML models, and run the device at higher speeds at the cost of battery life and having the fan run faster. The first two are privacy implications, but the second two are just changing device settings to shift trade-offs from what Apple has defined as “best”.</p><h3 id="whats-new-in-app-intents">What’s new in App Intents</h3><p>This session covers some of the new functionality in App intents, and how to tie into those features in the system. In particular it covers a lot of details on how the Transferable API can be used to pass data both from spotlight to your app, and also from your app into other apps.</p><h3 id="broadcast-updates-to-your-live-activities">Broadcast updates to your Live Activities</h3><p>This session cover how to use channels to be able to send a push notification to all interested users at once via APNS instead of needing to schedule a push notification for every interested user.</p><h3 id="bring-your-app-to-siri">Bring your app to Siri</h3><p>This session explains the data schemas required to expose intents to Siri, and how your app data can be shared with Siri both in search and in executing intents.</p><h3 id="bring-your-apps-core-features-to-users-with-app-intents">Bring your app’s core features to users with App Intents</h3><p>Keeping on the intents theme, this session shows how to expose app functionality as an Intent, including both creating controls, and app shortcuts that can be involved via Siri and render the UI in a modal view instead of needing to launch your app, or even answering in audio if the user is asking a device that doesn’t have a screen.</p><h3 id="streamline-sign-in-with-passkey-upgrades-and-credential-managers">Streamline sign-in with passkey upgrades and credential managers</h3><p>This session is the next step in the push towards passkeys, and shows how to opt your app or website into automatic passkey creation, so that when a user logs in successfully with other credentials the system can check if a passkey should be created, and if it is it will store it in your preferred credential manager.</p><h3 id="meet-accessorysetupkit">Meet AccessorySetupKit</h3><p>AccessorySetupKit is a new framework that both makes it so an app doesn’t need to ask for permission to access bluetooth devices or wifi to set up accessories, and offers an experience similar to Airpots setup. It’s another bit of UI that runs out of process with your app to handle the permissions sandboxing, and looks like it will make the pairing process for many accessories much smoother.</p><h3 id="add-personality-to-your-app-through-ux-writing">Add personality to your app through UX writing</h3><p>This session walks through the process of deciding of a voice and tone for your app, and then thinking about how to write to match that tone. The idea of having key personality traits to write from, and thinking of which ones are the most appropriate to emphasize in a given situation is a very interesting way of looking at UX writing.</p><h3 id="design-advanced-games-for-apple-platforms">Design advanced games for Apple platforms</h3><p>This session contains lots of details on how to get the UX right when porting a game to Apple platforms, including both minimum sizes for text and controls across platforms, and guidelines on how to implement touch controls for a game.</p><h3 id="capture-hdr-content-with-screencapturekit">Capture HDR content with ScreenCaptureKit</h3><p>ScreenCaptureKit now supports HDR video, microphone capture, and recording directly to files. It’s nice seeing cleanly supported ways of doing all of these things as these things used to be fairly complex to implement and often required very low level work.</p><h3 id="discover-media-performance-metrics-in-avfoundation">Discover media performance metrics in AVFoundation</h3><p>AVPlayer now supports generating detailed matrices for HLS playback, and this session shows how to subscribe to those metrics and gather data on performance of your HLS streams on user devices.</p><h3 id="use-cloudkit-console-to-monitor-and-optimize-database-activity">Use CloudKit Console to monitor and optimize database activity</h3><p>This session explains how to use the CloudKit console to set up notifications for activity, so that you can be alerted when issues arise. It also covers how to use the telemetry pages to debug app issues.</p><h2 id="modeling-progress">Modeling Progress</h2><p>I wrapped up the tiny kit this week, and am very happy with how it came out especially as I picked the color scheme myself instead of following the original design for the kit. I also primed a set of Gloomhaven minis for a co-worker who's interested in getting into painting.</p><p><img alt="Tiny kit, Front" src="https://atelierclockwork.net/Images/WWDC24/Week_4/1.jpg"><img alt="Tiny kit, side" src="https://atelierclockwork.net/Images/WWDC24/Week_4/2.jpg"><img alt="Gloomhaven, primed" src="https://atelierclockwork.net/Images/WWDC24/Week_4/3.jpg"><img alt="Gloomhaven, primed" src="https://atelierclockwork.net/Images/WWDC24/Week_4/4.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2024/07/05/wwdc24-week-4</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2024/07/05/wwdc24-week-4</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Fri, 05 Jul 2024 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC24 Week 3]]></title>
            <description><![CDATA[<p>I'm a day early this week, I doubled up on session videos today as my evening tomorrow is booked. I'm starting to hit the point where I'm watching session videos that are outside of the tools and technologies that I usually use, but am still picking up useful things in the videos.</p><h3 id="dive-deep-into-volumes-and-immersive-spaces">Dive deep into volumes and immersive spaces</h3><p>This session explains the new features in visionOS related to volumes and spaces. It also covers a lot of the thought process of how to structure volumes, for instance breaking out content that should scale and track the user into ornaments.</p><h3 id="evolve-your-document-launch-experience">Evolve your document launch experience</h3><p>The new document launch experience is a new default that will just show up in an app if the configuration is set up correctly in iOS 18. This session covers some customization options, and also how to the into features like creating documents from templates.</p><h3 id="support-semantic-search-with-core-spotlight">Support semantic search with Core Spotlight</h3><p>This session walks through what’s needed to donate content to spotlight, and how that donated content.will be stores in such a way that it’s only accessible to your app and spotlight. If content is added to spotlight in formats that support semantic indexing like text or pictures, semantic indexing will automatically become available.</p><h3 id="extend-your-apps-controls-across-the-system">Extend your app’s controls across the system</h3><p>This session explains how to create controls. Which are a new class of widget that can either be a button, or a toggle. It also covers customizing those controls, and how to share state from the controls between multiple devices via push notifications.</p><h3 id="enhance-your-ui-animations-and-transitions">Enhance your UI animations and transitions</h3><p>This session explains the improvements to the animation system, in particular how to use SwiftUI animations to get cancellable, interactive animations in UIKit views, and also how to make use of the new zoom animation.</p><h3 id="build-multilingual-ready-apps">Build multilingual-ready apps</h3><p>This session explains best practices for localization, and how to tap into some of the new system behaviors, like being able to learn user keyboard preferences for individual sections of your app, and new inline formatter behaviors. It also explains some common issues that can be run into with text, like bold-ing not behaving as expected in some languages, the fact italics aren’t rendered in some languages, and how to not use autocomplete suggestions for partial characters.</p><h3 id="create-custom-hover-effects-in-visionos">Create custom hover effects in visionOS</h3><p>Learning how to work with hover effects on visionOS is particularly interesting. All of the effect states are rendered at the same time, so that the OS can handle moving between them out of process without revealing eye tracking data to your app. Multiple effects can be composited together in groups, and there are delay modifiers to help not have effects toggle on and off multiple time in quick succession.</p><h3 id="tailor-macos-windows-with-swiftui">Tailor macOS windows with SwiftUI</h3><p>This session covers how to handle common window management tasks in SwiftUI, including making windows not re-open when restored, and handling placement and scaling on a screen.</p><h3 id="whats-new-in-sf-symbols-6">What’s new in SF Symbols 6</h3><p>SF symbols adds some new animation tricks, adds a lot more glyphs, and generally builds on top of the existing library. All of these effects are aimed at adding subtle indications of functionality or activity to an app and fill out the functionality nicely.</p><h3 id="demystify-swiftui-containers">Demystify SwiftUI containers</h3><p>This session walks through all of the new APIs that were introduced to let you build your own container views. These cover reading out all of the resolved subviews, how to perform layout, how to react to the amount of content, and how to add custom drawing elements that are set as modifiers on the child view, but that are rendered by the container.</p><h3 id="create-custom-visual-effects-with-swiftui">Create custom visual effects with SwiftUI</h3><p>This session explains several different ways to create visual effects. The new Text Effects API looks quite impressive, as does the new APIs for creating transitions or animations of content in a scroll view. It also has a refresher on the ability to use metal shaders, which was added last year.</p><h3 id="meet-the-contact-access-button">Meet the Contact Access Button</h3><p>The contact access button is a view that allows an app to request momentary access to selected contacts on demand. This along with the changes to the contacts store and contacts authorization make it easy for apps to request limited access at the time of use instead of needing to request access preemptively.access for ongoing access to all contacts.</p><h3 id="bring-context-to-todays-weather">Bring context to today’s weather</h3><p>The weather API now has the ability to give updates on significant changes to weather, so it’s easier to create contextual information about temperature, precipitation, etc. There’s also more forecast data added to the API, and support for more compact data formats on the Javascript API.</p><h3 id="whats-new-in-location-authorization">What’s new in location authorization</h3><p>There’s a entirely new version of location authorization available, and now location authorization is tied to sessions, and has lots of new rules on when a feature can be started, especially that a full accuracy tracking session can only be started when the app is foregrounded. It also adds some much improved diagnostic information for keeping track of the status of a session including things like losing user permissions as well as things like insufficient accuracy.</p><h3 id="unlock-the-power-of-places-with-mapkit">Unlock the power of places with MapKit</h3><p>Market now has unique place identifiers for points of interest so they can be shared and referenced more easily, there’s also automatic UI for displaying those places as cards. The Javascript version of MapKit also has a simplified token format that lets you use a token tied to a domain instead of needing to create JWTs.</p><h3 id="design-live-activities-for-apple-watch">Design Live Activities for Apple Watch</h3><p>This session covers design considerations for live activities for the watch. By default the most compact version of the iPhone activity is used to automatically make a watch activity, but there are opportunities for customization and places where you can add interactivity if it will help.</p><h3 id="design-great-visionos-apps">Design great visionOS apps</h3><p>This covers the design concerns for making an visionOS app, and has several quotes from developers of existing visionOS apps. There’s a focus on coming up with a “wow” experience for the for visionOS, and making sure all of the interactions are comfortable for the user.</p><h3 id="explore-multiview-video-playback-in-visionos">Explore multiview video playback in visionOS</h3><p>This shows off the ability to play multiple videos at once using AVPlayer on visionOS, how and how to combine that with a content browser to allow the user to pick which videos to add.</p><h3 id="say-hello-to-the-next-generation-of-carplay-design-system">Say hello to the next generation of CarPlay design system</h3><p>This is an interesting look into Apple’s opinions and goals in building an automotive interface. A lot of this is quite possibly taken form the Apple Car project and has been repurposed for CarPlay 2. It will be interesting to look back</p><h3 id="create-custom-environments-for-your-immersive-apps-in-visionos">Create custom environments for your immersive apps in visionOS</h3><p>This session goes through the process of taking an environment built in a 3d authoring too and how to optimize it for use in visionOS. This involves optimizing the models of objects that are far away from the center of the environment, pre-rendering the lighting, and combining the textures of many elements into a shared texture.</p><h3 id="design-app-intents-for-system-experiences">Design App Intents for system experiences</h3><p>This session explains how to design intents. The care details were creating an intent that reads like a sentence, not making duplicate intents for similar actions, and providing sensible defaults where possible.</p><h2 id="modeling-progress">Modeling Progress</h2><p>This week was painting the little kit. I did half of the kit, and have almost finished the detail work on the second half so should finish this weekend. I'm happy with the color scheme, and how the kit is coming along, especially given how hard it is to do the detailing on something this small. The kit is Pallas Athene, so I looked up colors associated with Athena and used that to build the palette.</p><p><img alt="Base coat" src="https://atelierclockwork.net/Images/WWDC24/Week_3/1.jpg"><img alt="Detailed" src="https://atelierclockwork.net/Images/WWDC24/Week_3/2.jpg"><img alt="Panel lined, gloss coat, assembled" src="https://atelierclockwork.net/Images/WWDC24/Week_3/3.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2024/06/30/wwdc24-week-3</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2024/06/30/wwdc24-week-3</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 30 Jun 2024 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC24 Week 2]]></title>
            <description><![CDATA[<p>Another week, another bit of progress going through WWDC content, I'm managing to keep up my 3 sessions a day momentum. I also managed to get my re-designed version of the site in good enough shape that I'm happy with how it looks.</p><h3 id="go-small-with-embedded-swift">Go small with Embedded Swift</h3><p>This session is a practical demo on deploying Embedded Swift to a matter device. It also covers some of the limitations of Embedded Swift, and how to work around them, such as having to use generics instead of a <code class="">any</code> type.</p><h3 id="go-further-with-swift-testing">Go further with Swift Testing</h3><p>This session goes into detail on the new Swift Testing setup and how to work with it. One thing that stuck out to me was being able to mark tests as known issues so they’re still compiled, run, and will report when they’re fixed. Having more tools to sort tests, such as tags and suites also seems very useful, as is the fact that tests are now run in parallel and a random order by default.</p><h3 id="explore-the-swift-on-server-ecosystem">Explore the Swift on Server ecosystem</h3><p>This session is a tutorial showing how to use Swift on Server to get up and running, and how versos tools from the community can be used together. It also covers best practices, such as the order which different subsystems should be initiated.</p><h3 id="run-break-inspect-explore-effective-debugging-in-lldb">Run, Break, Inspect: Explore effective debugging in LLDB</h3><p>This is the yearly reminder of all the ways to use the debugger. In particular this covered debugging from a crash log, how to use breakpoints, and a refresher on the debugger itself. The most useful new bit of into is the Swift error symbolic breakpoint.</p><h3 id="consume-noncopyable-types-in-swift">Consume noncopyable types in Swift</h3><p>This session explains what non copyable types are, and covers some of the complications of how to work with them. There’s the lifecycle of a non copyable type, how to pass it, and also how to make generics that are conditionally noncopyable so they work as expected when they contain a copyable type.</p><h3 id="migrate-your-app-to-swift-6">Migrate your app to Swift 6</h3><p>This session is a practical tutorial on migrating a sample app to Swift 6 with strict concurrency checking.. It covers both quick wins and how to deal with some of the more complex cases. There is also a lot of good explanation of why these checks are important.</p><h3 id="catch-up-on-accessibility-in-swiftui">Catch up on accessibility in SwiftUI</h3><p>This session goes through some of the common ways to add accessibility in SwiftUI, and some of the new features that are available. One detail that sticks out is the ability to conditionally add an accessibility label so that if the default value is enough most of the time, then you can use that and only override as needed.</p><h3 id="get-started-with-dynamic-type">Get started with Dynamic Type</h3><p>This session is a catch up on how to use dynamic type, and walks through common scenarios in both SwiftUI and UIKit. IT re-iterates the use of <code class="">AnyLayout</code> and swapping horizontal and vertical stacks based on the text size class.</p><h3 id="track-model-changes-with-swiftdata-history">Track model changes with SwiftData history</h3><p>This session covers how SwiftData history works, how to integrate with it custom backed. It explains concepts like the history token, and how to set up tombstone values to be able to track deletions so that you can avoid re-instating deleted items when applying a change set.</p><h3 id="explore-swift-performance">Explore Swift Performance</h3><p>This is a deep dive into working with micro level performance issues in Swift. It covers the cost of different types of memory allocations, and how the compiler applies optimizations. It also was very interesting to see how <code class="">async</code> calls are translated into machine code.</p><h3 id="analyze-heap-memory">Analyze heap memory</h3><p>This session covers how to use the various memory debugging tools in Xcode and Instruments, and how to fix common issues like memory leaks and memory spikes.</p><h3 id="elevate-your-tab-and-sidebar-experience-in-ipados">Elevate your tab and sidebar experience in iPadOS</h3><p>This session covers how to make use of the new tab view in both SwuftUI and UIKit. There’s both a new more type safe way of specifying tabs in SwiftUI, and the customization of.a Tab View can be saved in <code class="">@AppStorage</code> to make it easy to persist user changes.</p><h3 id="whats-new-in-appkit">What’s new in AppKit</h3><p>The changes that came to AppKit are mostly exposing features that are also available in SwiftUI or UKIt, but it’s nice to see parity being maintained there. The interesting AppKit specific features are support for the various window tiling modes, and the ability to use system cursors.</p><h3 id="whats-new-in-storekit-and-in-app-purchase">What’s new in StoreKit and In-App Purchase</h3><p>The focus of this was on using win back offers, and the new SwiftUI views that can render store kit content. Another item of note is that StoreKit 1 is deprecated and will be removed in a future release so there’s a strong push to update to the new framework.</p><h3 id="whats-new-in-watchos-11">What’s new in watchOS 11</h3><p>Some of the most interesting changes here are that watchOS will be able to show live activities on the Smart Stack with no configuration, and lots of changes to widgets. Having widgets that can perform an action without launching your app will potentially add lots of new interactions on watchOS.</p><h3 id="swift-charts-vectorized-and-function-plots">Swift Charts: Vectorized and function plots</h3><p>This update allows rendering functions and vector data using Swift Charts. This makes It shows off how to combine a function plot with a standard chart to show how the data matches a distribution curve.</p><h3 id="work-with-windows-in-swiftui">Work with windows in SwiftUI</h3><p>This showcases how to work with windows in SwiftUI, particularly how to transition between different windows, and how to react to user behavior rejigging and moving widows.</p><h3 id="migrate-your-tvml-app-to-swiftui">Migrate your TVML app to SwiftUI</h3><p>Apple is deprecating TVML, so this session is a walkthrough of how to reproduce may of the stock TVML elements and behaviors in SwiftUI.</p><h3 id="get-started-with-writing-tools">Get started with Writing Tools</h3><p>Integrating the new writing tools in app looks to be quite simple.as most of it is automatic and handled by the system. The session does explain how to exclude portions of text from the writing tools, for example code blocks, and how it integrates with the underlying text storage and undo manager.</p><h3 id="squeeze-the-most-out-of-apple-pencil">Squeeze the most out of Apple Pencil</h3><p>This session explains how to make custom tools for the Apple Pencil, how to work with the tool picker, and how to respond to the new gestures and rotation data sent from the new Apple Pencil.</p><h3 id="bring-expression-to-your-app-with-genmoji">Bring expression to your app with Genmoji</h3><p>This session covers how to handle using Genmoji in your app, and especially how to support including the generated image glyphs as embedded content in an attributed string and serialization into an RTF format so than the default data store, and what it would take to implement it for your own custom data store.</p><h2 id="modeling-progress">Modeling Progress</h2><p>This week, I spent a lot of time working on a <em>very</em> small kit that I picked up in Japan. I prepped the whole kit, primed all of the pieces in black, put it together, and did a zenithal prime that will help accentuate the details when the final paints go on. Finally I took it back apart and started prepping each section for painting with base colors.
<img alt="Primed, Black" src="https://atelierclockwork.net/Images/WWDC24/Week_2/1.jpg"><img alt="Primed, Zenithal" src="https://atelierclockwork.net/Images/WWDC24/Week_2/2.jpg"><img alt="Disassembled" src="https://atelierclockwork.net/Images/WWDC24/Week_2/3.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2024/06/21/wwdc24-week-2</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2024/06/21/wwdc24-week-2</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Fri, 21 Jun 2024 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Warm Boot]]></title>
            <description><![CDATA[<h3 id="fresh-design-fresh-tools">Fresh design, fresh tools</h3><p>As an attempt to learn more about the development landscape outside of the iOS ecosystem, I went through a course on using <a href="https://nuxt.com/" rel="nofollow">Nuxt</a>, and so I used the things I learned from that to re-work my blog again as part of my prep for this year's WWDC as I plan on trying to watch all of the sessions and post a brief summary of each one here.</p><p>The new stack that I'm using to run this blog is:</p><ul><li><a href="https://nova.app/" rel="nofollow">Nova</a> to edit text.</li><li><a href="https://github.com/" rel="nofollow">GitHub</a> to host the code</li><li><a href="https://nuxt.com/" rel="nofollow">Nuxt.js</a> to generate the page</li><li><a href="https://content.nuxt.com/" rel="nofollow">Nuxt Content</a> to let me use Markdown files to generate posts</li><li><a href="https://tailwindcss.com/" rel="nofollow">Tailwind CSS</a> for styling</li><li><a href="https://pages.github.com/" rel="nofollow">GitHub Pages</a> now that pages supports deploying via actions, it was easy to set up a statically rendered version of the site</li><li><a href="https://fonts.google.com/" rel="nofollow">Google Fonts</a> to delivery custom fonts</li><li><a href="https://unsplash.com/" rel="nofollow">Unsplash</a> to pull images to add some visual interest</li></ul>]]></description>
            <link>https://atelierclockwork.net/posts/2024/06/20/warm-boot</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2024/06/20/warm-boot</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Thu, 20 Jun 2024 00:00:00 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC24 Week 1]]></title>
            <description><![CDATA[<h2 id="kicking-off">Kicking Off</h2><p>Once again, I’m on a mission to watch 100% of the WWDC session videos. I have some tools to help with that that are <em>almost</em> done, so hopefully in the next week or two I’ll be ready to share a bit more about those. For now I managed to keep a solid momentum of 3 videos a day.</p><h3 id="keynote">Keynote</h3><p>I don’t have a lot to say here, most of the announcements were for things that won’t be developer facing. I am a bit amused that Apple is essentially implying that AI image generators are good for turning out custom clip art.</p><h3 id="platforms-state-of-the-union">Platforms State of the Union</h3><p>The platforms sate of the union is hard to talk about in a whole different way as it’s so dense with things to think about. The new code completion is promising, Swift 6 concurrency is both promising and looms large as pretty much every project out there has work to do to pass muster there. Swift Testing looks promising, and the new presentation animations in SwiftUI look nice. I’m sure I missed a bunch of things, but will talk about them in the right session videos.</p><h2 id="week-1">Week 1</h2><h3 id="whats-new-in-xcode-16">What’s new in Xcode 16</h3><p>An overview of the new features in Xcode 16. The stand out features to me are the new macros to make working with tests and previews easier, and the new tools to help with performance issues. The flame graph in Instruments should be very useful for analyzing performance issues in apps.</p><h3 id="swiftui-essentials">SwiftUI Essentials</h3><p>Nothing new here, but it was the best overview I’ve seen of the design goals that went into SwiftUI. It also was very interesting that this talk spent a fair amount of time explaining how to interoperate with UIKit and AppKit.</p><h3 id="whats-new-in-swiftui">What’s New in SwiftUI</h3><p>Lots of feature improvements here that will make a big improvement. The zoom transition will be very interesting to play with, the scrolling enhancements cover things that required a bunch of trickery to get right. Bridging animations between UIKit and SwiftUI and using UIKit gesture recognizers in SwiftUI has some interesting potential uses, and the TextRenderer looks very promising for adding some more exciting effects to text.</p><h3 id="meet-swift-testing">Meet Swift Testing</h3><p>Swift Testing looks like it will be a really nice improvement un usability compared to XCTest. Being able to use a handful of macros instead of remembering all of the XCTExpect rules should be a very nice improvement.</p><h3 id="xcode-essentials">Xcode Essentials</h3><p>This is a great talk for anyone who wants to learn ways to make working in Xcode faster. It covered some of my favorites like quick open, reminded me that there’s a command Manu, and went over how to get the most value out of the logging and testing tools.</p><h3 id="whats-new-in-swiftdata">What’s new in SwiftData</h3><p>Looks like there are some nice things added to Swift Data, in particular the ability to specify that a combinations of values need to be unique for a record. Custom storage backends have a ton of potential for teams that want to migrate to Swift Data from another system. <code class="">#Expression</code> also looks like it fixes a lot of my issues I had working with Predicates.</p><p>The biggest thing that I caught here isn’t directly Swift Data, the new PreviewModifier protocol will make working with previews ,such easier.</p><h3 id="whats-new-in-swift">What’s new in Swift</h3><p>This covers the new features in Swift 6. The most day to day usable things will be the data race safety (after the pain of fixing all of the potential; problems the new system will expose), and types Throws. There’s also a lot of things that are interesting for the long term, like the C++ interoperability, and improvements to the swift for Linux toolchain.</p><h3 id="a-swift-tour-explore-swifts-features-and-design">A Swift Tour: Explore Swift’s features and design</h3><p>I think this video would be a great video to show to someone who’s an experienced developer who wants to learn about Swift, or someone very new to Swift. It did a great job covering core information about the language but there was no new information that hasn’t been covered before in other places.</p><h3 id="whats-new-in-uikit">What’s new in UIKit</h3><p>Unsurprisingly the majority of the content here is either UIKit getting updates to make sure it stays in sync with SwiftUI like the new tab bar stye, SF Symbol animations, etc.</p><h3 id="extend-your-xcode-cloud-workflows">Extend your Xcode Cloud workflows</h3><p>This session shows off some of the more advanced features in Xcode Cloud. In particular it showed how to integrate running tasks at the request of an outside service via the API, and then report the results via. a webhook.</p><h3 id="demystify-explicitly-built-modules">Demystify explicitly built modules</h3><p>This session explains explicitly build modules, and how to turn them on and fine tune project settings to save build time. I’ll be interested to try this out and hopefully see some build time improvements on my projects.</p><h3 id="create-a-custom-data-store-with-swiftdata">Create a custom data store with SwiftData</h3><p>This covers how to create your own data store for SwiftData. It looks surprisingly easy to get the basics off the ground, and then there are many optional improvements that can be made. Lots of potential here for using this to allow for a phased adoption of Swift Data in an app by bridging to an existing store.</p><h2 id="model-shots">Model Shots</h2><p>I spent the week wrapping up a kit I started earlier, it's a request so it's not on my usual theme, but it was a very fun project to work on. It's 90% done and looking good:</p><p><img alt="Painting, Base" src="https://atelierclockwork.net/Images/WWDC24/Week_1/1.jpg"><img alt="Painting, Layered" src="https://atelierclockwork.net/Images/WWDC24/Week_1/2.jpg"><img alt="Painting, Wash" src="https://atelierclockwork.net/Images/WWDC24/Week_1/3.jpg"><img alt="Assembled" src="https://atelierclockwork.net/Images/WWDC24/Week_1/4.jpg"><img alt="Diorama" src="https://atelierclockwork.net/Images/WWDC24/Week_1/5.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2024/06/14/wwdc24-week-1</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2024/06/14/wwdc24-week-1</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Fri, 14 Jun 2024 19:23:09 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC23 Week 6 - Finale]]></title>
            <description><![CDATA[<h2 id="week-6-progress">Week 6 Progress</h2><p>Progress report: 177 of 177 videos watched and summarized. So that means I've managed to watch all of the session videos and now have a passable index of ideas introduced at WWDC this year. So now that I've finished the ”watch all the things” task I set out for myself, I need to figure out what to do with all of the information, aside from the obvious part of using it in my day job, and also see if now that I've managed to get some momentum going if I can stick to the blogging thing again.</p><h3 id="discover-metal-for-immersive-apps">Discover Metal for immersive apps</h3><h3 id="enhance-your-spatial-computing-app-with-realitykit">Enhance your spatial computing app with RealityKit</h3><h3 id="create-immersive-unity-apps">Create immersive Unity apps</h3><h3 id="bring-your-unity-vr-app-to-a-fully-immersive-space">Bring your Unity VR app to a fully immersive space</h3><p>These sessions all focused on developing games in visionOS, and other low level implementation topics for the OS. It’s promising to see that Unity has lots of integrations for visionOS close to ready, and that it’s using things like MaterialX to bridge the unity Materials system to visionOS.</p><h3 id="customize-on-device-speech-recognition">Customize on-device speech recognition</h3><p>This session explained how to train the on device speech recognizer for both user specific and domain specific content. It explains how to weight terms, and how to do things like use phonetic annotations to ensure that the words are parsed properly.</p><h3 id="explore-3d-body-pose-and-person-segmentation-in-vision">Explore 3D body pose and person segmentation in Vision</h3><h3 id="detect-animal-poses-in-vision">Detect animal poses in Vision</h3><p>These sessions show off the new enhancements to Vision. Having support for 3d pose detection for people adds interesting potential for modeling and animation, and the new person segmentation should allow lots of interesting new effects.</p><h3 id="explore-natural-language-multilingual-models">Explore Natural Language multilingual models</h3><h3 id="discover-machine-learning-enhancements-in-create-ml">Discover machine learning enhancements in Create ML</h3><h3 id="optimize-machine-learning-for-metal-apps">Optimize machine learning for Metal apps</h3><h3 id="improve-core-ml-integration-with-async-prediction">Improve Core ML integration with async prediction</h3><h3 id="use-core-ml-tools-for-machine-learning-model-compression">Use Core ML Tools for machine learning model compression</h3><p>These sessions explained all of the new features in CoreML and the related tooling that are coming to the new OSs. This includes support for multilingual models, support for LLMs, improved model compression options, and the ability to use async predictions to efficiently handle many prediction requests.</p><h3 id="integrate-with-motorized-iphone-stands-using-dockkit">Integrate with motorized iPhone stands using DockKit</h3><p>This session showed off what you can do with DockKit. This is a system level toolkit for interacting with compatible motorized iPhone docs. It offers both person and point of interest tracking.</p><h3 id="whats-new-in-privacy">What’s new in privacy</h3><p>This session is an overview of all of the new privacy technologies coming in the new OSs. As I watched this late, there wasn’t anything of interest that wasn’t in another session video,</p><h3 id="protect-your-mac-app-with-environment-constraints">Protect your Mac app with environment constraints</h3><p>This session explains how to add environment constraints to Mac apps which allow for more control over the allowed execution environment. In particular, it allows for developers to do things like only allow helper tools for the application to be launched by the app itself to avoid parent processes being used to alter execution behavior.</p><h3 id="support-hdr-images-in-your-app">Support HDR images in your app</h3><p>This session explains the new tooling that’s coming to the OS for working with HDR images. Image views now support setting the displayed content to SDR, limited HDR or full HDR. Limited HDR is meant to be used in situations where there is mixed SDR and HDR content, or where HDR content would be distracting.</p><h3 id="support-external-cameras-in-your-ipados-app">Support external cameras in your iPadOS app</h3><p>This session explains how to use wired external camera on an iPadOS app, and how to handle the fact that device rotation and camera rotation can now be independent of each other.</p><h3 id="whats-new-in-css">What’s new in CSS</h3><h3 id="explore-media-formats-for-the-web">Explore media formats for the web</h3><p>These sessions show off the work that the WebKit team has been doing to support emerging web standards. It’s interesting seeing successor formats to JPG, PNG and GIF coming after all of there years.</p><h3 id="whats-new-in-web-apps">What’s new in web apps</h3><p>This session demonstrates the new features that are available in web apps. The most important feature is that you can now create web apps from Safari on macOS. And there are new configuration options to help site maintainers scope and customize what a web app will look like.</p><h3 id="whats-new-in-safari-extensions">What’s new in Safari extensions</h3><p>This session explains all of the new features coming to Safari extensions. Among other things it includes more CSS selectors for content blockers, and new permissions for redirecting requests or modifying headers.</p><h3 id="meet-object-capture-for-ios">Meet Object Capture for iOS</h3><p>This session shows the new ObjectCaptureView on iOS. It’s a SwiftUI view, and uses a state machine to manage state, which is very interesting as an architecture note. It supports capturing LIDAR data alongside photos, and can either generate the 3d model on device, or has tooling to export the images for processing on a Mac in Reality Composer Pro.</p><h3 id="explore-enhancements-to-roomplan">Explore enhancements to RoomPlan</h3><p>This session has details on improvements to RoomPlan. The marquee feature is multi-room scans, but it also has support for more object types, more complex rooms, and exporting metadata along with the USDZ file.</p><h3 id="discover-quick-look-for-spatial-computing">Discover Quick Look for spatial computing</h3><p>This session shows off Quick Look into visionOS. It has full support of the quicklook features of other platforms, and also the ability to drag content out of you app into a windowed quicklook view.</p><h3 id="create-3d-models-for-quick-look-spatial-experiences">Create 3D models for Quick Look spatial experiences</h3><p>This session covers details on using 3d models in Quick Look on visionOS, and includes lots of details about how to optimize materials, textures, and models to keep the model performant when being used.</p><h3 id="work-with-reality-composer-pro-content-in-xcode">Work with Reality Composer Pro content in Xcode</h3><p>This session explains how to work with content from Reality Composer pro, and explains both how to load the content into the view, and also how the data in the Entity Component System is structured and how to work with them from Swift.</p><h3 id="explore-rendering-for-spatial-computing">Explore rendering for spatial computing</h3><p>This session covers details on how to optimize content for rendering in visionOS. In particular, it covers optimizing models and layer based content to better handle the foveated rendering.</p><h3 id="build-spatial-shareplay-experiences">Build spatial SharePlay experiences</h3><p>This session explains how SharePlay works in visionOS, how the scenes are set up so that people can collaborate around different elements, and how to control which windows are shared in the SharePlay session.</p><h3 id="meet-arkit-for-spatial-computing">Meet ARKit for spatial computing</h3><p>This session summarizes the API updates to ARKit that were added for support in visionOS. This includes world tracking, hand tracking, and scene reconstruction.</p><h3 id="get-started-with-building-apps-for-spatial-computing">Get started with building apps for spatial computing</h3><p>This session is a kick off guide showing how to work with spatial computing shows off the variety of sample projects Apple has provided and shows how to get started.</p>]]></description>
            <link>https://atelierclockwork.net/posts/2023/07/16/wwdc23-week-6-finale</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2023/07/16/wwdc23-week-6-finale</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 16 Jul 2023 20:49:35 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC23 Week 5]]></title>
            <description><![CDATA[<h2 id="week-5-progress">Week 5 Progress</h2><p>Progress report: 147 of 177 videos watched and summarized, or 83.0%. I averaged of 3 videos a day, so a significant slowdown but not surprising it's getting outside of the areas of interest for me. In particular things like enterprise device management are going in the "not useful now, but may be useful later" column.</p><h3 id="support-cinematic-mode-videos-in-your-app">Support Cinematic mode videos in your app</h3><p>This session explains how to integrate viewing and editing content created with the cinematic camera on iOS in an app. It also describes the format of data that Cinematic mode produces, which is the video stream, a depth stream, channels that describe points of interest in the scene, and then a control track.</p><h3 id="add-shareplay-to-your-app">Add SharePlay to your app</h3><h3 id="share-files-with-shareplay">Share files with SharePlay</h3><p>These sessions cover the new SharePlay functionality, in particular the ability to start of SharePlay sessions via share sheets and other contexts, and the ability to send files via SharePlay.</p><h3 id="whats-new-in-screencapturekit">What’s new in ScreenCaptureKit</h3><p>ScreenCaptureKit continues to add features. This release adds a system picker for screen sharing and allows for users to share content from a window to another app via the title bar. It also adds a tool for taking screenshots with most of the same affordances of the video capture tooling.</p><h3 id="tune-up-your-airplay-audio-experience">Tune up your AirPlay audio experience</h3><h3 id="explore-airplay-with-interstitials">Explore AirPlay with interstitials</h3><p>These sessions go over fine details of AirPlay integration, both how to make sure you properly use the buffer to allow instant reaction t control events in audio apps, and how to support interstitial events both for live and VOD content.</p><h3 id="enhance-your-apps-audio-experience-with-airpods">Enhance your app’s audio experience with AirPods</h3><h3 id="whats-new-in-voice-processing">What’s new in voice processing</h3><p>These sessions talk about how to integrates with the new features coming to AirPods to enable features like muted talker detection, and tap to mute on the devices.</p><h3 id="create-a-great-spatial-playback-experience">Create a great spatial playback experience</h3><h3 id="deliver-video-content-for-spatial-experiences">Deliver video content for spatial experiences</h3><p>These sessions go over how to set up video playback in visionOS, and also detailed information about how to encode 3d video in a format that visionOS can use well.</p><h3 id="discover-continuity-camera-for-tvos">Discover Continuity Camera for tvOS</h3><p>This session goes over how to integrate with Continuity Camera on tvOS. Most of it is similar to iOS, but then with the need to manage devices being added or removed as the cameras are connected wirelessly and can change at any time.</p><h3 id="deploy-passkeys-at-work">Deploy passkeys at work</h3><h3 id="whats-new-in-managing-apple-devices">What’s new in managing Apple devices</h3><h3 id="explore-advances-in-declarative-device-management">Explore advances in declarative device management</h3><h3 id="do-more-with-managed-apple-ids">Do more with Managed Apple IDs</h3><h3 id="meet-device-management-for-apple-watch">Meet device management for Apple Watch</h3><p>As I don’t dealt with managed devices, these session largely don’t apply to me, but it’s always interesting to see what Apple is working on. The interesting bits are that there’s now a way to set up passkeys in enterprise environments and restrict them to managed devices, and that declarative device management is being pushed as the main way to manage devices.</p><h3 id="build-a-multi-device-workout-app">Build a multi-device workout app</h3><p>This session covers how to set up a multi-device workout. This involves a new API that lets you mirror the session between devices and handle sending data in both directions.</p><h3 id="your-guide-to-metal-ray-tracing">Your guide to Metal ray tracing</h3><p>This session goes over how to set up acceleration structures for metal ray tracing, and shows off the new curves type. It’s nice to see consistent progress on this.</p><h3 id="explore-the-usd-ecosystem">Explore the USD ecosystem</h3><p>This session talks about working with USD files, and talks about the integration with MaterialX, which is coming to visionOS first but will probably roll out to more platforms over time.</p><h3 id="explore-materials-in-reality-composer-pro">Explore materials in Reality Composer Pro</h3><p>This session walks through building materials in the material graph in Reality Composer Pro, starting out with some very basic color manipulation and ending with swapping between two material and geometry sets.</p><h3 id="build-great-games-for-spatial-computing">Build great games for spatial computing</h3><p>This session goes over the basics of how to build games for spatial computing, focused on explaining the available input types and also the different rendering modes that you can use and the performance implications of those modes.</p><h2 id="modeling-progress">Modeling Progress</h2><p>Evangelion unit 0 is now completely assembled, and I'm waiting until I'm in the right mood to spend a couple hours doing lots of very fussy details.</p><p><img alt="Assembled, Side" src="https://atelierclockwork.net/Images/WWDC23/Week_5/3.jpg"></p><p>I also managed to get this kit assembled and painted, and the weathering about 2/3 of the way done.</p><p><img alt="Assembled, Front" src="https://atelierclockwork.net/Images/WWDC23/Week_5/1.jpg"><img alt="Assembled, Side" src="https://atelierclockwork.net/Images/WWDC23/Week_5/2.jpg"></p><p>And I started on another kit, and am playing with enamels for the metallics. Which look quite nice, and they're more durable, but require a lot more fuss in terms of handling:</p><p><img alt="Assembled, Side" src="https://atelierclockwork.net/Images/WWDC23/Week_5/4.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2023/07/09/wwdc23-week-5</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2023/07/09/wwdc23-week-5</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 09 Jul 2023 20:41:39 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC23 Week 4]]></title>
            <description><![CDATA[<h2 id="week-4-progress">Week 4 Progress</h2><p>Progress report: 126 of 177 videos watched and summarized, or 71.0%. I managed to keep a slightly higher average of 4.14 videos compared to last week. More of the features in sessions are starting to fall into the “things to keep an eye on for future projects” bucket than things I'll use day to day, but it's good to have a mental index of what's out there.</p><h3 id="build-robust-and-resumable-file-transfers">Build robust and resumable file transfers</h3><p>This session covers both how to use the existing capabilities to resume a failed download, and also how to use resumable uploads. Most of the support for this is built into <code class="">URLSession</code>, and supporting resuming uploads will require a server that supports it as it’s using a brand new specification.</p><h3 id="design-dynamic-live-activities">Design dynamic Live Activities</h3><p>This session covers lots of the considerations about crafting a live activity that will fit into the dynamic island well, and also maintain the personality of your app when place in that space.</p><h3 id="design-considerations-for-vision-and-motion">Design considerations for vision and motion</h3><p>This session explains how to design for visionOS in a way that won’t make the user uncomfortable. It focuses on how to avoid eyestrain by sending the right messages about depth to the user, and understanding which eye movements take more energy. It also covers how to move elements on screen to avoid triggering motion sickness.</p><h3 id="keep-up-with-the-keyboard">Keep up with the keyboard</h3><p>This session explains the upcoming changes coming as the keyboard moves out of process with the currently running app and more of the operations that it performs are asynchronous. It also shows off the improvements to the keyboard layout guides, and how to handle the keyboard in contexts like Stage Manager where the app isn’t full screen.</p><h3 id="create-a-great-shazamkit-experience">Create a great ShazamKit experience</h3><p>This session goes over improvements to ShazamKit, in particular it adds the ability to use a managed session for audio recognition to make the setup for use in app much easier.</p><h3 id="whats-new-in-visionkit">What’s new in VisionKit</h3><h3 id="lift-subjects-from-images-in-your-app">Lift subjects from images in your app</h3><p>These sessions show off the improvements in VisionKit, in particular the ability to recognize and lift the subjects out of images. Given how hard it used to be to manually do this, getting a decent mask of the subject in an image for free is quite impressive.</p><h3 id="integrate-your-media-app-with-homepod">Integrate your media app with HomePod</h3><p>This session explains how to integrate your media app with HomePod, including how commands are routed to phones to help understand potential issues with that handoff process.</p><h3 id="whats-new-in-storekit-2-and-storekit-testing-in-xcode">What’s new in StoreKit 2 and StoreKit Testing in Xcode</h3><h3 id="explore-testing-in-app-purchases">Explore testing in-app purchases</h3><h3 id="whats-new-in-app-store-server-apis">What’s new in App Store server APIs</h3><h3 id="meet-the-app-store-server-library">Meet the App Store Server Library</h3><p>These sessions all cover how to integrate with the App Store, both on the app side and server side. It’s nice to see that there’s more tools for testing, and that there’s a library to help with server implementation that’s  available in Swift, Node, and Python.</p><h3 id="explore-enhancements-to-app-intents">Explore enhancements to App Intents</h3><p>This session shows how to provide more data to app intents, and how to use the new features like providing the name of produced data at the end of a step in Shortcuts to make it easier to reason about the flow of data.</p><h3 id="whats-new-in-app-clips">What’s new in App Clips</h3><p>This session goes over improvements to App Clips. The interesting news is that the clip size is now 50 mb for digital invocations, but still 15 mb physical invocations. There’s now also default app clip links that are hosted on an Apple domain so that you can create an app clip link without having to perform all of the set up work on a live domain.</p><h3 id="meet-core-location-for-spatial-computing">Meet Core Location for spatial computing</h3><p>This is a session explaining how Core Location works on visionOS. It only supports while in use location data, and “in use” is when the app is being looked at or is in the users peripheral vision, which is interesting.</p><h3 id="whats-new-in-wallet-and-apple-pay">What’s new in Wallet and Apple Pay</h3><p>This covers all fit new features in Wallet and Apple Pay. It’s interesting to see how may categories Apple Pay is moving into. Including allowing transferring funds There is also improvements to order tracking improvements to integrate more deeply into the system. The ID information is also interesting, but is of limited use to me until there is digital ID support in the EU.</p><h3 id="whats-new-in-core-motion">What’s new in Core Motion</h3><p>This session covers new Core Motion features, most of which are focused on watchOS. Newer watches can capture high frequency motion data, and also data related to water depth and temperature.</p><h3 id="update-live-activities-with-push-notifications">Update Live Activities with push notifications</h3><p>This session explains how to use push notifications to update Live Activities, including important concepts like how different priority levels of notifications are handled and how the system may throttle updates.</p><h3 id="whats-new-in-background-assets">What’s new in Background Assets</h3><p>This session describes improvements to background assets. Among other things, how to mark assets as "essential" so they will attempt to download before the app can be launched, but if the download fails the app can be launched without those assets.</p><h3 id="discover-streamlined-location-updates">Discover streamlined location updates</h3><h3 id="meet-core-location-monitor">Meet Core Location Monitor</h3><p>These sessions show off the improvements to Core Location, in particular there is a new API for subscribing to updates that returns them as an <code class="">AsyncSequence</code> and attempts to simplify the process of monitoring updates, including more robust background tracking.</p><h3 id="meet-assistive-access">Meet Assistive Access</h3><p>This session describes how Assistive Access works, and what you can do to improve support for assistive access in your app by adding configuration keys to let the system know your app has a layout that will work with the large bottom back button that's always on screen in Assistive Access.</p><h3 id="extend-speech-synthesis-with-personal-and-custom-voices">Extend Speech Synthesis with personal and custom voices</h3><p>This session shows how to add annotations to better control speech synthesis, and also what you need to do to request permission from the user to use a personal voice.</p><h3 id="create-accessible-spatial-experiences">Create accessible spatial experiences</h3><p>This session went over the new accessibility needs that are specific to visionOS, including how to allow app hand gestures, and how to integrate RealityKit content into the accessibility system.</p><h3 id="build-accessible-apps-with-swiftui-and-uikit">Build accessible apps with SwiftUI and UIKit</h3><p>Lots of detail about basic accessibility features, the most interesting detail in the session was the ability to use closures to set UIKit accessibility values so that they'll be evaluated on read and therefore always up to date.</p><h3 id="whats-new-in-app-store-pre-orders">What’s new in App Store pre-orders</h3><h3 id="whats-new-in-app-store-pricing">What’s new in App Store pricing</h3><h3 id="explore-app-store-connect-for-spatial-computing">Explore App Store Connect for spatial computing</h3><p>Not much of interest here, having per-region pre-orders is nice, and it went over the extended pricing options that were previously announced, and did a nice job of explaining how to manage different pricing options.</p><h2 id="modeling-progress">Modeling Progress</h2><p>Evangelion Unit 0 is close to complete, the last rack of parts are painting and waiting for assembly. After the kit is assembled, there's a bunch of stickers + weathering to do to make the kit look finished. Plus I need to figure out what to do for a diorama for the kit.</p><p><img alt="Assembled, Front" src="https://atelierclockwork.net/Images/WWDC23/Week_4/1.jpg"><img alt="Assembled, Side" src="https://atelierclockwork.net/Images/WWDC23/Week_4/2.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2023/07/02/wwdc23-week-4</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2023/07/02/wwdc23-week-4</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 02 Jul 2023 20:32:10 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC23 Week 3]]></title>
            <description><![CDATA[<h2 id="week-3-progress">Week 3 Progress</h2><p>Progress report: 97 of 177 videos watched and summarized, or 54.80%. Another slight slowdown since last week, but not a huge one since I averaged 3.85 videos per day this week compared to last week. And I still have a couple sessions that had really interesting tidbits that I hadn't heard anywhere else this week, in particular there's a lot of potential in the support for app level network relays.</p><h3 id="debug-with-structured-logging">Debug with structured logging</h3><p>This session explains some improvements to the debug console in Xcode, in particular <code class="">p</code> now uses a “do what I mean print” command under the hood, to make it easier to print data when debugging. It also shows off some of the new console tools that you get access to when using <code class="">os_log</code> instead of <code class="">print</code> based logging.</p><h3 id="perform-accessibility-audits-for-your-app">Perform accessibility audits for your app</h3><p>This is and overview of the newly added features in to be able to use accessibility audits while running UI tests to expose features there, and some basic troubleshooting issues to fix some common issues.</p><h3 id="bring-your-game-to-mac-part-2-compile-your-shaders">Bring your game to Mac, Part 2: Compile your shaders</h3><p>This session goes into depth in how to convert DirectX shaders to Metal, and also how to make use of features like precompiled shaders. One item of note is that Apple is porting some of these tools to Windows to make it easier to integrate into the game development process</p><h3 id="bring-your-game-to-mac-part-3-render-with-metal">Bring your game to Mac, Part 3: Render with Metal</h3><h3 id="optimize-gpu-renderers-with-metal">Optimize GPU renderers with Metal</h3><p>These sessions went deep in depth on how to optimize a metal rendering pipeline. Which is far enough outside of the realm of what I do that I don’t think there’s anything particularly interesting that I can say about it.</p><h3 id="fix-failures-faster-with-xcode-test-reports">Fix failures faster with Xcode test reports</h3><p>This shows off some of the new features in Xcode test reports. Being able to scrub through a video of a UI test to pinpoint a failure seems really useful, as does Xcode being able to show trends in your tests to help catch slow tests and patterns of failures.</p><h3 id="whats-new-in-app-store-connect">What’s new in App Store Connect</h3><p>This session goes over some of the changes that have happened in App Store Connect in the last year. One thing of note is that you can now create user scoped API tokens, which may make using App Store Connect API tooling a bit easier.</p><h3 id="rediscover-safari-developer-features">Rediscover Safari developer features</h3><h3 id="whats-new-in-web-inspector">What’s new in Web Inspector</h3><p>Most of these sessions apply to web developers, but the new in-app web view debugging may be useful when web content in an app isn’t behaving properly. The standards based automation tooling also may be useful in the future.</p><h3 id="optimize-app-power-and-performance-for-spatial-computing">Optimize app power and performance for spatial computing</h3><h3 id="meet-realitykit-trace">Meet RealityKit Trace</h3><p>These sessions covered performance concerns in RealityKit and how to address them. One mine technique of note is that it was called out at drop shadows are expensive over the translucent backgrounds used by default in visionOS, so a useful thing to keep in mind.</p><h3 id="create-practical-workflows-in-xcode-cloud">Create practical workflows in Xcode Cloud</h3><p>This session was a quick run through of setting up a handful of Xcode cloud workflows. It’s nice to see some of the tooling that’s been put in place to make it easier to migrate into Xcode cloud from other CI environments.</p><h3 id="analyze-hangs-with-instruments">Analyze hangs with Instruments</h3><p>This session shows how to diagnose and handle hangs in Instruments, and walks through several common scenarios. It also does an excellent job of showing how to combine multiple instruments, and pull out extra details to make it easier to fully solve the problem.</p><h3 id="principles-of-spatial-design">Principles of Spatial Design</h3><h3 id="design-for-spatial-input">Design for spatial input</h3><h3 id="design-for-spatial-user-interfaces">Design for spatial user interfaces</h3><h3 id="design-spatial-shareplay-experiences">Design spatial SharePlay experiences</h3><p>These sessions help teach the foundations of designing for visionOS. One of the more interesting topics covered is how to structure content so that the eye is drawn to the center of the element, and also how the system will use adaptive scaling so that UI elements remain the same relative size and the window will scroll up if it’s placed further away from the user.</p><h3 id="whats-new-in-core-data">What’s new in Core Data</h3><p>I can’t help but wonder how many of the new things added to Core Data were added to support Swift Data. The most interesting new feature in this session is composite attributes, which are a way of structuring your data without having to add relationships or deal with transformable objects.</p><h3 id="ready-set-relay-protect-app-traffic-with-network-relays">Ready, set, relay: Protect app traffic with network relays</h3><p>This session shows off a few things with interesting potential. Apps can now specify relays and route traffic through them. Devices will also be able to specify that all traffic to specific domains need to route through a relay, so there’s potential to replace a proxy server with a relay.</p><h3 id="create-seamless-experiences-with-virtualization">Create seamless experiences with Virtualization</h3><p>This shows off some of the new Virtualization features, of particular interest, support is coming for saving the active state of a VM to reload later, and a display type that will change the resolution to match the window size.</p><h3 id="sync-to-icloud-with-cksyncengine">Sync to iCloud with CKSyncEngine</h3><p>This shows off using CKSyncEngine to drive sync with CloudKit. This handles most of the integration required to sync via CloudKit and lets your app focus on handling conflicts without needing to worry about things like network errors, iCloud account changes, and the like.</p><h3 id="explore-immersive-sound-design">Explore immersive sound design</h3><p>This session explains how to work with immersive sound design, and how Apple build the sound of environments that will ship with visionOS. One key detail is using random variation to break up the repetitiveness of sound using randomness and variation.</p><h3 id="optimize-carplay-for-vehicle-systems">Optimize CarPlay for vehicle systems</h3><p>This covers new features added to CarPlay. These include AirPlay2 style audio buffering, and also support for automatically pairing a phone when it’s used as a car key.</p><h3 id="reduce-network-delays-with-l4s">Reduce network delays with L4S</h3><p>This session goes into a lot of detail about how Low Latency Low Loss Scalable Throughput works. From the app level, the implementation details are handled by URLSession, but at the transport and server level, it involves reacting to congested network conditions and changing the strategy for sending out packets.</p><h3 id="meet-push-notifications-console">Meet Push Notifications Console</h3><p>Testing push notifications has slowly improved, first you had to figure it out yourself and only could test on device, then you could send payloads to the simulator via drag and drop. Now there’s a web console that lets you test all sorts of APNS scenarios and track notification delivery status.</p><h3 id="discover-calendar-and-eventkit">Discover Calendar and EventKit</h3><p>This session shows off some of the new EventKit tooling coming to the latest OSs. Of note, you can now write to the calendar via an out of process UI with no user permissions.s if they’ll be used often.</p><h3 id="spotlight-your-app-with-app-shortcuts">Spotlight your app with App Shortcuts</h3><h3 id="design-shortcuts-for-spotlight">Design Shortcuts for Spotlight</h3><p>A pair of sessions describing the design and development considerations for Shortcuts. It's nice to see that device side ML is going to be used to help Siri match voice prompts to Shortcuts, even if there isn't a perfect match, and that there's now a good way of testing Shortcuts without having to install and test on device.</p><h2 id="modeling-progress">Modeling Progress</h2><p>I completed assembly of the kit I've been working on, I'm still contemplating if I want to do any sort of weathering or base for it.</p><p><img alt="Assembly Complete" src="https://atelierclockwork.net/Images/WWDC23/Week_3/1.jpg"></p><p>This is the torso of the next kit that I'm working on, eight pages into the instruction book. It's particularly interesting as there's articulation all along the spine, so the finished kit should be quite flexible.</p><p><img alt="Torso WIP" src="https://atelierclockwork.net/Images/WWDC23/Week_3/2.jpg"></p><p>And this is where I finished the week, with the torso, head, and one leg completed. This is somewhere around half of the steps in the kit, so I'll probably be close to finishing it up by this time next week.</p><p><img alt="One leg, front" src="https://atelierclockwork.net/Images/WWDC23/Week_3/3.jpg"><img alt="One leg, rear" src="https://atelierclockwork.net/Images/WWDC23/Week_3/4.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2023/06/25/wwdc23-week-3</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2023/06/25/wwdc23-week-3</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 25 Jun 2023 21:50:32 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC23 Week 2]]></title>
            <description><![CDATA[<h2 id="week-2-progress">Week 2 Progress</h2><p>Progress report: 70 of 177 videos watched and summarized, or 39.55%. So less progress than last week, but I still averaged 4 videos a day which is respectable progress with about a month left. I'm guessing that I'll keep slowing down as the videos get more and more off topic from my core interests.</p><h3 id="create-rich-documentation-with-swift-docc">Create rich documentation with Swift-DocC</h3><p>This gets into detail on some of the more advances DoCC features. The rendering directives look really nice, and having a live preview is a big improvement for iteration.</p><h3 id="write-swift-macros">Write Swift Macros</h3><p>A great step by step guide for writing macros. When I need to write a Macro for something, I’ll come back to this session for reference. One interesting detail of note is how much Apple is pushing using testing to ensure correct macro behavior.</p><h3 id="beyond-scroll-views">Beyond scroll views</h3><p>This goes over several new SwiftUI features that make scroll view behaviors that were previously very difficult like snapping to content at the end of a scroll easy to do with help from the system. This should be a huge win for implementing custom designs faster and with fewer ugly hacks.</p><h3 id="whats-new-in-sf-symbols-5">What’s new in SF Symbols 5</h3><p>Seeing how animation in SF Symbols works is interesting. It’s a relatively limited set of animations, and there’s no keyframes or other information, instead it’s using the layer information to choose which parts of the symbol to highlight or scale</p><h3 id="meet-storekit-for-swiftui">Meet StoreKit for SwiftUI</h3><p>This continues the pattern of Apple working to make working with subscriptions and in app purchases significantly cleaner. The StoreKit in SwiftUI additions give you a decent base view, and offers lots of places for customization, and also allows you to observe StoreKit state and update your view hierarchy based on state from that.</p><h3 id="wind-your-way-through-advanced-animations-in-swiftui">Wind your way through advanced animations in SwiftUI</h3><p>This explains phase based and keyframe animations in detail. Phase based animations are a series of discrete steps, whereas keyframe based animations let you manually set up the keyframes and curves within an animation. This adds some of the last parts of animation support that was missing from previous release.</p><h3 id="bring-widgets-to-new-places">Bring widgets to new places</h3><p>A light overview of the new places that widgets are supported, and the changes in the widgets API that were made to support those new places, in particular moving backgrounds to a new modifier so they can be removed easily, and changes to how to use the container padding.</p><h3 id="unlock-the-power-of-grammatical-agreement">Unlock the power of grammatical agreement</h3><p>This covers the new grammatical agreement modes to both help make sure that you use the correct versions of words in grammar sensitive languages, and support for intelligently changing text based on preferred pronouns.</p><h3 id="design-and-build-apps-for-watchos-10">Design and Build apps for watchOS 10</h3><p>This session is presented by both a designer and a developer, and explains both design and functional decisions for the UI updates in watchOS 10. It explains how to use the new layouts, and some of the new UI features like the color full bleed backgrounds.</p><h3 id="take-swiftui-to-the-next-dimension">Take SwiftUI to the next Dimension</h3><p>This session goes into detail on how to integrate 3d elements into SwiftUI views in visionOS. Having done a bit of 3d work in the past, I’m very impressed with how much work is going into making it easy to use familiar design patterns, and how those patterns are extended into 3 dimensions.</p><h3 id="update-your-app-for-watchos-10">Update your app for watchOS 10</h3><p>This was a hands on demo of applying the design principles mentioned in other sessions to your app. If you’re looking for some concise code examples for a how to use the new design elements in watchOS this is a good session to refer to.</p><h3 id="evolve-your-arkit-app-for-spatial-experiences">Evolve your ARKit app for spatial experiences</h3><p>This session covers the new and updated capabilities that are available on visionOS. In particular it adds the ability to track world anchors at the system level, and hand tracking.</p><h3 id="design-widgets-for-the-smart-stack-on-apple-watch">Design widgets for the Smart Stack on Apple Watch</h3><h3 id="build-widgets-for-the-smart-stack-on-apple-watch">Build widgets for the Smart Stack on Apple Watch</h3><p>These sessions cover the design decisions to think about when putting together widgets in watchOS 10, and also detailed instructions for how to build a widget on watchOS 10. Definitely worth a look when sitting down to start that task.</p><h3 id="create-animated-symbols">Create animated symbols</h3><h3 id="animate-symbols-in-your-app">Animate symbols in your app</h3><p>These sessions show off how to create and annotate symbols for animation, and then how to use the annotations to trigger animations in your apps. Create animated symbols also shows off using symbol components to add modifiers to your custom symbols and get system like behavior with minimal effort. Animate symbols in your app shows off how the different animation types can be triggered and how they work in an app.</p><h3 id="explore-swiftui-animation">Explore SwiftUI animation</h3><p>This video does a great job explaining how animations work in SwiftUI, in particular it diagrams how data flows through animations and that <code class="">Animatable</code> and <code class="">Animation</code> are in SwiftUI. It also shows off the new scoped animation modifiers, custom animations, and shows how to work with transactions.</p><h3 id="build-better-document-based-apps">Build better document-based apps</h3><p>A good overview on new features for document based app. The code example was UIKit focused, showed off how much code you could remove from. Document based app and still integrate well with the system.</p><h3 id="create-a-more-responsive-camera-experience">Create a more responsive camera experience</h3><p>This covers how to use the new APIs for zero shutter lag, and deferred photo processing and explains how those settings work with the photos processing pipeline. It also explains how to work with reaction effects in video streams.</p><h3 id="embed-the-photos-picker-in-your-app">Embed the Photos Picker in your app</h3><p>The headline of this video is that you can now embed an out of process photos picker in your app, there are a lot of other improvements set up to allow you set up your own interactions with that. It also shows the correct flags to use if you want to export HDR content instead of down</p><h3 id="whats-new-in-text-and-text-interactions">What’s new in text and text interactions</h3><p>This shows off the changes to text interaction in the new OS versions, and how to take advantage of those new interactions. It also shows off new text features, and the one that caught my eye is support for lists in attributed strings.</p><h3 id="demystify-swiftui-performance">Demystify SwiftUI performance</h3><p>This session has lots of useful in-depth explanations of the view update cycle in SwiftUI, and explains some targeted optimizations that can be made to improve the rendering speed of SwiftUI views.</p><h3 id="explore-pie-charts-and-interactivity-in-swift-charts">Explore pie charts and interactivity in Swift Charts</h3><p>This shows off the new chart types added to Swift Charts, and also how to add new interactive behaviors. Having the ability to easily select and get more data on an element, and define scroll snapping makes Swift Charts now handle a lot of the use cases that previously required pulling in other libraries or doing lots of challenging work.</p><h3 id="go-beyond-the-window-with-swiftui-and">Go beyond the window with SwiftUI and</h3><h3 id="elevate-your-windowed-app-for-spatial-computing">Elevate your windowed app for spatial computing</h3><p>Another pair of sessions showing how to use the different visonOS scene types in SwiftUI, and how to integrate existing views into visionOS. One interesting item shown off in this session was how to creates meshes that can be displayed tracked to the users hands while in a fully immersive scene.</p><h3 id="the-swiftui-cookbook-for-focus">The SwiftUI cookbook for focus</h3><p>This session walks through how the focus system works in SwiftUI, and explains how to use it in several common scenarios. Of particular interest, it explains how to automatically set keyboard focus for text views properly and also how to add focus behaviors to custom controls.</p><h3 id="animate-with-springs">Animate with springs</h3><p>An excellent overview of <em>how</em> and <em>why</em> to use spring based animations, and an explainer of the math behind the spring animations. It's also nice to see that since it's non-trivially complex to work with springs, Apple has been adding more affordances to the API to make them easier to reason about.</p><h2 id="modeling-progress">Modeling Progress</h2><p>I finished the core of the kit that I've been working on, and have all of the accessories in various states of completed-ness. So primary assembly of that model is <em>almost</em> done, and then I'll have to decide what I'd like to do for extra detail work.</p><p><img alt="Core Kit" src="https://atelierclockwork.net/Images/WWDC23/Week_2/1.jpg"></p><p>I also prepared the next kit that I'll be working on when I finished cutting out the last pieces from the sprues of the current kit. This kit is the second tier of complexity of kit, so there's more pieces and more colors of plastic. It's also an Evangelion instead of a Gundam, so it's a very different design of robot to work with.</p><p><img alt="Laid out" src="https://atelierclockwork.net/Images/WWDC23/Week_2/2.jpg"><img alt="Sorted" src="https://atelierclockwork.net/Images/WWDC23/Week_2/3.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2023/06/18/wwdc23-week-2</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2023/06/18/wwdc23-week-2</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 18 Jun 2023 21:28:12 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC23 Week 1]]></title>
            <description><![CDATA[<h2 id="week-1-progress">Week 1 Progress</h2><p>Progress report: 42 of 177 videos watched and summarized, 23.74% of videos viewed. Not a bad dent for the first week. I took in a lot of really fascinating sessions, and am really excited about the new tooling, and how much of it has been focused on developer ergonomics.</p><p>I'm going back to a normal schedule next week, which means watching fewer videos per day, so I'll probably be posting weekly from now on.</p><h3 id="model-your-schema-with-swiftdata">Model your schema with SwiftData</h3><p>Nice documentation of how annotation work, the most surprising thing in there is that basic relationships between model types don't even require annotations. Also of note, having migrations between schemas driven by the type system looks like it will be quite nice.</p><h3 id="dive-deeper-into-swiftdata">Dive deeper into SwiftData</h3><p>This explains more of the low level features in SwiftData. This explains how ModelContext and ModelContainer work together, how to build queries with fetch and sort descriptors, and how to work with SwiftData in non-view contexts. Having type safe queries and sorts looks great. It also looks like SwiftData does a to make things work well by default, but offers customization under the hood as needed.</p><h3 id="beyond-the-basics-of-structured-concurrency">Beyond the basics of structured concurrency</h3><p>This has a great overview of how to manage groups of tasks, and how to pass data through the task hierarchy. It also feels like the section about distributed tracing is potential very interesting groundwork for tooling to build distributed systems using Swift concurrency.</p><h3 id="mix-swift-and-c">Mix Swift and C++</h3><p>I don't think I'll end up using this much directly, but I'm guessing that over time I'll start using it indirectly as cross platform C++ libraries will be much easier to use directly in Swift without needing a middle bridging layer.</p><h3 id="generalize-apis-with-parameter-packs">Generalize APIs with parameter packs</h3><p>Seeing parameter packs ship is great as it removes the need to define multiple variants of the same function to take and emit a number of mixed parameters. At the point of use, this should be completely transparent and fix the annoying SwiftUI error when you have too many subviews in a result builder, and it looks like it should be relatively straight forward to implement, though I'm guessing that I'll be referring back to this video when I need to do this myself.</p><h3 id="meet-mergable-libraries">Meet mergable libraries</h3><p>It looks like an interesting way to get the benefits of both static and dynamic linking for frameworks that will be included in the bundle, and especially should make it easier to work with apps that are broken down into many sub-modules.</p><h3 id="design-with-swiftui">Design with SwiftUI</h3><p>This talk is aimed at designers, encouraging them to use SwiftUI as a prototyping tool. It's an interesting idea, but is a harder sell somewhere that needs to support both iOS and Android.</p><h3 id="migrate-to-swiftdata">Migrate to SwiftData</h3><p>I don't currently maintain any apps using CoreData, but the session was still useful to watch because it helps explain the mechanisms that Swift Data uses to share the same underlying data structure as Core Data.</p><h3 id="discover-observation-in-swiftui">Discover Observation in SwiftUI</h3><p>This session did a great job of diagramming when to use the new observation macros, and how they work under the hood. It also covered caveats of how to let the macros know about how to handle observation of computed values that aren't derived by state on the object itself, which is useful but an edge case.</p><h3 id="expand-on-swift-macros">Expand on Swift Macros</h3><p>This was a really nice, dense session that explains how the macro system works in Swift, and what it can and can't do. It was nice to see how much work was put into making macros safe and predictable. One of the more interesting little details is that it takes what looks like strings in the macro definition and converts them into useful input for the code generation system.</p><h3 id="enhance-your-ipad-and-iphone-apps-for-the-shared-space">Enhance your iPad and iPhone apps for the Shared Space</h3><p>A nice overview of what it will take to make iPad and iPhone apps fit in nicely with visionOS, especially how to make sure controls will work with the eye tracking system since the hover effect is abstracted away from your app</p>]]></description>
            <link>https://atelierclockwork.net/posts/2023/06/11/wwdc23-week-1</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2023/06/11/wwdc23-week-1</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 11 Jun 2023 21:57:23 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC23 Day Five]]></title>
            <description><![CDATA[<h2 id="quick-summary">Quick Summary</h2><p>Progress report: 31 of 177 videos watched and summarized, 17.51% of videos viewed.</p><p>I'm finished the last video in “Essentials” category, so starting tomorrow I get to start working on the “Swift” category.</p><h3 id="meet-activitykit">Meet ActivityKit</h3><p>This was a nice overview of what's in ActivityKit and how to use it. I didn't catch anything particularly new in this video, but I think it's the first comprehensive video on ActivityKit since it was a beta feature during last years WWDC and release cycle.</p><h3 id="unleash-the-uikit-trait-system">Unleash the UIKit trait system</h3><p>This had the best description of how traits flow through the view hierarchy, and they change to make it easier to reason about makes lot of sense. I find it interesting how much they mentioned performance overhead for adding custom traits here. Finally it looks really nice that you can bridge traits with the SwiftUI environment to share custom configuration between frameworks.</p><h3 id="build-programmatic-ui-with-xcode-previews">Build programmatic UI with Xcode Previews</h3><p>The new <code class="">#Preview</code> Macro makes it a lot nicer to add previews compared to the previous syntax, and it's also exciting that previews now explicitly support UIKit and AppKit instead of having to hack around by wrapping in SwiftUI. Pinned previews and on-device previews both look really useful additions.</p><h3 id="bring-widgets-to-life">Bring widgets to life</h3><p>Nice overview on how to work with animated and interactive widgets. Having the ability to put together preview timelines for widgets to check out state changes is great. Seeing the use of intents to drive interactions in widgets out of process makes a lot of sense, and it's interesting seeing how different parts of the system compliment each other.</p><h3 id="verify-app-dependencies-with-digital-signatures">Verify app dependencies with digital signatures</h3><p>Nice improvements for ensuring dependencies haven't been tampered with automatically. It's impressive to see continued and improving support of 3rd party dependencies having started work back when it felt like Xcode was downright hostile to using them and you were pretty much forced to use complex systems like CocoaPods.</p><h3 id="simplify-distribution-in-xcode-and-xcode-cloud">Simplify distribution in Xcode and Xcode Cloud</h3><p>There's some nice improvements on the local release side, especially things like push notifications when a build finishes processing. Same for the improvements in Xcode Cloud, but since I mostly work with self hosted CI for deployment, I don't get to use any of the new stuff in this session.</p><h3 id="get-started-with-privacy-manifests">Get started with privacy manifests</h3><p>This was a surprisingly packed session. Privacy manifests themselves aren't big because they're just used to help you fill out the privacy nutrition label for you app, but there were some other big changes that are then backed by the date you get from those. In particular the OS will now block access to domains tied to tracking until the user has opted in to tracking, and there are going to be frameworks listed as “privacy impacting” that will have requirements of signatures, and declare tracking domains in the manifests. APIs that can be used for fingerprinting devices will also now required to choose from a list of valid reasons why they need to use those APIs.</p><h3 id="inspectors-in-swiftui-discover-the-details">Inspectors in SwiftUI: Discover the details</h3><p>Inspectors look like a nice way to show more detail about elements in an app, This session also had a good reminder of some of the presentation customization options that were added in iOS 16.4 to let you specify things like which detents to dim the view that is being presented over.</p><h3 id="whats-new-in-appkit">What's new in AppKit</h3><p>An a (primarily) iOS developer, I mostly watch these sessions to get an idea of convergence between the OSs. On that note, the fact that AppKit is going to do the same thing as UIKit and not clip views by default is interesting.</p><h3 id="prototype-with-xcode-playgrounds">Prototype with Xcode Playgrounds</h3><p>Having the ability to make sure that Xcode always builds the active scheme so that you can use playgrounds for active prototyping is nice, and looks like there's a lot of affordances to do  things like test out UI. It looks like it will be worth adding a playgrounds scratchpad to most projects to be able to use this.</p><h3 id="meet-swift-openapi-generator">Meet Swift OpenAPI Generator</h3><p>This is something I've been chasing since my first full time iOS job. I'm very interested in seeing how well this works in practice, and how much work it is to make a custom re-usable transport library. Probably the most exciting session of the day in terms of potential to save developer time.</p><h2 id="modeling-progress">Modeling Progress</h2><p>I varnished the rack that had detail paint done yesterday, and cut out, sanded down, and primed the parts for the next step of the kit.</p><p><img alt="Primed and varnished" src="https://atelierclockwork.net/Images/WWDC23/Day_5/1.jpg"></p><p>The varnished pieces then got some basic panel lining in place, and then I assembled the completed section of the kit. With the completed torso the upper body of the kit is done, along with most of the parts for the feet, which then will be assembled into the legs in the next two steps.</p><p><img alt="Kit progress" src="https://atelierclockwork.net/Images/WWDC23/Day_5/2.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2023/06/09/wwdc23-day-five</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2023/06/09/wwdc23-day-five</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Fri, 09 Jun 2023 21:19:36 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC23 Day Four]]></title>
            <description><![CDATA[<h2 id="quick-summary">Quick Summary</h2><p>Progress report: 21 of 177 videos watched and summarized, 11.86% of videos viewed.</p><p>I'm closing in on finishing everything in the “Essentials” category, so in a day or two I'll start focusing on whatever areas of tech catch my eye. And probably rotating between sessions I'm very interested in, and sessions that will be great to follow along with while painting or doing other hobby related things.</p><h3 id="build-an-app-with-swiftdata">Build an app with SwiftData</h3><p>This was a detailed walkthrough of setting up a very basic SwiftData App. There wasn't much new to learn compared to the Meet SwiftData session, but this will probably be useful for future reference. The one big takeaway was how easy it is to set up a document based app that's backed by SwiftData.</p><h3 id="make-features-discoverable-with-tipkit">Make features discoverable with TipKit</h3><p>How to add onboarding hints is something that I've had to implement in lots of apps. Having a rules based system for choosing when to display tips is great, and looks like there's a lot of customizability and granularity to only show tips at the right time. Plus it has an option to sync tip state via iCloud so users don't get duplicate onboarding across multiple devices.</p><h3 id="meet-mapkit-for-swiftui">Meet MapKit for SwiftUI</h3><p>I used to find working with MapKit in UIKit very cumbersome, so all of the SwiftUI updates for MapKit look really nice to me. In particular it looks like there's a lot of affordances to make positioning the map “correctly” easier, and it should also make managing annotations simpler. If I need to do anything with maps in an app, this is definitely the tool that I'm going to want to use.</p><h3 id="build-spatial-experiences-with-realitykit">Build spatial experiences with RealityKit</h3><p>Really impressive demo of how to add RealityKit backed content a spatial computing app. Good info on how to bridge with SwiftUI gestures, and how to non-destructively work with USDZ files via nesting. When I have a chance to play with a spatial computing app I'll be giving this another watch to pick up more details.</p><h3 id="bring-your-game-to-mac-part-1-make-a-game-plan">Bring your game to Mac, Part 1: Make a game plan</h3><p>I'm not a game developer, so this was mostly interesting to me in terms of how much attention Apple is paying to gaming. Having a easy to use translation layer is really interesting, and it may mean games get ported over to Apple platforms faster.</p><h3 id="meet-reality-composer-pro">Meet Reality Composer Pro</h3><p>Reality composer pro looks like a fascinating tool for building 3d scenes, but without the complexity of a game dev environment or 3d modeling and animation set up. I have some fun ideas for things to do involving assembling virtual dioramas and integrating models created using object capture.</p><h3 id="meet-safari-for-spatial-computing">Meet Safari for spatial computing</h3><p>Another not directly applicable to me talk, but seeing the push for a support to USDZ models directly in browser is interesting, and has potential applications in regular desktop browsers. I also look forward to seeing the fun ways that sites break when windows are resized to be larger than the virtual screen.</p><h3 id="meet-watchos-10">Meet watchOS 10</h3><p>It looks like there's a lot of thought that went into how the updates to watchOS are structured. Vertical pagination looks like it will make breaking UI into usable chunks on the watch easier.</p><h3 id="discover-string-catalogs">Discover String Catalogs</h3><p>This adds a much nicer UI to working with strings, especially pluralized strings. I'm very interested in seeing how this integrates with 3rd party localization tools. Also of interest, it can detect potentially unused strings and make it easier to clean up stale translations.</p><h3 id="run-your-ipad-and-iphone-apps-in-the-shared-space">Run your iPad and iPhone apps in the Shared Space</h3><p>Another talk on integrating with visionOS. This a nail in the coffin on Storyboards as they're not supported natively in visionOS, only in iPad compatibility mode which is limited compared to a full port.</p><h3 id="develop-your-first-immersive-app">Develop your first immersive app</h3><p>A super high level demo of building something for visonOS from scratch. One of the most interesting details is that the export format from Reality Composer into Xcode is a Swift package. Aside from that, it's something that will be good to refer back to when the visionOS SDK is available and I can try things out.</p><h2 id="modeling-progress">Modeling progress</h2><p>Here's the results of the first round of painting for the day. I base coated on rack of parts and did detail highlights on the other.</p><p><img alt="Base and Detail" src="https://atelierclockwork.net/Images/WWDC23/Day_4/1.jpg"></p><p>Then the second round of painting, which adds detail highlights to the rack that was base coated last time, and varnish to the rack that had detail coats in place.</p><p><img alt="Detail and Varnished" src="https://atelierclockwork.net/Images/WWDC23/Day_4/2.jpg"></p><p>Finally, here's the parts that were varnished have had panel liner added, were assembled, and are now sitting with the rest of the kit that's been put together.</p><p><img alt="After assembly" src="https://atelierclockwork.net/Images/WWDC23/Day_4/3.jpg"></p>]]></description>
            <link>https://atelierclockwork.net/posts/2023/06/08/wwdc23-day-four</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2023/06/08/wwdc23-day-four</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Thu, 08 Jun 2023 21:03:22 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC23 Day Three]]></title>
            <description><![CDATA[<h2 id="keeping-up-momentum">Keeping up momentum</h2><p>I had another good day in terms of taking in content, all of the “What’s new” sessions had a lot of things I'm looking forward to digging in to in more detail, and I'm impressed with how complete most of the new things that are coming out this year feel. I'm really enjoying having taken the week off to focus on diving in to the new content, and am having a ton of fun playing with my viewing tracking app.</p><h3 id="whats-new-in-xcode-15">What's new in Xcode 15</h3><p>Macros look even more exciting, and the ability to expand them inline for debugging is amazing. The type safe asset catalog stuff will be fun to play with, it'd be nice to drop yet another dependency. String catalogs look great, but it'll be interesting to see how long it takes 3rd party tooling to integrate with the new format. I'm interested to see how it feels using Xcode bookmarks in practice. Overall it looks more promising. TestFlight internal only builds are a welcome surprise that fixes some distribution workflow problems.</p><h3 id="whats-new-in-swiftui">What's new in SwiftUI</h3><p>It's nice to see SwiftUI getting more views, in particular the MapView looks really promising. The simplified data flow parts of SwiftUI should solve a lot of problems, particularly the magic it's pulling off where <code class="">@Observable</code> will only invalidate a view for changes that it observes. More Swift Data stuff, nothing new from other sessions but still exciting to see it. The new Table stuff looks nice, but also like almost all of the improvements are for macOS and iPadOS, which I rarely have a chance to work on. Keyframes look nice for when you need a super detailed animation, but having the option to cleanly chain together a few animations will go a long way for less effort.</p><p>I've implemented some of what <code class="">.visualEffect</code> and <code class="">scrollTransition</code> does by hand, so it's nice to have a cleaner and easier way to add visual flare to views in the future. Adding styles to texts in string interpolations looks great, as does animated SF Symbols. There's so many exciting scroll view things mentioned here, but there's an entire session on <code class="">ScrollView</code> improvements.</p><h3 id="whats-new-in-swift">What's new in Swift</h3><p>Treating if and case statements as expressions should help with code readability, and reduce the number of ternary operators used in code, which is really nice. Type parameter packs allowing any number of arguments for result builders should be great in practice. Macros continue to look really well implemented and I'm looking forward to getting even more details in those session videos</p><p>It will be interesting to see how ownership and C++ interoperability shows up in iOS development, especially as it looks like the C++ bridging will allow for more direct calling of C++ apis, so it could open up use of C++ frameworks from Swift code.</p><h3 id="whats-new-in-uikit">What's new in UIKit</h3><p>UIKit previews without having to wrap in SwiftUI will make building UIKit views easier. <code class="">viewIsAppearing</code> fills a missing hole in UIKit lifecycle, and it's great that it's back deployed to iOS 13. The trait system enhancements sound interesting, but there wasn't enough info in this video to say anything useful. The content unavailable configuration for views is something that consistently needs to be added to views, so it's great seeing framework level support for it. There are also lots of new tools to make internationalization easier to handle, especially the dynamic line heights. The ability to share layout info across siblings in a collection view compositional layout is really nice. The grab bag at the end had lots of interesting stuff, the one that particularly catches my eye is the new page control progress APIs.</p><h3 id="build-custom-workouts-with-workoutkit">Build custom workouts with WorkoutKit</h3><p>Custom workouts look interesting, but are also really of the range of work I'm likely to be doing on iOS, so this one is filed as for future reference in case something that needs it comes up rather than having any detailed opinions on it.</p><h3 id="meet-swiftui-for-spatial-computing-meet-uikit-for-spatial-computing">Meet SwiftUI for Spatial Computing &#x26; Meet UIKit for Spatial Computing</h3><p>It's really interesting to see how Apple is building the elements for the new OS, and how it adapts components from previous platforms. Lots of emphasis on using Materials instead of colors for backgrounds, and having content automatically adapt to the environment because of that. Having the ability to put RealityKit data directly into SwiftUI views also is quite nice. I also will be interested in seeing how the new ornament elements are applied to other operating systems over time, especially macOS.</p><h2 id="wwd-see-progress">WWD See progress</h2><p>I refactored the data to support many to many relationships between sessions and topics, and made a bunch of UI refinements to make the app easier to use, so I can now search for talks and quickly mark things as ignored or seen.</p><h2 id="viewing-progress">Viewing progress</h2><p>10 of 177 videos, 5.65%, 4 videos ignored.</p><h2 id="modeling-progress">Modeling progress</h2><p>I watched a few non-critical sessions today, so I got some painting in too. Nothing super exciting to look at, but I got a base coat down on on rack of parts, and primed a second rack of parts:</p><p>Here's the parts that needed a base coat:
<img alt="Before painting" src="https://atelierclockwork.net/Images/WWDC23/Day_3/before.jpg"></p><p>And here's the parts with a base coat, and the freshly primed parts:
<img alt="After painting" src="https://atelierclockwork.net/Images/WWDC23/Day_3/after.jpg"></p><p>The painting tomorrow should be more exciting as I'll be putting highlights and varnish on the first rack of pieces, which makes the parts "pop" a lot more, and gets me close to assembly.</p>]]></description>
            <link>https://atelierclockwork.net/posts/2023/06/07/wwdc23-day-three</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2023/06/07/wwdc23-day-three</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Wed, 07 Jun 2023 21:32:35 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC23 Days One & Two]]></title>
            <description><![CDATA[<h3 id="keynote">Keynote</h3><p>It's nice to see the Mac Studio getting a yearly upgrade, and the new 15" MacBook air looks like a really nice computer for the vast majority of users. On the other side, the Mac Pro is now targeted at an even smaller audience than ever. Lots of user facing features that I'm looking forward to on all of the OSs. The most interesting part of the keynote was Apple Vision Pro. I don't know what the killer app, if anything, will be for the platform, but the hardware sounds impressive and I'm looking forward to getting a chance to try it out <em>eventually</em>.</p><h3 id="platforms-state-of-the-union">Platforms State of the Union</h3><p>This was probably the most exciting Platforms State of the Union for me since the introduction of Swift in 2014. Tons of exciting new features, I've been hoping for a Swift first replacement for Core Data since Swift became my "daily driver" language. Plus things like sherlocking SwiftGen for asset catalogs, Swift Macros, keyframes in SwiftUI, interactive Widgets. Plus the VisionOS stuff. 🤯</p><h3 id="minor-detour">Minor Detour</h3><p>The session videos didn't come out until this afternoon, so I spent a bit of time playing with a <a href="https://github.com/ateliercw/wwd-see" rel="nofollow">fun project</a> to kick the tires on the new tools. As I learn things in the session videos, I'll be making more improvements and iterating on it.</p><h3 id="meet-swiftdata">Meet SwiftData</h3><p>First impressions of SwiftData are that it's amazing. Before the session videos dropped I already got a <em>very</em> basic app using SwiftData off the ground. This session only covered surface details, but I'm now even more excited for the sessions with more details when they come out.</p><h2 id="current-progress-estimate">Current Progress estimate:</h2><p>3 of 177 videos, 1.69%.</p>]]></description>
            <link>https://atelierclockwork.net/posts/2023/06/06/wwdc23-days-one-and-two</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2023/06/06/wwdc23-days-one-and-two</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Tue, 06 Jun 2023 20:07:23 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC Viewing Plans]]></title>
            <description><![CDATA[<h2 id="time-for-another-project">Time for another project</h2><p>For anyone not in the the Apple developer community, its WWDC week, which means apple is going to announce new technologies and tooling. They also are going to release about 200 videos to help developers learn about all the new things over the course of the week.</p><p>Over the next few months, I plan to watch all of these videos. There are core videos that will get my full attention, but for the vast majority I build and paint model robots with the video playing and make note of anything of interest to pay attention to later.</p><p>This year, I also plan of tracking my progress and posting very short summaries of the talks here, along with work in progress shots of the model robots. Hopefully this gets me back in the habit of writing again, as it gives me a something to post about weekly for a few months.</p>]]></description>
            <link>https://atelierclockwork.net/posts/2023/06/05/wwdc-viewing-plans</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2023/06/05/wwdc-viewing-plans</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Mon, 05 Jun 2023 08:34:12 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Building Minesweeper 5]]></title>
            <description><![CDATA[<h2 id="wrapping-up-for-now">Wrapping up (for now)</h2><p>With all of the basic UI in place, the last thing to get in place is figuring out how to get the right click gesture working. I came up with a [proof of concept](<binding value="< ref &#x22;/content/posts/2022/2022-07-10-minesweeper-2.5.md&#x22; >"></binding>) so now it's just a matter of putting the pieces together.</p><p>The first step was to split the cell based on the OS involved, and I had to change the structure to get the behavior to work correctly. In particular, I needed to set an explicit frame, and set the foreground color before disabling the view to make sure that un-clickable cells kept the correct colors.</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="struct Cell: View {
    let value: Game.SpaceState
    let click: () -> Void
    let flag: () -> Void

    @ViewBuilder var body: some View {
        #if os(macOS)
            AppKitCell(state: value,
                       click: click,
                       flag: flag)
            .frame(width: 30, height: 30)
            .foregroundColor(value.foregroundColor)
            .disabled(isDisabled)
        #else
            if isDisabled {
                buttonContent
            } else {
                Button(action: click) {
                    buttonContent
                }
                .foregroundColor(value.foregroundColor)
                .highPriorityGesture(flagGesture)
                .buttonStyle(.plain)
            }
        #endif
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Cell</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">View </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="2"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> value: Game.SpaceState
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> click: () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void
</span></span><span class="line" line="4"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> flag: () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void
</span></span><span class="line" line="5"><span emptyLinePlaceholder>
</span></span><span class="line" line="6"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @ViewBuilder</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> body: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="7"><span style="--shiki-default:#B58900;--shiki-dark:#B58900">        #</span><span style="--shiki-default:#859900;--shiki-dark:#859900">if</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> os</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900">(macOS)
</span></span><span class="line" line="8"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            AppKitCell</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">state</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: value,
</span></span><span class="line" line="9"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                       click</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: click,
</span></span><span class="line" line="10"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                       flag</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: flag)
</span></span><span class="line" line="11"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">frame</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">30</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">30</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="12"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">foregroundColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(value.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">foregroundColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="13"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">disabled</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(isDisabled)
</span></span><span class="line" line="14"><span style="--shiki-default:#B58900;--shiki-dark:#B58900">        #</span><span style="--shiki-default:#859900;--shiki-dark:#859900">else
</span></span><span class="line" line="15"><span style="--shiki-default:#859900;--shiki-dark:#859900">            if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isDisabled {
</span></span><span class="line" line="16"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                buttonContent
</span></span><span class="line" line="17"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            } </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="18"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                Button</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">action</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: click) {
</span></span><span class="line" line="19"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                    buttonContent
</span></span><span class="line" line="20"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                }
</span></span><span class="line" line="21"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">foregroundColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(value.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">foregroundColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="22"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">highPriorityGesture</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(flagGesture)
</span></span><span class="line" line="23"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">buttonStyle</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">plain</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="24"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="25"><span style="--shiki-default:#B58900;--shiki-dark:#B58900">        #</span><span style="--shiki-default:#859900;--shiki-dark:#859900">endif
</span></span><span class="line" line="26"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="27"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>I then got to re-use the updating gesture recognizer from my proof of concept, but there was a lot of book keeping involved. Also of note, this is created as a NSHostingController that wraps a SwiftUI View, so all of the UI is still driven by SwiftUI, but with a thin enough shim of AppKit exposed that we can add some gesture recognizers.</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="final class ButtonHostingViewController: NSHostingController<ButtonHostingViewController.Body> {
    class ButtonViewModel: ObservableObject {
        @Published var state: Game.SpaceState = .unrevealed
        @Published var isPrimaryDown = false
        @Published var isSecondaryDown = false
        @Published var primaryAction: (() -> Void)? = nil
        @Published var secondaryAction: (() -> Void)? = nil

        var isButtonDown: Bool {
            isPrimaryDown || isSecondaryDown
        }
    }

    struct Body: View {
        @ObservedObject var viewModel: ButtonViewModel

        var body: some View {
            viewModel.state.content
                .frame(width: 30, height: 30)
                .background(viewModel.state.fillColor)
                .opacity(viewModel.isButtonDown ? 0.3 : 1)
        }
    }

    private let viewModel = ButtonViewModel()

    private lazy var primaryGestureRecognizer: NSClickGestureRecognizer = {
        let primaryGestureRecognizer = UpdatingGestureRecognizer(target: self, action: #selector(primaryEvent))
        primaryGestureRecognizer.onMouseUp = { [viewModel] in
            viewModel.isPrimaryDown = false
        }
        primaryGestureRecognizer.onMouseDown = { [viewModel] in
            viewModel.isPrimaryDown = true
        }
        return primaryGestureRecognizer
    }()

    private lazy var secondaryGestureRecognizer: NSClickGestureRecognizer = {
        let secondaryGestureRecognizer = UpdatingGestureRecognizer(target: self, action: #selector(secondaryEvent))
        secondaryGestureRecognizer.onMouseUp = { [viewModel] in
            viewModel.isSecondaryDown = false
        }
        secondaryGestureRecognizer.onMouseDown = { [viewModel] in
            viewModel.isSecondaryDown = true
        }
        secondaryGestureRecognizer.buttonMask = 0x2
        return secondaryGestureRecognizer
    }()

    init(state: Game.SpaceState,
         primaryAction: @escaping () -> Void,
         secondaryAction: @escaping () -> Void) {
        super.init(rootView: Body(viewModel: viewModel))
        view.addGestureRecognizer(primaryGestureRecognizer)
        view.addGestureRecognizer(secondaryGestureRecognizer)
        update(state: state,
               primaryAction: primaryAction,
               secondaryAction: secondaryAction)
    }

    @MainActor required dynamic init?(coder: NSCoder) {
        fatalError(&#x22;init(coder:) has not been implemented&#x22;)
    }

    func update(state: Game.SpaceState,
                primaryAction: @escaping () -> Void,
                secondaryAction: @escaping () -> Void) {
        viewModel.state = state
        viewModel.primaryAction = primaryAction
        viewModel.secondaryAction = secondaryAction
    }

    @objc private func primaryEvent() -> Void {
        viewModel.primaryAction?()
    }

    @objc private func secondaryEvent() -> Void {
        viewModel.secondaryAction?()
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">final</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> class</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> ButtonHostingViewController</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">NSHostingController&#x3C;ButtonHostingViewController.Body></span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="2"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    class</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> ButtonViewModel</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">ObservableObject </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="3"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        @Published</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> state: Game.SpaceState </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealed
</span></span><span class="line" line="4"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        @Published</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isPrimaryDown </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="5"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        @Published</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isSecondaryDown </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="6"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        @Published</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> primaryAction: (() </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> nil
</span></span><span class="line" line="7"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        @Published</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> secondaryAction: (() </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> nil
</span></span><span class="line" line="8"><span emptyLinePlaceholder>
</span></span><span class="line" line="9"><span style="--shiki-default:#859900;--shiki-dark:#859900">        var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isButtonDown: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Bool</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="10"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            isPrimaryDown </span><span style="--shiki-default:#859900;--shiki-dark:#859900">||</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isSecondaryDown
</span></span><span class="line" line="11"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="12"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="13"><span emptyLinePlaceholder>
</span></span><span class="line" line="14"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Body</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">View </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="15"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        @ObservedObject</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> viewModel: ButtonViewModel
</span></span><span class="line" line="16"><span emptyLinePlaceholder>
</span></span><span class="line" line="17"><span style="--shiki-default:#859900;--shiki-dark:#859900">        var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> body: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="18"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">state</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">content
</span></span><span class="line" line="19"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">frame</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">30</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">30</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="20"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">background</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">state</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">fillColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="21"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">opacity</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isButtonDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> ?</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0.3</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> :</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 1</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="22"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="23"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="24"><span emptyLinePlaceholder>
</span></span><span class="line" line="25"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> viewModel </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> ButtonViewModel</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="26"><span emptyLinePlaceholder>
</span></span><span class="line" line="27"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> lazy</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> primaryGestureRecognizer: NSClickGestureRecognizer </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="28"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> primaryGestureRecognizer </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> UpdatingGestureRecognizer</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">target</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">action</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">#selector</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(primaryEvent))
</span></span><span class="line" line="29"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        primaryGestureRecognizer.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">onMouseUp</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { [viewModel] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="30"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isPrimaryDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="31"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="32"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        primaryGestureRecognizer.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">onMouseDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { [viewModel] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="33"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isPrimaryDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> true
</span></span><span class="line" line="34"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="35"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> primaryGestureRecognizer
</span></span><span class="line" line="36"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }()
</span></span><span class="line" line="37"><span emptyLinePlaceholder>
</span></span><span class="line" line="38"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> lazy</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> secondaryGestureRecognizer: NSClickGestureRecognizer </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="39"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> secondaryGestureRecognizer </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> UpdatingGestureRecognizer</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">target</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">action</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">#selector</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(secondaryEvent))
</span></span><span class="line" line="40"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        secondaryGestureRecognizer.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">onMouseUp</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { [viewModel] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="41"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isSecondaryDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="42"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="43"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        secondaryGestureRecognizer.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">onMouseDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { [viewModel] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="44"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isSecondaryDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> true
</span></span><span class="line" line="45"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="46"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        secondaryGestureRecognizer.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">buttonMask</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0x2
</span></span><span class="line" line="47"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> secondaryGestureRecognizer
</span></span><span class="line" line="48"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }()
</span></span><span class="line" line="49"><span emptyLinePlaceholder>
</span></span><span class="line" line="50"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">state</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: Game.SpaceState,
</span></span><span class="line" line="51"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">         primaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">@escaping</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">,
</span></span><span class="line" line="52"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">         secondaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">@escaping</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="53"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        super</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">rootView</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">Body</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">viewModel</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: viewModel))
</span></span><span class="line" line="54"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        view.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">addGestureRecognizer</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(primaryGestureRecognizer)
</span></span><span class="line" line="55"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        view.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">addGestureRecognizer</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(secondaryGestureRecognizer)
</span></span><span class="line" line="56"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        update</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">state</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: state,
</span></span><span class="line" line="57"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">               primaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: primaryAction,
</span></span><span class="line" line="58"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">               secondaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: secondaryAction)
</span></span><span class="line" line="59"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="60"><span emptyLinePlaceholder>
</span></span><span class="line" line="61"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @MainActor</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> required</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> dynamic</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> init?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">coder</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: NSCoder) {
</span></span><span class="line" line="62"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        fatalError</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"init(coder:) has not been implemented"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="63"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="64"><span emptyLinePlaceholder>
</span></span><span class="line" line="65"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> update</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">state</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: Game.SpaceState,
</span></span><span class="line" line="66"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                primaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">@escaping</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">,
</span></span><span class="line" line="67"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                secondaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">@escaping</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="68"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">state</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> state
</span></span><span class="line" line="69"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">primaryAction</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> primaryAction
</span></span><span class="line" line="70"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">secondaryAction</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> secondaryAction
</span></span><span class="line" line="71"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="72"><span emptyLinePlaceholder>
</span></span><span class="line" line="73"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @objc</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> primaryEvent</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">() </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="74"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">primaryAction</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="75"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="76"><span emptyLinePlaceholder>
</span></span><span class="line" line="77"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @objc</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> secondaryEvent</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">() </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="78"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">secondaryAction</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="79"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="80"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>This handles creating the view, adding the gesture recognizers that update the state as it changes, and updating the SwiftUI view as needed. It feels like a lot of code for a really small behavioral change, but I'm thrilled that I got it working.</p><style>html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}</style>]]></description>
            <link>https://atelierclockwork.net/posts/2022/07/31/building-minesweeper-5</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2022/07/31/building-minesweeper-5</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 31 Jul 2022 20:24:20 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Building Minesweeper 4]]></title>
            <description><![CDATA[<h2 id="showing-the-header">Showing the header</h2><p>Now that there's a working game board, we need a header that shows how many mines are left, how much time has elapsed, and the win / loss state. It's a simple view, but there are a few interesting tricks involved.</p><p>The first view of interest is the timer view:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="private struct TimerView: View {
    @ObservedObject var gameService: GameService
    @State private var elapsedTime: TimeInterval = 0

    var body: some View {
        Text(&#x22;\(elapsedTime, format: .number.precision(.integerAndFractionLength(integer: 3, fraction: 0)))&#x22;)
            .monospacedDigit()
            .onReceive(Timer.publish(every: 0.2, on: .main, in: .default).autoconnect()) { _ in
                elapsedTime = gameService.elapsedTime
            }
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> TimerView</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">View </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="2"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @ObservedObject</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gameService: GameService
</span></span><span class="line" line="3"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @State</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> elapsedTime: TimeInterval </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0
</span></span><span class="line" line="4"><span emptyLinePlaceholder>
</span></span><span class="line" line="5"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> body: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="6"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        Text</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">\(</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">elapsedTime, format</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">number</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">precision</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">integerAndFractionLength</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">integer</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">3</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">fraction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">0</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">))</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">)</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="7"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">monospacedDigit</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="8"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">onReceive</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(Timer.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">publish</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">every</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">0.2</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">on</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">main</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">in</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">default</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">).</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">autoconnect</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()) { _ </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="9"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                elapsedTime </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gameService.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">elapsedTime
</span></span><span class="line" line="10"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="11"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="12"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>The game service can return an elapsed time, but we need to use a <code class="">Timer</code> to force the view to update. The game service could do that internally, but that would invalidate all views that observe the service, so it's less disruptive to the rest of the app to put the timer in this view.</p><p>Next we have the time label, mines label, and action button. One minor detail of note is the modern <code class="">format</code> syntax available in iOS 15.</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="private extension Header {
    private var timeLabel: some View {
        HStack(alignment: .firstTextBaseline, spacing: 0) {
            Text(&#x22;Time:&#x22;)
                .foregroundColor(.secondary)
                .font(.callout)
            TimerView(gameService: gameService)
        }
    }

    private var minesLabel: some View {
        HStack(alignment: .firstTextBaseline, spacing: 0) {
            Text(&#x22;Mines:&#x22;)
                .foregroundColor(.secondary)
                .font(.callout)
            Text(&#x22;\(gameService.remainingMines, format: .number.precision(.integerLength(3)))&#x22;)
                .monospacedDigit()
        }
    }

    private var actionButton: some View {
        Button {
            gameService.resetGame()
        } label: {
            Image(systemName: gameService.state.image)
        }
        .buttonStyle(.borderedProminent)
    }
}

private extension Game.State {
    var image: String {
        switch self {
        case .active:
            return &#x22;face.smiling.fill&#x22;
        case .win:
            return &#x22;flag.checkered&#x22;
        case .lose:
            return &#x22;xmark.seal.fill&#x22;
        }
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Header</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="2"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> timeLabel: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="3"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        HStack</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">alignment</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">firstTextBaseline</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">spacing</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">0</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="4"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            Text</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"Time:"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="5"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">foregroundColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">secondary</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="6"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">font</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">callout</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="7"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            TimerView</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">gameService</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: gameService)
</span></span><span class="line" line="8"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="9"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="10"><span emptyLinePlaceholder>
</span></span><span class="line" line="11"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> minesLabel: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="12"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        HStack</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">alignment</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">firstTextBaseline</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">spacing</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">0</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="13"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            Text</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"Mines:"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="14"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">foregroundColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">secondary</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="15"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">font</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">callout</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="16"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            Text</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">\(</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">gameService.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">remainingMines</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, format</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">number</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">precision</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">integerLength</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">3</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">))</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">)</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="17"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">monospacedDigit</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="18"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="19"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="20"><span emptyLinePlaceholder>
</span></span><span class="line" line="21"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> actionButton: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="22"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        Button</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="23"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            gameService.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">resetGame</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="24"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        } </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">label</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: {
</span></span><span class="line" line="25"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            Image</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">systemName</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: gameService.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">state</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.image)
</span></span><span class="line" line="26"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="27"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">buttonStyle</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">borderedProminent</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="28"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="29"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="30"><span emptyLinePlaceholder>
</span></span><span class="line" line="31"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.State {
</span></span><span class="line" line="32"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> image: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">String</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="33"><span style="--shiki-default:#859900;--shiki-dark:#859900">        switch</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="34"><span style="--shiki-default:#859900;--shiki-dark:#859900">        case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">active</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="35"><span style="--shiki-default:#859900;--shiki-dark:#859900">            return</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198"> "face.smiling.fill"
</span></span><span class="line" line="36"><span style="--shiki-default:#859900;--shiki-dark:#859900">        case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">win</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="37"><span style="--shiki-default:#859900;--shiki-dark:#859900">            return</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198"> "flag.checkered"
</span></span><span class="line" line="38"><span style="--shiki-default:#859900;--shiki-dark:#859900">        case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">lose</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="39"><span style="--shiki-default:#859900;--shiki-dark:#859900">            return</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198"> "xmark.seal.fill"
</span></span><span class="line" line="40"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="41"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="42"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>Finally, the whole thing is wrapped up in a view modifier, and is packaged to use the toolbar on iOS, but to inject the views as a header on macOS because I couldn't quite get the right layout of content in the menu bar.</p><p>Now the game is play-able, so next post is going to be pulling in the ability to right click on the mac, and the project will be some level of done for now.</p><style>html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}</style>]]></description>
            <link>https://atelierclockwork.net/posts/2022/07/24/building-minesweeper-4</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2022/07/24/building-minesweeper-4</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 24 Jul 2022 21:04:45 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Building Minesweeper 3]]></title>
            <description><![CDATA[<h2 id="building-a-board">Building a Board</h2><p>Now that the game logic is in place, I'm ready to build a game board. Since there's a new layout system to work with, I'm going to use a <code class="">Grid</code>. The first thing that I need to do is to make an <code class="">Identifiable</code> type that stores a row worth of grid cells, and expose it in the GameService:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="struct GridPointRow: Hashable, Identifiable {
    var id: Int { row }
    let row: Int
    let points: [GridPoint]

    init(row: Int, width: Int) {
        self.row = row
        points = (0..<width).map { column in
            GridPoint(x: column, y: row)
        }
    }
}

class GameService: ObservableObject {
    var gridRows: [GridPointRow] {
        (0..<height).map { row in
            GridPointRow(row: row, width: width)
        }
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> GridPointRow</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Hashable</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">Identifiable </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="2"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> id: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { row }
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> row: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int
</span></span><span class="line" line="4"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> points: [GridPoint]
</span></span><span class="line" line="5"><span emptyLinePlaceholder>
</span></span><span class="line" line="6"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">row</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="7"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">row</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> row
</span></span><span class="line" line="8"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        points </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> (</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">0</span><span style="--shiki-default:#859900;--shiki-dark:#859900">..&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">width).</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">map</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { column </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="9"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            GridPoint</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">x</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: column, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">y</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: row)
</span></span><span class="line" line="10"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="11"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="12"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="13"><span emptyLinePlaceholder>
</span></span><span class="line" line="14"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">class</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> GameService</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">ObservableObject </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="15"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridRows: [GridPointRow] {
</span></span><span class="line" line="16"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        (</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">0</span><span style="--shiki-default:#859900;--shiki-dark:#859900">..&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">height).</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">map</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { row </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="17"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            GridPointRow</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">row</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: row, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: width)
</span></span><span class="line" line="18"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="19"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="20"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>Since <code class="">GridRow</code> is already <code class="">Identifiable</code>, this puts everything in place to be able to put together the board. Since I've done work to put the game logic together in other places, the board view is very simple:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="struct GameView: View {
    @EnvironmentObject var gameService: GameService

    var body: some View {
        Grid(horizontalSpacing: 1, verticalSpacing: 1) {
            ForEach(gameService.gridRows) { row in
                GridRow {
                    ForEach(row.points) { point in
                        Cell(gridPoint: point, gameService: gameService)
                    }
                }
            }
        }
        .padding(1)
        .background(Theme.gameBackground)
        .disabled(gameService.state.isDisabled)
    }
}

private extension Cell {
    @MainActor
    init(gridPoint: GridPoint, gameService: GameService) {
        let value = gameService[gridPoint]

        self = Cell(value: value) {
            if case .revealed = value {
                gameService.revealSurroundingIfSafe(gridPoint)
            } else {
                gameService.reveal(gridPoint)
            }
        } flag: {
            gameService.toggleFlag(gridPoint)
        }
    }
}

private extension Game.State {
    var isDisabled: Bool {
        switch self {
        case .lose, .win:
            return true
        case .active:
            return false
        }
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> GameView</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">View </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="2"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @EnvironmentObject</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gameService: GameService
</span></span><span class="line" line="3"><span emptyLinePlaceholder>
</span></span><span class="line" line="4"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> body: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="5"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        Grid</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">horizontalSpacing</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">1</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">verticalSpacing</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">1</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="6"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            ForEach</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gameService.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">gridRows</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) { row </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="7"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                GridRow</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="8"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                    ForEach</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(row.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">points</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) { point </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="9"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                        Cell</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">gridPoint</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: point, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">gameService</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: gameService)
</span></span><span class="line" line="10"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                    }
</span></span><span class="line" line="11"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                }
</span></span><span class="line" line="12"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="13"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="14"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">padding</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">1</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="15"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">background</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(Theme.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">gameBackground</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="16"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">disabled</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gameService.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">state</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isDisabled</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="17"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="18"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="19"><span emptyLinePlaceholder>
</span></span><span class="line" line="20"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Cell</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="21"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @MainActor
</span></span><span class="line" line="22"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">gridPoint</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: GridPoint, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">gameService</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: GameService) {
</span></span><span class="line" line="23"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> value </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gameService[gridPoint]
</span></span><span class="line" line="24"><span emptyLinePlaceholder>
</span></span><span class="line" line="25"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> Cell</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">value</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: value) {
</span></span><span class="line" line="26"><span style="--shiki-default:#859900;--shiki-dark:#859900">            if</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .revealed </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> value {
</span></span><span class="line" line="27"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                gameService.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealSurroundingIfSafe</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint)
</span></span><span class="line" line="28"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            } </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="29"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                gameService.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">reveal</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint)
</span></span><span class="line" line="30"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="31"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        } </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flag</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: {
</span></span><span class="line" line="32"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            gameService.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">toggleFlag</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint)
</span></span><span class="line" line="33"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="34"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="35"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="36"><span emptyLinePlaceholder>
</span></span><span class="line" line="37"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.State {
</span></span><span class="line" line="38"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isDisabled: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Bool</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="39"><span style="--shiki-default:#859900;--shiki-dark:#859900">        switch</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="40"><span style="--shiki-default:#859900;--shiki-dark:#859900">        case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">lose</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">win</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="41"><span style="--shiki-default:#859900;--shiki-dark:#859900">            return</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> true
</span></span><span class="line" line="42"><span style="--shiki-default:#859900;--shiki-dark:#859900">        case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">active</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="43"><span style="--shiki-default:#859900;--shiki-dark:#859900">            return</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="44"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="45"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="46"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>This creates a grid with a 1px border and 1px line between all of the cells, so the next thing to do is to create the cell. for the first pass implementation, I created a simple version of the view that works on iOS and macOS, with the intention of integrating the ability to right click later. The body switches when the view is disabled to avoid having the button style change the opacity of the content when disabled. I've also split the creation of the content and colors out of this view so that when we switch based on the OS, there won't be duplicated code:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="struct Cell: View {
   let value: Game.SpaceState
   let click: () -> Void
   let flag: () -> Void

   var body: some View {
       if isDisabled {
           buttonContent
       } else {
           Button(action: click) {
               buttonContent
           }
           .foregroundColor(value.foregroundColor)
           .highPriorityGesture(flagGesture)
           .buttonStyle(.plain)
       }
   }
}

private extension Cell {
   var buttonContent: some View {
       value.content
           .frame(width: 30, height: 30)
           .background { value.fillColor }
           .foregroundColor(value.foregroundColor)
   }

   var isDisabled: Bool {
       switch value {
       case .revealed(let count):
           return count == 0
       case .flaggedMine,
               .incorrectFlag,
               .unrevealedMine,
               .revealedMine:
           return true
       case .unrevealed, .flagged:
           return false
       }
   }

   var flagGesture: some Gesture {
       LongPressGesture()
           .onEnded { _ in
               flag()
           }
   }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Cell</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">View </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="2"><span style="--shiki-default:#859900;--shiki-dark:#859900">   let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> value: Game.SpaceState
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">   let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> click: () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void
</span></span><span class="line" line="4"><span style="--shiki-default:#859900;--shiki-dark:#859900">   let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> flag: () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void
</span></span><span class="line" line="5"><span emptyLinePlaceholder>
</span></span><span class="line" line="6"><span style="--shiki-default:#859900;--shiki-dark:#859900">   var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> body: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="7"><span style="--shiki-default:#859900;--shiki-dark:#859900">       if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isDisabled {
</span></span><span class="line" line="8"><span style="--shiki-default:#839496;--shiki-dark:#657B83">           buttonContent
</span></span><span class="line" line="9"><span style="--shiki-default:#839496;--shiki-dark:#657B83">       } </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="10"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">           Button</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">action</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: click) {
</span></span><span class="line" line="11"><span style="--shiki-default:#839496;--shiki-dark:#657B83">               buttonContent
</span></span><span class="line" line="12"><span style="--shiki-default:#839496;--shiki-dark:#657B83">           }
</span></span><span class="line" line="13"><span style="--shiki-default:#839496;--shiki-dark:#657B83">           .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">foregroundColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(value.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">foregroundColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="14"><span style="--shiki-default:#839496;--shiki-dark:#657B83">           .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">highPriorityGesture</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(flagGesture)
</span></span><span class="line" line="15"><span style="--shiki-default:#839496;--shiki-dark:#657B83">           .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">buttonStyle</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">plain</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="16"><span style="--shiki-default:#839496;--shiki-dark:#657B83">       }
</span></span><span class="line" line="17"><span style="--shiki-default:#839496;--shiki-dark:#657B83">   }
</span></span><span class="line" line="18"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="19"><span emptyLinePlaceholder>
</span></span><span class="line" line="20"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Cell</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="21"><span style="--shiki-default:#859900;--shiki-dark:#859900">   var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> buttonContent: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="22"><span style="--shiki-default:#839496;--shiki-dark:#657B83">       value.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">content
</span></span><span class="line" line="23"><span style="--shiki-default:#839496;--shiki-dark:#657B83">           .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">frame</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">30</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">30</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="24"><span style="--shiki-default:#839496;--shiki-dark:#657B83">           .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">background</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { value.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">fillColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="25"><span style="--shiki-default:#839496;--shiki-dark:#657B83">           .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">foregroundColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(value.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">foregroundColor</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="26"><span style="--shiki-default:#839496;--shiki-dark:#657B83">   }
</span></span><span class="line" line="27"><span emptyLinePlaceholder>
</span></span><span class="line" line="28"><span style="--shiki-default:#859900;--shiki-dark:#859900">   var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isDisabled: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Bool</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="29"><span style="--shiki-default:#859900;--shiki-dark:#859900">       switch</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> value {
</span></span><span class="line" line="30"><span style="--shiki-default:#859900;--shiki-dark:#859900">       case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealed</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#859900;--shiki-dark:#859900">let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> count)</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="31"><span style="--shiki-default:#859900;--shiki-dark:#859900">           return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> count </span><span style="--shiki-default:#859900;--shiki-dark:#859900">==</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0
</span></span><span class="line" line="32"><span style="--shiki-default:#859900;--shiki-dark:#859900">       case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flaggedMine</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">,
</span></span><span class="line" line="33"><span style="--shiki-default:#839496;--shiki-dark:#657B83">               .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">incorrectFlag</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">,
</span></span><span class="line" line="34"><span style="--shiki-default:#839496;--shiki-dark:#657B83">               .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealedMine</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">,
</span></span><span class="line" line="35"><span style="--shiki-default:#839496;--shiki-dark:#657B83">               .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealedMine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="36"><span style="--shiki-default:#859900;--shiki-dark:#859900">           return</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> true
</span></span><span class="line" line="37"><span style="--shiki-default:#859900;--shiki-dark:#859900">       case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealed</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flagged</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="38"><span style="--shiki-default:#859900;--shiki-dark:#859900">           return</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="39"><span style="--shiki-default:#839496;--shiki-dark:#657B83">       }
</span></span><span class="line" line="40"><span style="--shiki-default:#839496;--shiki-dark:#657B83">   }
</span></span><span class="line" line="41"><span emptyLinePlaceholder>
</span></span><span class="line" line="42"><span style="--shiki-default:#859900;--shiki-dark:#859900">   var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> flagGesture: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Gesture {
</span></span><span class="line" line="43"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">       LongPressGesture</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="44"><span style="--shiki-default:#839496;--shiki-dark:#657B83">           .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">onEnded</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { _ </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="45"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">               flag</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="46"><span style="--shiki-default:#839496;--shiki-dark:#657B83">           }
</span></span><span class="line" line="47"><span style="--shiki-default:#839496;--shiki-dark:#657B83">   }
</span></span><span class="line" line="48"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="extension Game.SpaceState {
@ViewBuilder var content: some View {
    switch self {
    case .unrevealed:
        Color.clear
    case .revealedMine:
        Image(systemName: &#x22;xmark.seal.fill&#x22;)
    case .flagged:
        Image(systemName: &#x22;flag&#x22;)
    case .revealed(let count):
        if count > 0 {
            Text(count, format: .number)
                .fontWeight(.bold)
        } else {
            Color.clear
        }
    case .flaggedMine:
        Image(systemName: &#x22;flag.fill&#x22;)
    case .incorrectFlag:
        Image(systemName: &#x22;flag.slash&#x22;)
    case .unrevealedMine:
        Image(systemName: &#x22;seal.fill&#x22;)
    }
}

var fillColor: Color {
    switch self {
    case .flagged, .unrevealed, .unrevealedMine, .incorrectFlag, .flaggedMine:
        return Theme.unrevealedFill
    case .revealed:
        return Theme.revealedFill
    case .revealedMine:
        return Theme.failure
    }
}

var foregroundColor: Color {
    switch self {
    case .revealedMine, .unrevealedMine, .unrevealed, .flagged:
        return Theme.primary
    case .revealed(let count):
        return Theme.foreground(for: count)
    case .incorrectFlag:
        return Theme.failure
    case .flaggedMine:
        return Theme.success
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.SpaceState {
</span></span><span class="line" line="2"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">@ViewBuilder</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> content: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">    switch</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="4"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealed</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="5"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        Color.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">clear
</span></span><span class="line" line="6"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealedMine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="7"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        Image</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">systemName</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"xmark.seal.fill"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="8"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flagged</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="9"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        Image</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">systemName</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"flag"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="10"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealed</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#859900;--shiki-dark:#859900">let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> count)</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="11"><span style="--shiki-default:#859900;--shiki-dark:#859900">        if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> count </span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="12"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            Text</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(count, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">format</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">number</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="13"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">fontWeight</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">bold</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="14"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        } </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="15"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            Color.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">clear
</span></span><span class="line" line="16"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="17"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flaggedMine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="18"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        Image</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">systemName</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"flag.fill"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="19"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">incorrectFlag</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="20"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        Image</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">systemName</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"flag.slash"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="21"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealedMine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="22"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        Image</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">systemName</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"seal.fill"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="23"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="24"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="25"><span emptyLinePlaceholder>
</span></span><span class="line" line="26"><span style="--shiki-default:#859900;--shiki-dark:#859900">var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> fillColor: Color {
</span></span><span class="line" line="27"><span style="--shiki-default:#859900;--shiki-dark:#859900">    switch</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="28"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flagged</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealed</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealedMine</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">incorrectFlag</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flaggedMine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="29"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Theme.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealedFill
</span></span><span class="line" line="30"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealed</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="31"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Theme.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealedFill
</span></span><span class="line" line="32"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealedMine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="33"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Theme.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">failure
</span></span><span class="line" line="34"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="35"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="36"><span emptyLinePlaceholder>
</span></span><span class="line" line="37"><span style="--shiki-default:#859900;--shiki-dark:#859900">var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> foregroundColor: Color {
</span></span><span class="line" line="38"><span style="--shiki-default:#859900;--shiki-dark:#859900">    switch</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="39"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealedMine</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealedMine</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealed</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flagged</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="40"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Theme.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">primary
</span></span><span class="line" line="41"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealed</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#859900;--shiki-dark:#859900">let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> count)</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="42"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Theme.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">foreground</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">for</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: count)
</span></span><span class="line" line="43"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">incorrectFlag</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="44"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Theme.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">failure
</span></span><span class="line" line="45"><span style="--shiki-default:#859900;--shiki-dark:#859900">    case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flaggedMine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="46"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Theme.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">success
</span></span><span class="line" line="47"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="48"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>So with all of this, the base game is working, so next is going to be adding right click, and possibly some other small playability enhancements.</p><style>html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}</style>]]></description>
            <link>https://atelierclockwork.net/posts/2022/07/17/building-minesweeper-3</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2022/07/17/building-minesweeper-3</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 17 Jul 2022 14:59:35 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Building Minesweeper 2.5]]></title>
            <description><![CDATA[<h2 id="adding-the-missing-part">Adding the missing part</h2><p>One of the "missing" features in SwiftUI that's tripped me up since the first version of Minesweeper that I implemented is that due to the cross platform nature of SwiftUI, there's no concept of a "right click", in previous version I've worked around that by supporting an option click recognizer for the touch gesture recognizer, but that isn't ideal for the macOS implementation.</p><p>As far as I can tell, there's still no right click support natively in SwiftUI, so I decided to hack something together myself. I decided that it should support:</p><ul><li>Left Click</li><li>Right Click</li><li>Pass the up / down state into the SwiftUI view rather than having some of the view state handled by AppKit.</li></ul><p>To get that working, requires a handful of layers. The outermost layer is:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="struct SecondaryActionButton: NSViewControllerRepresentable {
    let status: Int
    let primaryAction: () -> Void
    let secondaryAction: () -> Void

    func makeNSViewController(context: Context) -> ButtonHostingViewController {
        ButtonHostingViewController(status: status,
                                    primaryAction: primaryAction,
                                    secondaryAction: secondaryAction)
    }

    func updateNSViewController(_ nsViewController: ButtonHostingViewController, context: Context) {
        nsViewController.update(state: status)
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> SecondaryActionButton</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">NSViewControllerRepresentable </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="2"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> status: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> primaryAction: () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void
</span></span><span class="line" line="4"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> secondaryAction: () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void
</span></span><span class="line" line="5"><span emptyLinePlaceholder>
</span></span><span class="line" line="6"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> makeNSViewController</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">context</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: Context) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> ButtonHostingViewController {
</span></span><span class="line" line="7"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        ButtonHostingViewController</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">status</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: status,
</span></span><span class="line" line="8"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                                    primaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: primaryAction,
</span></span><span class="line" line="9"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                                    secondaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: secondaryAction)
</span></span><span class="line" line="10"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="11"><span emptyLinePlaceholder>
</span></span><span class="line" line="12"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> updateNSViewController</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> nsViewController: ButtonHostingViewController, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">context</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: Context) {
</span></span><span class="line" line="13"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        nsViewController.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">update</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">state</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: status)
</span></span><span class="line" line="14"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="15"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>This is the SwiftUI view that wraps the AppKit controller, and handles passing the actions / state into the AppKit controller. Since this was build out as a test, right now I'm, just using an <code class="">Int</code> and the test code increments / decrements the value.</p><p>The next thing that came up was to get the up + down events from the gesture recoginzer, I needed to create a subclass that let me add events for mouse up / down events, as well an handling cancel events:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="private class UpdatingGestureRecognizer: NSClickGestureRecognizer {
    var onMouseDown: (() -> Void)?
    var onMouseUp: (() -> Void)?

    override func reset() {
        super.reset()
        onMouseUp?()
    }

    override func mouseDown(with event: NSEvent) {
        super.mouseDown(with: event)
        if buttonMask == 0x1 {
            onMouseDown?()
        }
    }

    override func rightMouseDown(with event: NSEvent) {
        super.rightMouseDown(with: event)
        if buttonMask == 0x2 {
            onMouseDown?()
        }
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> class</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> UpdatingGestureRecognizer</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">NSClickGestureRecognizer </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="2"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> onMouseDown: (() </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> onMouseUp: (() </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?
</span></span><span class="line" line="4"><span emptyLinePlaceholder>
</span></span><span class="line" line="5"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    override</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> reset</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">() {
</span></span><span class="line" line="6"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        super</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">reset</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="7"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        onMouseUp</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="8"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="9"><span emptyLinePlaceholder>
</span></span><span class="line" line="10"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    override</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> mouseDown</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">with</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> event: NSEvent) {
</span></span><span class="line" line="11"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        super</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mouseDown</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">with</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: event)
</span></span><span class="line" line="12"><span style="--shiki-default:#859900;--shiki-dark:#859900">        if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> buttonMask </span><span style="--shiki-default:#859900;--shiki-dark:#859900">==</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0x1</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="13"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            onMouseDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="14"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="15"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="16"><span emptyLinePlaceholder>
</span></span><span class="line" line="17"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    override</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> rightMouseDown</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">with</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> event: NSEvent) {
</span></span><span class="line" line="18"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        super</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">rightMouseDown</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">with</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: event)
</span></span><span class="line" line="19"><span style="--shiki-default:#859900;--shiki-dark:#859900">        if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> buttonMask </span><span style="--shiki-default:#859900;--shiki-dark:#859900">==</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0x2</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="20"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            onMouseDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="21"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="22"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="23"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>Finally, I had to create the view controller that wraps the SwiftUI view and adds the gesture recognizers. To pass data from UIKit into SwiftUI, I created an <code class="">ObservableObject</code> that the <code class="">ViewController</code> owns and passes into the SwiftUI view, which keeps the UIKit and SwiftUI states in sync.</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="final class ButtonHostingViewController: NSHostingController<ButtonHostingViewController.Body> {
    class ViewModel: ObservableObject {
        @Published var status: Int = 0
        @Published var isPrimaryDown = false
        @Published var isSecondaryDown = false

        var isButtonDown: Bool {
            isPrimaryDown || isSecondaryDown
        }
    }

    struct Body: View {
        @ObservedObject var viewModel: ViewModel

        var body: some View {
            Text(viewModel.status, format: .number)
                .opacity(viewModel.isButtonDown ? 0.3 : 1)
        }
    }

    private let viewModel = ViewModel()
    private let primaryAction: () -> Void
    private let secondaryAction: () -> Void

    private lazy var primaryGestureRecognizer: NSClickGestureRecognizer = {
        let primaryGestureRecognizer = UpdatingGestureRecognizer(target: self, action: #selector(primaryEvent))
        primaryGestureRecognizer.onMouseUp = { [viewModel] in
            viewModel.isPrimaryDown = false
        }
        primaryGestureRecognizer.onMouseDown = { [viewModel] in
            viewModel.isPrimaryDown = true
        }
        return primaryGestureRecognizer
    }()

    private lazy var secondaryGestureRecognizer: NSClickGestureRecognizer = {
        let secondaryGestureRecognizer = UpdatingGestureRecognizer(target: self, action: #selector(secondaryEvent))
        secondaryGestureRecognizer.onMouseUp = { [viewModel] in
            viewModel.isSecondaryDown = false
        }
        secondaryGestureRecognizer.onMouseDown = { [viewModel] in
            viewModel.isSecondaryDown = true
        }
        secondaryGestureRecognizer.buttonMask = 0x2
        return secondaryGestureRecognizer
    }()

    init(status: Int,
         primaryAction: @escaping () -> Void,
         secondaryAction: @escaping () -> Void) {
        self.primaryAction = primaryAction
        self.secondaryAction = secondaryAction
        super.init(rootView: Body(viewModel: viewModel))
        view.addGestureRecognizer(primaryGestureRecognizer)
        view.addGestureRecognizer(secondaryGestureRecognizer)
    }

    @MainActor required dynamic init?(coder: NSCoder) {
        fatalError(&#x22;init(coder:) has not been implemented&#x22;)
    }

    func update(state: Int) {
        viewModel.status = state
    }

    @objc private func primaryEvent() -> Void {
        primaryAction()
    }

    @objc private func secondaryEvent() -> Void {
        secondaryAction()
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">final</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> class</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> ButtonHostingViewController</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">NSHostingController&#x3C;ButtonHostingViewController.Body></span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="2"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    class</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> ViewModel</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">ObservableObject </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="3"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        @Published</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> status: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0
</span></span><span class="line" line="4"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        @Published</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isPrimaryDown </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="5"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        @Published</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isSecondaryDown </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="6"><span emptyLinePlaceholder>
</span></span><span class="line" line="7"><span style="--shiki-default:#859900;--shiki-dark:#859900">        var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isButtonDown: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Bool</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="8"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            isPrimaryDown </span><span style="--shiki-default:#859900;--shiki-dark:#859900">||</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> isSecondaryDown
</span></span><span class="line" line="9"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="10"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="11"><span emptyLinePlaceholder>
</span></span><span class="line" line="12"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Body</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">View </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="13"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        @ObservedObject</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> viewModel: ViewModel
</span></span><span class="line" line="14"><span emptyLinePlaceholder>
</span></span><span class="line" line="15"><span style="--shiki-default:#859900;--shiki-dark:#859900">        var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> body: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="16"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            Text</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">status</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">format</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">number</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="17"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">opacity</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isButtonDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> ?</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0.3</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> :</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 1</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="18"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="19"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="20"><span emptyLinePlaceholder>
</span></span><span class="line" line="21"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> viewModel </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> ViewModel</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="22"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> primaryAction: () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void
</span></span><span class="line" line="23"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> secondaryAction: () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void
</span></span><span class="line" line="24"><span emptyLinePlaceholder>
</span></span><span class="line" line="25"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> lazy</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> primaryGestureRecognizer: NSClickGestureRecognizer </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="26"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> primaryGestureRecognizer </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> UpdatingGestureRecognizer</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">target</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">action</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">#selector</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(primaryEvent))
</span></span><span class="line" line="27"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        primaryGestureRecognizer.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">onMouseUp</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { [viewModel] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="28"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isPrimaryDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="29"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="30"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        primaryGestureRecognizer.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">onMouseDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { [viewModel] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="31"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isPrimaryDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> true
</span></span><span class="line" line="32"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="33"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> primaryGestureRecognizer
</span></span><span class="line" line="34"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }()
</span></span><span class="line" line="35"><span emptyLinePlaceholder>
</span></span><span class="line" line="36"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> lazy</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> secondaryGestureRecognizer: NSClickGestureRecognizer </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="37"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> secondaryGestureRecognizer </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> UpdatingGestureRecognizer</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">target</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">action</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">#selector</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(secondaryEvent))
</span></span><span class="line" line="38"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        secondaryGestureRecognizer.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">onMouseUp</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { [viewModel] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="39"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isSecondaryDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="40"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="41"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        secondaryGestureRecognizer.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">onMouseDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { [viewModel] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="42"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isSecondaryDown</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> true
</span></span><span class="line" line="43"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="44"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        secondaryGestureRecognizer.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">buttonMask</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0x2
</span></span><span class="line" line="45"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> secondaryGestureRecognizer
</span></span><span class="line" line="46"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }()
</span></span><span class="line" line="47"><span emptyLinePlaceholder>
</span></span><span class="line" line="48"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">status</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">,
</span></span><span class="line" line="49"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">         primaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">@escaping</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">,
</span></span><span class="line" line="50"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">         secondaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">@escaping</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> () </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="51"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">primaryAction</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> primaryAction
</span></span><span class="line" line="52"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">secondaryAction</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> secondaryAction
</span></span><span class="line" line="53"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        super</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">rootView</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">Body</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">viewModel</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: viewModel))
</span></span><span class="line" line="54"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        view.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">addGestureRecognizer</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(primaryGestureRecognizer)
</span></span><span class="line" line="55"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        view.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">addGestureRecognizer</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(secondaryGestureRecognizer)
</span></span><span class="line" line="56"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="57"><span emptyLinePlaceholder>
</span></span><span class="line" line="58"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @MainActor</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> required</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> dynamic</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> init?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">coder</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: NSCoder) {
</span></span><span class="line" line="59"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        fatalError</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"init(coder:) has not been implemented"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="60"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="61"><span emptyLinePlaceholder>
</span></span><span class="line" line="62"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> update</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">state</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="63"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        viewModel.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">status</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> state
</span></span><span class="line" line="64"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="65"><span emptyLinePlaceholder>
</span></span><span class="line" line="66"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @objc</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> primaryEvent</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">() </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="67"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        primaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="68"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="69"><span emptyLinePlaceholder>
</span></span><span class="line" line="70"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @objc</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> secondaryEvent</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">() </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Void</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="71"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        secondaryAction</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="72"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="73"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>It's more verbose than I'd like, and this particular version tightly couples the button wrapper and the content, but it should work and it will be interesting to see if there's a performance hit on larger boards.</p><p>Part of why I implemented this is that I'm not running the macOS Ventura betas on my Mac yet, I'm holding out for the public betas, so I don't have a place to run the Mac build of Minesweeper quite yet. I'm interested in putting this together with my other code when I get a chance.</p><style>html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}</style>]]></description>
            <link>https://atelierclockwork.net/posts/2022/07/10/building-minesweeper-25</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2022/07/10/building-minesweeper-25</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 10 Jul 2022 13:48:13 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Building Minesweeper 2]]></title>
            <description><![CDATA[<h2 id="making-up-the-rules-as-we-go-along">Making up the rules as we go along</h2><p>I've done enough UI development to be able to test out the rules, but I'm going to just ignore that for now. Since I've built Minesweeper a handful of times now, I had a decent place to start from. This requires:</p><ul><li>A <code class="">GridPoint</code> struct to allow for addressing points on the grid</li><li>A <code class="">Game</code> struct that tracks all of the game state and allows the gameplay actions</li><li>Enums that report the state of the game (active, win, lose), and the possible states that each grid point can occupy.</li></ul><p>The interfaces for all of that are:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="struct GridPoint: Hashable, Identifiable {
    var id: GridPoint { self }

    let x: Int
    let y: Int
}

struct Game {
    var state: GameState { get }
    var elapsedTime: TimeInterval { get }
    var remainingMines: Int { get }

    init(width: Int, height: Int, mineCount: Int)

    subscript(_ gridPoint: GridPoint) -> Game.SpaceState { get }

    mutating func reveal(_ gridPoint: GridPoint)
    mutating func revealSurroundingIfSafe(_ gridPoint: GridPoint)
    mutating func toggleFlag(_ gridPoint: GridPoint)
}

extension Game {
    enum State {
        case active
        case win
        case lose
    }
}

extension Game {
    enum SpaceState {
        case unrevealed
        case revealed(Int)
        case flagged
        case revealedMine
        case flaggedMine
        case incorrectFlag
        case unrevealedMine
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> GridPoint</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Hashable</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">Identifiable </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="2"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> id: GridPoint { </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="3"><span emptyLinePlaceholder>
</span></span><span class="line" line="4"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> x: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int
</span></span><span class="line" line="5"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> y: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int
</span></span><span class="line" line="6"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="7"><span emptyLinePlaceholder>
</span></span><span class="line" line="8"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="9"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> state: GameState { </span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">get</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="10"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> elapsedTime: TimeInterval { </span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">get</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="11"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> remainingMines: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { </span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">get</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="12"><span emptyLinePlaceholder>
</span></span><span class="line" line="13"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mineCount</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="14"><span emptyLinePlaceholder>
</span></span><span class="line" line="15"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    subscript</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Game.SpaceState { </span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">get</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="16"><span emptyLinePlaceholder>
</span></span><span class="line" line="17"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    mutating</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> reveal</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint)
</span></span><span class="line" line="18"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    mutating</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> revealSurroundingIfSafe</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint)
</span></span><span class="line" line="19"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    mutating</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> toggleFlag</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint)
</span></span><span class="line" line="20"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="21"><span emptyLinePlaceholder>
</span></span><span class="line" line="22"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="23"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    enum</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> State</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="24"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> active
</span></span><span class="line" line="25"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> win
</span></span><span class="line" line="26"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> lose
</span></span><span class="line" line="27"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="28"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="29"><span emptyLinePlaceholder>
</span></span><span class="line" line="30"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="31"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    enum</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> SpaceState</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="32"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> unrevealed
</span></span><span class="line" line="33"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> revealed</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="34"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> flagged
</span></span><span class="line" line="35"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> revealedMine
</span></span><span class="line" line="36"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> flaggedMine
</span></span><span class="line" line="37"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> incorrectFlag
</span></span><span class="line" line="38"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> unrevealedMine
</span></span><span class="line" line="39"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="40"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>One interesting detail to note is that the <code class="">SpaceState</code> includes both the states used for when the game is active, and when the game has been won or lost.</p><p>The <code class="">Game</code> struct is also used to only allow valid moves. Hence why <code class="">subscript</code> is read only, and then there are <code class="">flag</code> and <code class="">reveal</code> functions instead of allowing the game board direct write access to the game data. The data included in the <code class="">Game</code> struct is:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="struct Game {}
    private let width: Int
    private let height: Int
    private let mineCount: Int
    private let startDate = Date()
    private var endDate: Date?
    private (set) var state: State = .active

    private var board: [GridPoint: Space]?
    private var flagged = Set<GridPoint>()
    private var mines = Set<GridPoint>()
    private var revealed = Set<GridPoint>()
}

private extension Game {
    enum Space {
        case mine
        case empty(Int)
    }
}

" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {}
</span></span><span class="line" line="2"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> width: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> height: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int
</span></span><span class="line" line="4"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mineCount: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int
</span></span><span class="line" line="5"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> startDate </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> Date</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="6"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> endDate: Date</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?
</span></span><span class="line" line="7"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> (</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">set</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> state: State </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">active
</span></span><span class="line" line="8"><span emptyLinePlaceholder>
</span></span><span class="line" line="9"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board: [GridPoint: Space]</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?
</span></span><span class="line" line="10"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> flagged </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="11"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mines </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="12"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> revealed </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="13"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="14"><span emptyLinePlaceholder>
</span></span><span class="line" line="15"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="16"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    enum</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Space</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="17"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> mine
</span></span><span class="line" line="18"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="19"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="20"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>The <code class="">board</code> is an optional var, because in Minesweeper the first move should never be a mine. That can be accomplished in different ways. I went with the relatively simple option of removing the first revealed tile from the list of tiles that are allowed to hold mines. It also means that the board isn't created until the user reveals a square, event if they flag squares first.</p><p>I've decided on using a <code class="">Dictionary</code> to store the grid points because in previous iterations I've used nested arrays, and have managed to write bugs related to grid / column lookup. To isolate out all of the code related to creating the board, it's implemented as extensions on Dictionary, including the initializer that takes all of the values and uses that to populate the grid. One thing that made me really happy in the implementation was that I could use <code class="">some Collection&#x3C;GridPoint></code> for the helper functions and not need to convert from <code class="">ArraySlice</code> to <code class="">Array</code> in the init.</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="private extension Dictionary where Key == GridPoint, Value == Game.Space {
    init(width: Int, height: Int, mineCount: Int, initialMove: GridPoint) {
        let allGridPoints = Set<GridPoint>(width: 0..<width, height: 0..<height)
        var board = [GridPoint: Game.Space](minimumCapacity: width * height)
        board.setMines(allGridPoints.subtracting([initialMove]).shuffled().prefix(mineCount))
        board.calculateCounts(allGridPoints)
        self = board
    }

    mutating private func setMines(_ mineGridPoints: some Collection<GridPoint>) {
        for index in mineGridPoints {
            self[index] = .mine
        }
    }

    mutating private func calculateCounts(_ gridPoints: some Collection<GridPoint>) {
        for index in gridPoints {
            switch self[index] {
            case .mine: break
            case .empty, .none:
                self[index] = .empty(emptyCount(index))
            }
        }
    }

    private func emptyCount(_ gridPoint: GridPoint) -> Int {
        gridPoint.surroundingGridPoints.filter { index in
            switch self[index] {
            case .empty, .none: return false
            case .mine: return true
            }
        }.count
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Dictionary</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> where</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Key</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> ==</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> GridPoint, </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Value</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> ==</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Game.Space {
</span></span><span class="line" line="2"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mineCount</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">initialMove</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: GridPoint) {
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> allGridPoints </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">0</span><span style="--shiki-default:#859900;--shiki-dark:#859900">..&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">width, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">0</span><span style="--shiki-default:#859900;--shiki-dark:#859900">..&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">height)
</span></span><span class="line" line="4"><span style="--shiki-default:#859900;--shiki-dark:#859900">        var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> [GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Game.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">Space</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">](</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">minimumCapacity</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: width </span><span style="--shiki-default:#859900;--shiki-dark:#859900">*</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> height)
</span></span><span class="line" line="5"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        board.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">setMines</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(allGridPoints.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">subtracting</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">([initialMove]).</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">shuffled</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">().</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">prefix</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(mineCount))
</span></span><span class="line" line="6"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        board.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">calculateCounts</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(allGridPoints)
</span></span><span class="line" line="7"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board
</span></span><span class="line" line="8"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="9"><span emptyLinePlaceholder>
</span></span><span class="line" line="10"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    mutating</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> setMines</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mineGridPoints: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Collection</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">&#x3C;GridPoint>) {
</span></span><span class="line" line="11"><span style="--shiki-default:#859900;--shiki-dark:#859900">        for</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> index </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mineGridPoints {
</span></span><span class="line" line="12"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[index] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mine
</span></span><span class="line" line="13"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="14"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="15"><span emptyLinePlaceholder>
</span></span><span class="line" line="16"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    mutating</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> calculateCounts</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoints: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Collection</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">&#x3C;GridPoint>) {
</span></span><span class="line" line="17"><span style="--shiki-default:#859900;--shiki-dark:#859900">        for</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> index </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoints {
</span></span><span class="line" line="18"><span style="--shiki-default:#859900;--shiki-dark:#859900">            switch</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[index] {
</span></span><span class="line" line="19"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> break
</span></span><span class="line" line="20"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .none</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="21"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[index] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">emptyCount</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(index))
</span></span><span class="line" line="22"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="23"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="24"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="25"><span emptyLinePlaceholder>
</span></span><span class="line" line="26"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> emptyCount</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="27"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        gridPoint.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">surroundingGridPoints</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">filter</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { index </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="28"><span style="--shiki-default:#859900;--shiki-dark:#859900">            switch</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[index] {
</span></span><span class="line" line="29"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .none</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> return</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="30"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> return</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> true
</span></span><span class="line" line="31"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="32"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }.count
</span></span><span class="line" line="33"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="34"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>The last function of interest is the reveal function on the <code class="">Game</code>, since it also handles calculating the win / loss state.</p><p>It enforces that:</p><ul><li>Only unrevealed, unflagged points can be revealed</li><li>If the board doesn't exist, create it</li><li>Only reveal points that already exist on the board, every valid tile is populated when the board is created. This lets us not worry about overflowing the board when revealing surrounding tiles when a point with zero adjacent mines is revealed.</li><li>Reveal the point</li><li>If the revealed tile has zero adjacent mines, reveal all tiles surrounding this tile</li><li>If a tile containing a mine is revealed, end the game in failure</li><li>Otherwise, end the game is the number of revealed tiles equals the number of tiles on the board minus the mine count</li></ul><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="extension Game {
    mutating func reveal(_ gridPoint: GridPoint) {
        guard !flagged.contains(gridPoint), !revealed.contains(gridPoint) else { return }
        createBoardIfNeeded(initialMove: gridPoint)
        guard board?[gridPoint] != nil else { return }
        revealed.insert(gridPoint)
        if case .empty(let count) = board?[gridPoint], count == 0 {
            for surroundingIndex in gridPoint.surroundingGridPoints {
                reveal(surroundingIndex)
            }
        }
        if mines.isDisjoint(with: revealed) {
            if revealed.count == (width * height) - mineCount {
                endDate = Date()
                state = .win
            }
        } else {
            endDate = Date()
            state = .lose
        }
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="2"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    mutating</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> reveal</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint) {
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">        guard</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> !</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">flagged.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint), </span><span style="--shiki-default:#859900;--shiki-dark:#859900">!</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">revealed.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { </span><span style="--shiki-default:#859900;--shiki-dark:#859900">return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="4"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        createBoardIfNeeded</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">initialMove</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: gridPoint)
</span></span><span class="line" line="5"><span style="--shiki-default:#859900;--shiki-dark:#859900">        guard</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[gridPoint] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">!=</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> nil</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { </span><span style="--shiki-default:#859900;--shiki-dark:#859900">return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="6"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        revealed.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">insert</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint)
</span></span><span class="line" line="7"><span style="--shiki-default:#859900;--shiki-dark:#859900">        if</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#859900;--shiki-dark:#859900">let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> count) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[gridPoint], count </span><span style="--shiki-default:#859900;--shiki-dark:#859900">==</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="8"><span style="--shiki-default:#859900;--shiki-dark:#859900">            for</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> surroundingIndex </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint.surroundingGridPoints {
</span></span><span class="line" line="9"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                reveal</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(surroundingIndex)
</span></span><span class="line" line="10"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="11"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="12"><span style="--shiki-default:#859900;--shiki-dark:#859900">        if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mines.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isDisjoint</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">with</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: revealed) {
</span></span><span class="line" line="13"><span style="--shiki-default:#859900;--shiki-dark:#859900">            if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> revealed.count </span><span style="--shiki-default:#859900;--shiki-dark:#859900">==</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> (width </span><span style="--shiki-default:#859900;--shiki-dark:#859900">*</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> height) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mineCount {
</span></span><span class="line" line="14"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                endDate </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> Date</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="15"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                state </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">win
</span></span><span class="line" line="16"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="17"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        } </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="18"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            endDate </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> Date</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="19"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            state </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">lose
</span></span><span class="line" line="20"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="21"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="22"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>There's a lot more going on in this struct to drive the rules of the game, so for anyone interested in taking a look at all of the code:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="import Foundation

struct Game {
    private let width: Int
    private let height: Int
    private let mineCount: Int
    private let startDate = Date()
    private var endDate: Date?
    private (set) var state: State = .active

    private var board: [GridPoint: Space]?
    private var flagged = Set<GridPoint>()
    private var mines = Set<GridPoint>()
    private var revealed = Set<GridPoint>()

    var elapsedTime: TimeInterval {
        (endDate ?? Date()).timeIntervalSince(startDate)
    }

    var remainingMines: Int {
        mineCount - flagged.count
    }

    init(width: Int, height: Int, mineCount: Int) {
        self.width = width
        self.height = height
        self.mineCount = mineCount
    }

    subscript(_ gridPoint: GridPoint) -> SpaceState {
        switch board?[gridPoint] {
        case .none:
            return .unrevealed
        case .mine:
            return mineState(gridPoint)
        case .empty(let count):
            return emptyState(gridPoint, count: count)
        }
    }

    mutating func reveal(_ gridPoint: GridPoint) {
        guard !flagged.contains(gridPoint), !revealed.contains(gridPoint) else { return }
        createBoardIfNeeded(initialMove: gridPoint)
        guard board?.keys.contains(gridPoint) == true else { return }
        revealed.insert(gridPoint)
        if case .empty(let count) = board?[gridPoint], count == 0 {
            for surroundingIndex in gridPoint.surroundingGridPoints {
                reveal(surroundingIndex)
            }
        }
        if mines.isDisjoint(with: revealed) {
            if revealed.count == (width * height) - mineCount {
                endDate = Date()
                state = .win
            }
        } else {
            endDate = Date()
            state = .lose
        }
    }

    mutating func revealSurroundingIfSafe(_ gridPoint: GridPoint) {
        guard revealed.contains(gridPoint),
              case .empty(let mines) = board?[gridPoint]
        else { return }
        let surroundingGridPoints = gridPoint.surroundingGridPoints
        guard surroundingGridPoints.intersection(flagged).count == mines else { return }
        for gridPoint in surroundingGridPoints.subtracting(flagged) {
            reveal(gridPoint)
        }
    }

    mutating func toggleFlag(_ gridPoint: GridPoint) {
        guard !revealed.contains(gridPoint) else { return }
        flagged.formSymmetricDifference([gridPoint])
    }
}

// MARK: - Private functions
private extension Game {
    func mineState(_ gridPoint: GridPoint) -> SpaceState {
        if revealed.contains(gridPoint) {
            return .revealedMine
        } else {
            switch state {
            case .lose, .win:
                if flagged.contains(gridPoint) {
                    return .flaggedMine
                } else {
                    return .unrevealedMine
                }
            case .active:
                if flagged.contains(gridPoint) {
                    return .flagged
                } else {
                    return .unrevealed
                }
            }
        }
    }

    func emptyState(_ gridPoint: GridPoint, count: Int) -> SpaceState {
        if revealed.contains(gridPoint) {
            return .revealed(count)
        } else if flagged.contains(gridPoint) {
            switch state {
            case .win, .lose:
                return .incorrectFlag
            case .active:
                return .flagged
            }
        } else {
            return .unrevealed
        }
    }

    private mutating func createBoardIfNeeded(initialMove: GridPoint) {
        guard board == nil else { return }
        self.board = [GridPoint: Game.Space](width: width,
                                             height: height,
                                             mineCount: mineCount,
                                             initialMove: initialMove)
        mines = board?.mineGridPoints ?? []
    }
}

// MARK: - Space Values
private extension Game {
    enum Space {
        case mine
        case empty(Int)
    }
}

// MARK: - Convenience inits for sets of GridPoints
private extension Set where Element == GridPoint {
    init(width: Range<Int>, height: Range<Int>) {
        self = width.reduce(into: Set<GridPoint>()) { grid, col in
            let column = height.reduce(into: Set<GridPoint>()) { column, row in
                column.insert(GridPoint(x: col, y: row))
            }
            grid.formUnion(column)
        }
    }

    init(width: ClosedRange<Int>, height: ClosedRange<Int>) {
        self = width.reduce(into: Set<GridPoint>()) { grid, col in
            let column = height.reduce(into: Set<GridPoint>()) { column, row in
                column.insert(GridPoint(x: col, y: row))
            }
            grid.formUnion(column)
        }
    }
}

// MARK: - Dictionary helpers
private extension Dictionary where Key == GridPoint, Value == Game.Space {
    init(width: Int, height: Int, mineCount: Int, initialMove: GridPoint) {
        let allGridPoints = Set<GridPoint>(width: 0..<width, height: 0..<height)
        var board = [GridPoint: Game.Space](minimumCapacity: width * height)
        board.setMines(allGridPoints.subtracting([initialMove]).shuffled().prefix(mineCount))
        board.calculateCounts(allGridPoints)
        self = board
    }

    mutating private func setMines(_ mineGridPoints: some Collection<GridPoint>) {
        for index in mineGridPoints {
            self[index] = .mine
        }
    }

    mutating private func calculateCounts(_ gridPoints: some Collection<GridPoint>) {
        for index in gridPoints {
            switch self[index] {
            case .mine: break
            case .empty, .none:
                self[index] = .empty(emptyCount(index))
            }
        }
    }

    private func emptyCount(_ gridPoint: GridPoint) -> Int {
        gridPoint.surroundingGridPoints.filter { index in
            switch self[index] {
            case .empty, .none: return false
            case .mine: return true
            }
        }.count
    }

    var mineGridPoints: Set<GridPoint> {
        let gridPoints = self.filter { _, value in
            switch value {
            case .empty: return false
            case .mine: return true
            }
        }.keys
        return Set(gridPoints)
    }
}

private extension GridPoint {
    var surroundingGridPoints: Set<GridPoint> {
        let minColumn = x - 1
        let maxColumn = x + 1
        let minRow = y - 1
        let maxRow = y + 1
        var surroundingGridPoints = Set<GridPoint>(
            width: minColumn...maxColumn,
            height: minRow...maxRow)
        surroundingGridPoints.remove(self)
        return surroundingGridPoints
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#859900;--shiki-dark:#859900">import</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Foundation
</span></span><span class="line" line="2"><span emptyLinePlaceholder>
</span></span><span class="line" line="3"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="4"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> width: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int
</span></span><span class="line" line="5"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> height: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int
</span></span><span class="line" line="6"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mineCount: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int
</span></span><span class="line" line="7"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> startDate </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> Date</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="8"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> endDate: Date</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?
</span></span><span class="line" line="9"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> (</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">set</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> state: State </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">active
</span></span><span class="line" line="10"><span emptyLinePlaceholder>
</span></span><span class="line" line="11"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board: [GridPoint: Space]</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?
</span></span><span class="line" line="12"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> flagged </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="13"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mines </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="14"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> revealed </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="15"><span emptyLinePlaceholder>
</span></span><span class="line" line="16"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> elapsedTime: TimeInterval {
</span></span><span class="line" line="17"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        (endDate </span><span style="--shiki-default:#859900;--shiki-dark:#859900">??</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> Date</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()).</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">timeIntervalSince</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(startDate)
</span></span><span class="line" line="18"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="19"><span emptyLinePlaceholder>
</span></span><span class="line" line="20"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> remainingMines: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="21"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        mineCount </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> flagged.count
</span></span><span class="line" line="22"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="23"><span emptyLinePlaceholder>
</span></span><span class="line" line="24"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mineCount</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="25"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> width
</span></span><span class="line" line="26"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> height
</span></span><span class="line" line="27"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mineCount</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mineCount
</span></span><span class="line" line="28"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="29"><span emptyLinePlaceholder>
</span></span><span class="line" line="30"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    subscript</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> SpaceState {
</span></span><span class="line" line="31"><span style="--shiki-default:#859900;--shiki-dark:#859900">        switch</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[gridPoint] {
</span></span><span class="line" line="32"><span style="--shiki-default:#859900;--shiki-dark:#859900">        case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .none</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="33"><span style="--shiki-default:#859900;--shiki-dark:#859900">            return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealed
</span></span><span class="line" line="34"><span style="--shiki-default:#859900;--shiki-dark:#859900">        case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="35"><span style="--shiki-default:#859900;--shiki-dark:#859900">            return</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> mineState</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint)
</span></span><span class="line" line="36"><span style="--shiki-default:#859900;--shiki-dark:#859900">        case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#859900;--shiki-dark:#859900">let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> count)</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="37"><span style="--shiki-default:#859900;--shiki-dark:#859900">            return</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> emptyState</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">count</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: count)
</span></span><span class="line" line="38"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="39"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="40"><span emptyLinePlaceholder>
</span></span><span class="line" line="41"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    mutating</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> reveal</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint) {
</span></span><span class="line" line="42"><span style="--shiki-default:#859900;--shiki-dark:#859900">        guard</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> !</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">flagged.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint), </span><span style="--shiki-default:#859900;--shiki-dark:#859900">!</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">revealed.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { </span><span style="--shiki-default:#859900;--shiki-dark:#859900">return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="43"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        createBoardIfNeeded</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">initialMove</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: gridPoint)
</span></span><span class="line" line="44"><span style="--shiki-default:#859900;--shiki-dark:#859900">        guard</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.keys.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">==</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> true</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { </span><span style="--shiki-default:#859900;--shiki-dark:#859900">return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="45"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        revealed.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">insert</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint)
</span></span><span class="line" line="46"><span style="--shiki-default:#859900;--shiki-dark:#859900">        if</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#859900;--shiki-dark:#859900">let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> count) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[gridPoint], count </span><span style="--shiki-default:#859900;--shiki-dark:#859900">==</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 0</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="47"><span style="--shiki-default:#859900;--shiki-dark:#859900">            for</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> surroundingIndex </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint.surroundingGridPoints {
</span></span><span class="line" line="48"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                reveal</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(surroundingIndex)
</span></span><span class="line" line="49"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="50"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="51"><span style="--shiki-default:#859900;--shiki-dark:#859900">        if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mines.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">isDisjoint</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">with</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: revealed) {
</span></span><span class="line" line="52"><span style="--shiki-default:#859900;--shiki-dark:#859900">            if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> revealed.count </span><span style="--shiki-default:#859900;--shiki-dark:#859900">==</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> (width </span><span style="--shiki-default:#859900;--shiki-dark:#859900">*</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> height) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mineCount {
</span></span><span class="line" line="53"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                endDate </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> Date</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="54"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                state </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">win
</span></span><span class="line" line="55"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="56"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        } </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="57"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            endDate </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> Date</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="58"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            state </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">lose
</span></span><span class="line" line="59"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="60"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="61"><span emptyLinePlaceholder>
</span></span><span class="line" line="62"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    mutating</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> revealSurroundingIfSafe</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint) {
</span></span><span class="line" line="63"><span style="--shiki-default:#859900;--shiki-dark:#859900">        guard</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> revealed.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint),
</span></span><span class="line" line="64"><span style="--shiki-default:#859900;--shiki-dark:#859900">              case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#859900;--shiki-dark:#859900">let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mines) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[gridPoint]
</span></span><span class="line" line="65"><span style="--shiki-default:#859900;--shiki-dark:#859900">        else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { </span><span style="--shiki-default:#859900;--shiki-dark:#859900">return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="66"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> surroundingGridPoints </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">surroundingGridPoints
</span></span><span class="line" line="67"><span style="--shiki-default:#859900;--shiki-dark:#859900">        guard</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> surroundingGridPoints.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">intersection</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(flagged).count </span><span style="--shiki-default:#859900;--shiki-dark:#859900">==</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mines </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { </span><span style="--shiki-default:#859900;--shiki-dark:#859900">return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="68"><span style="--shiki-default:#859900;--shiki-dark:#859900">        for</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> surroundingGridPoints.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">subtracting</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(flagged) {
</span></span><span class="line" line="69"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            reveal</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint)
</span></span><span class="line" line="70"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="71"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="72"><span emptyLinePlaceholder>
</span></span><span class="line" line="73"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    mutating</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> toggleFlag</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint) {
</span></span><span class="line" line="74"><span style="--shiki-default:#859900;--shiki-dark:#859900">        guard</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> !</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">revealed.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { </span><span style="--shiki-default:#859900;--shiki-dark:#859900">return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="75"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        flagged.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">formSymmetricDifference</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">([gridPoint])
</span></span><span class="line" line="76"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="77"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="78"><span emptyLinePlaceholder>
</span></span><span class="line" line="79"><span style="--shiki-default:#586E75;--shiki-dark:#93A1A1;--shiki-default-font-style:italic;--shiki-dark-font-style:italic">// MARK: - Private functions
</span></span><span class="line" line="80"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="81"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> mineState</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> SpaceState {
</span></span><span class="line" line="82"><span style="--shiki-default:#859900;--shiki-dark:#859900">        if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> revealed.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint) {
</span></span><span class="line" line="83"><span style="--shiki-default:#859900;--shiki-dark:#859900">            return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealedMine
</span></span><span class="line" line="84"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        } </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="85"><span style="--shiki-default:#859900;--shiki-dark:#859900">            switch</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> state {
</span></span><span class="line" line="86"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">lose</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">win</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="87"><span style="--shiki-default:#859900;--shiki-dark:#859900">                if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> flagged.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint) {
</span></span><span class="line" line="88"><span style="--shiki-default:#859900;--shiki-dark:#859900">                    return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flaggedMine
</span></span><span class="line" line="89"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                } </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="90"><span style="--shiki-default:#859900;--shiki-dark:#859900">                    return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealedMine
</span></span><span class="line" line="91"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                }
</span></span><span class="line" line="92"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">active</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="93"><span style="--shiki-default:#859900;--shiki-dark:#859900">                if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> flagged.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint) {
</span></span><span class="line" line="94"><span style="--shiki-default:#859900;--shiki-dark:#859900">                    return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flagged
</span></span><span class="line" line="95"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                } </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="96"><span style="--shiki-default:#859900;--shiki-dark:#859900">                    return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealed
</span></span><span class="line" line="97"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                }
</span></span><span class="line" line="98"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="99"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="100"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="101"><span emptyLinePlaceholder>
</span></span><span class="line" line="102"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> emptyState</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">count</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> SpaceState {
</span></span><span class="line" line="103"><span style="--shiki-default:#859900;--shiki-dark:#859900">        if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> revealed.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint) {
</span></span><span class="line" line="104"><span style="--shiki-default:#859900;--shiki-dark:#859900">            return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">revealed</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(count)
</span></span><span class="line" line="105"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        } </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> if</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> flagged.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">contains</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoint) {
</span></span><span class="line" line="106"><span style="--shiki-default:#859900;--shiki-dark:#859900">            switch</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> state {
</span></span><span class="line" line="107"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">win</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">lose</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="108"><span style="--shiki-default:#859900;--shiki-dark:#859900">                return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">incorrectFlag
</span></span><span class="line" line="109"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">active</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="110"><span style="--shiki-default:#859900;--shiki-dark:#859900">                return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">flagged
</span></span><span class="line" line="111"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="112"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        } </span><span style="--shiki-default:#859900;--shiki-dark:#859900">else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="113"><span style="--shiki-default:#859900;--shiki-dark:#859900">            return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">unrevealed
</span></span><span class="line" line="114"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="115"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="116"><span emptyLinePlaceholder>
</span></span><span class="line" line="117"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> mutating</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> createBoardIfNeeded</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">initialMove</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: GridPoint) {
</span></span><span class="line" line="118"><span style="--shiki-default:#859900;--shiki-dark:#859900">        guard</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board </span><span style="--shiki-default:#859900;--shiki-dark:#859900">==</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> nil</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> else</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { </span><span style="--shiki-default:#859900;--shiki-dark:#859900">return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> }
</span></span><span class="line" line="119"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">board</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> [GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Game.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">Space</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">](</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: width,
</span></span><span class="line" line="120"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                                             height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: height,
</span></span><span class="line" line="121"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                                             mineCount</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: mineCount,
</span></span><span class="line" line="122"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                                             initialMove</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: initialMove)
</span></span><span class="line" line="123"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        mines </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board</span><span style="--shiki-default:#859900;--shiki-dark:#859900">?</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mineGridPoints</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> ??</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> []
</span></span><span class="line" line="124"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="125"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="126"><span emptyLinePlaceholder>
</span></span><span class="line" line="127"><span style="--shiki-default:#586E75;--shiki-dark:#93A1A1;--shiki-default-font-style:italic;--shiki-dark-font-style:italic">// MARK: - Space Values
</span></span><span class="line" line="128"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Game</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="129"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    enum</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> Space</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="130"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> mine
</span></span><span class="line" line="131"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">        case</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="132"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="133"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="134"><span emptyLinePlaceholder>
</span></span><span class="line" line="135"><span style="--shiki-default:#586E75;--shiki-dark:#93A1A1;--shiki-default-font-style:italic;--shiki-dark-font-style:italic">// MARK: - Convenience inits for sets of GridPoints
</span></span><span class="line" line="136"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Set</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> where</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Element</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> ==</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> GridPoint {
</span></span><span class="line" line="137"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Range</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">&#x3C;</span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">>, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Range</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">&#x3C;</span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">>) {
</span></span><span class="line" line="138"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> width.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">reduce</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">into</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()) { grid, col </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="139"><span style="--shiki-default:#859900;--shiki-dark:#859900">            let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> column </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> height.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">reduce</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">into</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()) { column, row </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="140"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                column.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">insert</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">GridPoint</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">x</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: col, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">y</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: row))
</span></span><span class="line" line="141"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="142"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            grid.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">formUnion</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(column)
</span></span><span class="line" line="143"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="144"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="145"><span emptyLinePlaceholder>
</span></span><span class="line" line="146"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">ClosedRange</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">&#x3C;</span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">>, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">ClosedRange</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">&#x3C;</span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">>) {
</span></span><span class="line" line="147"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> width.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">reduce</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">into</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()) { grid, col </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="148"><span style="--shiki-default:#859900;--shiki-dark:#859900">            let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> column </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> height.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">reduce</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">into</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()) { column, row </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="149"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                column.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">insert</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">GridPoint</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">x</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: col, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">y</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: row))
</span></span><span class="line" line="150"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="151"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            grid.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">formUnion</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(column)
</span></span><span class="line" line="152"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="153"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="154"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="155"><span emptyLinePlaceholder>
</span></span><span class="line" line="156"><span style="--shiki-default:#586E75;--shiki-dark:#93A1A1;--shiki-default-font-style:italic;--shiki-dark-font-style:italic">// MARK: - Dictionary helpers
</span></span><span class="line" line="157"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Dictionary</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> where</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Key</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> ==</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> GridPoint, </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Value</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> ==</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Game.Space {
</span></span><span class="line" line="158"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    init</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mineCount</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">initialMove</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: GridPoint) {
</span></span><span class="line" line="159"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> allGridPoints </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">0</span><span style="--shiki-default:#859900;--shiki-dark:#859900">..&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">width, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">0</span><span style="--shiki-default:#859900;--shiki-dark:#859900">..&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">height)
</span></span><span class="line" line="160"><span style="--shiki-default:#859900;--shiki-dark:#859900">        var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> [GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> Game.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">Space</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">](</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">minimumCapacity</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: width </span><span style="--shiki-default:#859900;--shiki-dark:#859900">*</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> height)
</span></span><span class="line" line="161"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        board.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">setMines</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(allGridPoints.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">subtracting</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">([initialMove]).</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">shuffled</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">().</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">prefix</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(mineCount))
</span></span><span class="line" line="162"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        board.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">calculateCounts</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(allGridPoints)
</span></span><span class="line" line="163"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        self</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> =</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> board
</span></span><span class="line" line="164"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="165"><span emptyLinePlaceholder>
</span></span><span class="line" line="166"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    mutating</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> setMines</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mineGridPoints: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Collection</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">&#x3C;GridPoint>) {
</span></span><span class="line" line="167"><span style="--shiki-default:#859900;--shiki-dark:#859900">        for</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> index </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mineGridPoints {
</span></span><span class="line" line="168"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[index] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mine
</span></span><span class="line" line="169"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="170"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="171"><span emptyLinePlaceholder>
</span></span><span class="line" line="172"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    mutating</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> calculateCounts</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoints: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Collection</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">&#x3C;GridPoint>) {
</span></span><span class="line" line="173"><span style="--shiki-default:#859900;--shiki-dark:#859900">        for</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> index </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoints {
</span></span><span class="line" line="174"><span style="--shiki-default:#859900;--shiki-dark:#859900">            switch</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[index] {
</span></span><span class="line" line="175"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> break
</span></span><span class="line" line="176"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .none</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:
</span></span><span class="line" line="177"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[index] </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">emptyCount</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(index))
</span></span><span class="line" line="178"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="179"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="180"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="181"><span emptyLinePlaceholder>
</span></span><span class="line" line="182"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> func</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> emptyCount</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">_</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoint: GridPoint) </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-></span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="183"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        gridPoint.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">surroundingGridPoints</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">filter</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { index </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="184"><span style="--shiki-default:#859900;--shiki-dark:#859900">            switch</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">[index] {
</span></span><span class="line" line="185"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">empty</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, .none</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> return</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="186"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> return</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> true
</span></span><span class="line" line="187"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="188"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }.count
</span></span><span class="line" line="189"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="190"><span emptyLinePlaceholder>
</span></span><span class="line" line="191"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> mineGridPoints: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Set</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">&#x3C;GridPoint> {
</span></span><span class="line" line="192"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> gridPoints </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">filter</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> { _, value </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="193"><span style="--shiki-default:#859900;--shiki-dark:#859900">            switch</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> value {
</span></span><span class="line" line="194"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">empty</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> return</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> false
</span></span><span class="line" line="195"><span style="--shiki-default:#859900;--shiki-dark:#859900">            case</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">mine</span><span style="--shiki-default:#859900;--shiki-dark:#859900">:</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> return</span><span style="--shiki-default:#B58900;--shiki-dark:#B58900"> true
</span></span><span class="line" line="196"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="197"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }.keys
</span></span><span class="line" line="198"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Set</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(gridPoints)
</span></span><span class="line" line="199"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="200"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span><span class="line" line="201"><span emptyLinePlaceholder>
</span></span><span class="line" line="202"><span style="--shiki-default:#859900;--shiki-dark:#859900">private</span><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold"> extension</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> GridPoint</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="203"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> surroundingGridPoints: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Set</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">&#x3C;GridPoint> {
</span></span><span class="line" line="204"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> minColumn </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> x </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 1
</span></span><span class="line" line="205"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> maxColumn </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> x </span><span style="--shiki-default:#859900;--shiki-dark:#859900">+</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 1
</span></span><span class="line" line="206"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> minRow </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> y </span><span style="--shiki-default:#859900;--shiki-dark:#859900">-</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 1
</span></span><span class="line" line="207"><span style="--shiki-default:#859900;--shiki-dark:#859900">        let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> maxRow </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> y </span><span style="--shiki-default:#859900;--shiki-dark:#859900">+</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682"> 1
</span></span><span class="line" line="208"><span style="--shiki-default:#859900;--shiki-dark:#859900">        var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> surroundingGridPoints </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> Set&#x3C;</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">GridPoint</span><span style="--shiki-default:#859900;--shiki-dark:#859900">></span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(
</span></span><span class="line" line="209"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            width</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: minColumn</span><span style="--shiki-default:#859900;--shiki-dark:#859900">...</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">maxColumn</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">,
</span></span><span class="line" line="210"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            height</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: minRow</span><span style="--shiki-default:#859900;--shiki-dark:#859900">...</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">maxRow</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="211"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        surroundingGridPoints.</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">remove</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="212"><span style="--shiki-default:#859900;--shiki-dark:#859900">        return</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> surroundingGridPoints
</span></span><span class="line" line="213"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="214"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><style>html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}</style>]]></description>
            <link>https://atelierclockwork.net/posts/2022/06/30/building-minesweeper-2</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2022/06/30/building-minesweeper-2</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Thu, 30 Jun 2022 20:40:30 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Building Minesweeper 1]]></title>
            <description><![CDATA[<h2 id="shall-we-make-a-game">Shall we make a game?</h2><p>One of my first things I built when learning SwiftUI was an implementation of <a href="https://en.wikipedia.org/wiki/Minesweeper_(video_game)" rel="nofollow">Minesweeper</a>. I've done a major refactor of the game every year as more SwiftUI features have been added, and
I've learned more about working in SwiftUI.</p><p>I'm starting the process again this year, and as I complete the implementation I'll write blog posts covering any interesting points that I've learned.</p><p>My goal for features this year is:</p><ul><li>Use the new Multiplatform app design and support iPad + Mac</li><li>Settings and high scores persisted via <code class="">AppStorage</code></li><li>Minesweeper gameplay including
<ul><li>Revealing mines</li><li>Flagging mines</li><li>Revealing all of the mines if the user taps a numbered square</li></ul></li></ul><p>Out of the list, the one that is the most fiddly is flagging mines, because SwiftUI doesn't natively support two finger taps or right clicks. I've worked around that in the past by supporting option click, but I'll take another swing at improving it.</p><p>I have what I think is a good version of the data model for the game board itself set up, so I'll make an attempt to refine that as well but I don't need to build that from the ground up.</p>]]></description>
            <link>https://atelierclockwork.net/posts/2022/06/21/building-minesweeper-1</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2022/06/21/building-minesweeper-1</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Tue, 21 Jun 2022 19:47:05 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Navigation Refined]]></title>
            <description><![CDATA[<h2 id="testing-out-some-of-the-new-toys">Testing out some of the new toys</h2><p>On the long list of interesting new things to check out from WWDC this year, the first one that I wanted to take a look at was the new <code class="">NavigationStack</code>, as the limitations in the previous <code class="">NavigationView</code> led to my previous project using UIKit to back navigation in a SwiftUI first project.</p><p>After a couple minutes playing around with the docs, I came up with:</p><p>A root view:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="struct NewNavigationView: View {
    private let numbers = [1, 2, 3]
    private let strings = [&#x22;a&#x22;, &#x22;b&#x22;, &#x22;c&#x22;]
    @State private var navigationPath = NavigationPath()

    var body: some View {
        NavigationStack(path: $navigationPath) {
            List {
                Section(&#x22;Numbers&#x22;) {
                    ForEach(numbers, id: \.self) {
                        NavigationLink(&#x22;\($0)&#x22;, value: $0)
                    }
                }
                Section(&#x22;Strings&#x22;) {
                    ForEach(strings, id: \.self) {
                        NavigationLink($0, value: $0)
                    }
                }
            }
            .navigationDestination(for: Int.self) { int in
                NumberView(selection: int, navigationPath: $navigationPath)
            }
            .navigationDestination(for: String.self) { string in
                StringView(selection: string, navigationPath: $navigationPath)
            }
            .navigationTitle(&#x22;Root&#x22;)
        }
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> NewNavigationView</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">View </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="2"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> numbers </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> [</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">1</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">2</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">3</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">]
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> strings </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> [</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"a"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"b"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"c"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">]
</span></span><span class="line" line="4"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @State</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> navigationPath </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> NavigationPath</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="5"><span emptyLinePlaceholder>
</span></span><span class="line" line="6"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> body: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="7"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        NavigationStack</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">path</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: $navigationPath) {
</span></span><span class="line" line="8"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            List</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="9"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                Section</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"Numbers"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="10"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                    ForEach</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(numbers, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">id</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: \.</span><span style="--shiki-default:#859900;--shiki-dark:#859900">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="11"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                        NavigationLink</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">\(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">$0</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">)</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">value</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">$0</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="12"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                    }
</span></span><span class="line" line="13"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                }
</span></span><span class="line" line="14"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                Section</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"Strings"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="15"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                    ForEach</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(strings, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">id</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: \.</span><span style="--shiki-default:#859900;--shiki-dark:#859900">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="16"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                        NavigationLink</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">$0</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">value</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">$0</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="17"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                    }
</span></span><span class="line" line="18"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                }
</span></span><span class="line" line="19"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="20"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">navigationDestination</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">for</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#859900;--shiki-dark:#859900">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) { int </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="21"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                NumberView</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">selection</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: int, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">navigationPath</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: $navigationPath)
</span></span><span class="line" line="22"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="23"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">navigationDestination</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">for</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">String</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">.</span><span style="--shiki-default:#859900;--shiki-dark:#859900">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) { string </span><span style="--shiki-default:#859900;--shiki-dark:#859900">in
</span></span><span class="line" line="24"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                StringView</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">selection</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: string, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">navigationPath</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: $navigationPath)
</span></span><span class="line" line="25"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="26"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">navigationTitle</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"Root"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="27"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="28"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="29"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>A view that displays details for a number:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="struct NumberView: View{
    private let numbers = [1, 2, 3, 4, 5]
    let selection: Int
    @Binding var navigationPath: NavigationPath

    var body: some View {
        List {
            Section(&#x22;Numbers&#x22;) {
                ForEach(numbers, id: \.self) {
                    NavigationLink(&#x22;\($0)&#x22;, value: $0)
                }
            }
            Button(&#x22;Pop to root&#x22;) {
                navigationPath = NavigationPath()
            }
        }
        .navigationTitle(&#x22;Detail \(selection)&#x22;)
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> NumberView</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">View</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="2"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> numbers </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> [</span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">1</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">2</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">3</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">4</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#D33682;--shiki-dark:#D33682">5</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">]
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> selection: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">Int
</span></span><span class="line" line="4"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @Binding</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> navigationPath: NavigationPath
</span></span><span class="line" line="5"><span emptyLinePlaceholder>
</span></span><span class="line" line="6"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> body: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="7"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        List</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="8"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            Section</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"Numbers"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="9"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                ForEach</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(numbers, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">id</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: \.</span><span style="--shiki-default:#859900;--shiki-dark:#859900">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="10"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                    NavigationLink</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">\(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">$0</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">)</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">value</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">$0</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="11"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                }
</span></span><span class="line" line="12"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="13"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            Button</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"Pop to root"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="14"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                navigationPath </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> NavigationPath</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="15"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="16"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="17"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">navigationTitle</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"Detail </span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">\(</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">selection</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">)</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="18"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="19"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>And a view that displays details for a string:</p><pre class="language-swift shiki shiki-themes solarized-dark solarized-light" code="struct StringView: View {
    private let strings = [&#x22;a&#x22;, &#x22;b&#x22;, &#x22;c&#x22;, &#x22;d&#x22;, &#x22;e&#x22;]
    let selection: String
    @Binding var navigationPath: NavigationPath

    var body: some View {
        List {
            Section(&#x22;Strings&#x22;) {
                ForEach(strings, id: \.self) {
                    NavigationLink(&#x22;\($0)&#x22;, value: $0)
                }
            }
            Button(&#x22;Pop to root&#x22;) {
                navigationPath = NavigationPath()
            }
        }
        .navigationTitle(&#x22;Detail \(selection)&#x22;)
    }
}
" language="swift" meta="" style=""><code __ignoreMap=""><span class="line" line="1"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">struct</span><span style="--shiki-default:#CB4B16;--shiki-dark:#CB4B16"> StringView</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#6C71C4;--shiki-dark:#6C71C4">View </span><span style="--shiki-default:#839496;--shiki-dark:#657B83">{
</span></span><span class="line" line="2"><span style="--shiki-default:#859900;--shiki-dark:#859900">    private</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> strings </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> [</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"a"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"b"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"c"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"d"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"e"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">]
</span></span><span class="line" line="3"><span style="--shiki-default:#859900;--shiki-dark:#859900">    let</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> selection: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">String
</span></span><span class="line" line="4"><span style="--shiki-default:#93A1A1;--shiki-dark:#586E75;--shiki-default-font-weight:bold;--shiki-dark-font-weight:bold">    @Binding</span><span style="--shiki-default:#859900;--shiki-dark:#859900"> var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> navigationPath: NavigationPath
</span></span><span class="line" line="5"><span emptyLinePlaceholder>
</span></span><span class="line" line="6"><span style="--shiki-default:#859900;--shiki-dark:#859900">    var</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> body: </span><span style="--shiki-default:#859900;--shiki-dark:#859900">some</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> View {
</span></span><span class="line" line="7"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">        List</span><span style="--shiki-default:#839496;--shiki-dark:#657B83"> {
</span></span><span class="line" line="8"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            Section</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"Strings"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="9"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                ForEach</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(strings, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">id</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: \.</span><span style="--shiki-default:#859900;--shiki-dark:#859900">self</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="10"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">                    NavigationLink</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">\(</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">$0</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">)</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">, </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">value</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">: </span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">$0</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="11"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                }
</span></span><span class="line" line="12"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="13"><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">            Button</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"Pop to root"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">) {
</span></span><span class="line" line="14"><span style="--shiki-default:#839496;--shiki-dark:#657B83">                navigationPath </span><span style="--shiki-default:#859900;--shiki-dark:#859900">=</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2"> NavigationPath</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">()
</span></span><span class="line" line="15"><span style="--shiki-default:#839496;--shiki-dark:#657B83">            }
</span></span><span class="line" line="16"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        }
</span></span><span class="line" line="17"><span style="--shiki-default:#839496;--shiki-dark:#657B83">        .</span><span style="--shiki-default:#268BD2;--shiki-dark:#268BD2">navigationTitle</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">(</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"Detail </span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">\(</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">selection</span><span style="--shiki-default:#DC322F;--shiki-dark:#DC322F">)</span><span style="--shiki-default:#2AA198;--shiki-dark:#2AA198">"</span><span style="--shiki-default:#839496;--shiki-dark:#657B83">)
</span></span><span class="line" line="18"><span style="--shiki-default:#839496;--shiki-dark:#657B83">    }
</span></span><span class="line" line="19"><span style="--shiki-default:#839496;--shiki-dark:#657B83">}
</span></span></code></pre><p>With just this code, I could:</p><ul><li>Keep pushing new views to the stack from the inner navigation links</li><li>Pop to the root of the stack from within any view</li></ul><p>It's particularly nice that you only need to add the <code class="">navigationDestination</code> modifiers at the top level of the navigation hierarchy and any child views that have a navigation link will find that destination and use it to push onto the stack.</p><p>The only caveat that I ran into in my experimenting is that if you add a second <code class="">navigationDestination</code> for the same type that another destination has claimed on a child node inside the <code class="">NavigationStack</code> it's undefined behavior and so things break in interesting ways.</p><style>html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}</style>]]></description>
            <link>https://atelierclockwork.net/posts/2022/06/12/navigation-refined</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2022/06/12/navigation-refined</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Sun, 12 Jun 2022 20:38:11 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[WWDC Plans]]></title>
            <description><![CDATA[<h2 id="i-have-interesting-ideas-about-how-to-spend-my-free-time">I have interesting ideas about how to spend my free time</h2><p>My personal ritual every WWDC is to try to watch as many session videos as I can, with a focus on having a general list of interesting things to do more research into later. This approach has been a lot easier in the online-first WWDC format, because there have been more videos, but they're shorter and more focused, which makes it easier to focus on the things I'm most interested in.</p><p>This year I decided it's time to try to improve my miniature painting skills again and picked up entry level airbrushing gear, so I'll be attempting to paint a few kits while taking in WWDC sessions.</p><h2 id="drinking-from-the-firehose">Drinking from the firehose</h2><p>Since I'm trying to be as broad as possible, my strategy is to favorite all of the session videos from WWDC, and then attack them with a bit of a strategy:</p><ul><li>Keynote &#x26; Platforms State of the Union</li><li>What's new in…</li><li>New tooling and language features</li><li>Videos about SwiftUI</li><li>Videos about iOS development</li><li>Anything else as it catches my eye</li></ul><p>That list is just a general guideline, if something looks interesting I'll watch it. Last year I managed to watch somewhere between 1/2 and 2/3 of the session videos, and picked up some really useful details. I'm hoping to do hit a similar ratio by the end of summer, and I'll post about anything interesting that I find.</p>]]></description>
            <link>https://atelierclockwork.net/posts/2022/06/07/wwdc-plans</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2022/06/07/wwdc-plans</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Tue, 07 Jun 2022 21:02:34 GMT</pubDate>
        </item>
        <item>
            <title><![CDATA[Fresh Boot]]></title>
            <description><![CDATA[<h2 id="a-long-time-coming">A long time coming</h2><p>So after far too long, I've managed to rebuild my blog in a format I'm at least sort of happy with, and am ready to start posting again.</p><p>The site is now built using:</p><ul><li><a href="https://gohugo.io/" rel="nofollow">Hugo</a> with a custom theme for generating the content.</li><li><a href="https://tailwindcss.com/" rel="nofollow">Tailwind</a> for all of the styling.</li><li><a href="https://pages.github.com/" rel="nofollow">GitHub Pages</a> is hosting the content.</li><li><a href="https://fonts.google.com/" rel="nofollow">Google Fonts</a> serving the fonts.</li><li><a href="https://fontawesome.com/" rel="nofollow">Font Awesome</a> for SVG icons.</li></ul>]]></description>
            <link>https://atelierclockwork.net/posts/2022/05/27/fresh-boot</link>
            <guid isPermaLink="true">https://atelierclockwork.net/posts/2022/05/27/fresh-boot</guid>
            <dc:creator><![CDATA[Michael Skiba]]></dc:creator>
            <pubDate>Fri, 27 May 2022 14:15:45 GMT</pubDate>
        </item>
    </channel>
</rss>