<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: North Growth Lab</title>
    <description>The latest articles on DEV Community by North Growth Lab (@northgrowthlab).</description>
    <link>https://dev.to/northgrowthlab</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4092865%2F94de9883-212f-4e02-9c67-c68371e6bef8.png</url>
      <title>DEV Community: North Growth Lab</title>
      <link>https://dev.to/northgrowthlab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/northgrowthlab"/>
    <language>en</language>
    <item>
      <title>Deterministic by Design: Building an Offline-First Verb Practice Engine in SwiftUI</title>
      <dc:creator>North Growth Lab</dc:creator>
      <pubDate>Tue, 25 Aug 2026 10:05:06 +0000</pubDate>
      <link>https://dev.to/northgrowthlab/deterministic-by-design-building-an-offline-first-verb-practice-engine-in-swiftui-51p1</link>
      <guid>https://dev.to/northgrowthlab/deterministic-by-design-building-an-offline-first-verb-practice-engine-in-swiftui-51p1</guid>
      <description>&lt;p&gt;Language-learning apps look simple from the outside: a search field, a conjugation table, a speaker button and a quiz. The difficult part is keeping those surfaces consistent when every supported language has different grammar, data quality and release constraints.&lt;/p&gt;

&lt;p&gt;We ran into that problem while building &lt;strong&gt;NGL Verb Lab&lt;/strong&gt;, a native SwiftUI product for verb lookup and active practice. The current release core covers English, French, Spanish and German. It contains 1,150 reviewed verbs across the four languages, while larger dictionaries remain build-time inputs rather than automatically becoming user-facing content.&lt;/p&gt;

&lt;p&gt;This article focuses on the architecture choices behind that boundary: a reusable core package, deterministic practice sessions, local-first product state and fail-closed translation packs.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Keep the learning engine outside the app shell
&lt;/h2&gt;

&lt;p&gt;The first decision was to separate reusable language behavior from SwiftUI screens. &lt;code&gt;VerbLabCore&lt;/code&gt; owns verb records, catalog lookup, search and practice-question generation. The app target owns navigation, views, speech, purchases and local preferences.&lt;/p&gt;

&lt;p&gt;That split matters for three reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Core behavior can be tested without launching a simulator.&lt;/li&gt;
&lt;li&gt;UI experiments cannot silently change grammar or answer generation.&lt;/li&gt;
&lt;li&gt;Build-time content tools can use the same models as the application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The package currently targets iOS 17+ and macOS 14+, which lets core tests run quickly on macOS while the product remains a native iPhone application.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Treat source dictionaries as inputs, not as the product
&lt;/h2&gt;

&lt;p&gt;A large dictionary is not automatically a trustworthy learning catalog. Source datasets can contain duplicate lemmas, fragments, uncommon long-tail entries, inconsistent glosses and forms that are technically valid but wrong for a first release.&lt;/p&gt;

&lt;p&gt;Our runtime boundary is therefore smaller than the authoring boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;French: 300 reviewed release-core verbs&lt;/li&gt;
&lt;li&gt;English: 250 reviewed release-core verbs&lt;/li&gt;
&lt;li&gt;Spanish: 300 reviewed release-core verbs&lt;/li&gt;
&lt;li&gt;German: 300 reviewed release-core verbs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The order is frequency-informed, with explicit safeguards for essential everyday vocabulary. Automated audits check for duplicate lemmas, invalid language identifiers, missing definitions, empty examples, malformed forms and accidental leakage from authoring-only data.&lt;/p&gt;

&lt;p&gt;This is intentionally conservative. Expanding a catalog is easy; defending every row presented to a learner is harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Search the form a learner actually saw
&lt;/h2&gt;

&lt;p&gt;Learners frequently encounter a conjugated form before they know its infinitive. Searching only by lemma would make the product fail at the exact moment it should be useful.&lt;/p&gt;

&lt;p&gt;The search layer therefore normalizes accents and searches both infinitives and known forms. Results are then ranked by a combination of signals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;exact conjugated-form match;&lt;/li&gt;
&lt;li&gt;infinitive prefix match;&lt;/li&gt;
&lt;li&gt;frequency rank.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pronominal forms need additional handling. A query such as a French reflexive form should not be reduced to a naïve substring match; reflexive markers and the base lemma have to be considered together. The product also represents dual-auxiliary cases where meaning or transitivity changes the correct compound form.&lt;/p&gt;

