<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.2">Jekyll</generator><link href="http://dev.shoppingukapp.com/feed.xml" rel="self" type="application/atom+xml" /><link href="http://dev.shoppingukapp.com/" rel="alternate" type="text/html" /><updated>2023-06-19T10:52:03+00:00</updated><id>http://dev.shoppingukapp.com/feed.xml</id><title type="html">Development Blog</title><subtitle>A behind the scenes look at how a popular shopping list app is built</subtitle><author><name>Stuart Wheelwright</name></author><entry><title type="html">Embracing CloudKit: Part 8</title><link href="http://dev.shoppingukapp.com/2023/06/19/embracing-cloudkit-for-data-sharing-part8.html" rel="alternate" type="text/html" title="Embracing CloudKit: Part 8" /><published>2023-06-19T00:00:00+00:00</published><updated>2023-06-19T00:00:00+00:00</updated><id>http://dev.shoppingukapp.com/2023/06/19/embracing-cloudkit-for-data-sharing-part8</id><content type="html" xml:base="http://dev.shoppingukapp.com/2023/06/19/embracing-cloudkit-for-data-sharing-part8.html"><![CDATA[<h1 id="part-8-when-things-go-wrong">Part 8: When Things Go Wrong</h1>

<p>This is the last in an <a href="/2023/05/01/embracing-cloudkit-for-data-sharing-contents.html">eight-part series</a> on implementing
data sharing in <em>Shopping UK</em> using CloudKit.</p>

<p><small><em><a href="https://shoppingukapp.com">Shopping UK</a> is a smart shopping list for UK shoppers. It knows almost every product in the supermarket and
will arrange them by aisle. Lists can be shared with family or friends.</em></small></p>

<p><a href="/2023/06/12/embracing-cloudkit-for-data-sharing-part7.html">Last week</a>, we looked at how to change or stop a share, what 
happens when a list is deleted and background maintenance. 
Today, we’ll wrap up the series with a look at error handling, merging data, and diagnosing problems.</p>

<h2 id="the-path-of-sadness">The Path of Sadness</h2>

<p><img src="/img/posts/cloudkit/paths.png" alt="Happy and sad paths" /></p>

<p>Throughout this series, the focus has been on the happy path — when everything goes to plan — but a synchronisation solution is only
as good as its ability to handle unexpected situations.</p>

<p>We’ll start with a look at what can go wrong before examining how <em>Shopping UK</em> handles things.</p>

<p>I can’t guarantee I’ve thought of all possible problematic scenarios, but I will
share what I built, what I learned from it, and some techniques I used to help diagnose problems when they do arise.</p>

<h2 id="why-do-things-go-wrong">Why do things go wrong?</h2>

<p>Synchronising data is hard for two reasons:</p>
<ul>
  <li><strong>Networking</strong>: data must be moved between physically separated systems — the user’s device and iCloud — using an
unreliable network.</li>
  <li><strong>Merging Data</strong>: data must be combined from different sources — between each user’s device and iCloud — while maintaining data
integrity and striving to keep data consistent across all devices.</li>
</ul>

<h3 id="the-problem-with-networks">The problem with networks</h3>

<p>CloudKit provides a clean interface for moving data between a device and iCloud, but it cannot change the
<a href="https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing">nature of networks</a>:</p>

<ul>
  <li>The network is <strong>never</strong> reliable.</li>
  <li>Latency is <strong>never</strong> zero.</li>
  <li>Bandwidth is <strong>never</strong> infinite.</li>
  <li>Transport cost is <strong>never</strong> zero.</li>
  <li>The network is <strong>never</strong> homogeneous.</li>
</ul>

<p>Every request sent from your app to iCloud must travel from the user’s device to iCloud across many networks.
Each message will start on a Wi-Fi or a mobile (cellular) link before finding larger internet backbones, and eventually arriving at
the datacenter that hosts Apple’s iCloud servers. The path taken to iCloud will be different for each user, and possibly for each message.</p>

<p>Wi-Fi will <em>not</em> always be available, mobile (cellular) data may be disabled (e.g. flight mode enabled or data roaming disabled) or
have patchy reception, iCloud may be down or busy, and messages may be lost.</p>

<p>Nothing happens instantly. Each message will take several milliseconds, or longer, to arrive — this is millions of times slower
than a message can be processed on the device.
Many messages could be in-flight at the same time. Many responses may arrive at the same time.</p>

<p>Everything has a cost. Larger messages need more processing,
use more iCloud storage, and eat up more of the user’s mobile (cellular) data plan.</p>

<p>Everyone will have a different experience. Some users have ultra fast broadband, some are limited to a weak or slow data signal.
Some parts of the world are closer to a datacenter than others.</p>

<h3 id="the-problem-with-data-merging">The problem with data merging</h3>

<p>The aim of a synchronisation solution is to move data between devices to give each user the same up-to-date view of the world.</p>

<p>But the network won’t always be available, messages take time to move between devices, and users expect their app to continue to work when
their mobile signal is patchy. The best we can hope for is for all devices to <a href="https://en.wikipedia.org/wiki/CAP_theorem"><em>eventually</em></a>
show a consistent view of the data.</p>

<p>And this means we must consider what to do when the same data is changed by two people at the same time.</p>

<p><img src="/img/posts/cloudkit/merge-conflicting-requests.png" alt="How should conflicting requests be merged?" /></p>

<p>How should this be resolved?</p>

<ol>
  <li>Should the changes be applied in order?</li>
  <li>Should they be somehow merged?</li>
  <li>Should they both be rejected?</li>
</ol>

<p>There isn’t a “right” answer to this — it depends on the nature of the app, the meaning of the data, and the expectation of the users.</p>

<h2 id="taming-the-network">Taming the network</h2>

<p>When designing the sharing strategy for <em>Shopping UK</em>, I had several guiding principles in mind:</p>

<ol>
  <li>Synchronise changes with others as soon as possible.</li>
  <li>Don’t lose any changes.</li>
  <li>Minimise user’s data costs.</li>
  <li>Use the battery efficiently.</li>
</ol>

<p>These are <em>not</em> universal principles that work for all apps. If your app shares the user’s location in real-time,
it may be ok for a single update to be lost. 
But lost items is not a good look for a shopping list app.</p>

<p>Because the network is unreliable, I chose a queue-based solution.</p>

<p>When the user adds an item, the app creates a <em>JournalEntry</em> record to represent the change. This is saved to a
local queue and then the queue is uploaded to iCloud (using CloudKit).</p>

<p>If the network goes down nothing is lost.</p>

<p><img src="/img/posts/cloudkit/upload-queue.png" alt="Changes are added to an upload queue before attempting upload" /></p>

<p>The same thing happens when fetching changes from iCloud.
Newly received <em>JournalEntry</em> records are saved to a local queue and the queue is processed, and 
each change is applied to the list in turn.</p>

<p>After a local change is uploaded, it is removed from the <em>upload</em> queue.</p>

<p>After a remote change is applied to the device, the entry is removed from the <em>to-apply</em> queue.</p>

<p>An upload may fail for many reasons. Look at the
<a href="https://developer.apple.com/documentation/cloudkit/ckerror/code">CloudKit errors</a> for a full reference.</p>

<h3 id="what-does-the-app-do-when-it-receives-an-error">What does the app do when it receives an error?</h3>

<p>Generally, <em>Shopping UK</em> will simply show a friendly form of the error in the Activity Log to let the user know
synchronisation may be delayed.</p>

<p>For example, when Airplane Mode is enabled, a 
<a href="https://developer.apple.com/documentation/cloudkit/ckerror/code/networkunavailable"><strong>NetworkUnavailable</strong></a> will be returned:</p>

<p><img src="/img/posts/cloudkit/error-shown-to-user.png" alt="App shows the details of the error and the affected items" /></p>

<h3 id="is-this-what-happens-for-all-errors">Is this what happens for all errors?</h3>

<p>Nope, just errors that cannot be fixed by the app.</p>

<p>CloudKit protects itself from huge messages.</p>

<ul>
  <li>
    <p>If a request is too large or contains too many records, CloudKit will reject it with a
<a href="https://developer.apple.com/documentation/cloudkit/ckerror/code/limitexceeded"><strong>LimitExceeded</strong></a> error.</p>
  </li>
  <li>
    <p>If requests are made too quickly, CloudKit may reject some with a
<a href="https://developer.apple.com/documentation/cloudkit/ckerror/code/requestratelimited"><strong>RequestRateLimited</strong></a> error.
I’ve also occasionally seen a
<a href="https://developer.apple.com/documentation/cloudkit/ckerror/code/partialfailure"><strong>PartialFailure</strong></a> error
with the message “will not be saved but can be retried as is”.
This appears to happen when iCloud is <a href="https://twitter.com/conradstoll/status/1241837149441470466">under heavy load</a>.</p>
  </li>
</ul>

<p>The app knows what to do about this type of error:</p>

<ul>
  <li>
    <p>When a request contains too many records
<em>Shopping UK</em> splits the list in half and sends each half separately. If either half fails, it is split in half
again. This halving continues until everything has been sent successfully.</p>
  </li>
  <li>
    <p>When the request is rejected due to rate limiting or when the “will not be saved but can be retried as is” message is returned,
<em>Shopping UK</em> will wait a while and try again.</p>
  </li>
</ul>

<h2 id="taming-the-merge">Taming the merge</h2>

<p>Is a merge always necessary?</p>

<p>No. It can be avoided by never caching anything on device, and treating iCloud as the single authoritative source:</p>
<ul>
  <li>When your app needs data, it fetches it from iCloud.</li>
  <li>To change data, your app immediately uploads the changes to iCloud.</li>
  <li>If the upload fails because another device made changes at the same time,
a fresh copy is fetched, and the user makes the changes again.</li>
</ul>

<p>This is the way websites worked in the very early days.</p>

<p>This may be ok for some types of app, but it doesn’t work for a shopping list.
If you’re in a rural shop with no data signal it would be frustrating if your shopping list was offline and could not be viewed.</p>

<p>So we’re stuck with the reality that data on the device and the data in iCloud can diverge. And this means merges are necessary.</p>

<h3 id="when-do-we-merge">When do we merge?</h3>

<p>The beauty of a using a write-only journal means merges are never needed when uploading changes to iCloud.
Each change is simply appended to the set of <em>JournalEntry</em> records in iCloud.</p>

<p><img src="/img/posts/cloudkit/append-to-journal.png" alt="Changes are appended to iCloud" /></p>

<p>But this doesn’t eliminate the merge, it just moves it elsewhere!
This is why I love design. Nothing is free; everything is a trade-off :)</p>

<p>For <em>Shopping UK</em>, the merge happens when we need to apply the changes to the user’s device.</p>

<p>Here’s some examples.</p>

<h3 id="adding-while-offline">Adding while offline</h3>

<p>Alice and Bob are both offline, with no phone reception.</p>

<p>Alice adds ‘bread’ below ‘milk’ moments before Bob adds ‘eggs’ below ‘milk’.</p>

<p><img src="/img/posts/cloudkit/add-while-offline.png" alt="Add while offline" /></p>

<p>What happens when their network returns — how do we resolve this conflict?</p>

<p><em>Shopping UK</em> will apply the changes in order.
This can result in Alice and Bob’s lists appearing in a slightly different order:</p>

<p><img src="/img/posts/cloudkit/merge-when-online.png" alt="Merge when online" /></p>

<p>Remember this is an unusual event. It is not often that both users would add items to their
shared list while both were offline. And, although each user has a different ordering, it won’t 
affect how the lists are used. Items are never lost, and when Alice and Bob start shopping, their lists will be
automatically categorised and ordered to match the aisles in the supermarket.</p>

<p>Arguments could be made for a better approach, but in my mind, this is a reasonable compromise that has the benefit of allowing the app
to continue to be used offline.</p>

<h3 id="renaming-or-recolouring-the-list-while-offline">Renaming or recolouring the list while offline</h3>

<p>Some merge situations are a lot simpler to resolve.</p>

<p>If two users were to change their shared shopping list’s colour or name while offline, <em>Shopping UK</em> would simply apply the
changes in the order they were made — the last one wins.</p>

<p>For example, Alice and Bob are both offline. Alice changes the list’s name to “Weekly Shop” a second before Bob renames the list
to “Alice and Bob’s List”. When the devices re-join the network, each device will upload the “rename” change, and both devices
will fetch the other person’s “rename”. On both devices, the operations will be applied in the same order. This will result in the list
being called “Alice and Bob’s List”, because Bob made his change after Alice.</p>

<h2 id="easily-diagnosing-problems">Easily Diagnosing problems</h2>

<p>Things often went wrong while I was building the synchronisation solution — due to my misunderstanding, bugs,
or undocumented iCloud behaviour.</p>

<p>Diagnosing the cause of the issue was not always easy.</p>

<p>Here’s some things that helped me.</p>

<h3 id="logging-to-console">Logging to Console</h3>

<p>Having detailed logs available was essential for diagnosing problems. Many bugs cannot be found simply by capturing the current state
of the system. It is often necessary to see how the state changed over time. Most of CloudKit’s calls are asynchronous and this adds
to the complexity.</p>

<p>When writing code, I added “write-to-log” messages <em>everywhere</em> — for almost every path through the code.
In many cases, these log messages became a substitute for code comments.</p>

<p><img src="/img/posts/cloudkit/log-messages.png" alt="Add logging liberally" /></p>

<p>My logging framework has configurable verbosity. While developing, I set it to DEBUG mode, to show every message in the console.
This meant I could see exactly what was happening internally, and in real-time. The logs showed every decision, the contents of every request,
the contents of every response, error messages, everything.</p>

<p><img src="/img/posts/cloudkit/console-log.png" alt="Example of console logging" /></p>

<p>For the version released to the App Store, only ERROR messages and critical information is logged.
This means logging won’t slow down normal operation.</p>

<h3 id="real-time-journal-diagnostics">Real Time Journal Diagnostics</h3>

<p>In addition to console logging, I found it useful to add a view to show the state of the <em>upload</em> and <em>to-apply</em> queues.
This floating window shows how many of each type of operation are waiting to be sent to iCloud and waiting to be applied to the device.</p>

<p><img src="/img/posts/cloudkit/journal-view-1.png" alt="" /></p>

<p>Tapping on the window will show the entries in the queue, and tapping an entry shows further detail:</p>

<p><img src="/img/posts/cloudkit/journal-view-2.png" alt="" /></p>

<p>This view is hidden for the App Store version of the app, but it is incredibly helpful while developing.</p>

<h3 id="diagnostic-log">Diagnostic Log</h3>

<p>Finally, I’ve extended the information included in the diagnostic log users can send me when they encounter a problem.</p>

<p><img src="/img/posts/cloudkit/diagnostic-log-option.png" alt="Send Diagnostic Log when reporting a problem" /></p>

