<?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[Aleksander Stojanowski Dev Blog: Apple Platforms, and Swift Development]]></title><description><![CDATA[Explore the journey of Aleksander Stojanowski, a seasoned software engineer with 9+ years of experience in C/C++ and a passion for Apple platforms and Swift.]]></description><link>https://stojanowski.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 20 Apr 2026 12:55:47 GMT</lastBuildDate><atom:link href="https://stojanowski.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Sort It Out]]></title><description><![CDATA[Every app contains data that is shown to the user, and in most cases, users like to have at least a little control over how the data is displayed to them. The easiest way to give users some control is to allow them to choose by which property and in ...]]></description><link>https://stojanowski.dev/sort-it-out</link><guid isPermaLink="true">https://stojanowski.dev/sort-it-out</guid><category><![CDATA[Swift]]></category><category><![CDATA[SwiftData]]></category><category><![CDATA[CoreData]]></category><category><![CDATA[SwiftUI]]></category><category><![CDATA[Core data]]></category><category><![CDATA[Swift Core Data]]></category><dc:creator><![CDATA[Aleksander Stojanowski]]></dc:creator><pubDate>Mon, 26 Aug 2024 16:00:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724782806107/257e84e2-9aa8-449b-9216-5bfd22ee7b67.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every app contains data that is shown to the user, and in most cases, users like to have at least a little control over how the data is displayed to them. The easiest way to give users some control is to allow them to choose by which property and in what order they want to sort the information. Most applications have more than one screen with some form of records, so following the DRY principle, we should at least try to create a somewhat generic solution. Let's try to do it!</p>
<h1 id="heading-first-things-first">First Things First</h1>
<p>Before we dive into creating a sort menu, we need to have something to sort. First, we'll create a simple SwiftData model and some helpers. (I've written a <a target="_blank" href="https://ostojan.hashnode.dev/look-at-my-data">separate article</a> on how I tend to work with SwiftData in previews, so I'll skip most of the details.)</p>
<pre><code class="lang-Swift"><span class="hljs-keyword">import</span> Foundation
<span class="hljs-keyword">import</span> SwiftData
<span class="hljs-keyword">import</span> UIKit

@<span class="hljs-type">Model</span>
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToDoItem</span>: <span class="hljs-title">Codable</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">CodingKeys</span>: <span class="hljs-title">CodingKey</span> </span>{
        <span class="hljs-keyword">case</span> name
        <span class="hljs-keyword">case</span> done
        <span class="hljs-keyword">case</span> dueDate
        <span class="hljs-keyword">case</span> creationDate
    }

    <span class="hljs-keyword">var</span> name = <span class="hljs-string">""</span>
    <span class="hljs-keyword">var</span> done = <span class="hljs-literal">false</span>
    <span class="hljs-keyword">var</span> dueDate: <span class="hljs-type">Date?</span>
    <span class="hljs-keyword">let</span> creationDate = <span class="hljs-type">Date</span>.now

    <span class="hljs-keyword">init</span>(name: <span class="hljs-type">String</span> = <span class="hljs-string">""</span>, done: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>, dueDate: <span class="hljs-type">Date?</span> = <span class="hljs-literal">nil</span>) {
        <span class="hljs-keyword">self</span>.name = name
        <span class="hljs-keyword">self</span>.done = done
        <span class="hljs-keyword">self</span>.dueDate = dueDate
    }

    <span class="hljs-keyword">required</span> <span class="hljs-keyword">init</span>(from decoder: <span class="hljs-type">Decoder</span>) <span class="hljs-keyword">throws</span> {
        <span class="hljs-keyword">let</span> container = <span class="hljs-keyword">try</span> decoder.container(keyedBy: <span class="hljs-type">CodingKeys</span>.<span class="hljs-keyword">self</span>)
        name = <span class="hljs-keyword">try</span> container.decode(<span class="hljs-type">String</span>.<span class="hljs-keyword">self</span>, forKey: .name)
        done = <span class="hljs-keyword">try</span> container.decode(<span class="hljs-type">Bool</span>.<span class="hljs-keyword">self</span>, forKey: .done)
        dueDate = <span class="hljs-keyword">try</span> container.decodeIfPresent(<span class="hljs-type">Date</span>.<span class="hljs-keyword">self</span>, forKey: .dueDate)
        creationDate = <span class="hljs-keyword">try</span> container.decode(<span class="hljs-type">Date</span>.<span class="hljs-keyword">self</span>, forKey: .creationDate)
    }

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">encode</span><span class="hljs-params">(to encoder: Encoder)</span></span> <span class="hljs-keyword">throws</span> {
        <span class="hljs-keyword">var</span> container = encoder.container(keyedBy: <span class="hljs-type">CodingKeys</span>.<span class="hljs-keyword">self</span>)
        <span class="hljs-keyword">try</span> container.encode(name, forKey: .name)
        <span class="hljs-keyword">try</span> container.encode(done, forKey: .done)
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> dueDate {
            <span class="hljs-keyword">try</span> container.encode(dueDate, forKey: .dueDate)
        }
        <span class="hljs-keyword">try</span> container.encode(creationDate, forKey: .creationDate)
    }
}

@<span class="hljs-type">MainActor</span>
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PreviewModelContainerProvider</span> </span>{
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">let</span> shared = <span class="hljs-type">PreviewModelContainerProvider</span>()

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> toDoItems: [<span class="hljs-type">ToDoItem</span>]
    <span class="hljs-keyword">let</span> modelContainer: <span class="hljs-type">ModelContainer</span>

    <span class="hljs-keyword">var</span> modelContext: <span class="hljs-type">ModelContext</span> {
        modelContainer.mainContext
    }

    <span class="hljs-keyword">var</span> toDoItem: <span class="hljs-type">ToDoItem</span> {
        <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> item = toDoItems.first <span class="hljs-keyword">else</span> {
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"There is no data loaded"</span>)
        }
        <span class="hljs-keyword">return</span> item
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">init</span>() {
        <span class="hljs-keyword">let</span> configuration = <span class="hljs-type">ModelConfiguration</span>(isStoredInMemoryOnly: <span class="hljs-literal">true</span>)
        <span class="hljs-keyword">do</span> {
            modelContainer = <span class="hljs-keyword">try</span> <span class="hljs-type">ModelContainer</span>(<span class="hljs-keyword">for</span>: <span class="hljs-type">ToDoItem</span>.<span class="hljs-keyword">self</span>, configurations: configuration)
        } <span class="hljs-keyword">catch</span> {
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Could not create ModelContainer"</span>)
        }

        <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> previewData = <span class="hljs-type">NSDataAsset</span>(name: <span class="hljs-string">"ToDoItemPreviewsData"</span>)?.data <span class="hljs-keyword">else</span> {
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Preview data not found"</span>)
        }

        <span class="hljs-keyword">do</span> {
            toDoItems = <span class="hljs-keyword">try</span> <span class="hljs-type">JSONDecoder</span>().decode([<span class="hljs-type">ToDoItem</span>].<span class="hljs-keyword">self</span>, from: previewData)
            <span class="hljs-keyword">for</span> toDoItem <span class="hljs-keyword">in</span> toDoItems {
                modelContext.insert(toDoItem)
            }
        } <span class="hljs-keyword">catch</span> {
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Could not decode preview data"</span>)
        }
    }
}
</code></pre>
<p>Here, we have a JSON file with the stub data that we need to add to our <code>Preview Assets</code>.</p>
<pre><code class="lang-JSON">[
    {
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Buy groceries"</span>,
        <span class="hljs-attr">"creationDate"</span>: <span class="hljs-number">746172000</span>
    },
    {
        <span class="hljs-attr">"dueDate"</span>: <span class="hljs-number">746258400</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Walk the dog"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"creationDate"</span>: <span class="hljs-number">746085600</span>
    },
    {
        <span class="hljs-attr">"dueDate"</span>: <span class="hljs-number">746344800</span>,
        <span class="hljs-attr">"creationDate"</span>: <span class="hljs-number">745999200</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Read a book"</span>
    },
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Complete homework"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"creationDate"</span>: <span class="hljs-number">745912800</span>
    },
    {
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"dueDate"</span>: <span class="hljs-number">746517600</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Call mom"</span>,
        <span class="hljs-attr">"creationDate"</span>: <span class="hljs-number">745826400</span>
    },
    {
        <span class="hljs-attr">"dueDate"</span>: <span class="hljs-number">746604000</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Pay bills"</span>,
        <span class="hljs-attr">"creationDate"</span>: <span class="hljs-number">745740000</span>
    },
    {
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"creationDate"</span>: <span class="hljs-number">745653600</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Schedule dentist appointment"</span>
    },
    {
        <span class="hljs-attr">"dueDate"</span>: <span class="hljs-number">746776800</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Clean the house"</span>,
        <span class="hljs-attr">"creationDate"</span>: <span class="hljs-number">745567200</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">true</span>
    },
    {
        <span class="hljs-attr">"creationDate"</span>: <span class="hljs-number">745480800</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">false</span>,
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Exercise"</span>,
        <span class="hljs-attr">"dueDate"</span>: <span class="hljs-number">746863200</span>
    },
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Prepare dinner"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"creationDate"</span>: <span class="hljs-number">745394400</span>
    }
]
</code></pre>
<p>The last thing we need to do is create a simple UI to display our data. Let's create a list with rows presenting all the information that each <code>ToDoItem</code> contains.</p>
<pre><code class="lang-Swift"><span class="hljs-keyword">import</span> SwiftData
<span class="hljs-keyword">import</span> SwiftUI

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ToDoItemRow</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> toDoItem: <span class="hljs-type">ToDoItem</span>

    <span class="hljs-keyword">init</span>(<span class="hljs-keyword">for</span> toDoItem: <span class="hljs-type">ToDoItem</span>) {
        <span class="hljs-keyword">self</span>.toDoItem = toDoItem
    }

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Button</span> {
            toDoItem.done.toggle()
        } label: {
            <span class="hljs-type">HStack</span> {
                <span class="hljs-type">Image</span>(systemName: toDoItem.done ? <span class="hljs-string">"checkmark.circle"</span> : <span class="hljs-string">"circle"</span>)
                    .font(.title)
                <span class="hljs-type">VStack</span>(alignment: .leading) {
                    <span class="hljs-type">Text</span>(toDoItem.name)
                        .font(.headline)
                        .strikethrough(toDoItem.done)
                    <span class="hljs-type">HStack</span> {
                        <span class="hljs-type">LabeledContent</span>(<span class="hljs-string">"Creation date"</span>) {
                            <span class="hljs-type">Text</span>(toDoItem.creationDate.formatted(date: .long, time: .omitted))
                        }
                        <span class="hljs-type">LabeledContent</span>(<span class="hljs-string">"Due date"</span>) {
                            <span class="hljs-type">Text</span>(toDoItem.dueDate?.formatted(date: .long, time: .omitted) ?? <span class="hljs-string">"-"</span>)
                        }
                    }
                    .font(.caption2)
                }
            }
        }
        .buttonStyle(.plain)
    }
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">Query</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> items: [<span class="hljs-type">ToDoItem</span>]

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">NavigationStack</span> {
            <span class="hljs-type">List</span> {
                <span class="hljs-type">ForEach</span>(items) { item <span class="hljs-keyword">in</span>
                    <span class="hljs-type">ToDoItemRow</span>(<span class="hljs-keyword">for</span>: item)
                }
            }
            .navigationTitle(<span class="hljs-string">"To Do"</span>)
        }
    }
}