&lt;p&gt;The useful design principle is broader than language apps: &lt;strong&gt;normalize input aggressively, but rank results using domain knowledge&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Generate daily practice deterministically
&lt;/h2&gt;

&lt;p&gt;The app creates a ten-question daily session from the same conjugation records used by search and reference screens. We wanted the session to be stable during a day, change on the next day and remain fully testable.&lt;/p&gt;

&lt;p&gt;Instead of relying on global randomness, the engine derives a seed from the start of the current day. A round number changes the sequence for an additional session. Position-specific offsets select the verb, tense and form.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;startOfDay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startOfDay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;daySeed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;calendar&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dateComponents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nv"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;timeIntervalSince1970&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;startOfDay&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;day&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;verbIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;positiveModulo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;daySeed&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;roundSeed&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;verbs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each language supplies its own preferred learning tenses. If the preferred set is unavailable for a particular record, the engine falls back to non-empty conjugations rather than manufacturing an answer.&lt;/p&gt;

&lt;p&gt;Distractors first come from other persons in the same tense. If that does not produce three unique choices, the engine looks for the same mood, tense and subject in other verbs. The correct answer is inserted at a deterministic position.&lt;/p&gt;

&lt;p&gt;This gives us repeatable tests without making the learner see the same static quiz forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Keep core use available offline
&lt;/h2&gt;

&lt;p&gt;Verb lookup, conjugation data, favorites, pronunciation controls and daily progress should not require an account or a network round trip.&lt;/p&gt;

&lt;p&gt;The product therefore bundles the release catalogs and stores lightweight state locally. Favorites, language choice, practice accuracy and streak data remain available without authentication. Apple’s &lt;code&gt;AVSpeechSynthesizer&lt;/code&gt; provides language-specific system voices for infinitives, examples and displayed forms, with slow and natural playback speeds.&lt;/p&gt;

&lt;p&gt;Offline-first does not mean “no services will ever exist.” It means the core learning job still works when the network, an API or a subscription backend does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Make paid translation content fail closed
&lt;/h2&gt;

&lt;p&gt;Cross-language explanation packs add a different risk: a pack can exist as a file while still being incomplete or unreviewed.&lt;/p&gt;

&lt;p&gt;For release builds, the translation store accepts only packs marked as reviewed. It validates key coverage and checks a SHA-256 digest against the bundled content. Packs are cached in memory after validation. Debug builds may expose machine-generated material for QA, but that state cannot silently qualify as paid release content.&lt;/p&gt;

&lt;p&gt;That distinction is important. Automated checks can detect missing keys, wrong scripts and placeholders. They cannot certify that grammar guidance is pedagogically correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Test invariants, not screenshots
&lt;/h2&gt;

&lt;p&gt;The highest-value tests protect rules that should remain true as UI changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every release-core lemma is unique inside its language;&lt;/li&gt;
&lt;li&gt;search returns infinitive and conjugated-form matches in a useful order;&lt;/li&gt;
&lt;li&gt;practice questions have one correct answer and unique distractors;&lt;/li&gt;
&lt;li&gt;forms and language identifiers are valid;&lt;/li&gt;
&lt;li&gt;authoring-only entries cannot leak into the runtime catalog;&lt;/li&gt;
&lt;li&gt;release translation packs satisfy their review and digest contract.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;UI tests still matter, but they are not the right first defense for data integrity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we would preserve in the next product
&lt;/h2&gt;

&lt;p&gt;The most reusable lesson is not a particular SwiftUI view or data format. It is the set of boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authoring data versus reviewed runtime data;&lt;/li&gt;
&lt;li&gt;pure learning logic versus the app shell;&lt;/li&gt;
&lt;li&gt;deterministic generation versus global randomness;&lt;/li&gt;
&lt;li&gt;offline core behavior versus optional services;&lt;/li&gt;
&lt;li&gt;automated validation versus human language review.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those boundaries make the product easier to test today and safer to expand later with recorded audio, cloud sync or additional reviewed language packs.&lt;/p&gt;

&lt;p&gt;NGL Verb Lab is still in development, so we are not claiming App Store adoption or learning outcomes. We are using the build as first-party evidence of how we approach native iOS architecture, content QA and product constraints.&lt;/p&gt;

&lt;p&gt;If you are planning a native iPhone product, North Growth Lab documents its &lt;a href="https://www.northgrowthlab.com/ios-app-development?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=verb_lab_architecture&amp;amp;utm_content=engineering_article" rel="noopener noreferrer"&gt;iOS app development approach&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>swift</category>
      <category>ios</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