<p>This log now contains details of the CloudKit configuration, and the contents of the upload and to-apply queues.</p>

<p><img src="/img/posts/cloudkit/diagnostic-log.png" alt="Diagnostic Log example" /></p>

<p>The information in this report should prove invaluable for tracking down the root cause if problems are reported by users.</p>

<h2 id="the-end">The End</h2>

<p>And that concludes the series. I hope it has been useful.
Please send any errors, omissions, or feedback to <a href="https://twitter.com/wheelies">@wheelies</a></p>

<p>Before I go, I’d like to share some CloudKit articles I found to be incredibly useful while adding CloudKit support to <em>Shopping UK</em>:</p>

<ul>
  <li>First, there’s the <a href="https://developer.apple.com/documentation/cloudkit">official Apple CloudKit documentation</a>. It’s rather good.</li>
  <li><a href="https://twitter.com/_inside">Guilherme Rambo</a> provides a wonderful <a href="https://www.rambo.codes/posts/2020-02-25-cloudkit-101">overview of CloudKit</a>.</li>
  <li><a href="https://twitter.com/kuba_suder">Kuba Suder</a> has a very useful <a href="https://mackuba.eu/notes/wwdc16/cloudkit-best-practices/">CloudKit Reference and Cheat Sheet</a>.</li>
  <li><a href="https://twitter.com/justtact">Jaanus Kase</a> has written some great <a href="https://blog.justtact.com">blog posts</a> on using CloudKit to build a messaging app.</li>
  <li>Dan Griffin has <a href="https://contagious.dev/blog/cloudkit-sharing-five-tips-and-tricks/">five excellent tips</a> for sharing data with CloudKit.</li>
  <li><a href="https://iosdev.space/@nemecek_f">Filip Němeček</a> has a detailed guide to <a href="https://nemecek.be/blog/31/how-to-setup-cloudkit-subscription-to-get-notified-for-changes">Configuring Subscriptions</a></li>
  <li><a href="https://twitter.com/_bartjacobs">Bart Jacobs</a> has a very useful <a href="https://cocoacasts.com/five-reasons-cloudkit-notifications-are-not-arriving">Troubleshooting Guide</a> for when subscription notifications don’t arrive.</li>
</ul>

<hr />

<p><img src="/img/posts/cloudkit/shoppinguk-app-screenshots.png" alt="*Shopping&nbsp;UK* App Screenshots" /></p>

<p><em>If you get a chance, please try <a href="https://apps.apple.com/gb/app/shopping-uk/id771220733">Shopping UK</a> and let me know what you think
at <a href="https://twitter.com/wheelies">@wheelies</a></em></p>]]></content><author><name>Stuart Wheelwright</name></author><summary type="html"><![CDATA[Part 8: When Things Go Wrong]]></summary></entry><entry><title type="html">Embracing CloudKit: Part 7</title><link href="http://dev.shoppingukapp.com/2023/06/12/embracing-cloudkit-for-data-sharing-part7.html" rel="alternate" type="text/html" title="Embracing CloudKit: Part 7" /><published>2023-06-12T00:00:00+00:00</published><updated>2023-06-12T00:00:00+00:00</updated><id>http://dev.shoppingukapp.com/2023/06/12/embracing-cloudkit-for-data-sharing-part7</id><content type="html" xml:base="http://dev.shoppingukapp.com/2023/06/12/embracing-cloudkit-for-data-sharing-part7.html"><![CDATA[<h1 id="part-7-managing-the-share-and-background-maintenance">Part 7: Managing the Share and Background Maintenance</h1>

<p>This is the seventh in an <a href="/2023/05/01/embracing-cloudkit-for-data-sharing-contents.html">eight-part series</a> on implementing
data sharing in <em>Shopping UK</em> using CloudKit.</p>

<p><small><em><a href="https://shoppingukapp.com">Shopping UK</a> is a smart shopping list for UK shoppers. It knows almost every product in the supermarket and
will arrange them by aisle. Lists can be shared with family or friends.</em></small></p>

<p><a href="/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html">Last week</a>, we looked at how data is synchronised between devices
and some of the challenges to consider. Today, we’ll look how to change or stop a share, what happens when a list is deleted and
background maintenance activities.</p>

<h2 id="managing-the-share-as-the-owner">Managing the Share as the Owner</h2>

<p>The “owner” of the list is the person that originally shared it.</p>

<p>You’ll remember from <a href="/2023/05/22/embracing-cloudkit-for-data-sharing-part4.html">Part 4</a> how a
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController</strong></a>
is used to create the initial share. Well, the same controller is also used for viewing and managing the share after it has
been created.</p>

<p><img src="/img/posts/cloudkit/manage-overview.png" alt="The share can be managed using the UICloudSharingController" /></p>

<p>As an owner of the list, you can:</p>
<ul>
  <li>View the list of participants.</li>
  <li>Invite new participants.</li>
  <li>Remove participants.</li>
  <li>Stop sharing the list.</li>
</ul>

<h3 id="inviting-new-participants">Inviting new participants</h3>

<p>Only the owner of the list can invite new participants.</p>

<p>You can invite up to 100 people. This <a href="https://developer.apple.com/documentation/cloudkit/ckerror/code/toomanyparticipants">limit</a>
is imposed by CloudKit, but it is more than adequate for our purposes.
The <a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController</strong></a>
has
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller/1649602-availablepermissions">options</a>
for controlling who can accept an invite
(<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller/permissionoptions/2274275-allowpublic">anyone with the link</a>
or
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller/permissionoptions/2274278-allowprivate">named accounts</a>),
and their access permissions
(<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller/permissionoptions/2274279-allowreadwrite">read-write</a>
or
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller/permissionoptions/2274277-allowreadonly">read-only</a>).</p>

<p>Lists in <em>Shopping UK</em> can only be shared with named accounts and read-write permissions — the user doesn’t get to choose.
This may change in the future, but I honestly couldn’t think of a use case for allowing a read-only shopping list or anonymous access
at present (<a href="https://twitter.com/wheelies">perhaps you have one?</a>).</p>

<p><img src="/img/posts/cloudkit/manage-invite-more.png" alt="New participants can be invited using the UICloudSharingController" /></p>

<p>The app doesn’t need to do anything special when a user adds a new participant. The 
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController</strong></a>
takes care of it all:</p>
<ul>
  <li>Generating and sending invites</li>
  <li>Updating the <em>cloudkit.share</em> record with the new <a href="https://developer.apple.com/documentation/cloudkit/ckshare/participant">participant’s</a>
details</li>
  <li>Updating the <em>cloudkit.share</em>
<a href="https://developer.apple.com/documentation/cloudkit/ckshare/participant/1640395-acceptancestatus">acceptance status</a></li>
</ul>

<p>When the <em>cloudkit.share</em> status changes to
<a href="https://developer.apple.com/documentation/cloudkit/ckshare/participantacceptancestatus/accepted">accepted</a>, the
<a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>
for the shared list will automatically appear in the participant’s <em>shared</em>
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a>.</p>

<h3 id="removing-participants">Removing participants</h3>

<p>Only the owner of the list can remove a participant, but anyone can remove themselves.</p>

<p>As an owner, you remove a participant using the “Remove Access” button:</p>

<p><img src="/img/posts/cloudkit/manage-remove-participant.png" alt="Participants can be removed using the UICloudSharingController" /></p>

<p><em>Note: If there is only one participant in the list, the list will stop sharing.</em></p>

<p>The app doesn’t need to do anything special to support this action. The 
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController</strong></a>
takes care of everything — i.e. updating the <em>cloudkit.share</em> record to remove the participant’s details, which will hide the list’s
<a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>
from the participant’s <em>shared</em>
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a>.</p>

<p>The removed participant will see this message before the list is deleted from their device:</p>

<p><img src="/img/posts/cloudkit/manage-participant-removed.png" alt="Participants see a message before their list is removed" /></p>

<p>This is handled by subscriptions and the
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation"><strong>CKFetchDatabaseChangesOperation</strong></a>
described in
<a href="/2023/05/15/embracing-cloudkit-for-data-sharing-part3.html">Part 3</a> and
<a href="/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html">Part 6</a>. The fetch operation
will return a <a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation/1640428-recordzonewithidwasdeletedblock"><strong>Deleted Record Zone</strong></a>
response and the app responds by showing this message.</p>

<p><em>Note: The list data is still in iCloud, in the owner’s Private database, and on the other participants’ devices.</em></p>

<h3 id="stop-sharing">Stop Sharing</h3>

<p>The “Stop Sharing” button will remove sharing for all participants and remove the data from iCloud.</p>

<p><img src="/img/posts/cloudkit/manage-stop-sharing.png" alt="List sharing can be removed completely" /></p>

<p>When this option is used, the app needs to do some housekeeping of its own to reset the local list to be a local-only list as it
was before the list was first shared.</p>

<p>The app must do three things:</p>
<ol>
  <li>Remove the locally stored <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeTokens</strong></a></li>
  <li>Delete the <a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a> that contains the list data
from iCloud</li>
  <li>Remove the local reference to the iCloud list.</li>
</ol>

<p>The app is notified of the “Stop Sharing” action by implementing the 
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontrollerdelegate/1649604-cloudsharingcontrollerdidstopsha"><strong>cloudSharingControllerDidStopSharing</strong></a>
method in the
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontrollerdelegate"><strong>UICloudSharingControllerDelegate</strong></a></p>

<p>The zone is deleted using
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabase/1449118-deleterecordzonewithid"><strong>deleteRecordZoneWithID</strong></a>
on the 
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a></p>

<p>When the zone is deleted, the participants will each receive a message like the one seen when a single participant is removed.</p>

<h2 id="managing-the-share-as-a-participant">Managing the Share as a Participant</h2>

<p>As a participant, the
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController</strong></a>
shows a different view. There are no options for inviting others, stopping the share or removing other participants.
The only option is to remove yourself from the list:</p>

<p><img src="/img/posts/cloudkit/manage-remove-self.png" alt="Participants can remove themselves from the share" /></p>

<p>The
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController</strong></a>
takes care of the mechanics of this by updating the <em>cloudkit.share</em> record.</p>

<h2 id="what-happens-when-a-list-is-deleted">What happens when a list is deleted</h2>

<p>A shared list can be deleted by either the owner or a participant. The behaviour is different for each.</p>

<h3 id="deleting-a-list-as-an-owner">Deleting a list as an owner</h3>

<p><img src="/img/posts/cloudkit/delete-list-owner.png" alt="Deleting a list as owner" /></p>

<p>When the owner deletes the list, everything is removed:</p>

<ol>
  <li>The list data from the owner’s device.</li>
  <li>The list’s <a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a> from the owner’s <em>private</em>
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a>
in iCloud.</li>
  <li>The list’s <a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a> from all participants’
<em>shared</em> <a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a> in iCloud.</li>
  <li>The list data from the devices of all participants.</li>
</ol>

<p>The owner’s app handles tasks 1 and 2, using the “Stop Sharing” logic to remove the zone.</p>

<p>Task 3 happens automatically because when the owner’s <em>private</em> zone is removed, the <em>shared</em> zones disappear too.</p>

<p>The participant’s app handles task 4, the same way as for “Stop Sharing”.</p>

<h3 id="deleting-a-list-as-a-participant">Deleting a list as a participant</h3>

<p><img src="/img/posts/cloudkit/delete-list-participant.png" alt="Deleting a list as owner" /></p>

<p>When a participant deletes a list, things are simpler, because the original list is not deleted — it remains in iCloud, and on the
owner’s device, and on the devices of other participants.</p>

<p>Only two things need to be removed:</p>
<ol>
  <li>The list data from the participant’s device.</li>
  <li>The <em>cloudkit.share</em> <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a> from the participant’s
<em>shared</em> <a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a> in iCloud.</li>
</ol>

<p>The app handles both tasks itself. The <em>cloudkit.share</em> is deleted like any other <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a>,
using <a href="https://developer.apple.com/documentation/cloudkit/ckdatabase/1449122-deleterecordwithid"><strong>deleteRecordWithID</strong></a>, which provides
a simple wrapper around
<a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation"><strong>CKModifyRecordsOperation</strong></a>.</p>

<p><em>Note: after deleting the list, a participant can re-join later using the same invitation they originally received.</em></p>

<h2 id="what-happens-when-a-user-signs-into-or-out-of-icloud">What happens when a user signs into or out of iCloud?</h2>

<p>It is important to understand that when a list is first shared, the shopping list is no longer considered to be a locally hosted list
that lives on the device.
It is now an iCloud-hosted list and the data on the local device is just a locally cached copy.</p>

<p>This means when a user signs out of iCloud, <em>all</em> iCloud-hosted lists must be deleted from the local device. If the app didn’t do this,
it would have no way to guarantee their integrity because the user no longer has access to the iCloud database that contains list changes.
Furthermore, permitting continued access to the list would violate the list’s security because the user is logged out of iCloud and
no longer authorised to view the list’s contents.</p>

<p>When a user signs-in to iCloud, the opposite happens. The app will restore a copy of all iCloud-hosted
lists from the <em>Private</em> and <em>Shared</em> databases of the newly available iCloud account.</p>

<p>The app detects a change of account status by observing the
<a href="https://developer.apple.com/documentation/foundation/nsnotification/name/1399172-ckaccountchanged"><strong>CkAccountChanged</strong></a>
notification.</p>

<p>When this notification is triggered, the app sends an
<a href="https://developer.apple.com/documentation/cloudkit/ckcontainer/1399180-accountstatus"><strong>accountStatus</strong></a>
message to CloudKit to check the account status.</p>

<p>If the response indicates a status change from
<a href="https://developer.apple.com/documentation/cloudkit/ckaccountstatus/noaccount"><strong>NoAccount</strong></a>
to
<a href="https://developer.apple.com/documentation/cloudkit/ckaccountstatus/available"><strong>Available</strong></a>,
all iCloud-hosted lists are <em>restored</em> to the device.</p>

<p>If the response indicates a status change from
<a href="https://developer.apple.com/documentation/cloudkit/ckaccountstatus/available"><strong>Available</strong></a>
to
<a href="https://developer.apple.com/documentation/cloudkit/ckaccountstatus/noaccount"><strong>NoAccount</strong></a>,
all iCloud-hosted lists are <em>removed</em> from the device.</p>

<p>The account change notifications have been reliable so far. But I like to have a manual option if things go wrong.
That’s why I added an option to force a re-fetch of all iCloud-hosted lists in the “Troubleshooting” section of the Help menu.</p>

<p><img src="/img/posts/cloudkit/refetch-cloud-hosted-lists.png" alt="Troubleshooting option to force a refetch of all iCloud-hosted lists" /></p>

<p>Without this option, the user would have to sign-out then sign back into iCloud to refresh, and this can be time consuming.</p>

<h2 id="background-maintenance">Background Maintenance</h2>

<p>Once a share is established, the user need not do anything. The app will happily synchronise changes all day long.</p>

<p>But, behind the scenes, nothing stands still.</p>

<p>If you’ve been following the previous posts, you’ll recall how <em>Shopping UK</em> doesn’t synchronise the <em>state</em> of the list. Instead, it 
synchronises the <em>Journal Entry</em> records that represent the history of <em>changes</em> made to the list (see
<a href="/2023/05/08/embracing-cloudkit-for-data-sharing-part2.html">Part 2</a>
and
<a href="/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html">Part 6</a>
for a refresher). And from these changes, the current list can be built.</p>

<p>But, you’re probably wondering what will happen after a few months, when there are thousands of <em>JournalEntry</em> records?</p>

<p>Firstly, for devices already sharing the list, it doesn’t matter how many <em>JournalEntry</em> records there are because, as we saw in
<a href="/2023/05/15/embracing-cloudkit-for-data-sharing-part3.html">Part 3</a>
and
<a href="/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html">Part 6</a>, the app uses
<a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeTokens</strong></a> to only fetch new records.</p>

<p>But this doesn’t let us off the hook entirely.</p>

<h3 id="what-would-happen-if-the-journal-keeps-growing">What would happen if the journal keeps growing?</h3>

<p>Two problems:</p>
<ol>
  <li>Storage and Quotas.</li>
  <li>New Participants joining the list.</li>
</ol>

<p>Every
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a>
takes up space (albeit just a few bytes), and this counts towards the share owner’s iCloud storage quota.
If the number of <em>JournalEntry</em> records were allowed to grow indefinitely, the owner’s iCloud storage would fill up.</p>

<p>Secondly, if a new participant joins the list later, the size of the journal becomes relevant. Remember how,
in <a href="/2023/05/29/embracing-cloudkit-for-data-sharing-part5.html">Part 5</a>,
a <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeToken</strong></a> is not used when retrieving the
list initially. With a large journal, it could take a considerable time to fetch data from iCloud, and to apply the changes to the view in the
user interface.</p>

<p>Neither of these situations is good.</p>

<h3 id="whats-the-answer">What’s the answer?</h3>

<p>The approach I chose for <em>Shopping UK</em> was to compress the journal by removing old <em>JournalEntry</em> records that
aren’t necessary for reconstructing the current list.</p>

<p>For example, the two lists below are identical but the second was built using fewer <em>JournalEntry</em> records:</p>

<p><img src="/img/posts/cloudkit/compression-1a.png" alt="Compression Example 1 - Before" /></p>

<p><img src="/img/posts/cloudkit/compression-1b.png" alt="Compression Example 1 - After" /></p>

<p>Admittedly, by removing rows 1 and 3, we have lost some of the list’s history, but the final state of both lists is the same.</p>

<p>Let’s look at another example. Are these two lists equivalent?</p>

<p><img src="/img/posts/cloudkit/compression-2a.png" alt="Compression Example 2 - Before" /></p>

<p><img src="/img/posts/cloudkit/compression-2b.png" alt="Compression Example 2 - After" /></p>

<p>No, not quite. The first list includes “bread” in a marked-off state, but the second doesn’t show “bread” at all.</p>

<p>Does this matter? Maybe not.</p>

<p>If “bread” was bought a long time ago, does it need to be on the list? After all, the list’s
purpose is to tell us what we <em>need to buy</em> not what was bought.</p>

<p>Then again, if my wife has just bought “bread”, I’d find it useful to see it crossed-off the list, so I know it was bought
and not just deleted.</p>

<p>From these examples, we can infer some general principles:</p>

<ol>
  <li>Recent items should be left alone, and</li>
  <li>The full history of changes should be preserved in an Activity view.</li>
</ol>

<p>And this is how journal compression works in <em>Shopping UK</em>.</p>

<p>Compression is never applied to the full set of <em>JournalEntry</em> changes, only to those created before a cut-off time.</p>

<p>And every change made to the list is also recorded in an Activity view that won’t be affected when the journal is compressed.</p>

<h3 id="when-are-journalentry-records-compressed">When are JournalEntry records compressed?</h3>

<p>Compression occurs in two places:</p>
<ol>
  <li>Before updating the user interface</li>
  <li>In iCloud, at regular intervals</li>
</ol>

<h4 id="before-updating-the-user-interface">Before updating the user interface</h4>

<p>When a device requests new changes from iCloud, many <em>JournalEntry</em> records may be returned.
Updating the user interface rapidly with so many changes can make interaction sluggish and distract the user.
To prevent this, the app will compress <em>JournalEntry</em> records older than three hours
and only apply the remaining changes to the user interface.</p>

<p>Practically, this means if a user on another device added and marked-off “milk” more than 3 hours ago it
won’t be shown in the list (because the “add” and the “mark-off” both happened more than 3 hours ago).
But if the user added and marked-off “milk” <em>within 3 hours</em> the item <em>would</em> be shown in the list like this in a
marked-off state, like this: <del>milk</del></p>

<h4 id="in-icloud">In iCloud</h4>

<p>The journal in iCloud will also be regularly compressed.</p>

<p>Every device that has access to a shared list is responsible for compressing the <em>JournalEntry</em> records in iCloud.
After each iCloud upload (see 
<a href="/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html">Part 6</a>),
the app will check if the journal can be compressed.
But, to save battery and bandwidth, this won’t happen more often than every 30 minutes.</p>

<p>Compression is a three-stage process:</p>
<ol>
  <li>Fetch eligible <em>JournalEntry</em> records from iCloud.</li>
  <li>Attempt to compress them on the app.</li>
  <li>Delete the discarded <em>JournalEntry</em> records from iCloud and update the <code class="language-plaintext highlighter-rouge">CompressedUntil</code> date on the <em>ShoppingList</em> record</li>
</ol>

<p><em>JournalEntry</em> records older than 14 days are considered eligible for compression.</p>

<p>The app uses a
<a href="https://developer.apple.com/documentation/cloudkit/ckqueryoperation"><strong>CKQueryOperation</strong></a>
to fetch the eligible <em>JournalEntry</em> records.</p>

<p>If the compression logic identifies records to be deleted, a
<a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation"><strong>CKModifyRecordsOperation</strong></a>
is used to remove the records from iCloud and update the <code class="language-plaintext highlighter-rouge">CompressedUntil</code> date.</p>

<p>The default <a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation/1447488-savepolicy"><strong>SavePolicy</strong></a>
of <a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation/recordsavepolicy/ifserverrecordunchanged"><strong>IfServerRecordUnchanged</strong></a>
is used, which means the entire operation will succeed or fail atomically.</p>

<h3 id="consequences-of-compression">Consequences of Compression</h3>

<p>Journal compression is a housekeeping activity, and users should mostly be oblivious to its existence.
The only time the effect of compression will be surfaced to users is if their device has not been used for more than 14 days. If this happens,
the app will perform a full re-fetch of the shared lists from CloudKit to ensure they are fully up-to-date.</p>

<p>Incidentally, if you used the app before version 3.3 was released, you may have experienced an issue that caused me
no end of stress trying to reproduce and diagnose. The old, home-grown sharing mechanism used a primitive form of journalling,
and didn’t have a way to detect if a device hadn’t been updated recently.
Frustrated users would send me messages asking why the sync stopped working after the app hadn’t been used in a while. If you were one
of the users affected by this issue, I’m truly sorry it took me so long to find and fix.</p>

<h3 id="alternatives-to-compression">Alternatives to compression</h3>

<p>Journal compression is a convenient way of reducing the size of the app’s
<a href="https://martinfowler.com/articles/patterns-of-distributed-systems/wal.html">write-ahead journal</a>.
It works well for a shopping list because the state of items on a list follows a predictable pattern: they are <code class="language-plaintext highlighter-rouge">Added</code> then
<code class="language-plaintext highlighter-rouge">Marked-Off</code> or <code class="language-plaintext highlighter-rouge">Deleted</code>. And each pair of start-end states (<code class="language-plaintext highlighter-rouge">Add</code> then <code class="language-plaintext highlighter-rouge">Mark-Off</code>) and (<code class="language-plaintext highlighter-rouge">Add</code> then <code class="language-plaintext highlighter-rouge">Delete</code>) can be neatly purged
without affecting other items. For other types of app, a more appropriate clean-up strategy for a write-ahead journal may involve a
<a href="https://martinfowler.com/articles/patterns-of-distributed-systems/low-watermark.html">low-water mark</a> of some sort.</p>

<p><a href="/2023/06/19/embracing-cloudkit-for-data-sharing-part8.html">Next week</a>, in the final part of the series, we’ll look at what happens when things go wrong:
error handling, merging data, and diagnosing problems.</p>

<hr />

<p><em>If you get a chance, please try <a href="https://apps.apple.com/gb/app/shopping-uk/id771220733">Shopping UK</a> and let me know what you think
at <a href="https://twitter.com/wheelies">@wheelies</a></em></p>]]></content><author><name>Stuart Wheelwright</name></author><summary type="html"><![CDATA[Part 7: Managing the Share and Background Maintenance]]></summary></entry><entry><title type="html">Embracing CloudKit: Part 6</title><link href="http://dev.shoppingukapp.com/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html" rel="alternate" type="text/html" title="Embracing CloudKit: Part 6" /><published>2023-06-05T00:00:00+00:00</published><updated>2023-06-05T00:00:00+00:00</updated><id>http://dev.shoppingukapp.com/2023/06/05/embracing-cloudkit-for-data-sharing-part6</id><content type="html" xml:base="http://dev.shoppingukapp.com/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html"><![CDATA[<h1 id="part-6-synchronising-data">Part 6: Synchronising Data</h1>