#<span class="hljs-type">Preview</span> {
    <span class="hljs-type">ContentView</span>()
        .modelContainer(<span class="hljs-type">PreviewModelContainerProvider</span>.shared.modelContainer)
}
</code></pre>
<p>With all that in place, we have a solid foundation to continue our work. Let's dive in!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724582222835/18b86e92-8464-44a6-8360-a74169d77774.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-brace-yourselves-sorting-is-coming">Brace Yourselves - Sorting Is Coming</h1>
<p>As you may have noticed, every time our app (or rather the preview) reloads, the data is presented in a different order. This is because each time we load our data in the preview helper, it creates new entries in memory, and we haven't specified any sorting for our <code>@Query</code>. We can fix that really quickly.</p>
<pre><code class="lang-Swift">@<span class="hljs-type">Query</span>(<span class="hljs-built_in">sort</span>: [<span class="hljs-type">SortDescriptor</span>(\<span class="hljs-type">ToDoItem</span>.name)])
<span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> items: [<span class="hljs-type">ToDoItem</span>]
</code></pre>
<p>Much better, we no longer have a random element, even in our previews. However, we want to allow users to change the sort parameter and order dynamically. This is a bit trickier, as we can't just create a <code>@State</code> with our sort descriptors. We need to extract the <code>@Query</code> to a separate <code>View</code> and feed it with our dynamic sort descriptors.</p>
<pre><code class="lang-Swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ToDoItemsListing</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">Query</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> items: [<span class="hljs-type">ToDoItem</span>]

    <span class="hljs-keyword">init</span>(<span class="hljs-built_in">sort</span> sortDescriptors: [<span class="hljs-type">SortDescriptor</span>&lt;<span class="hljs-type">ToDoItem</span>&gt;]) {
        _items = <span class="hljs-type">Query</span>(
            <span class="hljs-built_in">sort</span>: sortDescriptors,
            animation: .<span class="hljs-keyword">default</span>
        )
    }

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">List</span> {
            <span class="hljs-type">ForEach</span>(items) { item <span class="hljs-keyword">in</span>
                <span class="hljs-type">ToDoItemRow</span>(<span class="hljs-keyword">for</span>: item)
            }
        }
    }
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">NavigationStack</span> {
            <span class="hljs-type">ToDoItemsListing</span>(<span class="hljs-built_in">sort</span>: [<span class="hljs-type">SortDescriptor</span>(\.name)])
                .navigationTitle(<span class="hljs-string">"To Do"</span>)
        }
    }
}
</code></pre>
<p>That's it! We can now easily and dynamically change the sorting. However, there is one caveat: we can't sort by the <code>done</code> property of our <code>ToDoItem</code>. This is because <code>Bool</code> doesn't conform to the <code>Comparable</code> protocol. Luckily, that's a really simple fix.</p>
<pre><code class="lang-Swift"><span class="hljs-keyword">import</span> Foundation

<span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">Bool</span>: <span class="hljs-title">Comparable</span> </span>{
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-function"><span class="hljs-keyword">func</span> &lt; (lhs: Bool, rhs: Bool) -&gt; <span class="hljs-title">Bool</span> </span>{
        !lhs &amp;&amp; rhs
    }
}
</code></pre>
<p>Now, all properties of our model can be used for sorting. We can dynamically change <code>SortDescriptor</code>s, so we are ready to give users control over the order in which the application presents data.</p>
<h1 id="heading-trust-me-im-a-user">Trust Me, I'm a User</h1>
<p>Before we give users control, we need to make sure all the gears are ready for that. We want users to be able to choose between the properties of our model. This sounds like a task for some <code>enum</code>. Let's create one for our <code>ToDoItem</code>. Of course, a simple enum is not enough; we need something more, like some form of <code>SortDescriptor</code> provider.</p>
<pre><code class="lang-Swift"><span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">ToDoItem</span> </span>{
    <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">ToDoItemSortDescriptorProvider</span> </span>{
        <span class="hljs-keyword">case</span> name
        <span class="hljs-keyword">case</span> done
        <span class="hljs-keyword">case</span> dueDate
        <span class="hljs-keyword">case</span> creationDate

        <span class="hljs-keyword">var</span> sortDescriptors: [<span class="hljs-type">SortDescriptor</span>&lt;<span class="hljs-type">ToDoItem</span>&gt;] {
            <span class="hljs-keyword">switch</span> <span class="hljs-keyword">self</span> {
            <span class="hljs-keyword">case</span> .name:
                [<span class="hljs-type">SortDescriptor</span>(\.name)]
            <span class="hljs-keyword">case</span> .done:
                [<span class="hljs-type">SortDescriptor</span>(\.done)]
            <span class="hljs-keyword">case</span> .dueDate:
                [<span class="hljs-type">SortDescriptor</span>(\.dueDate)]
            <span class="hljs-keyword">case</span> .creationDate:
                [<span class="hljs-type">SortDescriptor</span>(\.creationDate)]
            }
        }
    }
}
</code></pre>
<p>Nice! But… let's think about what happens if we use those single-item <code>SortDescriptor</code> lists. Take, for example, the descriptor for the <code>done</code> property. It sorts items as it should, with <em>false</em> items on top and <em>true</em> items on the bottom, but all other properties are not considered.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724582234525/f30b0950-7b1b-445d-ac26-7f1e95ef3e90.png" alt class="image--center mx-auto" /></p>
<p>Fortunately, we return a list of sort descriptors, so we just need to add more descriptors for each case. For that, we need to arbitrarily choose an order for them, so let's use this as the <em>default</em> order: <code>done</code>, <code>dueDate</code>, <code>name</code>, and <code>creationDate</code>. This seems to make the most sense.</p>
<pre><code class="lang-Swift"><span class="hljs-keyword">var</span> sortDescriptors: [<span class="hljs-type">SortDescriptor</span>&lt;<span class="hljs-type">ToDoItem</span>&gt;] {
    <span class="hljs-keyword">let</span> doneDescriptor = <span class="hljs-type">SortDescriptor</span>(\<span class="hljs-type">ToDoItem</span>.done)
    <span class="hljs-keyword">let</span> dueDateDescriptor = <span class="hljs-type">SortDescriptor</span>(\<span class="hljs-type">ToDoItem</span>.dueDate)
    <span class="hljs-keyword">let</span> nameDescriptor = <span class="hljs-type">SortDescriptor</span>(\<span class="hljs-type">ToDoItem</span>.name)
    <span class="hljs-keyword">let</span> creationDateDescriptor = <span class="hljs-type">SortDescriptor</span>(\<span class="hljs-type">ToDoItem</span>.creationDate)
    <span class="hljs-keyword">switch</span> <span class="hljs-keyword">self</span> {
    <span class="hljs-keyword">case</span> .name:
        <span class="hljs-keyword">return</span> [nameDescriptor, doneDescriptor, dueDateDescriptor, creationDateDescriptor]
    <span class="hljs-keyword">case</span> .done:
        <span class="hljs-keyword">return</span> [doneDescriptor, dueDateDescriptor, nameDescriptor, creationDateDescriptor]
    <span class="hljs-keyword">case</span> .dueDate:
        <span class="hljs-keyword">return</span> [dueDateDescriptor, doneDescriptor, nameDescriptor, creationDateDescriptor]
    <span class="hljs-keyword">case</span> .creationDate:
        <span class="hljs-keyword">return</span> [creationDateDescriptor, doneDescriptor, dueDateDescriptor, nameDescriptor]
    }
}
</code></pre>
<p>Much better! Next, we need to actually use it. Since we want it to be changeable by the user, we need to add some <code>@State</code> to our view.</p>
<pre><code class="lang-Swift">@<span class="hljs-type">State</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> sortDescriptorProvider = <span class="hljs-type">ToDoItem</span>.<span class="hljs-type">ToDoItemSortDescriptorProvider</span>.done