<p>This is the sixth in an <a href="/2023/05/01/embracing-cloudkit-for-data-sharing-contents.html">eight-part series</a> on implementing
data sharing in <em>Shopping UK</em> using CloudKit.</p>

<p><small><em><a href="https://shoppingukapp.com">Shopping UK</a> is a smart shopping list for UK shoppers. It knows almost every product in the supermarket and
will arrange them by aisle. Lists can be shared with family or friends.</em></small></p>

<p><a href="/2023/05/29/embracing-cloudkit-for-data-sharing-part5.html">Last week</a>, we looked at what happens when another device accepts
an invitation to collaborate on a shared list. Today, we’ll look at how data is synchronised between devices and some of the challenges to consider.</p>

<h2 id="what-is-synchronised">What is Synchronised?</h2>

<p>In <em>Shopping UK</em>, we need to synchronise data from two
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord/1462206-recordtype"><strong>Record Types</strong></a>:</p>
<ol>
  <li><strong>cloudkit.share</strong><br />This tells us who joined or left the shared list</li>
  <li><strong>JournalEntry</strong><br />This contains all changes made to the shopping list</li>
</ol>

<p>Here’s how they are related:</p>

<p><img src="/img/posts/cloudkit/shoppinguk-recordtypes.png" alt="Record Types used in *Shopping&nbsp;UK*" /></p>

<p><em>ShoppingList</em> represents the list itself but from a synchronisation perspective, it is very dull. <em>cloudkit.share</em> and
<em>JournalEntry</em> are where the action happens.</p>

<h3 id="what-is-special-about-cloudkitshare">What is special about cloudkit.share?</h3>

<p><em>Cloudkit.share</em> is a built-in CloudKit
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord/1462206-recordtype"><strong>Record Type</strong></a>
It represents a
<a href="https://developer.apple.com/documentation/cloudkit/ckshare"><strong>CKShare</strong></a>
object. It’s interesting because it contains a list of who has accepted the share.</p>

<p>The app doesn’t manage the list of participants directly (this is the job of 
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController</strong></a>
(see <a href="/2023/05/22/embracing-cloudkit-for-data-sharing-part4.html">Part 4</a>
and <a href="/2023/05/29/embracing-cloudkit-for-data-sharing-part5.html">Part 5</a> for details).</p>

<p>However, the app <em>is</em> interested in tracking when a participant joins or leaves the list so it can update the Activity view:</p>

<p><img src="/img/posts/cloudkit/activity-join-leave.png" alt="User Join/Leave Activity" /></p>

<p>Whenever a change is detected in a <em>cloudkit.share</em> CKRecord, the app compares the share’s
<a href="https://developer.apple.com/documentation/cloudkit/ckshare/1640453-participants"><strong>participants</strong></a>
list with the previously cached list to identify what has changed:</p>

<p><img src="/img/posts/cloudkit/ckshare-changes.png" alt="CKShare Changeset" /></p>

<p>In this example, Charlie and Diane joined the list and Bob left.</p>

<p>This list of participant changes is used to populate the Activity list.</p>

<h3 id="what-is-special-about-journalentry">What is special about JournalEntry?</h3>
<p>For sharing purposes, every list is represented as a set of <em>JournalEntry</em> records, as discussed in
<a href="/2023/05/08/embracing-cloudkit-for-data-sharing-part2.html">Part 2</a>. <em>JournalEntry</em> can
be used to represent every type of change made to a list, and the full list of <em>JournalEntry</em> records is enough to reconstruct the
entire list from scratch.</p>

<p>For example, this list:</p>

<p><img src="/img/posts/cloudkit/sharing-activity.png" alt="Example Sharing Activity" /></p>

<p>Is represented as seven <em>JournalEntry</em>
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>
(newest entry at the top):</p>

<table>
  <thead>
    <tr>
      <th>Sequence</th>
      <th>ItemId</th>
      <th>Operation</th>
      <th>Text</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>7</td>
      <td>a…</td>
      <td>Mark-off</td>
      <td>milk</td>
    </tr>
    <tr>
      <td>6</td>
      <td>b…</td>
      <td>Delete</td>
      <td>bread</td>
    </tr>
    <tr>
      <td>5</td>
      <td>c…</td>
      <td>Add</td>
      <td>eggs</td>
    </tr>
    <tr>
      <td>4</td>
      <td> </td>
      <td>Recolour List</td>
      <td>Yellow</td>
    </tr>
    <tr>
      <td>3</td>
      <td> </td>
      <td>Rename List</td>
      <td>Weekly Shopping</td>
    </tr>
    <tr>
      <td>2</td>
      <td>b…</td>
      <td>Add</td>
      <td>bread</td>
    </tr>
    <tr>
      <td>1</td>
      <td>a…</td>
      <td>Add</td>
      <td>milk</td>
    </tr>
  </tbody>
</table>

<p>In eight steps, we can see how these seven <em>JournalEntry</em> records are applied to a blank list to recreate the full shopping list:</p>

<p><img src="/img/posts/cloudkit/applying-journal.png" alt="Applying the JournalEntry records" /></p>

<p>Once a <em>JournalEntry</em> has been added to iCloud, it will never be modified — it is a read-only log of changes.
The only way <em>JournalEntry</em> records are ever removed is when the app compresses the list, which will happen regularly to prevent
the list becoming large and causing performance issues. List compression will be discussed in detail in <a href="/2023/06/12/embracing-cloudkit-for-data-sharing-part7.html">part 7</a>.</p>

<p>Every newly created <em>JournalEntry</em> record must be synchronised between each device and iCloud.</p>

<h2 id="how-does-synchronisation-work">How does Synchronisation Work?</h2>

<p>In <a href="/2023/05/15/embracing-cloudkit-for-data-sharing-part3.html">Part 3</a>, we looked at how data in iCloud can be changed using 
<a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation"><strong>CKModifyRecordsOperation</strong></a>.</p>

<p>We also looked at three ways data can be read from iCloud:</p>
<ol>
  <li><a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordsoperation"><strong>CKFetchRecordsOperation</strong></a></li>
  <li><a href="https://developer.apple.com/documentation/cloudkit/ckqueryoperation"><strong>CKQueryOperation</strong></a></li>
  <li><a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation"><strong>CKFetchDatabaseChangesOperation</strong></a>
with
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonechangesoperation"><strong>CKFetchRecordZoneChangesOperation</strong></a></li>
</ol>

<p>These operations form the basis of synchronisation. Now let’s look at the full cycle and how each device knows when a change has been made.</p>

<h3 id="sync-step-1-upload-changes">Sync Step 1: Upload Changes</h3>
<p>When a change is made to a list on one device, a record representing that change must be uploaded to iCloud as soon as possible.</p>

<p>The device that made the change will send a 
<a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation"><strong>CKModifyRecordsOperation</strong></a>
request to iCloud using CloudKit:</p>

<p><img src="/img/posts/cloudkit/shoppinguk-sync1.png" alt="Device uploads to iCloud" /></p>

<h3 id="sync-step-2-notify-other-devices">Sync Step 2: Notify other devices</h3>

<p>After CloudKit has updated the iCloud database with the new entry, it sends a
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabasenotification"><strong>CKDatabaseNotification</strong></a>
message to all other devices running the app.</p>

<p><img src="/img/posts/cloudkit/shoppinguk-sync2.png" alt="iCloud sends notification to all devices" /></p>

<h3 id="sync-step-3-other-device-requests-change">Sync Step 3: Other device requests change</h3>

<p>Upon receipt of the notification, the other device will issue a
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation"><strong>CKFetchDatabaseChangesOperation</strong></a>
followed by a
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonechangesoperation"><strong>CKFetchRecordZoneChangesOperation</strong></a>
to request a list of all changes.
<img src="/img/posts/cloudkit/shoppinguk-sync3.png" alt="Device requests changes from iCloud" /></p>

<h3 id="sync-step-4-icloud-sends-changes">Sync Step 4: iCloud sends changes</h3>

<p>iCloud sends a list of changes made since the last request. This list will include the newly created “Add potatoes” record:</p>

<p><img src="/img/posts/cloudkit/shoppinguk-sync4.png" alt="iCloud sends changes to device" /></p>

<h3 id="how-does-icloud-know-which-devices-to-notify">How does iCloud know which devices to notify?</h3>

<p><em>Shopping UK</em> uses CloudKit Subscriptions to inform the app when another user makes a change to a shared list.</p>

<p>After the app has registered a subscription with CloudKit, the app will be notified when the watched data changes.</p>

<h4 id="how-are-subscriptions-registered">How are subscriptions registered?</h4>

<p>Subscriptions are managed separately for each user. They are <em>not</em> global — they live in the user’s iCloud account.</p>

<p>There are three types of subscription:</p>
<ol>
  <li><a href="https://developer.apple.com/documentation/cloudkit/ckquerysubscription"><strong>CKQuerySubscription</strong></a></li>
  <li><a href="https://developer.apple.com/documentation/cloudkit/ckrecordzonesubscription"><strong>CKRecordZoneSubscription</strong></a></li>
  <li><a href="https://developer.apple.com/documentation/cloudkit/ckdatabasesubscription"><strong>CKDatabaseSubscription</strong></a></li>
</ol>

<p><em>Shopping UK</em> uses
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabasesubscription"><strong>CKDatabaseSubscription</strong></a>,
which is used to watch changes in a specific
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a>
and it can be further limited to
specific <a href="https://developer.apple.com/documentation/cloudkit/ckrecord/1462206-recordtype"><strong>Record Types</strong></a>.</p>

<p>During app start up
(<a href="https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623032-application"><strong>willFinishLaunchingWithOptions</strong></a>),
<em>Shopping UK</em> will attempt to register four subscriptions,
one for each
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord/1462206-recordtype"><strong>Record Type</strong></a>
(<em>cloudkit.share</em> and <em>JournalEntry</em>) and one for each
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a>
(<em>Private</em> and <em>Shared</em>)</p>

<p><a href="https://developer.apple.com/documentation/cloudkit/ckmodifysubscriptionsoperation"><strong>CKModifySubscriptionsOperation</strong></a>
is used to register the four subscriptions:</p>
<ol>
  <li>PrivateJournalEntryChanges</li>
  <li>Privatecloudkit.shareChanges</li>
  <li>SharedJournalEntryChanges</li>
  <li>Sharedcloudkit.shareChanges</li>
</ol>

<h4 id="receiving-notifications-from-subscriptions">Receiving notifications from subscriptions?</h4>

<p>Once a subscription has been registered, iOS will call the AppDelegate’s
<a href="https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623013-application"><strong>didReceiveRemoteNotification</strong></a>
method to tell the app when a change happens.</p>

<p>This notification can be received while the app is active, in the background or even when it is not running.
(<a href="https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623013-application">source</a>)</p>

<blockquote>
  <p>the system calls this method when your app is running in the foreground or background.
In addition, if you enabled the remote notifications background mode,
the system launches your app (or wakes it from the suspended state)
and puts it in the background state when a remote notification arrives</p>
</blockquote>

<p>The basics:</p>
<ul>
  <li>Call the
<a href="https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623013-application">completion handler</a>
after processing the notification.</li>
  <li>Return the appropriate <a href="https://developer.apple.com/documentation/uikit/uibackgroundfetchresult">result</a>:
<code class="language-plaintext highlighter-rouge">NewData</code>, <code class="language-plaintext highlighter-rouge">NoData</code> or <code class="language-plaintext highlighter-rouge">Failed</code>. Be consistent with how you use these.</li>
  <li>You only have 30 seconds to process the notification. Be as quick and energy efficient as possible.</li>
</ul>

<p><strong>Important: A notification will not always be received</strong></p>

<p>A notification is not guaranteed to be sent from iCloud.
iOS uses heuristics for deciding when to send a notification.</p>

<p>Apple doesn’t document the heuristics. Presumably because it doesn’t want developers to game the system.
The best official document I found was
<a href="https://developer.apple.com/library/archive/technotes/tn2265/_index.html">Apple TechNote TN2265</a> from 2016:</p>

<blockquote>
  <p>your app will receive the notification if iOS or OS X determines it is energy-efficient to do so.
If the energy or data budget for the device has been exceeded, your app will not receive any more notifications with
the content-available key until the budget has been reset.
This occurs once a day and cannot be changed by user or developer action.</p>
</blockquote>

<p>iOS tracks how long your app takes to process each notification and what result was returned by your app.</p>

<p>Example:</p>

<table>
  <thead>
    <tr>
      <th style="text-align: right">Notification</th>
      <th style="text-align: right">Time Taken (ms)</th>
      <th>Result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: right">1</td>
      <td style="text-align: right">2000</td>
      <td>newdata</td>
    </tr>
    <tr>
      <td style="text-align: right">2</td>
      <td style="text-align: right">3000</td>
      <td>newdata</td>
    </tr>
    <tr>
      <td style="text-align: right">3</td>
      <td style="text-align: right">100</td>
      <td>nodata</td>
    </tr>
    <tr>
      <td style="text-align: right">4</td>
      <td style="text-align: right">5000</td>
      <td>newdata</td>
    </tr>
    <tr>
      <td style="text-align: right">5</td>
      <td style="text-align: right">7000</td>
      <td>failed</td>
    </tr>
    <tr>
      <td style="text-align: right">6</td>
      <td style="text-align: right">600</td>
      <td>newdata</td>
    </tr>
    <tr>
      <td style="text-align: right">7</td>
      <td style="text-align: right">100</td>
      <td>failed</td>
    </tr>
    <tr>
      <td style="text-align: right">8</td>
      <td style="text-align: right">2500</td>
      <td>newdata</td>
    </tr>
    <tr>
      <td style="text-align: right">9</td>
      <td style="text-align: right">3500</td>
      <td>newdata</td>
    </tr>
    <tr>
      <td style="text-align: right">10</td>
      <td style="text-align: right">20</td>
      <td>nodata</td>
    </tr>
  </tbody>
</table>

<p>iOS uses this data to decide how to handle the next notification. Should it be sent to the app <em>at all</em>?
And, if judged worthy, <em>how soon</em> should it be sent?</p>

<p>My guess, based on a lot of testing, is iOS groups these results by type, then calculates stats on each group:</p>
<ul>
  <li>Median time for <code class="language-plaintext highlighter-rouge">newdata</code> was 2750ms</li>
  <li>Median time for <code class="language-plaintext highlighter-rouge">nodata</code> was 60ms</li>
</ul>

<p>iOS appears to use this information, and other data (battery charge, battery decrease rate, number of notifications received) to decide
how much budget to give your app for future notifications.</p>

<p>While testing, I learnt a couple of important lessons.</p>

<p><strong>Lesson 1: Always return a result</strong></p>

<p>I found a bug in my app, which meant a result was not returned when the app was in the background. When this happened, CloudKit would refuse to send
notifications for a few minutes. My app wasn’t playing by the rules so it was being punished.</p>

<p><strong>Lesson 2: Classify results correctly</strong></p>

<p>Another bug meant my app would sometimes return <code class="language-plaintext highlighter-rouge">nodata</code> instead of <code class="language-plaintext highlighter-rouge">newdata</code>, despite actually fetching and processing genuine data.
This would happen about 50% of the time.
I’d add an item to a list on one device but the notification wouldn’t appear on the second device. 
I’d add a second item and this time a notification would be received on the second device.</p>

<p>Because the app was incorrectly reporting work as <code class="language-plaintext highlighter-rouge">nodata</code>, iOS reduced the number of notifications it sent by half.
From iOS’s perspective this made sense — why waste resources on notifications that take the same time to process
but don’t produce useful work.</p>

<p>It’s all about saving battery. If an app abuses the time given to process remote notifications,
iOS will grant it less time in the future. If the app fails to respond at all, iOS will penalise the app.
If the app says it hasn’t done anything useful with the time it was given, iOS will grant it less time in future.
This can result in notifications being skipped, combined or delayed.</p>

<p>The best strategy is to be efficient with resources — don’t do more than you need to when a notification arrives — and be
truthful with what you did. If your app used its time to fetch new data, return <code class="language-plaintext highlighter-rouge">newdata</code>. If your app didn’t need to do anything, return <code class="language-plaintext highlighter-rouge">nodata</code>.
And if your app could not complete processing for whatever reason, return <code class="language-plaintext highlighter-rouge">failed</code>. This will allow iOS to classify the time taken in the
correct dataset so it can draw the correct conclusions.</p>

<p>One final tip, when testing, if your notifications arrive on some devices but not others, try:</p>
<ul>
  <li>Charging the device. iOS will try harder to save battery when it is low, and skipping notifications is one way to do this.</li>
  <li>Disable “Do Not Disturb”. I saw a situation when this prevented notifications from arriving. As soon as I disabled “Do Not Disturb”
notifications began to arrive again.</li>
  <li>Wait a while. iOS seems to reset its stats after a while. Try waiting a few minutes or even a few hours. Or try a different device.</li>
</ul>

<h4 id="how-does-the-app-respond-to-subscription-notifications">How does the app respond to subscription notifications?</h4>

<p>After receiving the notification, <em>Shopping UK</em> fetches the list of changes using
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation"><strong>CKFetchDatabaseChangesOperation</strong></a>
and 
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonechangesoperation"><strong>CKFetchRecordZoneChangesOperation</strong></a>
as described in Step 3 above, and <a href="/2023/05/15/embracing-cloudkit-for-data-sharing-part3.html">Part 3</a> of this series.</p>

<p>It may be tempting to skip the “fetch” step and use the information provided in the notification message, but this is unreliable.
iOS may decide to combine or drop notifications for individual changes so an explicit fetch is required to ensure no changes are missed.</p>

<h4 id="how-is-the-list-of-changes-processed">How is the list of changes processed?</h4>

<p>After the app has made the request, CloudKit will send several responses, which may include:</p>
<ol>
  <li>a set of
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a></li>
  <li>a list of deleted <a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone/id"><strong>RecordZone.IDs</strong></a>,</li>
  <li>a list of deleted <a href="https://developer.apple.com/documentation/cloudkit/ckrecord/id"><strong>Record.IDs</strong></a></li>
</ol>

<p>See <a href="/2023/05/15/embracing-cloudkit-for-data-sharing-part3.html">Part 3</a> for full details.</p>

<p>When the app receives changes, it stores them locally until it is the right time to apply them to the user interface.</p>

<p>The “right time” depends on which list is active, and on what the user is doing.</p>

<p>When the app is in the “Planning” screen, the <em>JournalEntry</em> operations can be applied to the current list straight away.
But, if the app is in the “Shopping” screen, a different approach is needed. If items were added
to the visible list immediately, it would disrupt the user’s flow (imagine if a set of new items were added to your list just as you were about
to mark it off. It would be annoying to accidentally tap the wrong item because the app had decided to shuffle the list items around just
at that moment).</p>

<p>To provide a better user experience, <em>Shopping UK</em> displays each “Add” as a visible notification at the top of the list.
Then, when the user taps the notification, the queued items will move to the right category in the list.</p>

<p><img src="/img/posts/cloudkit/shopping-screen-add.png" alt="&quot;Add&quot; notifications in &quot;shopping&quot; mode" /></p>

<p>When an item is “Marked-off” or “Deleted”, the app handles things differently.
Since the item is already in the list, there is no need to display a separate notification. Instead, the item’s table cell is updated
to reflect the state change.</p>

<p><img src="/img/posts/cloudkit/shopping-screen-delete-mark-off.png" alt="&quot;Delete&quot; and &quot;Marked-off&quot; indications in &quot;shopping&quot; mode" /></p>

<p>This behaviour makes it a delight to use <em>Shopping UK</em> when family and friends are shopping together. Some people like to split up in the
supermarket to save time. To make this work, it is vital that changes appear quickly and are visually obvious, so the same item isn’t
purchased twice.</p>

<h2 id="when-to-synchronise">When to synchronise?</h2>

<p>Most of the time, synchronisation happens automatically. If a user makes a change to the list, a <em>JournalEntry</em> record representing the change
will be immediately uploaded to iCloud. If a
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabasenotification"><strong>CKDatabaseNotification</strong></a>
is received, which indicates someone else has made a change, the app will immediately fetch changes from iCloud.</p>

<p>In addition, the app will fetch when the app is launched (in case any changes were made by other participants while the app was in the
background).</p>

<p>But this is not always enough.</p>

<p>To handle unforeseen situations, such as CloudKit subscriptions not arriving on time, I also added a way to do invoke the full
upload-and-fetch synchronisation process manually.</p>

<p>This can be triggered by pulling down on the “planning” screen (shown below) or the “shopping” screen (the one with checkboxes)</p>

<p><img src="/img/posts/cloudkit/planning-screen-pull-to-refresh.png" alt="Pull to invoke a manual synchronisation" /></p>

<p>Another way to invoke manual sync is the “Synchronise Now” button on the Activity sheet:</p>

<p><img src="/img/posts/cloudkit/shoppinguk-force-sync.png" alt="Manual sync from Activity view" /></p>

<p>I don’t normally like adding multiple ways of doing the same thing — I’d rather ensure there is a single reliable way to
accomplish something — but when there are a lot of moving parts, some of which out of my control (i.e. iCloud, database subscriptions,
and the network), it is reassuring to know users have a manual alternative available if needed.</p>

<p><a href="/2023/06/12/embracing-cloudkit-for-data-sharing-part7.html">Next week</a>, we’ll look at how a share is changed or stopped, what happens when a list is deleted, and background maintenance.</p>

<hr />

<p><em>If you get a chance, please try <a href="https://apps.apple.com/gb/app/shopping-uk/id771220733">Shopping UK</a> and let me know what you think
at <a href="https://twitter.com/wheelies">@wheelies</a></em></p>]]></content><author><name>Stuart Wheelwright</name></author><summary type="html"><![CDATA[Part 6: Synchronising Data]]></summary></entry><entry><title type="html">Embracing CloudKit: Part 5</title><link href="http://dev.shoppingukapp.com/2023/05/29/embracing-cloudkit-for-data-sharing-part5.html" rel="alternate" type="text/html" title="Embracing CloudKit: Part 5" /><published>2023-05-29T00:00:00+00:00</published><updated>2023-05-29T00:00:00+00:00</updated><id>http://dev.shoppingukapp.com/2023/05/29/embracing-cloudkit-for-data-sharing-part5</id><content type="html" xml:base="http://dev.shoppingukapp.com/2023/05/29/embracing-cloudkit-for-data-sharing-part5.html"><![CDATA[<h1 id="part-5-accepting-a-sharing-invitation">Part 5: Accepting a Sharing Invitation</h1>