<span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
    <span class="hljs-type">NavigationStack</span> {
        <span class="hljs-type">ToDoItemsListing</span>(<span class="hljs-built_in">sort</span>: sortDescriptorProvider.sortDescriptors)
            .navigationTitle(<span class="hljs-string">"To Do"</span>)
    }
}
</code></pre>
<p>The easy part is behind us. Now, we need to create a UI that allows the user to change sorting parameters. There are many possible ways to implement such an interaction, but we'll create a menu accessible from the toolbar.</p>
<p>Of course, we could create this menu manually, but a large part of the code would be the same for each sort property. To avoid that, let's first make our <code>enum</code> conform to the <code>CaseIterable</code> protocol, especially since this change is trivial. However, that's not all we need to do before we can use it. It would be nice to have a conversion between our <code>enum</code> and a user-readable string. This is also painless; we just need to add a raw value for it. The last thing we need to add is yet another protocol conformance, this time to <code>Identifiable</code>, as we want to use it in a <code>ForEach</code> view builder.</p>
<pre><code class="lang-Swift"><span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">ToDoItemSortDescriptorProvider</span>: <span class="hljs-title">String</span>, <span class="hljs-title">CaseIterable</span>, <span class="hljs-title">Identifiable</span> </span>{
    <span class="hljs-keyword">case</span> name = <span class="hljs-string">"Name"</span>
    <span class="hljs-keyword">case</span> done = <span class="hljs-string">"Done"</span>
    <span class="hljs-keyword">case</span> dueDate = <span class="hljs-string">"Due Date"</span>
    <span class="hljs-keyword">case</span> creationDate = <span class="hljs-string">"Creation Date"</span>

    <span class="hljs-keyword">var</span> id: <span class="hljs-type">Self</span> {
        <span class="hljs-keyword">self</span>
    }
    ...
}
</code></pre>
<p>With that in place, we can create our <code>Menu</code> pretty straightforwardly.</p>
<pre><code class="lang-Swift"><span class="hljs-type">ToDoItemsListing</span>(<span class="hljs-built_in">sort</span>: sortDescriptorProvider.sortDescriptors)
    .navigationTitle(<span class="hljs-string">"To Do"</span>)
    .toolbar {
        <span class="hljs-type">Menu</span> {
            <span class="hljs-type">ForEach</span>(<span class="hljs-type">ToDoItem</span>.<span class="hljs-type">ToDoItemSortDescriptorProvider</span>.allCases) { provider <span class="hljs-keyword">in</span>
                <span class="hljs-type">Button</span> {
                    withAnimation {
                        sortDescriptorProvider = provider
                    }
                } label: {
                    <span class="hljs-keyword">if</span> provider == sortDescriptorProvider {
                        <span class="hljs-type">Label</span>(provider.rawValue, systemImage: <span class="hljs-string">"checkmark"</span>)
                    } <span class="hljs-keyword">else</span> {
                        <span class="hljs-type">Text</span>(provider.rawValue)
                    }
                }
            }
        } label: {
            <span class="hljs-type">Label</span>(<span class="hljs-string">"Sort"</span>, systemImage: <span class="hljs-string">"arrow.left.arrow.right"</span>)
        }
    }
</code></pre>
<p>Finally, we have a menu that the user can use to choose which property the application should use to sort and present data. However, there is a flaw in the current solution: the user can choose the property but not the sort order (ascending or descending). Luckily, our code is easily modifiable to handle that as well. All we need to do is change the provider's <code>sortDescriptors</code> property into a method that accepts <code>SortOrder</code> and modify the sort menu a little.</p>
<pre><code class="lang-Swift"><span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">ToDoItemSortDescriptorProvider</span>: <span class="hljs-title">String</span>, <span class="hljs-title">CaseIterable</span>, <span class="hljs-title">Identifiable</span> </span>{
    ...
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">sortDescriptors</span><span class="hljs-params">(order: SortOrder)</span></span> -&gt; [<span class="hljs-type">SortDescriptor</span>&lt;<span class="hljs-type">ToDoItem</span>&gt;] {
        <span class="hljs-keyword">let</span> doneDescriptor = <span class="hljs-type">SortDescriptor</span>(\<span class="hljs-type">ToDoItem</span>.done, order: order)
        <span class="hljs-keyword">let</span> dueDateDescriptor = <span class="hljs-type">SortDescriptor</span>(\<span class="hljs-type">ToDoItem</span>.dueDate, order: order)
        <span class="hljs-keyword">let</span> nameDescriptor = <span class="hljs-type">SortDescriptor</span>(\<span class="hljs-type">ToDoItem</span>.name, order: order)
        <span class="hljs-keyword">let</span> creationDateDescriptor = <span class="hljs-type">SortDescriptor</span>(\<span class="hljs-type">ToDoItem</span>.creationDate, order: order)
        <span class="hljs-keyword">switch</span> <span class="hljs-keyword">self</span> {
        <span class="hljs-keyword">case</span> .name:
            <span class="hljs-keyword">return</span> [nameDescriptor, doneDescriptor, dueDateDescriptor, creationDateDescriptor]
        <span class="hljs-keyword">case</span> .done:
            <span class="hljs-keyword">return</span> [doneDescriptor, dueDateDescriptor, nameDescriptor, creationDateDescriptor]
        <span class="hljs-keyword">case</span> .dueDate:
            <span class="hljs-keyword">return</span> [dueDateDescriptor, doneDescriptor, nameDescriptor, creationDateDescriptor]
        <span class="hljs-keyword">case</span> .creationDate:
            <span class="hljs-keyword">return</span> [creationDateDescriptor, doneDescriptor, dueDateDescriptor, nameDescriptor]
        }
    }
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">State</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> sortDescriptorProvider = <span class="hljs-type">ToDoItem</span>.<span class="hljs-type">ToDoItemSortDescriptorProvider</span>.done
    @<span class="hljs-type">State</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> sortOrder = <span class="hljs-type">SortOrder</span>.forward

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">NavigationStack</span> {
            <span class="hljs-type">ToDoItemsListing</span>(<span class="hljs-built_in">sort</span>: sortDescriptorProvider.sortDescriptors(order: sortOrder))
                .navigationTitle(<span class="hljs-string">"To Do"</span>)
                .toolbar {
                    <span class="hljs-type">Menu</span> {
                        <span class="hljs-type">ForEach</span>(<span class="hljs-type">ToDoItem</span>.<span class="hljs-type">ToDoItemSortDescriptorProvider</span>.allCases) { provider <span class="hljs-keyword">in</span>
                            <span class="hljs-type">Button</span> {
                                withAnimation {
                                    <span class="hljs-keyword">if</span> provider == sortDescriptorProvider {
                                        sortOrder = sortOrder == .forward ? .<span class="hljs-built_in">reverse</span> : .forward
                                    } <span class="hljs-keyword">else</span> {
                                        sortDescriptorProvider = provider
                                        sortOrder = .forward
                                    }
                                }
                            } label: {
                                <span class="hljs-keyword">if</span> provider == sortDescriptorProvider {
                                    <span class="hljs-type">Label</span>(
                                        provider.rawValue,
                                        systemImage: sortOrder == .forward ? <span class="hljs-string">"arrow.up"</span> : <span class="hljs-string">"arrow.down"</span>
                                    )
                                } <span class="hljs-keyword">else</span> {
                                    <span class="hljs-type">Text</span>(provider.rawValue)
                                }
                            }
                        }
                    } label: {
                        <span class="hljs-type">Label</span>(<span class="hljs-string">"Sort"</span>, systemImage: <span class="hljs-string">"arrow.left.arrow.right"</span>)
                    }
                }
        }
    }
}
</code></pre>
<p>We created a nice solution that allows the user to select not only the sort property but also the sort order. Moreover, the code is simple and maintainable, but it is not portable. Let's say we have a second model and a view presenting it. We would need to create almost all of the code again, just this time for the new model and its properties. We need to change that and stick to the DRY principle.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724582259033/9c67cbeb-c1ff-4631-99d4-8f83d371cead.gif" alt class="image--center mx-auto" /></p>
<h1 id="heading-prepare-for-more">Prepare for More</h1>
<p>Think a bit about what we can make generic in our case. The <code>ToDoItemSortDescriptorProvider</code> seems to be a good candidate, despite the fact that it needs to be different for each model. The sort menu we built operates directly on the <code>enum</code> for the <code>ToDoItem</code> provider, but it doesn't care about what information it provides, just how it provides it. That's an excellent candidate to abstract as a protocol that provider enums should conform to. We can even make it a bit more readable and add a helper property for our user-readable value, so we'll start with that.</p>
<pre><code class="lang-Swift"><span class="hljs-keyword">import</span> Foundation
<span class="hljs-keyword">import</span> SwiftData