<p>This is the fifth in an <a href="/2023/05/01/embracing-cloudkit-for-data-sharing-contents.html">eight-part series</a> on implementing
data sharing in <em>Shopping UK</em> using CloudKit.</p>

<p><small><em><a href="https://shoppingukapp.com">Shopping UK</a> is a smart shopping list for UK shoppers. It knows almost every product in the supermarket and
will arrange them by aisle. Lists can be shared with family or friends.</em></small></p>

<p><a href="/2023/05/22/embracing-cloudkit-for-data-sharing-part4.html">Last week</a>, we looked at how sharing works and how to start
sharing the list. Today, we’ll look at what happens when another device accepts an invitation to collaborate on a shared list.</p>

<h2 id="accepting-a-share">Accepting a share</h2>

<p>A sharing invitation is sent from the owner to one or more participants, typically via Messages or Email. The “owner” is the account
that initiates the sharing.</p>

<p><img src="/img/posts/cloudkit/accept-step0.png" alt="The sharing invitation is typically a message" /></p>

<p>The invitation itself is just a link to iCloud, like this:</p>

<p><a href="https://www.icloud.com/share/0123456789abcdefghijklmno#Shopping">https://www.icloud.com/share/0123456789abcdefghijklmno#Shopping</a></p>

<p>The fragment (i.e. the part after the #) is the title of the share, in this case, “Shopping”.</p>

<h3 id="step-1-user-clicks-sharing-invitation-link">Step 1: User clicks sharing invitation link</h3>

<p>When a user clicks on the invitation link, iOS first checks if the app is installed, and it is right version to accept shares.</p>

<p>The <a href="https://developer.apple.com/documentation/bundleresources/information_property_list/cksharingsupported">CKSharingSupported key</a>
in info.plist indicates this.</p>

<p>Before releasing v3.3 (the first version of <em>Shopping UK</em> to support CloudKit sharing), I
<a href="/2023/04/10/fast-and-reliable-list-sharing.html">released v3.2</a>, which had sharing enabled.
The idea was to improve the adoption of sharing. When v3.3 was published, all existing v3.2 users could
accept shares immediately without having to first update their app.</p>

<p>Once the system completes its checks, the user sees a message:</p>

<p><img src="/img/posts/cloudkit/accept-step1.png" alt="iOS asks the user for permission to accept share" /></p>

<h3 id="step-2-app-informs-cloudkit-that-share-has-been-accepted">Step 2: App Informs CloudKit that share has been accepted</h3>

<p>After the user accepts the link, iOS calls
<a href="https://developer.apple.com/documentation/uikit/uiapplicationdelegate/2206721-application"><strong>userDidAcceptCloudKitShareWith</strong></a>.</p>

<p>A <a href="https://developer.apple.com/documentation/cloudkit/ckshare/metadata"><strong>CKShare.Metadata</strong></a> object is provided as a parameter.
This has everything the app needs to find the shared data in iCloud.</p>

<p><img src="/img/posts/cloudkit/accept-step2.png" alt="The CKShare.Metadata contains pointers to the shared data" /></p>

<p>To accept the share, <em>Shopping UK</em> sends a
<a href="https://developer.apple.com/documentation/cloudkit/ckacceptsharesoperation"><strong>CKAcceptSharesOperation</strong></a>
message to the identified
<a href="https://developer.apple.com/documentation/cloudkit/ckcontainer"><strong>CKContainer</strong></a>.</p>

<p>CloudKit will change the participant’s status from
<a href="https://developer.apple.com/documentation/cloudkit/ckshare/participantacceptancestatus/pending"><strong>Pending</strong></a>
to
<a href="https://developer.apple.com/documentation/cloudkit/ckshare/participantacceptancestatus/accepted"><strong>Accepted</strong></a>.</p>

<p>Note: Before calling <a href="https://developer.apple.com/documentation/cloudkit/ckacceptsharesoperation"><strong>CKAcceptSharesOperation</strong></a>,
<em>Shopping UK</em> checks the list hasn’t already been added to the device. If it already exists, the shopping list is 
opened in the app.</p>

<h3 id="step-3-app-creates-a-new-list-and-assigns-the-sharing-details">Step 3: App creates a new list and assigns the sharing details</h3>

<p>The app creates a new empty list on the device using the info in
<a href="https://developer.apple.com/documentation/cloudkit/ckshare/metadata"><strong>CKShare.Metadata</strong></a>:</p>

<table>
  <thead>
    <tr>
      <th>List Property</th>
      <th>Copied From</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ListId</td>
      <td><a href="https://developer.apple.com/documentation/cloudkit/ckrecordid/1500973-recordname">RootRecordId Name</a></td>
    </tr>
    <tr>
      <td>ListName</td>
      <td>CKShare’s <a href="https://developer.apple.com/documentation/cloudkit/cksharetitlekey">CKShareTitleKey</a></td>
    </tr>
    <tr>
      <td>ZoneId</td>
      <td><a href="https://developer.apple.com/documentation/cloudkit/ckrecordid/1500969-zoneid">RootRecordId ZoneId</a></td>
    </tr>
    <tr>
      <td>Database</td>
      <td>“shared”</td>
    </tr>
  </tbody>
</table>

<h4 id="why-is-the-database-set-to-shared">Why is the Database set to “Shared”?</h4>

<p>This confused me initially, but this is what I learned:</p>

<p>When Alice shares a list, it is uploaded to her <em>Private</em>
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a>.</p>

<p>But, when Bob accepts the invitation to join Alice’s list, the <em>same records</em> are visible to him in his <em>Shared</em> Database.
Both accounts can make changes to the underlying data via their respective database: Alice via her <em>Private</em> database and
Bob via his <em>Shared</em> database.</p>

<p><strong>Data appears in a different database depending on whether your iCloud account is the owner or a participant.</strong></p>

<p><img src="/img/posts/cloudkit/accept-step3.png" alt="Owners and participants see the same data differently" /></p>

<h3 id="step-4-app-fetches-data-from-icloud-and-applies-it-to-the-device">Step 4: App fetches data from iCloud and applies it to the device</h3>

<p>Next, the app fetches a list of changes using <strong>Fetch Changes</strong>
(described in <a href="/2023/05/15/embracing-cloudkit-for-data-sharing-part3.html">Part 3</a>).
But the <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeToken</strong></a>
is ignored to ensure <em>all</em> changes are fetched.</p>

<p>As changes are received, the app applies them to the new list then shows the list to the
user.</p>

<p><img src="/img/posts/cloudkit/accept-step4.png" alt="After all changes have been fetched " /></p>

<p><a href="/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html">Next week</a>, we’ll take a closer look at how data is synchronised between devices and some of the challenges to consider.</p>

<hr />

<p><em>If you get a chance, please try <a href="https://apps.apple.com/gb/app/shopping-uk/id771220733">Shopping UK</a> and let me know what you think
at <a href="https://twitter.com/wheelies">@wheelies</a></em></p>]]></content><author><name>Stuart Wheelwright</name></author><summary type="html"><![CDATA[Part 5: Accepting a Sharing Invitation]]></summary></entry><entry><title type="html">Embracing CloudKit: Part 4</title><link href="http://dev.shoppingukapp.com/2023/05/22/embracing-cloudkit-for-data-sharing-part4.html" rel="alternate" type="text/html" title="Embracing CloudKit: Part 4" /><published>2023-05-22T00:00:00+00:00</published><updated>2023-05-22T00:00:00+00:00</updated><id>http://dev.shoppingukapp.com/2023/05/22/embracing-cloudkit-for-data-sharing-part4</id><content type="html" xml:base="http://dev.shoppingukapp.com/2023/05/22/embracing-cloudkit-for-data-sharing-part4.html"><![CDATA[<h1 id="part-4-sharing-concepts-and-how-to-begin-sharing">Part 4: Sharing Concepts and How to Begin Sharing</h1>

<p>This is the fourth in an <a href="/2023/05/01/embracing-cloudkit-for-data-sharing-contents.html">eight-part series</a> on implementing
data sharing in <em>Shopping UK</em> using CloudKit.</p>

<p><small><em><a href="https://shoppingukapp.com">Shopping UK</a> is a smart shopping list for UK shoppers. It knows almost every product in the supermarket and
will arrange them by aisle. Lists can be shared with family or friends.</em></small></p>

<p><a href="/2023/05/15/embracing-cloudkit-for-data-sharing-part3.html">Last week</a>, we looked at the three ways records can be retrieved from iCloud,
and how the data can be added and changed. Today, we’ll look at how sharing works and how to initiate the sharing of a list.</p>

<h2 id="where-does-the-data-live">Where does the data live?</h2>

<p>Before we look at sharing, let’s review how data is segregated in iCloud.</p>

<p>Every iCloud account sees three separate <a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabases</strong></a>:</p>

<p><img src="/img/posts/cloudkit/cloudkit-databases.png" alt="How data is segregated in CloudKit" /></p>

<p>When the app sends a
<a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation"><strong>CKModifyRecordsOperation</strong></a>
to iCloud to add data, it chooses which of the three <a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabases</strong></a> to use.</p>

<p><em>Public</em> and <em>Private</em> databases are easy to understand:</p>
<ul>
  <li>
    <p>Data added to <em>Public</em> can be viewed or modified by anyone (access can be restricted to only users signed into iCloud, or 
made read-only for everyone except the creator)</p>
  </li>
  <li>
    <p>Data added to <em>Private</em> can only be seen and changed by the iCloud account holder. No-one else, not even app developers like me, can see your 
<em>Private</em> database.</p>
  </li>
</ul>

<p>The <em>Shared</em> database is more interesting because it doesn’t really contain data. It simply exposes it. Think of it like a database view.</p>

<h2 id="what-happens-when-a-list-is-shared">What happens when a list is shared?</h2>

<p>There are three key activities involved in sharing a list:</p>
<ol>
  <li><strong>Preparing the data to be shared</strong>. For <em>Shopping UK</em>, the data includes the list’s items (e.g. “bread”, “milk”) and the attributes
of the list itself (name, colour).
The list will already exist on the “owner’s” device, but it must be uploaded to iCloud before it is available for other people.</li>
  <li><strong>Sending the sharing invitation</strong>. The invitation will be sent from the owner’s device to other people who will become “participants”
in the share.</li>
  <li><strong>Accepting the invite</strong>. The other participants must open and explicitly accept the share before data can be exchanged.</li>
</ol>

<p>Once setup, all changes to the shared list will be synchronised between devices.</p>

<p>Next week, in <a href="/2023/05/29/embracing-cloudkit-for-data-sharing-part5.html">part 5</a>,
we’ll see how a sharing invitation is accepted. Later, in <a href="/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html">part 6</a>,
we’ll look at how data is synchronised between devices.</p>

<p>Today, we’ll look at the first two activities: preparing the data and sending the invite.</p>

<h3 id="step-1-user-requests-the-list-to-be-shared">Step 1: User requests the list to be shared</h3>

<p>List sharing begins when the user chooses the “Share Changes to List” option from the menu:</p>

<p><img src="/img/posts/cloudkit/sharing-step1.png" alt="The user begins the sharing flow using the &quot;Share Changes to List&quot; menu option" /></p>

<h3 id="step-2-the-app-checks-the-icloud-account-and-list-state">Step 2: The app checks the iCloud account and list state</h3>

<p>Before data is exchanged, the app checks:</p>
<ol>
  <li>An iCloud account is available.</li>
  <li>The list is not already being shared.</li>
</ol>

<p>A message will be shown if either check fails.</p>

<p>The iCloud status is checked using 
<a href="https://developer.apple.com/documentation/cloudkit/ckcontainer/1399180-accountstatuswithcompletionhandl"><strong>accountStatusWithCompletionHandler</strong></a>
available on the
<a href="https://developer.apple.com/documentation/cloudkit/ckcontainer"><strong>CKContainer</strong></a>.</p>

<p>The app knows the list is shared if a <em>ShareId</em> is set in the locally cached list record on the device. Later, we’ll see how this is set.</p>

<h3 id="step-3-the-app-creates-a-controller-for-managing-the-sharing-activity">Step 3: The app creates a controller for managing the sharing activity</h3>

<p>The app creates a
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController</strong></a>
to manage the interaction with the user. It is possible to manage the interaction with custom code but
the <a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController’s</strong></a>
user interface is familiar, widely used in Apple’s own apps, and it works well.</p>

<p><img src="/img/posts/cloudkit/sharing-step3.png" alt="The app creates a UICloudSharingController to provide UI and coordinate" /></p>

<p>A <a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontrollerdelegate"><strong>UICloudSharingControllerDelegate</strong></a>
is used with the
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController</strong></a>
to provide a
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontrollerdelegate/2274280-itemtitleforcloudsharingcontroll"><strong>title</strong></a>
and
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontrollerdelegate/2274282-itemthumbnaildataforcloudsharing"><strong>thumbnail image</strong></a>
. These are shown in the top-left of the sharing sheet and in the invitation message we’ll see in Step 6.</p>

<p><img src="/img/posts/cloudkit/sharing-thumbnail-title.png" alt="Thumbnail and Title" /></p>

<h3 id="step-4-preparationhandler">Step 4: PreparationHandler</h3>

<p>The
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller/1649607-init"><strong>PreparationHandler</strong></a>
is called when the user selects the method for sending data (e.g. Message, Email, AirDrop, WhatsApp).</p>

<p>The
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller/1649607-init"><strong>PreparationHandler</strong></a>
is responsible for adding the shared records to iCloud.</p>

<p><em>Shopping UK</em> first creates a new
<a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>
in the <em>Private</em> database using
<a href="https://developer.apple.com/documentation/cloudkit/ckdatabase/1449108-saverecordzone"><strong>SaveRecordZone</strong></a>.</p>

<p><img src="/img/posts/cloudkit/sharing-step4a.png" alt="The app requests a new CKRecordZone in iCloud for the list's records" /></p>

<p>The new
<a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>
name is derived from the identity of the list (e.g. “List-db6a6e29-…”).</p>

<p><img src="/img/posts/cloudkit/sharing-step4b.png" alt="CloudKit creates a new CKRecordZone for the list's records" /></p>

<p>By placing all records for a shopping list in a dedicated
<a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>
, it will be easier to delete the data atomically.
We’ll revisit this later, in <a href="/2023/06/12/embracing-cloudkit-for-data-sharing-part7.html">part 7</a>, when we explore what happens when the user stops sharing a list or signs out of iCloud.</p>

<p>Next, the
<a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>
will be populated with the records that represent the list.</p>

<h3 id="step-5-upload-records">Step 5: Upload Records</h3>

<p>After the
<a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>
has been successfully created, the app issues a
<a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation"><strong>CKModifyRecordsOperation</strong></a> to CloudKit:</p>

<p><img src="/img/posts/cloudkit/sharing-step5.png" alt="The app uploads a CKRecord for the list and a CKShare" /></p>

<p>Only two 
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>
are included in the upload message:</p>

<ul>
  <li>
    <p>A <em>ShoppingList</em> <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a>. This represents the
shopping list itself, but it doesn’t contain any information about the list’s Name, Colour, or Items. These will be sent later
as <em>JournalEntry</em> <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a></p>
  </li>
  <li>
    <p>A <a href="https://developer.apple.com/documentation/cloudkit/ckshare"><strong>CKShare</strong></a>. This is a special type of 
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a> (Record Type: <em>cloudkit.share</em>), which identifies the
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a> to expose.</p>
  </li>
</ul>

<p>The <a href="https://developer.apple.com/documentation/cloudkit/ckshare"><strong>CKShare</strong></a>
acts as a window through which other share participants can view the shared records.
There are two ways to use it:</p>
<ol>
  <li>Share a <a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a></li>
  <li>Share a root <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a> (and its children)</li>
</ol>

<h4 id="option-1-share-a-ckrecordzone">Option 1: Share a CKRecordZone</h4>

<p>If the
<a href="https://developer.apple.com/documentation/cloudkit/ckshare"><strong>CKShare</strong></a>
is created with a 
<a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>, the zone and its contents will all be shared.</p>

<p><img src="/img/posts/cloudkit/share-type-zone.png" alt="Sharing an entire CKRecordZone" /></p>

<h4 id="option-2-share-a-root-ckrecord-and-its-children">Option 2: Share a root CKRecord (and its children)</h4>

<p>If the
<a href="https://developer.apple.com/documentation/cloudkit/ckshare"><strong>CKShare</strong></a>
is created using a <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a>, this root record and its children
(and its children’s children, etc. — all the way to the bottom of the hierarchy) will be shared.</p>

<p><img src="/img/posts/cloudkit/share-type-hierarchy.png" alt="Sharing an CKRecord hierarchy" /></p>

<p>In <em>Shopping UK</em>, there is a dedicated
<a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>
for every shopping list, which means both options are available. But I chose to share via a root record (Option 2)
rather than by zone (Option 1) because I thought it may be useful in the
future to have records present in the zone that are not part of the share.</p>

<p>As well as defining the scope of visibility, the <a href="https://developer.apple.com/documentation/cloudkit/ckshare"><strong>CKShare</strong></a> is also
responsible for recording the owner of the shared records and the acceptance status of all participants. The “Owner” is the iCloud
account of the person who initiated the share.</p>

<p><img src="/img/posts/cloudkit/share-details.png" alt="A CKShare manages the share details: participants, invitation acceptance status, permissions" /></p>

<h4 id="what-happens-to-the-lists-items">What happens to the list’s items?</h4>

<p>So far, we’ve uploaded a list placeholder <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a>
and the <a href="https://developer.apple.com/documentation/cloudkit/ckshare"><strong>CKShare</strong></a>
record, but we haven’t uploaded the most important bit — the
contents of the list — “bread” and “milk”.</p>

<p>I excluded the list’s items from the initial payload because I encountered problems when sharing very large lists
(with several hundred items).</p>

<p>I discovered the upload would take a long time and trigger a timeout. No error was
reported but the <a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController</strong></a> would give up
without creating the share. This could be reproduced consistently, and the controller would close after about 10 seconds.</p>

<p>To fix this, I changed strategy to avoid uploading the items upfront. Instead, the items are now added to an internal upload queue,
which is processed after sharing has completed (as part of Step 7 below).</p>

<p>This has a nice consequence — simpler logic. The logic to upload list items <em>after the share has been initialised</em> is now
the same as the logic to upload list items <em>during normal synchronisation</em> (which will be discussed later, in <a href="/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html">part 6</a>).</p>

<p>Once the data has uploaded, the app calls the
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller/1649607-init"><strong>PreparationHandler’s</strong></a> completion block to
tell the
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController</strong></a>
to continue with the invitation workflow.</p>

<h3 id="step-6-sending-the-invitation">Step 6: Sending the invitation</h3>

<p>After the <a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller/1649607-init"><strong>PreparationHandler’s completion block</strong></a>
has receives the success message from the app, it launches the chosen method of sharing
(e.g. Messages)</p>

<p><img src="/img/posts/cloudkit/preparationhandler-complete.png" alt="The invitation is sent" /></p>

<p>The user will choose the message’s recipients in the usual way, add additional message contents if they choose, and hit the “Send” button.</p>

<h3 id="step-7-finishing-up">Step 7: Finishing up</h3>

<p>Once the invitation message has been sent, the controller calls the delegate’s
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontrollerdelegate/1649605-cloudsharingcontrollerdidsavesha"><strong>cloudSharingControllerDidSaveShare</strong></a>
method.</p>

<p><em>Note: I found the cloudSharingControllerDidSaveShare method was sometimes called more than once.
This seems to happen when sharing is initiated multiple times in the same session, as if the event handler is being wired-up multiple
times and never released. I don’t know if this is something I have done wrong, or a bug in the framework (it’s most
likely to be something I’ve done), but I had to work around this by ensuring all operations are
<a href="https://en.wikipedia.org/wiki/Idempotence#Computer_science_meaning">idempotent</a>.</em></p>

<p>Now the data is safely in iCloud, the next task is for the app to link the on-device list to the list in iCloud.</p>

<p>The iCloud information can be read from the
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller"><strong>UICloudSharingController’s</strong></a>
<a href="https://developer.apple.com/documentation/uikit/uicloudsharingcontroller/1649601-share"><strong>Share</strong></a>
property, which contains the new <a href="https://developer.apple.com/documentation/cloudkit/ckshare"><strong>CKShare</strong></a> created in step 5.</p>

<p>The following properties are sufficient to locate the iCloud data:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>CloudKitDatabase  = "private"
CloudKitZoneName  = share.Id.ZoneId.ZoneName
CloudKitZoneOwner = share.Id.ZoneId.OwnerName
CloudKitShareId   = share.Id.RecordName
</code></pre></div></div>

<p>Finally, the user interface is refreshed so the user knows the list is being shared. The app also triggers an upload of
the <em>JournalEntry</em> items (“bread”, “milk”) from the internal upload queue (as described in step 5).</p>

<p>Now sharing is complete:</p>

<p><img src="/img/posts/cloudkit/sharing-step7.png" alt="Sharing is now setup" /></p>

<p><a href="/2023/05/29/embracing-cloudkit-for-data-sharing-part5.html">Next week</a>, we’ll see what happens on the other side, when another device accepts the share.</p>

<hr />

<p><em>If you get a chance, please try <a href="https://apps.apple.com/gb/app/shopping-uk/id771220733">Shopping UK</a> and let me know what you think
at <a href="https://twitter.com/wheelies">@wheelies</a></em></p>]]></content><author><name>Stuart Wheelwright</name></author><summary type="html"><![CDATA[Part 4: Sharing Concepts and How to Begin Sharing]]></summary></entry><entry><title type="html">Embracing CloudKit: Part 3</title><link href="http://dev.shoppingukapp.com/2023/05/15/embracing-cloudkit-for-data-sharing-part3.html" rel="alternate" type="text/html" title="Embracing CloudKit: Part 3" /><published>2023-05-15T00:00:00+00:00</published><updated>2023-05-15T00:00:00+00:00</updated><id>http://dev.shoppingukapp.com/2023/05/15/embracing-cloudkit-for-data-sharing-part3</id><content type="html" xml:base="http://dev.shoppingukapp.com/2023/05/15/embracing-cloudkit-for-data-sharing-part3.html"><![CDATA[<h1 id="part-3-adding-updating-and-deleting-records">Part 3: Adding, Updating and Deleting Records</h1>

<p>This is the third in an <a href="/2023/05/01/embracing-cloudkit-for-data-sharing-contents.html">eight-part series</a> on implementing
data sharing in <em>Shopping UK</em> using CloudKit.</p>

<p><small><em><a href="https://shoppingukapp.com">Shopping UK</a> is a smart shopping list for UK shoppers. It knows almost every product in the supermarket and
will arrange them by aisle. Lists can be shared with family or friends.</em></small></p>

<p><a href="/2023/05/08/embracing-cloudkit-for-data-sharing-part2.html">Last week</a>, we looked at CloudKit concepts and the design of the <em>Shopping UK</em>
schema. Today, we’ll look at how <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>
are uploaded to iCloud using CloudKit; and the three ways the app can fetch
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>.</p>

<h2 id="adding-and-changing-data">Adding and Changing Data</h2>

<p>To create, modify or delete a <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a>
in iCloud, the app must send a
<a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation"><strong>CKModifyRecordsOperation</strong></a> message to CloudKit.</p>

<p><img src="/img/posts/cloudkit/cloudkit-changing-data.png" alt="Modifying data using CloudKit" /></p>

<p><em>Shopping UK</em> uses <a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation"><strong>CKModifyRecordsOperation</strong></a>
to upload new <em>JournalEntry</em> <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>
to iCloud so they can be fetched by other devices that share the shopping list.</p>

<p>Let’s look at what happens when a user adds two new items to the list.</p>

<h3 id="step-1-user-adds-items-to-the-shared-list">Step 1. User adds items to the shared list</h3>

<p>There are three ways a user can add items to the list:</p>
<ol>
  <li>Start typing and choose a suggestion.</li>
  <li>Copy items from another list.</li>
  <li>Paste from another app, such as Notes, using the clipboard.</li>
</ol>

<p><img src="/img/posts/cloudkit/cloudkit-modify-1.png" alt="User adds items from the drawer to the “Weekly Shop” list" /></p>

<p>It doesn’t matter how the user added “milk” and “bread” to their shopping list, the way they are uploaded to iCloud via CloudKit is the same.</p>

<h3 id="step-2-app-saves-changes-to-local-journal">Step 2. App saves changes to local journal</h3>

<p><em>Shopping UK</em> maintains a local journal of changes. All changes are first written to this journal before the app sends them to iCloud.</p>

<p>This step is not essential for uploading data to iCloud using CloudKit but, if your app must guarantee changes will be uploaded,
it is wise to first store them on the device, even if only temporarily, before uploading the changes to iCloud.</p>

<p>By doing this, we can be sure nothing will be lost if the network is not available.
We’ll examine how things can fail in depth in <a href="/2023/06/19/embracing-cloudkit-for-data-sharing-part8.html">part 8</a>.</p>

<p><img src="/img/posts/cloudkit/cloudkit-modify-2.png" alt="The app creates two JournalEntry records to represent the changes" /></p>

<h3 id="step-3-app-sends-ckrecords-to-icloud-using-cloudkit">Step 3. App sends CKRecords to iCloud using CloudKit</h3>

<p>The app creates a <a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation"><strong>CKModifyRecordsOperation</strong></a>
message and attaches the two <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>
that represent the changes from the local journal.</p>

<p><img src="/img/posts/cloudkit/cloudkit-modify-3.png" alt="The app creates a CKModifyRecordsOperation and sends it to iCloud using CloudKit" /></p>

<p>Note: The app can set a <a href="https://developer.apple.com/documentation/foundation/operation/1413553-qualityofservice"><strong>Quality of Service</strong></a>
(QoS) value for the 
<a href="https://developer.apple.com/documentation/cloudkit/ckmodifyrecordsoperation"><strong>CKModifyRecordsOperation</strong></a>.
A lower level QoS is used for operations that are not time critical.
iOS may delay these operations when the device is in low power mode or low on battery.</p>

<p><em>Shopping UK</em> uses the <a href="https://developer.apple.com/documentation/foundation/qualityofservice/userinitiated"><strong>User-Initiated</strong></a>
level for sending and fetching <em>JournalEntry</em> records. This provides nearly instantaneous messaging (a few seconds or less).</p>

<h3 id="step-4-cloudkit-sends-response-back-to-app">Step 4. CloudKit sends response back to app</h3>

<p>CloudKit adds the <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>
to the <a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a>
and returns three response messages.</p>