<span class="hljs-class"><span class="hljs-keyword">protocol</span> <span class="hljs-title">SortDescriptorProvider</span>: <span class="hljs-title">CaseIterable</span>, <span class="hljs-title">Identifiable</span>, <span class="hljs-title">RawRepresentable</span> <span class="hljs-title">where</span> <span class="hljs-title">RawValue</span> == <span class="hljs-title">String</span>, <span class="hljs-title">AllCases</span>: <span class="hljs-title">RandomAccessCollection</span> </span>{
    <span class="hljs-keyword">associatedtype</span> <span class="hljs-type">Model</span>: <span class="hljs-type">PersistentModel</span>

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">sortDescriptors</span><span class="hljs-params">(order: SortOrder)</span></span> -&gt; [<span class="hljs-type">SortDescriptor</span>&lt;<span class="hljs-type">Model</span>&gt;]
}

<span class="hljs-class"><span class="hljs-keyword">extension</span> <span class="hljs-title">SortDescriptorProvider</span> </span>{
    <span class="hljs-keyword">var</span> id: <span class="hljs-type">Self</span> {
        <span class="hljs-keyword">self</span>
    }

    <span class="hljs-keyword">var</span> name: <span class="hljs-type">String</span> {
        rawValue
    }
}
</code></pre>
<p>There is one limitation to this approach. Even though our protocol implements the <code>RawRepresentable</code> protocol with the <code>associatedtype</code> forced to <code>String</code>, when implementing concrete providers (considering we implement it as an enum), we need to repeat the raw value type that the provider supplies. Let's make our <code>ToDoItemSortDescriptorProvider</code> conform to our new protocol.</p>
<pre><code class="lang-Swift"><span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">ToDoItemSortDescriptorProvider</span>: <span class="hljs-title">String</span>, <span class="hljs-title">SortDescriptorProvider</span> </span>{
    <span class="hljs-keyword">case</span> name = <span class="hljs-string">"Name"</span>
    <span class="hljs-keyword">case</span> done = <span class="hljs-string">"Done"</span>
    <span class="hljs-keyword">case</span> dueDate = <span class="hljs-string">"Due Date"</span>
    <span class="hljs-keyword">case</span> creationDate = <span class="hljs-string">"Creation Date"</span>

    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">sortDescriptors</span><span class="hljs-params">(order: SortOrder)</span></span> -&gt; [<span class="hljs-type">SortDescriptor</span>&lt;<span class="hljs-type">ToDoItem</span>&gt;] {
            ...
    }
}
</code></pre>
<p>We could even remove some code here, which is always nice.</p>
<p>Now we need to make use of this new protocol and have the sort menu use it. For that, we need to extract it into another view. Doing that is also a good opportunity to clean up and refactor the code a bit.</p>
<pre><code class="lang-Swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">SortMenu</span>&lt;<span class="hljs-title">Provider</span>&gt;: <span class="hljs-title">View</span> <span class="hljs-title">where</span> <span class="hljs-title">Provider</span>: <span class="hljs-title">SortDescriptorProvider</span> </span>{
    @<span class="hljs-type">Binding</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> sortProvider: <span class="hljs-type">Provider</span>
    @<span class="hljs-type">Binding</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> sortOrder: <span class="hljs-type">SortOrder</span>

    <span class="hljs-keyword">init</span>(sortProvider: <span class="hljs-type">Binding</span>&lt;<span class="hljs-type">Provider</span>&gt;, sortOrder: <span class="hljs-type">Binding</span>&lt;<span class="hljs-type">SortOrder</span>&gt;) {
        _sortProvider = sortProvider
        _sortOrder = sortOrder
    }

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Menu</span> {
            <span class="hljs-type">ForEach</span>(<span class="hljs-type">Provider</span>.allCases) { provider <span class="hljs-keyword">in</span>
                <span class="hljs-keyword">if</span> provider == sortProvider {
                    currentProviderButton
                } <span class="hljs-keyword">else</span> {
                    button(<span class="hljs-keyword">for</span>: provider)
                }
            }
        } label: {
            <span class="hljs-type">Label</span>(<span class="hljs-string">"Sort"</span>, systemImage: <span class="hljs-string">"arrow.left.arrow.right"</span>)
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> currentProviderButton: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Button</span> {
            sortOrder = sortOrder == .forward ? .<span class="hljs-built_in">reverse</span> : .forward
        } label: {
            <span class="hljs-type">Label</span>(
                sortProvider.name,
                systemImage: sortOrder == .forward ? <span class="hljs-string">"arrow.up"</span> : <span class="hljs-string">"arrow.down"</span>
            )
        }
    }

    <span class="hljs-keyword">private</span> <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">button</span><span class="hljs-params">(<span class="hljs-keyword">for</span> provider: Provider)</span></span> -&gt; some <span class="hljs-type">View</span> {
        <span class="hljs-type">Button</span> {
            sortProvider = provider
            sortOrder = .forward
        } label: {
            <span class="hljs-type">Text</span>(provider.name)
        }
    }
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    ...
    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        ...
            .toolbar {
                <span class="hljs-type">SortMenu</span>(sortProvider: $sortDescriptorProvider.animation(), sortOrder: $sortOrder.animation())
            }
    }
}
</code></pre>
<p>That's it; we are ready for more models and more views with a sort menu to come!</p>
<h1 id="heading-whats-next">What's Next?</h1>
<p>Right now, it might seem like we didn't save much code and even had to write more to achieve the same simple result. That's true in this particular case, but think about more realistic examples, such as models with relationships where the application presents a list of entities that links to another list of different entities. With this approach, for each such list, we need a provider <code>enum</code> (or <code>struct</code>, or <code>class</code>, depending on what you choose), two state variables, and one line with the SortMenu view.</p>
<p>One might also ask if this approach is suitable for other persistent data frameworks, and the answer is yes, it is! I chose <code>SwiftData</code> because I've been working with it the most lately and came up with this idea specifically for that framework, but nothing prevents you from using it for something else. All you need to do is change the <code>SortDescriptor</code> type used by <code>SwiftData</code> to some other type (e.g., <code>SortComparator</code> for <code>Core Data</code>), and you can basically use the same code.</p>
<p>The whole solution can be found on my <a target="_blank" href="https://github.com/ostojan/SortItOut">GitHub</a>, and each section of this article is a separate commit in the repository.</p>
]]></content:encoded></item><item><title><![CDATA[Look at My Data]]></title><description><![CDATA[In the last few years, Apple has built a very convenient set of frameworks to build applications in their ecosystem. However, sometimes connecting all the bits and pieces together can be a bit cumbersome, especially since there is no one-size-fits-al...]]></description><link>https://stojanowski.dev/look-at-my-data</link><guid isPermaLink="true">https://stojanowski.dev/look-at-my-data</guid><category><![CDATA[previews]]></category><category><![CDATA[Swift]]></category><category><![CDATA[SwiftUI]]></category><category><![CDATA[SwiftData]]></category><category><![CDATA[SwiftUI previews]]></category><category><![CDATA[preview]]></category><dc:creator><![CDATA[Aleksander Stojanowski]]></dc:creator><pubDate>Mon, 12 Aug 2024 16:00:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724782845344/477429cd-7a9c-421e-8f5c-b794984dc621.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the last few years, Apple has built a very convenient set of frameworks to build applications in their ecosystem. However, sometimes connecting all the bits and pieces together can be a bit cumbersome, especially since there is no one-size-fits-all solution in the software development industry. Every engineer working on code has their own preferences and likings. With all that said, let me show you how I tend to work with the <code>SwiftData</code> and <code>SwiftUI</code> <code>Previews</code> mechanism.</p>
<h1 id="heading-it-all-starts-with-a-model">It All Starts With a Model</h1>
<p>Every (or almost every) application nowadays stores some kind of data, and to work with SwiftData, we need to create a model. Since this article focuses on working with <code>Previews</code> rather than creating sophisticated data models, we'll use the most clichéd example there is - a to-do item.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> SwiftData