<p><img src="/img/posts/cloudkit/cloudkit-modify-4.png" alt="iCloud sends back three messages: one for each record and a final message when the overall operation has completed" /></p>

<p>All CloudKit operations are asynchronous and typically follow this pattern:</p>
<ul>
  <li>The app sends the initial message to CloudKit</li>
  <li>CloudKit will return a response message for each completed <em>unit</em> (i.e. two responses, one for each
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a>)</li>
  <li>CloudKit will return another response message when the <em>entire operation</em> is complete.</li>
</ul>

<p>The happy path is shown here — when everything goes well — but, due to the nature of networks and remote systems,
things can go wrong. In <a href="/2023/06/19/embracing-cloudkit-for-data-sharing-part8.html">part 8</a>, we will revisit the error scenarios and how <em>Shopping UK</em> handles each one.</p>

<h2 id="fetching-data">Fetching Data</h2>

<p>We looked at uploading new <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>
to iCloud, but how is data fetched?</p>

<p>There are three options:</p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Operation to use</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Fetch By Name</td>
      <td><a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordsoperation"><strong>CKFetchRecordsOperation</strong></a></td>
    </tr>
    <tr>
      <td>Fetch By Query</td>
      <td><a href="https://developer.apple.com/documentation/cloudkit/ckqueryoperation"><strong>CKQueryOperation</strong></a></td>
    </tr>
    <tr>
      <td>Fetch Changes</td>
      <td><a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation"><strong>CKFetchDatabaseChangesOperation</strong></a><br /><a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonechangesoperation"><strong>CKFetchRecordZoneChangesOperation</strong></a></td>
    </tr>
  </tbody>
</table>

<p><img src="/img/posts/cloudkit/cloudkit-fetching-data.png" alt="Fetching data using CloudKit" /></p>

<p><em>Shopping UK</em> uses all three techniques in different places.</p>

<p><strong>Fetch By Name</strong> is used to:</p>
<ol>
  <li>Retrieve a <a href="https://developer.apple.com/documentation/cloudkit/ckshare"><strong>CKShare</strong></a> record to allow the user to manage
the shared list (more on this in <a href="/2023/05/22/embracing-cloudkit-for-data-sharing-part4.html">part 4</a> and <a href="/2023/06/12/embracing-cloudkit-for-data-sharing-part7.html">part 7</a>)</li>
  <li>Retrieve the current <em>ShoppingList</em> record for compression (more on this in <a href="/2023/06/12/embracing-cloudkit-for-data-sharing-part7.html">part 7</a>)</li>
</ol>

<p><strong>Fetch By Query</strong> is used to fetch <em>JournalEntry</em> records eligible for compression (more on this in <a href="/2023/06/12/embracing-cloudkit-for-data-sharing-part7.html">part 7</a>).</p>

<p><strong>Fetch Changes</strong> is the most interesting of the three. It uses
a <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeToken</strong></a> to identify each version in the
database’s history, which means the app can request just the records that changed since its last request. This is much more efficient than
fetching everything every time, and it is more reliable than creating a custom mechanism for tracking changes.</p>

<p><em>For my first attempt at implementing sharing using CloudKit, before I understood how <strong>Fetch Changes</strong> worked, I tried to build my own
change tracking mechanism using timestamps. I spent a long time on this dead-end, but it didn’t work well due to the precision of timestamps,
and tiny differences in the clock setting of each device</em></p>

<p><em>Shopping UK</em> uses <strong>Fetch Changes</strong> to retrieve changes made by other devices participating in a shared list.</p>

<h3 id="step-1-app-sends-a-database-changes-request-to-cloudkit">Step 1. App sends a Database Changes request to CloudKit</h3>

<p>Assume two <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>
already exist in iCloud and this is the first time the white iPhone is requesting changes.</p>

<p>Since this is the first time data will be fetched, the device won’t have a
Database <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeToken</strong></a>
so it will send <code class="language-plaintext highlighter-rouge">null</code> for the
token’s value. This instructs CloudKit to return all changes ever made.</p>

<p><img src="/img/posts/cloudkit/cloudkit-fetchchanges1.png" alt="App requests any changes to the database" /></p>

<h3 id="step-2-cloudkit-returns-changed-record-zones">Step 2. CloudKit returns changed Record Zones</h3>

<p>CloudKit will return three response messages:</p>
<ol>
  <li>A <a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation/1640467-changetokenupdatedblock"><strong>Change Token Updated</strong></a>
response containing the updated Database <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeToken</strong></a>
(e.g. ABC123)</li>
  <li>A single <a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation/1640391-recordzonewithidchangedblock"><strong>Changed Record Zone</strong></a>
response (because both 
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>
are contained within a single <a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>:
abcd1234-…)</li>
  <li>A final <a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation/1640434-fetchdatabasechangescompletionbl"><strong>Changes Completed</strong></a>
response</li>
</ol>

<p><img src="/img/posts/cloudkit/cloudkit-fetchchanges2.png" alt="The app receives a response for each zone that has changed" /></p>

<p>If multiple zones had changed, there would be a <a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation/1640391-recordzonewithidchangedblock"><strong>Changed Record Zone</strong></a>
response for each zone that contained a change.</p>

<p>If any zones had been deleted, an additional <a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation/1640428-recordzonewithidwasdeletedblock"><strong>Deleted Record Zone</strong></a>
response would have been returned too.</p>

<h3 id="step-3-app-saves-database-change-token">Step 3. App saves database change token</h3>

<p>The database change token is stored so it can be used next time
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchdatabasechangesoperation"><strong>CKFetchDatabaseChangesOperation</strong></a>
is sent.</p>

<p><img src="/img/posts/cloudkit/cloudkit-fetchchanges3.png" alt="The app stores the database change token" /></p>

<h3 id="step-4-app-requests-changes-in-each-zone">Step 4. App requests changes in each zone</h3>

<p>Now we have a list of <a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZones</strong></a>
that changed, but we don’t yet know which
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>. To find these, the
app issues a
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonechangesoperation"><strong>CKFetchRecordZoneChangesOperation</strong></a>.</p>

<p>Since this is the first time this has been issued, the app doesn’t yet have a
Zone <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeToken</strong></a>
so it sends <code class="language-plaintext highlighter-rouge">null</code>.</p>

<p><img src="/img/posts/cloudkit/cloudkit-fetchchanges4.png" alt="The app sends a CKFetchRecordZoneChangesOperation" /></p>

<p>Note: the
Database <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeToken</strong></a>
returned in step 2 is not the same as the
Zone <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeToken</strong></a></p>

<h3 id="step-5-cloudkit-responds-with-changed-records">Step 5. CloudKit responds with changed records</h3>

<p>CloudKit will send a separate
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonechangesoperation/3794339-recordwaschangedblock"><strong>Record Changed</strong></a>
for every record added or modified since the Zone <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeToken</strong></a>.
Because this is the first time this operation has been used, and the token was set to <code class="language-plaintext highlighter-rouge">null</code>, all records (milk and bread) will be returned.</p>

<p>A single 
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonechangesoperation/1640411-recordzonefetchcompletionblock"><strong>Fetch Completed</strong></a>
response will be sent at the end.</p>

<p><img src="/img/posts/cloudkit/cloudkit-fetchchanges5.png" alt="CloudKit sends the changed records back to the app" /></p>

<p>If a record had been deleted since the last fetch, a
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonechangesoperation/3003360-recordwithidwasdeletedblock"><strong>Record Id Deleted</strong></a>
response would also be received by the app.</p>

<p>When the fetch operation is complete, a 
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonechangesoperation/1640411-recordzonefetchcompletionblock"><strong>Fetch Completed</strong></a>
response will be received by the app. This will contain the
Zone <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeToken</strong></a>.</p>

<h3 id="step-6-app-updates-its-local-list-and-stores-the-zone-change-token">Step 6. App updates its local list and stores the Zone Change Token</h3>

<p>The app uses the received <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>
to update its local representation of the list, and it updates user interface with the new items.</p>

<p>The app stores the <a href="https://developer.apple.com/documentation/cloudkit/ckserverchangetoken"><strong>CKServerChangeToken</strong></a>
for zone “abcd1234-…” on the device. This will be used next time
<a href="https://developer.apple.com/documentation/cloudkit/ckfetchrecordzonechangesoperation"><strong>CKFetchRecordZoneChangesOperation</strong></a>
is used to fetch changes for the same zone.</p>

<p><img src="/img/posts/cloudkit/cloudkit-fetchchanges6.png" alt="The app stores the zone change token" /></p>

<p>CloudKit’s <strong>Fetch Changes</strong> mechanism provides an efficient way of synchronising the app’s local cache of the iCloud data. Instead of
fetching everything every time, only the recent changes need to be fetched.</p>

<p>In <a href="/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html">part 6</a> we’ll look at how the <strong>Fetch Changes</strong> request can be triggered: when the app receives a remote notification from a
CloudKit Subscription or when the user explicitly triggers the list synchronisation.</p>

<p>And, in <a href="/2023/06/12/embracing-cloudkit-for-data-sharing-part7.html">part 7</a>, we’ll look at situations when the app must fetch everything, such as when the user signs into a new iCloud account.</p>

<p><a href="/2023/05/22/embracing-cloudkit-for-data-sharing-part4.html">Next week</a>, we’ll see how sharing works, and how to share a list for the first time.</p>

<hr />

<p><em>If you get a chance, please try <a href="https://apps.apple.com/gb/app/shopping-uk/id771220733">Shopping UK</a> and let me know what you think
at <a href="https://twitter.com/wheelies">@wheelies</a></em></p>]]></content><author><name>Stuart Wheelwright</name></author><summary type="html"><![CDATA[Part 3: Adding, Updating and Deleting Records]]></summary></entry><entry><title type="html">Embracing CloudKit: Part 2</title><link href="http://dev.shoppingukapp.com/2023/05/08/embracing-cloudkit-for-data-sharing-part2.html" rel="alternate" type="text/html" title="Embracing CloudKit: Part 2" /><published>2023-05-08T00:00:00+00:00</published><updated>2023-05-08T00:00:00+00:00</updated><id>http://dev.shoppingukapp.com/2023/05/08/embracing-cloudkit-for-data-sharing-part2</id><content type="html" xml:base="http://dev.shoppingukapp.com/2023/05/08/embracing-cloudkit-for-data-sharing-part2.html"><![CDATA[<h1 id="part-2-what-is-cloudkit">Part 2: What is CloudKit?</h1>

<p>This is the second in an <a href="/2023/05/01/embracing-cloudkit-for-data-sharing-contents.html">eight-part series</a> on implementing
data sharing in <em>Shopping UK</em> using CloudKit.</p>

<p><small><em><a href="https://shoppingukapp.com">Shopping UK</a> is a smart shopping list for UK shoppers. It knows almost every product in the supermarket and
will arrange them by aisle. Lists can be shared with family or friends.</em></small></p>

<p><a href="/2023/05/01/embracing-cloudkit-for-data-sharing-part1.html">Last week</a>, we introduced <em>Shopping UK</em> and discussed limitations
of its old home-grown data sharing solution. Today, we’ll look at CloudKit’s basic concepts and how the <em>Shopping UK</em> schema is designed.</p>

<h2 id="concepts">Concepts</h2>

<p>We’ll start with a quick review of the core CloudKit concepts.</p>

<p>If you’re already familiar with the concepts, please skip to the “Creating a CloudKit Schema” section below, which explains how 
CloudKit is used in <em>Shopping UK</em>.</p>

<p>CloudKit is one of Apple’s key technologies for sharing data in the cloud. Apple offers three options for data sharing:</p>

<ol>
  <li><strong>iCloud Document Storage</strong> for file or document synchronisation.</li>
  <li><strong>iCloud Key-Value Storage</strong> for small key-value pairs.</li>
  <li><strong>CloudKit</strong> for sharing complex objects.</li>
</ol>

<p>CloudKit is a way to move structured data between your app and iCloud. It also has a web-based dashboard for viewing and manipulating the 
data in your own iCloud account (this is really useful for testing).</p>

<p><img src="/img/posts/cloudkit/cloudkit-overview.png" alt="CloudKit Concepts" /></p>

<p>CloudKit is free to use. It is natively supported in iOS and it also has a <a href="https://developer.apple.com/documentation/cloudkitjs">JavaScript API</a>
so it can also be used from web sites and outside the Apple ecosystem.</p>

<p>Let’s look at three key concepts: Records, Record Types and Record Zones.</p>

<h3 id="records">Records</h3>

<p>Data is stored in a <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a>. Think of these like records in a relational
database system.</p>

<p>Each <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a>
has a <a href="https://developer.apple.com/documentation/cloudkit/ckrecord/1462206-recordtype"><strong>Record Type</strong></a>
— a template for defining what values the 
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a> can contain.
A
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord/1462206-recordtype"><strong>Record Type</strong></a>
has a name and a list of
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord/3003381-allkeys"><strong>Keys</strong></a>. 
Each key is assigned a data-type (e.g String, Date, Bool).</p>

<p>Think of the
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord/1462206-recordtype"><strong>Record Type</strong></a>
like a table in a relational database system, and the
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord/3003381-allkeys"><strong>Keys</strong></a>
as fields.</p>

<h3 id="record-types">Record Types</h3>

<p>To store books by an author, we might create two
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord/1462206-recordtype"><strong>Record Types</strong></a>:
 <em>Book</em> and <em>Author</em>. A <em>Book</em> has a <em>Title</em>, a <em>PublishedOn</em> date, 
a number of <em>Pages</em>, an <em>ISBN</em> number and a link to an <em>Author</em>:</p>

<p><img src="/img/posts/cloudkit/cloudkit-record-types.png" alt="Example of CloudKit Record Types" /></p>

<p>Together, the <a href="https://developer.apple.com/documentation/cloudkit/ckrecord/1462206-recordtype"><strong>Record Type</strong></a>
describe the “shape” of the data and how it links together — its schema.
The data itself is represented by a <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a>.
This example uses a couple of my books from one of my favourite childhood authors, Roald Dahl:</p>

<p><img src="/img/posts/cloudkit/cloudkit-records.png" alt="Example of CloudKit records" /></p>

<h3 id="record-zones">Record Zones</h3>

<p>A <a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a> is a grouping of 
<a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>.</p>

<p><img src="/img/posts/cloudkit/cloudkit-record-zones.png" alt="Example of a CKRecordZone" /></p>

<p>All <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecords</strong></a>
live in one and only one <a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>.</p>

<p>A <a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>
lives in a <a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a>.
There are three databases: <em>Public</em>, <em>Private</em> and <em>Shared</em> (more on these later).</p>

<p>The <em>Public</em> and <em>Private</em> <a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabases</strong></a> each have a <em>Default</em>
<a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZone</strong></a>.</p>