@<span class="hljs-type">Model</span>
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ToDoItem</span> </span>{
    <span class="hljs-keyword">var</span> name = <span class="hljs-string">""</span>
    <span class="hljs-keyword">var</span> done = <span class="hljs-literal">false</span>

    <span class="hljs-keyword">init</span>(name: <span class="hljs-type">String</span> = <span class="hljs-string">""</span>, done: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>) {
        <span class="hljs-keyword">self</span>.name = name
        <span class="hljs-keyword">self</span>.done = done
    }
}
</code></pre>
<p>That could be it, but I like my models to implement the <code>Codable</code> protocol (my motivation will become clear in the next chapter). If our model class were a simple <code>struct</code> or <code>class</code>, we'd simply add <code>: Codable</code> to its definition. However, since it's wrapped in the @Model macro, we need to explicitly implement this protocol. To do so, we need to add an enum with CodingKey and implement the required methods.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">private</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">CodingKeys</span>: <span class="hljs-title">CodingKey</span> </span>{
    <span class="hljs-keyword">case</span> name
    <span class="hljs-keyword">case</span> done
}

<span class="hljs-keyword">required</span> <span class="hljs-keyword">init</span>(from decoder: <span class="hljs-type">Decoder</span>) <span class="hljs-keyword">throws</span> {
    <span class="hljs-keyword">let</span> container = <span class="hljs-keyword">try</span> decoder.container(keyedBy: <span class="hljs-type">CodingKeys</span>.<span class="hljs-keyword">self</span>)
    name = <span class="hljs-keyword">try</span> container.decode(<span class="hljs-type">String</span>.<span class="hljs-keyword">self</span>, forKey: .name)
    done = <span class="hljs-keyword">try</span> container.decode(<span class="hljs-type">Bool</span>.<span class="hljs-keyword">self</span>, forKey: .done)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">encode</span><span class="hljs-params">(to encoder: Encoder)</span></span> <span class="hljs-keyword">throws</span> {
    <span class="hljs-keyword">var</span> container = encoder.container(keyedBy: <span class="hljs-type">CodingKeys</span>.<span class="hljs-keyword">self</span>)
    <span class="hljs-keyword">try</span> container.encode(name, forKey: .name)
    <span class="hljs-keyword">try</span> container.encode(done, forKey: .done)
}
</code></pre>
<p>Our model class is ready, and we can move further with our work.</p>
<h1 id="heading-harness-the-terminator">Harness the Terminator</h1>
<p>The saddest part of working with previews (or rather, working on any UI in any framework and/or technology) for me has always been stub data. Everyone knows <code>Lorem ipsum</code>; everyone has created at least one <code>Item 1</code>, <code>Item 2</code>. All that just to show something on the screen, to see how the final product could look. Everything seems fine, but it has one big disadvantage - the final product contains <em>real</em> data, not some ancient thesis or sequences of generated numbers. Of course, we could write some real examples with actual data, but that's a waste of time. After all, there's a reason why the <code>Lorem ipsum</code> generator is so widely used.</p>
<p>However, nowadays we have tools that can help us with that kind of work and give us the needed data. It's, of course, <code>&lt;here place name of your favorite AI chatbot&gt;</code>! We can use it to generate stubs for our models. Even the free tiers on any given platform should be able to give us reasonable results.</p>
<p>As I mentioned earlier, I have my motivation to make model classes compliant with the <code>Codable</code> protocol, and you might know at this point what it is. With <em>codable</em> models, we can ask a chatbot to generate data in JSON format and easily manage it as a data asset in the project.</p>
<p>Having explained all that, let's create a prompt we can use to make a chatbot generate our stub data (<strong>important note here: providing any information to any chatbot may <em>feed</em> it with your data, so be careful what you send to it, especially when working for someone</strong>).</p>
<pre><code class="lang-Plaintext">Here is my model class:

&lt;code of our model&gt;

Generate for array of 10 items in JSON format.
</code></pre>
<p>We get, as a result, a ready-to-use list.</p>
<pre><code class="lang-JSON">[
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Buy groceries"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">false</span>
    },
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Walk the dog"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">true</span>
    },
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Read a book"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">false</span>
    },
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Complete homework"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">true</span>
    },
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Call mom"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">false</span>
    },
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Pay bills"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">true</span>
    },
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Schedule dentist appointment"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">false</span>
    },
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Clean the house"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">true</span>
    },
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Exercise"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">false</span>
    },
    {
        <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Prepare dinner"</span>,
        <span class="hljs-attr">"done"</span>: <span class="hljs-literal">true</span>
    }
]
</code></pre>
<p>The last thing left to do is to save the result to a file. We'll name it <code>ToDoItemPreviewsData.json</code> and add it to <code>Preview Assets</code> in our project (simply drag the file into <code>Preview Assets</code> in Xcode).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723319271403/04d53918-11f7-4e9f-b503-d58ca5e4b176.png" alt class="image--center mx-auto" /></p>
<p>Instead of adding this file to assets, we could add the file as part of the sources (and access it from within the bundle), but since assets can handle data files and not only images, I prefer to do it this way.</p>
<h1 id="heading-glue-it-all-together">Glue It All Together</h1>
<p>Our preparation is finished, and we can now start working on using stubs with previews. However, we don't want to create a <code>modelContainer</code>, load data, and create models for each view separately. Instead, we'll create a helper that does all that for us. We need to start somewhere, so let's create a simple singleton provider for our provider class.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> SwiftData

<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PreviewModelContainerProvider</span> </span>{
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">let</span> shared = <span class="hljs-type">PreviewModelContainerProvider</span>()

    <span class="hljs-keyword">let</span> modelContainer: <span class="hljs-type">ModelContainer</span>

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">init</span>() {
        <span class="hljs-keyword">let</span> configuration = <span class="hljs-type">ModelConfiguration</span>(isStoredInMemoryOnly: <span class="hljs-literal">true</span>)
        <span class="hljs-keyword">do</span> {
            modelContainer = <span class="hljs-keyword">try</span> <span class="hljs-type">ModelContainer</span>(<span class="hljs-keyword">for</span>: <span class="hljs-type">ToDoItem</span>.<span class="hljs-keyword">self</span>, configurations: configuration)
        } <span class="hljs-keyword">catch</span> {
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Could not create ModelContainer"</span>)
        }
    }
}
</code></pre>
<p>With that, we can use it in our view for its preview.</p>
<pre><code class="lang-Swift"><span class="hljs-keyword">import</span> SwiftData
<span class="hljs-keyword">import</span> SwiftUI

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">Query</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> toDoItems: [<span class="hljs-type">ToDoItem</span>]

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">List</span> {
            <span class="hljs-type">ForEach</span>(toDoItems) { toDoItem <span class="hljs-keyword">in</span>
                <span class="hljs-type">Label</span>(toDoItem.name, systemImage: toDoItem.done ? <span class="hljs-string">"checkmark.circle"</span> : <span class="hljs-string">"circle"</span>)
            }
        }
    }
}

#<span class="hljs-type">Preview</span> {
    <span class="hljs-type">ContentView</span>()
        .modelContainer(<span class="hljs-type">PreviewModelContainerProvider</span>.shared.modelContainer)
}
</code></pre>
<p>All that works, but we don't have any data to show. We'll fix that by loading data from the file inside our <code>PreviewModelContainerProvider</code> constructor. However, as simple as that task sounds, we need to write quite a few lines of code.</p>
<p>First, to insert some models, we need to access the <code>ModelContext</code> of our container. The problem with this is that the <code>mainContext</code> of a <code>ModelContainer</code> is a main actor-isolated property, so we either need to make the constructor <code>async</code> or use <code>@MainActor</code> to isolate the class. We can't do the former because we keep a static instance for the singleton, so we need to isolate the whole class.</p>
<pre><code class="lang-Swift">@<span class="hljs-type">MainActor</span>
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PreviewModelContainerProvider</span> </span>{
...
}
</code></pre>
<p>The second thing we need to do is load data from the file we created in the previous section. Luckily, that's really easy - we just use the <code>NSDataAsset</code> class from <code>UIKit</code>.</p>
<pre><code class="lang-Swift"><span class="hljs-comment">// Don't forget to add the UIKit import</span>
<span class="hljs-keyword">import</span> UIKit

@<span class="hljs-type">MainActor</span>
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PreviewModelContainerProvider</span> </span>{
    ...
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">init</span>() {
        ...
        <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> previewData = <span class="hljs-type">NSDataAsset</span>(name: <span class="hljs-string">"ToDoItemPreviewsData"</span>)?.data <span class="hljs-keyword">else</span> {
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Preview data not found"</span>)
        }

        <span class="hljs-keyword">do</span> {
            <span class="hljs-keyword">let</span> toDoItems = <span class="hljs-keyword">try</span> <span class="hljs-type">JSONDecoder</span>().decode([<span class="hljs-type">ToDoItem</span>].<span class="hljs-keyword">self</span>, from: previewData)
            <span class="hljs-keyword">for</span> toDoItem <span class="hljs-keyword">in</span> toDoItems {
                modelContainer.mainContext.insert(toDoItem)
            }
        } <span class="hljs-keyword">catch</span> {
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Could not decode preview data"</span>)
        }
    }
}
</code></pre>
<p>Now we have everything we need to actually use previews with stub data, as you can see in the image below.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723319287656/5ff9d6c0-6a0a-4adc-ac9d-67e3f3322fc3.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-what-else-could-be-done">What Else Could Be Done?</h1>
<p>Functionally, everything is in place. Nonetheless, I like adding some helpers to my container provider that make working with previews even more straightforward.</p>
<h2 id="heading-modelcontext">ModelContext</h2>
<p>We access the <code>ModelContext</code> through our <code>ModelContainer</code> instance, and we do it exactly once in this example. With a more complex data schema, we'd need to access it multiple times during initialization alone - not to mention the times we'd need access to the context inside a <code>Preview</code> for some delegate action or something similar. To make our code more readable, we just need to create a property inside the <code>PreviewModelContainerProvider</code>.</p>
<pre><code class="lang-Swift">@<span class="hljs-type">MainActor</span>
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PreviewModelContainerProvider</span> </span>{
    ...
    <span class="hljs-keyword">var</span> modelContext: <span class="hljs-type">ModelContext</span> {
        modelContainer.mainContext
    }

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">init</span>() {
        ...
        <span class="hljs-keyword">for</span> toDoItem <span class="hljs-keyword">in</span> toDoItems {
            <span class="hljs-comment">// Don't forget to use it instead of modelContainer.mainContext</span>
            modelContext.insert(toDoItem)
        }
        ...
    }
}
</code></pre>
<h2 id="heading-model-instances">Model Instances</h2>
<p>Some of the views in the application will use only a single instance of our models and will consume it using a constructor. In that case, we could create a property to access such models from our provider. We have all we need to return such a model, but we don't retain that data. Fortunately, to change that, we just need to create a property in the provider class and store the loaded data in it instead of a local variable.</p>
<pre><code class="lang-Swift">@<span class="hljs-type">MainActor</span>
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PreviewModelContainerProvider</span> </span>{
    ...
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> toDoItems: [<span class="hljs-type">ToDoItem</span>]
    ...
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">init</span>() {
        ...
        <span class="hljs-comment">// Store loaded data to property instead of local variable</span>
        toDoItems = <span class="hljs-keyword">try</span> <span class="hljs-type">JSONDecoder</span>().decode([<span class="hljs-type">ToDoItem</span>].<span class="hljs-keyword">self</span>, from: previewData)
        ...
    }
}
</code></pre>
<p>Now all that's left to do is to create a property to access a single <code>ToDoItem</code> instance.</p>
<pre><code class="lang-Swift"><span class="hljs-keyword">var</span> toDoItem: <span class="hljs-type">ToDoItem</span> {
    <span class="hljs-keyword">guard</span> <span class="hljs-keyword">let</span> item = toDoItems.first <span class="hljs-keyword">else</span> {
        <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"There is no ToDoItem data loaded"</span>)
    }
    <span class="hljs-keyword">return</span> item
}
</code></pre>
<p>We could force unwrap the <code>toDoItems.first</code> element, but I want to make sure we get a proper error message in case something went wrong during the initialization. Better safe than sorry, as they say.</p>
<p>Let's make use of our newly created property and extract the rows showing <code>ToDoItems</code> to a separate view.</p>
<pre><code class="lang-Swift"><span class="hljs-keyword">import</span> SwiftData
<span class="hljs-keyword">import</span> SwiftUI

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ToDoItemRow</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> toDoItem: <span class="hljs-type">ToDoItem</span>

    <span class="hljs-keyword">init</span>(<span class="hljs-keyword">for</span> toDoItem: <span class="hljs-type">ToDoItem</span>) {
        <span class="hljs-keyword">self</span>.toDoItem = toDoItem
    }

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Label</span>(toDoItem.name, systemImage: toDoItem.done ? <span class="hljs-string">"checkmark.circle"</span> : <span class="hljs-string">"circle"</span>)
    }
}