<p>New, custom <a href="https://developer.apple.com/documentation/cloudkit/ckrecordzone"><strong>CKRecordZones</strong></a>
can be created in the <em>Private</em> and <em>Shared</em> databases (but not in the <em>Public</em> database).</p>

<p>A <a href="https://developer.apple.com/documentation/cloudkit/ckdatabase"><strong>CKDatabase</strong></a>
lives in a <a href="https://developer.apple.com/documentation/cloudkit/ckcontainer"><strong>CKContainer</strong></a>.
There is typically only one <a href="https://developer.apple.com/documentation/cloudkit/ckcontainer"><strong>CKContainer</strong></a> for the whole app.</p>

<h3 id="summary-of-concepts">Summary of Concepts</h3>

<p>This shows how these concepts relate to each other:</p>

<p><img src="/img/posts/cloudkit/cloudkit-relationship-summary.png" alt="Summary of CloudKit Entity Relationships" /></p>

<h2 id="creating-a-cloudkit-schema">Creating a CloudKit Schema</h2>

<p>At first glance, the data structure for a shopping list would be similar to the Book/Author example above, with a parent “Shopping List”
record and a set of child “ShoppingItem” records:</p>

<p><img src="/img/posts/cloudkit/shoppinguk-state-based-schema.png" alt="Alternative, State-based shoppping list schema" /></p>

<p>This is the way most tutorials approach the problem, and this is similar to the way <em>Shopping UK</em> stores data internally,
but it isn’t the optimal structure for sharing a set of items on a list.</p>

<p>It is not enough to represent only the <em>current</em> state of the list. We need to see some history too, so we can answer questions like:</p>

<ul>
  <li><em>I added ‘milk’ yesterday, but it has disappeared. Is there a glitch in the app or did my wife remove it from the list?</em></li>
  <li><em>I added ‘bread’ last week, but it is still on the list. Did my wife buy some and have we run out already?</em></li>
  <li><em>When did we last buy coffee?</em></li>
</ul>

<p>So instead of transferring a <em>snapshot</em> of the current list, <em>Shopping UK</em> transfers the <em>ordered list of changes</em> that have been
applied to the list.</p>

<p><img src="/img/posts/cloudkit/shoppinguk-journal-based-schema.png" alt="Real journal-based shoppping list schema" /></p>

<p>This “journal-based” approach is similar to how a relational database works internally (see
<a href="https://martinfowler.com/articles/patterns-of-distributed-systems/wal.html">write-ahead logging</a>).
It is a well-established pattern, but it isn’t appropriate for all apps. If you have an app that doesn’t need to track every change,
a simple state-based sharing model, like the one shown earlier, would probably be simpler to implement.</p>

<p>When all changes have been applied to another device, the lists will become the same.</p>

<p>The journal-based approach has a couple of useful consequences:</p>

<ul>
  <li>
    <p>Items don’t have an absolute position in the list. Instead, they know which item they follow. This means if my wife and I are both adding
items to the list we won’t overwrite each other’s changes. Instead, each item will be added in turn.</p>
  </li>
  <li>
    <p>Changes can be queued rather than being applied immediately. This is important when the app is in “Shopping” mode. It would be annoying
if the app added an item to my checklist while I was shopping in the supermarket. It could cause me to mark-off the wrong item.
Instead, the app will show the newly added items as notifications, and I can tap on them when I’m ready to include them in my checklist:</p>
  </li>
</ul>

<p><img src="/img/posts/cloudkit/shoppinguk-queued-journal-entries.png" alt="Queued Changes on Shopping List screen" /></p>

<p>You’re probably wondering what happens after the list has been shared for several weeks and the journal contains thousands of changes.
We’ll look at how the journal is compressed in a later post.</p>

<p><a href="/2023/05/15/embracing-cloudkit-for-data-sharing-part3.html">Next week</a>,
we’ll look at the lifecycle of a <a href="https://developer.apple.com/documentation/cloudkit/ckrecord"><strong>CKRecord</strong></a>:
how they are created, read, modified and deleted.</p>

<hr />

<p><em>If you get a chance, please try <a href="https://apps.apple.com/gb/app/shopping-uk/id771220733">Shopping UK</a> and let me know what you think
at <a href="https://twitter.com/wheelies">@wheelies</a></em></p>]]></content><author><name>Stuart Wheelwright</name></author><summary type="html"><![CDATA[Part 2: What is CloudKit?]]></summary></entry><entry><title type="html">Paywall Experiments</title><link href="http://dev.shoppingukapp.com/2023/05/04/paywall-experiments.html" rel="alternate" type="text/html" title="Paywall Experiments" /><published>2023-05-04T00:00:00+00:00</published><updated>2023-05-04T00:00:00+00:00</updated><id>http://dev.shoppingukapp.com/2023/05/04/paywall-experiments</id><content type="html" xml:base="http://dev.shoppingukapp.com/2023/05/04/paywall-experiments.html"><![CDATA[<p>Yesterday, I published a minor update (v3.3.5). It has a new option for setting up sharing when creating a new list and
improvements to the <a href="/2023/05/02/help-yourself.html">in-app help</a>:</p>

<p><img src="/img/posts/v3-3-5-release/3-3-5-features.png" alt="Setup sharing when creating a new list" /></p>

<p>I’m also running an A/B test of a new paywall.</p>

<p><img src="/img/posts/v3-3-5-release/paywall-a-b.png" alt="A/B Testing Paywalls" /></p>

<p>When the app first starts, each user is randomly assigned a group: “A” or “B”, and this group stays with the app until it’s
uninstalled from the device.</p>

<p>Users in “Group A” will see the old paywall and those in “Group B” will see the new one.</p>

<p>Using <a href="https://appcenter.ms/apps">AppCenter</a>, I’ll track how many times each paywall is viewed, and
<a href="https://appstoreconnect.apple.com">App Store Connect</a> will show the number of sales for each.</p>

<p>I’m hoping the new paywall — with its clearer design, single call-to-action, and a free trial period — will outperform the old one.</p>

<p>I’m excited to find out if I’m right. But, nothing is certain and, to mudddy the water, the price is higher for Group B.
Ideally, I should have only changed a single variable (the appearance or the price) but I’m keen to experiment with
both variables so I took the risk of muddying the results in the hope of getting answers to both questions sooner.</p>

<hr />

<p><a href="/2023/04/05/planning-day.html"><strong>(Day 30 of 87)</strong></a></p>

<p><em>If you get a chance, please try <a href="https://apps.apple.com/gb/app/shopping-uk/id771220733">Shopping UK</a> and let me know what you think
at <a href="https://twitter.com/wheelies">@wheelies</a></em></p>]]></content><author><name>Stuart Wheelwright</name></author><summary type="html"><![CDATA[Yesterday, I published a minor update (v3.3.5). It has a new option for setting up sharing when creating a new list and improvements to the in-app help:]]></summary></entry><entry><title type="html">Help Yourself</title><link href="http://dev.shoppingukapp.com/2023/05/02/help-yourself.html" rel="alternate" type="text/html" title="Help Yourself" /><published>2023-05-02T00:00:00+00:00</published><updated>2023-05-02T00:00:00+00:00</updated><id>http://dev.shoppingukapp.com/2023/05/02/help-yourself</id><content type="html" xml:base="http://dev.shoppingukapp.com/2023/05/02/help-yourself.html"><![CDATA[<p>An app shouldn’t need documentation or help pages. It should be designed to be intuitive and easy to use.</p>

<p>But for anything other than a trivial app this is impossible.</p>

<p>Everybody is different. Some know their devices well, grasp concepts quickly and use your app all the time.
These people need the least help. But many people are occasional users, less willing to experiment, and they like
to know there’s somewhere to look when they get stuck.</p>

<p>For <em>Shopping UK</em>, my aim has been to keep the user interface simple and use iOS idioms and well-known UX patterns where possible.
But I also offer in-app help pages to explain tasks and answer common questions because some users need more hand-holding, and some parts
of the app are harder to understand.</p>

<p><img src="/img/posts/help-yourself//app-help-home.png" alt="In-app Help" /></p>

<p>I also provide the same content on the <a href="https://help.shoppingukapp.com">web</a> because some users like to search Google for answers.</p>

<p><img src="/img/posts/help-yourself/web-help-home.png" alt="Web-based help" /></p>

<p>Many apps provide help in both places — inside the app and on the web — using a
<a href="https://developer.apple.com/documentation/webkit/wkwebview"><strong>WkWebView</strong></a> control in their app to point at the help pages on
their public web server.</p>

<p><img src="/img/posts/help-yourself/on-server-pages.png" alt="Fetch app help from Web Server" /></p>

<p>This works well. Help pages can be published once yet still be corrected or improved later.</p>

<p>But there are downsides. Help is not available when the network is offline, and pages may be slow to load because they are fetched
from a remote web server over a high latency or limited bandwidth connection.</p>

<p>For <em>Shopping UK</em>, I wanted help pages to appear instantly.
And this meant bundling the help pages inside the app.</p>

<p><img src="/img/posts/help-yourself/in-app-pages.png" alt="Fetch app help from app resource" /></p>

<h2 id="how-does-this-work">How does this work?</h2>
<p>All help pages are written using <a href="https://daringfireball.net/projects/markdown/">Markdown</a>.</p>

<p><img src="/img/posts/help-yourself/markdown-source.png" alt="Markdown source" /></p>

<p>This is converted to HTML using the static site generator <a href="https://jekyllrb.com">Jekyll</a>.</p>

<p>All Markdown files are stored in <a href="https://github.com">GitHub</a>. When I commit a change, a GitHub Action runs to publish
the help pages to an <a href="https://github.com/Azure/static-web-apps">Azure Static Web App</a> (this only costs pennies to run).
You can <a href="https://help.shoppingukapp.com">view the live help pages here</a>.</p>

<p>I use a slightly elongated process when publishing the same content to the app.</p>

<p>Jekyll takes the same Markdown source and converts it into HTML, but this is then passed through a custom script.</p>

<p>The script does two things:</p>
<ol>
  <li>Flattens the hierarchy of pages.</li>
  <li>Creates a file containing navigation data.</li>
</ol>

<p>I flatten the folder hierarchy and place all files and images in a single resource folder in the app because it makes it easier
to link between pages without having to worry about absolute and relative paths.</p>

<p>And the navigation data file is needed so the app can present the help navigation in a
<a href="https://developer.apple.com/documentation/uikit/uitableview"><strong>UITableView</strong></a>, which is a lot easier to navigate than a
HTML page full of hyperlinks.</p>

<p><img src="/img/posts/help-yourself/app-help-howdoi.png" alt="Using a UITableView for navigation" /></p>

<p>Here’s the navigation data file that powers the UITableView:
<img src="/img/posts/help-yourself/structure-in-code.png" alt="Navigation structure in code" /></p>

<p>Here’s the full process for producing the same content on the web and inside the app:</p>

<p><img src="/img/posts/help-yourself/help-builder.png" alt="Full process for building help files" /></p>

<p>This approach won’t work for all apps. It won’t work if your help content changes frequently and outside the
app’s release cycle. But, if you can use it, the user will appreciate how fast they can find answers when they have the least
patience — when they need help.</p>

<hr />

<p><a href="/2023/04/05/planning-day.html"><strong>(Day 28 of 87)</strong></a></p>

<p><em>If you get a chance, please try <a href="https://apps.apple.com/gb/app/shopping-uk/id771220733">Shopping UK</a> and let me know what you think
at <a href="https://twitter.com/wheelies">@wheelies</a></em></p>]]></content><author><name>Stuart Wheelwright</name></author><summary type="html"><![CDATA[An app shouldn’t need documentation or help pages. It should be designed to be intuitive and easy to use.]]></summary></entry><entry><title type="html">Embracing CloudKit: Contents</title><link href="http://dev.shoppingukapp.com/2023/05/01/embracing-cloudkit-for-data-sharing-contents.html" rel="alternate" type="text/html" title="Embracing CloudKit: Contents" /><published>2023-05-01T00:00:00+00:00</published><updated>2023-05-01T00:00:00+00:00</updated><id>http://dev.shoppingukapp.com/2023/05/01/embracing-cloudkit-for-data-sharing-contents</id><content type="html" xml:base="http://dev.shoppingukapp.com/2023/05/01/embracing-cloudkit-for-data-sharing-contents.html"><![CDATA[<h1 id="contents">Contents</h1>

<ul>
  <li><a href="/2023/05/01/embracing-cloudkit-for-data-sharing-part1.html"><strong>Part 1: Introduction</strong></a>:
About <em>Shopping UK</em> and why its home-grown data sharing solution needed to be replaced.
<small>(1st May 2023)</small></li>
  <li><a href="/2023/05/08/embracing-cloudkit-for-data-sharing-part2.html"><strong>Part 2: What is CloudKit?</strong></a>:
A review of CloudKit concepts and how the <em>Shopping UK</em> schema was designed.
<small>(8th May 2023)</small></li>
  <li><a href="/2023/05/15/embracing-cloudkit-for-data-sharing-part3.html"><strong>Part 3: Adding, Updating and Deleting Records</strong></a>:
How to change data in iCloud and the different options for retrieving it.
<small>(15th May 2023)</small></li>
  <li><a href="/2023/05/22/embracing-cloudkit-for-data-sharing-part4.html"><strong>Part 4: Sharing Concepts and How to Begin Sharing</strong></a>:
Introduces sharing concepts and how to begin sharing a list.
<small>(22nd May 2023)</small></li>
  <li><a href="/2023/05/29/embracing-cloudkit-for-data-sharing-part5.html"><strong>Part 5: Accepting a Sharing Invitation</strong></a>:
How another device can accept an invitation to collaborate on a shared list.
<small>(29th May 2023)</small></li>
  <li><a href="/2023/06/05/embracing-cloudkit-for-data-sharing-part6.html"><strong>Part 6: Synchronising Data</strong></a>:
How data is synchronised between devices and some of the challenges to consider.
<small>(5th June 2023)</small></li>
  <li><a href="/2023/06/12/embracing-cloudkit-for-data-sharing-part7.html"><strong>Part 7: Managing the Share and Background Maintenance</strong></a>:
How to change or stop a share, what happens when a list is deleted, background maintenance.
<small>(12th June 2023)</small></li>
  <li><a href="/2023/06/19/embracing-cloudkit-for-data-sharing-part8.html"><strong>Part 8: When Things Go Wrong</strong></a>:
Error handling, merging data, and diagnosing problems.
<small>(19th June 2023)</small></li>
</ul>

<hr />

<p><em>If you get a chance, please try <a href="https://apps.apple.com/gb/app/shopping-uk/id771220733">Shopping UK</a> and let me know what you think
at <a href="https://twitter.com/wheelies">@wheelies</a></em></p>]]></content><author><name>Stuart Wheelwright</name></author><summary type="html"><![CDATA[Contents]]></summary></entry></feed>