#<span class="hljs-type">Preview</span>(traits: .sizeThatFitsLayout) {
    <span class="hljs-type">ToDoItemRow</span>(<span class="hljs-keyword">for</span>: <span class="hljs-type">PreviewModelContainerProvider</span>.shared.toDoItem)
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1723319298135/56be89b8-e56d-4e6c-86ee-c1bea6429b96.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-final-thoughts">Final Thoughts</h1>
<p>With everything we've created here, we can easily use <code>Preview</code>s inside our application and effortlessly add new models as the application grows, since everything we need is in just one place. We can also easily modify stub data, as it's stored in a separate, easy-to-read JSON file.</p>
<p>Is this the best solution? It might be, or it might not be. It all depends on your preferences. I wanted to show you how I like to work with <code>Preview</code>s in my applications, as I found it challenging to configure everything correctly when I first started working with <code>SwiftData</code>.</p>
<p>The whole solution can be found on my <a target="_blank" href="https://github.com/ostojan/LookAtMyData">GitHub</a>, and each section of this article is a separate commit in the repository.</p>
]]></content:encoded></item><item><title><![CDATA[Smart Searchable List in SwiftUI]]></title><description><![CDATA[While working on some applications, I found myself a few times in the position where I needed to give the user a choice from data that already existed in the application. However, I didn't want to constrain the choice only to already existing data bu...]]></description><link>https://stojanowski.dev/smart-searchable-list-in-swiftui</link><guid isPermaLink="true">https://stojanowski.dev/smart-searchable-list-in-swiftui</guid><category><![CDATA[Swift]]></category><category><![CDATA[SwiftUI]]></category><category><![CDATA[SwiftData]]></category><dc:creator><![CDATA[Aleksander Stojanowski]]></dc:creator><pubDate>Sun, 28 Jul 2024 13:13:22 GMT</pubDate><content:encoded><![CDATA[<p>While working on some applications, I found myself a few times in the position where I needed to give the user a choice from data that already existed in the application. However, I didn't want to constrain the choice only to already existing data but to give a simple mechanism to create a new entry if the preferable one didn't exist. It sounds really simple, but it might be much more complicated than one can think of! Let's dive in and see what choices we have and what problems we can encounter.</p>
<p>Oh, just before we begin, let me point out that we'll be using <code>SwiftUI</code> and <code>SwiftData</code>.</p>
<h1 id="heading-lets-start-with-data">Let's Start with Data</h1>
<p>To create a searchable list, we need some data we can search through. For this example, a single model class is more than enough.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> SwiftData

@<span class="hljs-type">Model</span>
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Product</span> </span>{
    <span class="hljs-keyword">var</span> name: <span class="hljs-type">String</span> = <span class="hljs-string">""</span>
    <span class="hljs-keyword">var</span> checked: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>

    <span class="hljs-keyword">init</span>(name: <span class="hljs-type">String</span>, checked: <span class="hljs-type">Bool</span> = <span class="hljs-literal">false</span>) {
        <span class="hljs-keyword">self</span>.name = name
        <span class="hljs-keyword">self</span>.checked = checked
    }
}
</code></pre>
<p>I prefer to work with <code>Previews</code>, so let's create a simple helper and add some data we can work with.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">import</span> SwiftData

@<span class="hljs-type">MainActor</span>
<span class="hljs-keyword">final</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">DataHelper</span> </span>{
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">let</span> shared = <span class="hljs-type">DataHelper</span>()

    <span class="hljs-keyword">private</span>(<span class="hljs-keyword">set</span>) <span class="hljs-keyword">var</span> modelContainer: <span class="hljs-type">ModelContainer</span>

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">init</span>() {
        <span class="hljs-keyword">do</span> {
            <span class="hljs-keyword">let</span> config = <span class="hljs-type">ModelConfiguration</span>(isStoredInMemoryOnly: <span class="hljs-literal">true</span>)
            modelContainer = <span class="hljs-keyword">try</span> <span class="hljs-type">ModelContainer</span>(<span class="hljs-keyword">for</span>: <span class="hljs-type">Product</span>.<span class="hljs-keyword">self</span>, configurations: config)
            <span class="hljs-keyword">for</span> productIdx <span class="hljs-keyword">in</span> <span class="hljs-number">1</span> ... <span class="hljs-number">10</span> {
                modelContainer.mainContext.insert(<span class="hljs-type">Product</span>(name: <span class="hljs-string">"Product \(productIdx)"</span>))
            }
        } <span class="hljs-keyword">catch</span> {
            <span class="hljs-built_in">fatalError</span>(<span class="hljs-string">"Couldn't create model container for Previews"</span>)
        }
    }
}
</code></pre>
<p>That's all we need to start working!</p>
<h1 id="heading-basic-view">Basic View</h1>
<p>Before we add <em>smart</em> functionality to our search, we need to build the most simple view with a basic search. To do that, we need two views so we can make use of <code>SwiftData</code>'s <code>@Query</code>, and <code>#Predicate</code> functionality.</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ProductsListing</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">Query</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> filteredProducts: [<span class="hljs-type">Product</span>]

    <span class="hljs-keyword">init</span>(withName searchText: <span class="hljs-type">String</span>) {
        <span class="hljs-keyword">let</span> sanitizedSearchText = searchText.trimmingCharacters(<span class="hljs-keyword">in</span>: .whitespacesAndNewlines)

        _filteredProducts = <span class="hljs-type">Query</span>(
            <span class="hljs-built_in">filter</span>: #<span class="hljs-type">Predicate</span> {
                sanitizedSearchText.isEmpty || $<span class="hljs-number">0</span>.name.localizedStandardContains(sanitizedSearchText)
            },
            <span class="hljs-built_in">sort</span>: [<span class="hljs-type">SortDescriptor</span>(\.name)]
        )
    }

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">List</span> {
            <span class="hljs-type">ForEach</span>(filteredProducts) {
                <span class="hljs-type">Text</span>($<span class="hljs-number">0</span>.name)
            }
        }
    }
}

<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ContentView</span>: <span class="hljs-title">View</span> </span>{
    @<span class="hljs-type">State</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> searchText = <span class="hljs-string">""</span>

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">NavigationStack</span> {
            <span class="hljs-type">ProductsListing</span>(withName: searchText)
                .searchable(text: $searchText)
        }
    }
}

#<span class="hljs-type">Preview</span> {
    <span class="hljs-type">ContentView</span>()
        .modelContainer(<span class="hljs-type">DataHelper</span>.shared.modelContainer)
}
</code></pre>
<p>Now we'll make our list <em>clickable</em>, so the user can change the state of the <code>Product</code> object. We should go ahead and create a new view that will be used inside <code>ForEach</code>.</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ProductTile</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> product: <span class="hljs-type">Product</span>

    <span class="hljs-keyword">init</span>(<span class="hljs-number">_</span> product: <span class="hljs-type">Product</span>) {
        <span class="hljs-keyword">self</span>.product = product
    }

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">Button</span> {
            product.checked.toggle()
        } label: {
            <span class="hljs-type">Label</span>(product.name, systemImage: product.checked ? <span class="hljs-string">"checkmark.circle"</span> : <span class="hljs-string">"circle"</span>)
        }
    }
}
</code></pre>
<p>Don't forget to actually use is in our view.</p>
<pre><code class="lang-swift"><span class="hljs-type">ForEach</span>(filteredProducts) {
    <span class="hljs-type">ProductTile</span>($<span class="hljs-number">0</span>)
}
</code></pre>
<p>The last thing we have to do is add animations. Normally, we'd simply add <code>animation: .default</code> to the <code>Query</code> constructor and call it a day. However, that would solve only half of our problems. We need to explicitly add the animation modifier to our view, as we dynamically create the <code>Query</code> based on the search text passed by the user.</p>
<p>Update our <code>Query</code> constructor call to animate on changes.</p>
<pre><code class="lang-swift">_filteredProducts = <span class="hljs-type">Query</span>(
    <span class="hljs-built_in">filter</span>: #<span class="hljs-type">Predicate</span> {
                sanitizedSearchText.isEmpty || $<span class="hljs-number">0</span>.name.localizedStandardContains(sanitizedSearchText)
    },
    <span class="hljs-built_in">sort</span>: [<span class="hljs-type">SortDescriptor</span>(\.name)],
    animation: .<span class="hljs-keyword">default</span>
)
</code></pre>
<p>Then add the <code>animation</code> modifier to the <code>List</code> view.</p>
<pre><code class="lang-swift"> <span class="hljs-type">List</span> {
    <span class="hljs-type">ForEach</span>(filteredProducts) {
        <span class="hljs-type">ProductTile</span>($<span class="hljs-number">0</span>)
    }
}
.animation(.<span class="hljs-keyword">default</span>, value: filteredProducts)
</code></pre>
<p>All that gives us a really simple searchable list of <code>Product</code>s.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722172244353/ef5819e7-3512-4975-ae43-012c49df8c6a.gif" alt class="image--center mx-auto" /></p>
<h1 id="heading-give-users-more-choices">Give Users More Choices</h1>
<p>To make our list <em>smart</em>, we should add an additional choice at the top of the list when the <code>Product</code> the user is looking for doesn't exist yet. Begin with some helpers and a new section containing a nonexistent product at the top of the list. To do so, we need to modify the <code>ProductsListing</code> view.</p>
<pre><code class="lang-swift"><span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">ProductsListing</span>: <span class="hljs-title">View</span> </span>{
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">let</span> nonexistentProduct: <span class="hljs-type">Product</span>
    @<span class="hljs-type">Query</span>
    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> filteredProducts: [<span class="hljs-type">Product</span>]

    <span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> showNewProduct: <span class="hljs-type">Bool</span> {
        !nonexistentProduct.name.isEmpty &amp;&amp; !filteredProducts.<span class="hljs-built_in">contains</span>(<span class="hljs-keyword">where</span>: { $<span class="hljs-number">0</span>.name.localizedCaseInsensitiveCompare(nonexistentProduct.name) == .orderedSame })
    }

    <span class="hljs-keyword">init</span>(withName searchText: <span class="hljs-type">String</span>) {
        <span class="hljs-keyword">let</span> sanitizedSearchText = searchText.trimmingCharacters(<span class="hljs-keyword">in</span>: .whitespacesAndNewlines)
        nonexistentProduct = <span class="hljs-type">Product</span>(name: sanitizedSearchText)
        _filteredProducts = <span class="hljs-type">Query</span>(
            <span class="hljs-built_in">filter</span>: #<span class="hljs-type">Predicate</span> {
                sanitizedSearchText.isEmpty || $<span class="hljs-number">0</span>.name.localizedStandardContains(sanitizedSearchText)
            },
            <span class="hljs-built_in">sort</span>: [<span class="hljs-type">SortDescriptor</span>(\.name)],
            animation: .<span class="hljs-keyword">default</span>
        )
    }

    <span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
        <span class="hljs-type">List</span> {
            <span class="hljs-keyword">if</span> showNewProduct {
                <span class="hljs-type">Section</span> {
                    <span class="hljs-type">ProductTile</span>(nonexistentProduct)
                }
            }
            <span class="hljs-type">Section</span> {
                <span class="hljs-type">ForEach</span>(filteredProducts) {
                    <span class="hljs-type">ProductTile</span>($<span class="hljs-number">0</span>)
                }
            }
        }
        .animation(.<span class="hljs-keyword">default</span>, value: filteredProducts)
        .animation(.<span class="hljs-keyword">default</span>, value: nonexistentProduct)
    }
}
</code></pre>
<p>Notice we needed to add yet another <code>animation</code> modifier; that's a must since we recreate <code>ProductsListing</code> each time the user passes a new value into the search text field.</p>
<p>It seems like we have the basic functionality ready, but we are not done yet. Although you can use the new <code>ProductTile</code> for nonexistent <code>Product</code> in the same way as for the other ones, the changes the user made vanish as soon as the search text is changed. That's happening because, although the <code>Product</code> is being created in the <code>ProductListing</code> constructor, we don't save it to the <code>ModelContext</code>. Fortunately, it's a simple fix: we just need to add a <code>Product</code> to the context when it doesn't exist in one in the <code>Button</code> action inside <code>ProductTile</code>.</p>
<pre><code class="lang-swift"><span class="hljs-type">Button</span> {
    product.checked.toggle()
    <span class="hljs-keyword">if</span> product.modelContext == <span class="hljs-literal">nil</span> {
        modelContext.insert(product)
    }
}
</code></pre>
<p>That's everything we need to do to have a simple searchable list with an additional choice on top, so we can call it <em>smart</em>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722172238623/2e47ad92-e447-466c-84a5-7eea8122adcd.gif" alt class="image--center mx-auto" /></p>
<h1 id="heading-final-touches">Final Touches</h1>
<p>The current solution works and looks acceptable, yet it still requires some polish to be ready for use, in my opinion.</p>
<p>First, we'll merge the section with items the user is looking for and the rest of the results. That's really straightforward - just add a new helper property and update the <code>body</code>.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> productsToShow: [<span class="hljs-type">Product</span>] {
    <span class="hljs-keyword">var</span> products = filteredProducts
    <span class="hljs-keyword">if</span> showNewProduct {
        products.insert(nonexistentProduct, at: <span class="hljs-number">0</span>)
    }
    <span class="hljs-keyword">return</span> products
}

<span class="hljs-keyword">var</span> body: some <span class="hljs-type">View</span> {
    <span class="hljs-type">List</span> {
        <span class="hljs-type">ForEach</span>(productsToShow) {
            <span class="hljs-type">ProductTile</span>($<span class="hljs-number">0</span>)
        }
    }
    .animation(.<span class="hljs-keyword">default</span>, value: filteredProducts)
    .animation(.<span class="hljs-keyword">default</span>, value: nonexistentProduct)
}
</code></pre>
<p>Thanks to that, we have a consistent UI for all items on our list.</p>
<p>Now let's face a potential problem we might encounter. When the user is looking for an item with a name that's a part of an already existing item (e.g., the user types <em>ground</em> and there is <em>background</em> already on the list), it might happen that when the user selects the new item, it'll be saved, causing the <code>Query</code> to refresh with new data, and the just selected <code>Product</code> will be moved from the top of the list to some other place (due to <code>Product</code>s' sorting). Ideally, we'd handle this by passing an additional custom <code>SortDescriptor</code> to the <code>Query</code> constructor, but that's not possible. Fortunately, all we need to do is handle such a case inside the <code>productsToShow</code> property.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> productsToShow: [<span class="hljs-type">Product</span>] {
    <span class="hljs-keyword">var</span> products = filteredProducts
    <span class="hljs-keyword">if</span> showNewProduct {
        products.insert(nonexistentProduct, at: <span class="hljs-number">0</span>)
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> productIndex = products.firstIndex(<span class="hljs-keyword">where</span>: { $<span class="hljs-number">0</span>.name.localizedCaseInsensitiveCompare(nonexistentProduct.name) == .orderedSame }) {
        products.move(fromOffsets: <span class="hljs-type">IndexSet</span>(integer: productIndex), toOffset: <span class="hljs-number">0</span>)
    }
    <span class="hljs-keyword">return</span> products
}
</code></pre>
<p>The last thing left to do is clean up our code a bit. Having all products in one property, we can remove multiple animation modifiers for the list and have just one.</p>
<pre><code class="lang-swift"><span class="hljs-type">List</span> {
    <span class="hljs-type">ForEach</span>(productsToShow) {
        <span class="hljs-type">ProductTile</span>($<span class="hljs-number">0</span>)
    }
}
.animation(.<span class="hljs-keyword">default</span>, value: productsToShow)
</code></pre>
<p>We access the string with the name the user is looking for in a few places, but we keep it inside the <code>Product</code> instance, so we can make it a property.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> searchText: <span class="hljs-type">String</span> {
    nonexistentProduct.name
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> showNewProduct: <span class="hljs-type">Bool</span> {
    !searchText.isEmpty &amp;&amp; !filteredProducts.<span class="hljs-built_in">contains</span>(<span class="hljs-keyword">where</span>: { $<span class="hljs-number">0</span>.name.localizedCaseInsensitiveCompare(searchText) == .orderedSame })
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> productsToShow: [<span class="hljs-type">Product</span>] {
    <span class="hljs-keyword">var</span> products = filteredProducts
    <span class="hljs-keyword">if</span> showNewProduct {
        products.insert(nonexistentProduct, at: <span class="hljs-number">0</span>)
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> productIndex = products.firstIndex(<span class="hljs-keyword">where</span>: { $<span class="hljs-number">0</span>.name.localizedCaseInsensitiveCompare(searchText) == .orderedSame }) {
        products.move(fromOffsets: <span class="hljs-type">IndexSet</span>(integer: productIndex), toOffset: <span class="hljs-number">0</span>)
    }
    <span class="hljs-keyword">return</span> products
}
</code></pre>
<p>The next improvement we can make is to move checking for the existence of an item inside <code>Query</code> results to a property.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> indexOfSearchProduct: <span class="hljs-type">Int?</span> {
    filteredProducts.firstIndex(<span class="hljs-keyword">where</span>: { $<span class="hljs-number">0</span>.name.localizedCaseInsensitiveCompare(searchText) == .orderedSame })
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> showNewProduct: <span class="hljs-type">Bool</span> {
    !searchText.isEmpty &amp;&amp; indexOfSearchProduct == <span class="hljs-literal">nil</span>
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> productsToShow: [<span class="hljs-type">Product</span>] {
    <span class="hljs-keyword">var</span> products = filteredProducts
    <span class="hljs-keyword">if</span> showNewProduct {
        products.insert(nonexistentProduct, at: <span class="hljs-number">0</span>)
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> indexOfSearchProduct {
        products.move(fromOffsets: <span class="hljs-type">IndexSet</span>(integer: indexOfSearchProduct), toOffset: <span class="hljs-number">0</span>)
    }
    <span class="hljs-keyword">return</span> products
}
</code></pre>
<p>Lastly, we can simplify our <code>if</code> statements a bit by removing some unnecessary checks.</p>
<pre><code class="lang-swift"><span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> indexOfSearchProduct: <span class="hljs-type">Int?</span> {
    filteredProducts.firstIndex(<span class="hljs-keyword">where</span>: { $<span class="hljs-number">0</span>.name.localizedCaseInsensitiveCompare(searchText) == .orderedSame })
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> showNewProduct: <span class="hljs-type">Bool</span> {
    !searchText.isEmpty
}

<span class="hljs-keyword">private</span> <span class="hljs-keyword">var</span> productsToShow: [<span class="hljs-type">Product</span>] {
    <span class="hljs-keyword">var</span> products = filteredProducts
    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> indexOfSearchProduct {
        products.move(fromOffsets: <span class="hljs-type">IndexSet</span>(integer: indexOfSearchProduct), toOffset: <span class="hljs-number">0</span>)
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> showNewProduct {
        products.insert(nonexistentProduct, at: <span class="hljs-number">0</span>)
    }
    <span class="hljs-keyword">return</span> products
}
</code></pre>
<p>With not too many lines of code, we created a nicely looking <em>smart</em> searchable list. Let's take a look at how the final view looks.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1722172203738/c322f077-5a87-4aae-aad6-b4a1747d6fcd.gif" alt class="image--center mx-auto" /></p>
<h1 id="heading-wrapping-up">Wrapping Up</h1>
<p>Creating a <em>smart</em> searchable list in SwiftUI with SwiftData is more than just simply displaying items. Even the current solution isn't ideal; for example, not all animations look as smooth as they should. That's okay - we can always make them better along the way. Nonetheless, the current code is <em>MVP</em> ready and can be easily used in many sorts of applications (e.g., for selecting tags).</p>
<p>The whole solution can be found on my <a target="_blank" href="https://github.com/ostojan/SmartSerchableList">GitHub</a>, and each chapter of this tutorial is a separate commit in the repository.</p>
]]></content:encoded></item></channel></rss>