<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Amin Benarieb&apos;s Blog</title><description>Amin Benarieb — Senior iOS engineer writing about mobile security, biometrics, and iOS development in regulated, high-stakes domains.</description><link>https://amin.benarieb.com/</link><language>en-us</language><item><title>Hashes: A Good Tool You Shouldn&apos;t Trust Blindly</title><link>https://amin.benarieb.com/posts/hashes-you-shouldnt-trust-blindly/</link><guid isPermaLink="true">https://amin.benarieb.com/posts/hashes-you-shouldnt-trust-blindly/</guid><description>You meet hashes all day as an iOS dev — hash maps, git, checksums. MD5 and SHA-1 are broken for security, yet hashes are still useful if you know what they actually prove. A short, concrete note.</description><pubDate>Sat, 04 Jul 2026 08:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;You already use hashes all day&lt;/h2&gt;
&lt;p&gt;As an iOS dev you already use hashes all day:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Set&lt;/code&gt; and &lt;code&gt;Dictionary&lt;/code&gt;.&lt;/strong&gt; Every lookup goes through a hash. Any type you use as a &lt;code&gt;Dictionary&lt;/code&gt; key or &lt;code&gt;Set&lt;/code&gt; member has to conform to &lt;code&gt;Hashable&lt;/code&gt; — but you almost never write that yourself: the compiler synthesizes it for your structs and enums, usually the same models you mark &lt;code&gt;Codable&lt;/code&gt;. That hash is just a number squeezed out of the value; it&apos;s what lets a dictionary jump straight to the right entry instead of scanning them all.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Git.&lt;/strong&gt; Every commit is a hash of its content; the short SHA you paste in standup is just the first 7 characters of it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Checksums and signatures.&lt;/strong&gt; The SHA-256 under a download link, or the code signature iOS verifies before it launches your app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hash maps on LeetCode.&lt;/strong&gt; Every time you go at another problem, your reflex is to &lt;a href=&quot;https://youtu.be/5bId3N7QZec?si=k50TUx0WyRYaM5yK&amp;amp;t=62&quot;&gt;throw it in a hash map&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What &quot;broken&quot; actually means&lt;/h2&gt;
&lt;p&gt;So it&apos;s a great tool — but you&apos;re also going to hear that MD5 and SHA-1 are &lt;em&gt;broken&lt;/em&gt;. That&apos;s true, and it has a specific meaning. &lt;strong&gt;Broken&lt;/strong&gt; here means an attacker can produce a &lt;em&gt;collision&lt;/em&gt;: two different files with the same hash. MD5 fell to this back in &lt;a href=&quot;https://en.wikipedia.org/wiki/MD5#Security&quot;&gt;2004&lt;/a&gt;, SHA-1 in &lt;a href=&quot;https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html&quot;&gt;2017&lt;/a&gt;. So if a signature or a trust decision rides on one of those hashes, it&apos;s exploitable. There are other hash pitfalls too. If you want the mechanics, a couple of older-but-still-worth-it videos from the earlier internet — &lt;a href=&quot;https://www.youtube.com/watch?v=b4b8ktEV4Bg&quot;&gt;Computerphile&lt;/a&gt; — or if you prefer something newer, &lt;a href=&quot;https://www.youtube.com/watch?v=R_mOWu3s6y4&quot;&gt;The Coding Gopher&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Two hashes that failed in public&lt;/h2&gt;
&lt;p&gt;Not only that. Two more hash-related failures worth knowing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A hash-match accusation on a collidable hash.&lt;/strong&gt; Apple&apos;s on-device &lt;a href=&quot;https://www.theregister.com/2021/08/18/apples_csam_hashing/&quot;&gt;NeuralHash&lt;/a&gt; scanned photos as they uploaded to iCloud and hash-matched them against a database of known illegal images; enough matches flagged your account for human review. Researchers forced a beagle and a gray square to the same hash within hours. Once different images can collide, an innocent photo can be crafted to match a flagged one (false positive — even a way to frame someone), and real targets can be nudged to &lt;em&gt;not&lt;/em&gt; match (evasion). Apple shelved it in 2022.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A hash bug that cost ~$611M.&lt;/strong&gt; In 2021 a hash-related flaw in a blockchain bridge led to one of the biggest thefts in crypto history — ~$611M (&lt;a href=&quot;https://www.coinspect.com/learn-evm-attacks/cases/polynetwork-bridge/&quot;&gt;full technical report&lt;/a&gt;, or a &lt;a href=&quot;https://blog.kraken.com/product/security/abusing-smart-contracts-to-steal-600-million-how-the-poly-network-hack-actually-happened&quot;&gt;more accessible walkthrough&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But both deserve an asterisk, and the asterisks are the interesting part.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NeuralHash&lt;/strong&gt; is a &lt;em&gt;perceptual&lt;/em&gt; hash — built so that &lt;em&gt;similar&lt;/em&gt; images share a value. So it&apos;s not the kind of hash we usually mean, and colliding it isn&apos;t exotic — it&apos;s the design. The real problem wasn&apos;t &quot;fingerprints aren&apos;t unique&quot; in the abstract; it&apos;s that an &lt;em&gt;accusation&lt;/em&gt; system was built on a hash that collides by design.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Ethereum hack&lt;/strong&gt; hinged on a &lt;em&gt;truncated&lt;/em&gt; hash — think of a git short SHA, the first 7 characters instead of the full 64. The fewer characters a system actually checks, the cheaper it is to grind out a different input that lands on the same short value. That bridge compared only 4 bytes to decide which function to run; an attacker found a throwaway function whose 4 bytes matched a privileged one, called it, and walked off with the money. &lt;strong&gt;A hash is only as strong as the bytes you actually compare.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;“Download and verify” can create false confidence&lt;/h2&gt;
&lt;p&gt;Take &lt;a href=&quot;https://github.com/34306/vphone-aio&quot;&gt;vphone-aio&lt;/a&gt; — a one-command script that spins up a &quot;virtual iPhone&quot; by running a bundled binary you don&apos;t see the source of. Its README lists SHA-256 sums for the download parts. The author uses them correctly: a &lt;em&gt;corruption&lt;/em&gt; check, nothing more — they never claim the sums prove the binary is safe or genuine. The trap is on the reader&apos;s side. A checksum sitting next to something you&apos;re about to &lt;em&gt;execute&lt;/em&gt; feels like &quot;verified, safe to run.&quot; It isn&apos;t. A matching sum means the bytes arrived intact — not that the code is trustworthy, not that it came from who you think. By the way — I wouldn&apos;t blindly run that vphone on your own machine: it&apos;s a closed-source binary, so you can&apos;t actually see what it does, and a matching checksum tells you nothing about that.&lt;/p&gt;
&lt;h2&gt;Integrity is not authenticity&lt;/h2&gt;
&lt;p&gt;That&apos;s the line worth internalizing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Integrity&lt;/strong&gt; — the bytes didn&apos;t change (accidentally). A bare hash gives you this.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authenticity&lt;/strong&gt; — the bytes really came from the right source, untampered. This needs a &lt;em&gt;signature&lt;/em&gt; — the hash delivered through something an attacker can&apos;t also edit.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A matching hash on the same server as the file gives you neither against an attacker: whoever swaps the file swaps the hash line right next to it. Same on mobile — a phone that hashes a selfie and sends &quot;image + hash&quot; to a server proves nothing if someone in the pipeline swaps both, and if the expected images are known ahead of time they can prepare a matching twin.&lt;/p&gt;
&lt;h2&gt;The concrete mobile trap: password storage&lt;/h2&gt;
&lt;p&gt;OWASP still flags shipped apps using MD5/SHA-1 for passwords (&lt;a href=&quot;https://mas.owasp.org/MASTG/0x04g-Testing-Cryptography/&quot;&gt;MASTG crypto tests&lt;/a&gt;). But &lt;a href=&quot;https://www.kodeco.com/129-basic-ios-security-keychain-and-hashing&quot;&gt;&quot;salted SHA-256 in the Keychain,&quot;&lt;/a&gt; passed around as best practice, isn&apos;t ideal either — for two separate reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;To verify a password, SHA-256 is too fast.&lt;/strong&gt; GPUs try billions of guesses per second, so salted-SHA-256 hashes fall in bulk. Passwords need a deliberately &lt;em&gt;slow&lt;/em&gt; function: bcrypt, scrypt, or Argon2 — Argon2id is the current default (&lt;a href=&quot;https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html&quot;&gt;OWASP Password Storage&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;To reuse a password, you don&apos;t hash it at all.&lt;/strong&gt; Hashing is one-way. A reusable secret goes into the Keychain &lt;em&gt;encrypted&lt;/em&gt;, not hashed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Hashing &lt;em&gt;verifies&lt;/em&gt;; the Keychain &lt;em&gt;stores&lt;/em&gt;. Mixing them up — especially verifying with MD5/SHA-1 — is the worst of both. And yet even MD5 still has honest work: catching accidental corruption (a bad download, a flipped bit) and deduplication (&quot;seen this exact file before?&quot;). Broken-for-security isn&apos;t broken-for-everything.&lt;/p&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Hashes are a good tool: MD5/SHA-1 still earn their keep for corruption checks and dedup, but SHA-256 is the baseline the moment security matters.&lt;/li&gt;
&lt;li&gt;A checksum proves &lt;em&gt;integrity&lt;/em&gt;, not &lt;em&gt;authenticity&lt;/em&gt; or &lt;em&gt;safety&lt;/em&gt; — authenticity needs a signature.&lt;/li&gt;
&lt;li&gt;…and only if you compare &lt;strong&gt;all&lt;/strong&gt; of it: check just part of a hash and that guarantee shrinks to the bytes you actually compared (that&apos;s the $611M bug).&lt;/li&gt;
&lt;li&gt;Storing a password? Put it &lt;a href=&quot;https://youtu.be/R_mOWu3s6y4?si=RaJd7aCBfvYDOcyU&amp;amp;t=442&quot;&gt;through a slow, purpose-built hash&lt;/a&gt; — Argon2id, not MD5, SHA-1, or plain SHA-256.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>security</category><category>hashing</category><category>ios</category><category>keychain</category><category>forensics</category></item><item><title>iOS compressionQuality Is Not the JPEG Q-factor</title><link>https://amin.benarieb.com/posts/ios-jpeg-compressionquality-not-q-factor/</link><guid isPermaLink="true">https://amin.benarieb.com/posts/ios-jpeg-compressionquality-not-q-factor/</guid><description>Apple&apos;s compressionQuality parameter is not the JPEG Q-factor — 0.8 produces Q94, not Q80. Calibration table from Apple&apos;s parameter to the real JPEG Q-factor, plus why iOS files come out smaller than libjpeg at the same nominal Q.</description><pubDate>Sun, 28 Jun 2026 07:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Have you ever checked whether &lt;code&gt;UIImage.jpegData(compressionQuality:)&lt;/code&gt; on iOS actually gives you the JPEG quality you asked for? I hadn&apos;t. Then I did — and the answer surprised me. Here&apos;s what I learned.&lt;/p&gt;
&lt;p&gt;Here&apos;s the setup. Say you&apos;re generating JPEGs on iOS and you want them roughly the same size as what libjpeg spits out on your Linux backend or maybe Android app. You pass &lt;code&gt;compressionQuality: 0.8&lt;/code&gt;. Feels right — that&apos;s Q80, no? So I need a tool to check this out.&lt;/p&gt;
&lt;p&gt;The first tool I reach for is &lt;a href=&quot;https://exiftool.org&quot;&gt;ExifTool&lt;/a&gt; — Phil Harvey&apos;s Swiss-army knife for reading and writing image metadata, the thing every photographer and forensics person already has installed. It&apos;ll tell you the dimensions and file size straight out of the JPEG header:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;exiftool output.jpg | grep -E &quot;Quality|ImageSize|FileSize&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Image Size   : 720x1280
File Size    : 215 kB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then &lt;code&gt;identify&lt;/code&gt; chimes in. The next tool ships with &lt;a href=&quot;https://imagemagick.org&quot;&gt;ImageMagick&lt;/a&gt; — the big open-source image toolkit — and unlike ExifTool it&apos;ll actually dig into the JPEG&apos;s compression internals and hand you a Quality number:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;identify -verbose output.jpg | grep Quality
# Quality: 94
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You asked for 0.8. You got Q94. Not Q80. That&apos;s the whole problem in one line.&lt;/p&gt;
&lt;p&gt;The reason is that &lt;code&gt;compressionQuality&lt;/code&gt; is just a internal Apple&apos;s  &lt;code&gt;CGFloat&lt;/code&gt; number from 0.0 to 1.0, and it is not the JPEG Q-factor. It&apos;s some internal Apple knob that ImageIO maps onto its own quantization tables, along a curve that&apos;s nonlinear and — as far as I can tell — documented absolutely nowhere. The annoying part is that almost all the useful range lives in the bottom half. Look:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;compressionQuality&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;JPEG Q (per &lt;code&gt;identify&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.35&lt;/td&gt;
&lt;td&gt;Q53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.45&lt;/td&gt;
&lt;td&gt;Q72&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;Q78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.52&lt;/td&gt;
&lt;td&gt;Q80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.58&lt;/td&gt;
&lt;td&gt;Q85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;Q90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;Q93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;Q95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;Q96&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;Q100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Anything above 0.75 barely moves — you&apos;re talking one or two Q-units between steps. So the top quarter of the slider is basically wasted.&lt;/p&gt;
&lt;p&gt;If you want to build that table yourself, here&apos;s the script I used. Runs on macOS, and &lt;code&gt;NSBitmapImageRep&lt;/code&gt; goes through the same ImageIO that &lt;code&gt;UIImage&lt;/code&gt; uses on the phone, so the curve matches:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env swift
import AppKit
import Foundation

let dir = URL(fileURLWithPath: &quot;/tmp/jpeg_map&quot;)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)

// Gradient, not a flat color — a solid color compresses too well and ruins the test
let size = NSSize(width: 100, height: 100)
let image = NSImage(size: size)
image.lockFocus()
NSGradient(colors: [.red, .blue, .green, .yellow])!
    .draw(in: NSRect(origin: .zero, size: size), angle: 45)
image.unlockFocus()

guard let tiff = image.tiffRepresentation,
      let bitmap = NSBitmapImageRep(data: tiff) else { exit(1) }

for q in stride(from: 0.05, through: 1.0, by: 0.05) {
    let props: [NSBitmapImageRep.PropertyKey: Any] = [.compressionFactor: q]
    guard let data = bitmap.representation(using: .jpeg, properties: props) else { continue }
    let filename = String(format: &quot;q%.2f.jpg&quot;, q)
    try! data.write(to: dir.appendingPathComponent(filename))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;swift jpeg_map.swift
identify -verbose /tmp/jpeg_map/*.jpg | grep -E &quot;jpeg_map|Quality&quot; | paste - -
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;How &lt;code&gt;identify&lt;/code&gt; guesses the Q-factor&lt;/h2&gt;
&lt;p&gt;Now, a thing that tripped me up — how does &lt;code&gt;identify&lt;/code&gt; even know the Q-factor? It doesn&apos;t read it from metadata; there&apos;s no Quality field in the JPEG standard, that number just isn&apos;t stored anywhere in the file. What it actually does is read the &lt;code&gt;DQT&lt;/code&gt; marker (Define Quantization Table, &lt;code&gt;0xFFDB&lt;/code&gt;) — the 8×8 matrix of quantization coefficients — and run a cheap heuristic over it: it sums the coefficients (plus a couple of values at fixed positions) and matches that aggregate against precomputed values for libjpeg&apos;s standard tables at every Q from 1 to 100. It&apos;s an estimate, not a readout — and a collision-prone one.&lt;/p&gt;
&lt;p&gt;And here&apos;s the catch: that heuristic is calibrated to libjpeg&apos;s tables. Apple-encoded files don&apos;t necessarily use the same quantization tables, so whatever &lt;code&gt;identify&lt;/code&gt; reports for an Apple file is a &lt;em&gt;libjpeg-equivalent&lt;/em&gt; Q, not Apple&apos;s real internal value. I expect it to be off by roughly ±2–5 units — fine for comparing files to each other, no good if you need an absolute number.&lt;/p&gt;
&lt;h2&gt;iOS vs libjpeg: same Q, different bytes&lt;/h2&gt;
&lt;p&gt;I wanted to see that gap with my own eyes, so I took one image — 720×1280 — and ran it through both encoders.&lt;/p&gt;
&lt;p&gt;Make the test image:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;magick -size 720x1280 plasma:fractal -blur 0x2 test_source.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;libjpeg, via ImageMagick:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for q in 80 90 95; do
  magick test_source.png -quality $q libjpeg_q${q}.jpg
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;iOS side, same ImageIO through NSBitmapImageRep, picking the compressionQuality values that map to those Q targets:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let qualities: [(Double, String)] = [
    (0.52, &quot;ios_q0.52&quot;),
    (0.67, &quot;ios_q0.67&quot;),
    (0.85, &quot;ios_q0.85&quot;)
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the results, same image, same resolution:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Encoder&lt;/th&gt;
&lt;th&gt;&lt;code&gt;compressionQuality&lt;/code&gt; / Q&lt;/th&gt;
&lt;th&gt;&lt;code&gt;identify&lt;/code&gt; Q&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;libjpeg&lt;/td&gt;
&lt;td&gt;Q80&lt;/td&gt;
&lt;td&gt;Q80&lt;/td&gt;
&lt;td&gt;60 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;0.52&lt;/td&gt;
&lt;td&gt;Q80&lt;/td&gt;
&lt;td&gt;64 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libjpeg&lt;/td&gt;
&lt;td&gt;Q90&lt;/td&gt;
&lt;td&gt;Q90&lt;/td&gt;
&lt;td&gt;144 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;td&gt;Q90&lt;/td&gt;
&lt;td&gt;87 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;libjpeg&lt;/td&gt;
&lt;td&gt;Q95&lt;/td&gt;
&lt;td&gt;Q95&lt;/td&gt;
&lt;td&gt;212 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iOS&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;td&gt;Q95&lt;/td&gt;
&lt;td&gt;132 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Look at Q90 and Q95. Same &lt;code&gt;identify&lt;/code&gt; Q on both sides, and the iOS file is smaller — noticeably smaller. At first I thought I&apos;d messed up the mapping. But no, that&apos;s the real answer: the two encoders land on the same nominal Q but clearly aren&apos;t using the same quantization — Apple&apos;s output leans harder on the high-frequency detail. So &quot;Q90 on iOS&quot; and &quot;Q90 on libjpeg&quot; are genuinely not the same picture.&lt;/p&gt;
&lt;h2&gt;Calibration table: target Q → iOS compressionQuality&lt;/h2&gt;
&lt;p&gt;So if you just want a target Q-factor on iOS without thinking too hard, here&apos;s the lookup I ended up using:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;iOS &lt;code&gt;compressionQuality&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Q75&lt;/td&gt;
&lt;td&gt;0.47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q80&lt;/td&gt;
&lt;td&gt;0.52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q85&lt;/td&gt;
&lt;td&gt;0.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q90&lt;/td&gt;
&lt;td&gt;0.67&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Q95&lt;/td&gt;
&lt;td&gt;0.85&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Alternative: libjpeg-turbo&lt;/h2&gt;
&lt;p&gt;If you need Q80 that &lt;code&gt;identify&lt;/code&gt; reads back as exactly Q80 — same tables, no approximation — link libjpeg-turbo directly. It adds roughly 600 KB to your binary and gives you a quality parameter from 1–100 that means what it says.&lt;/p&gt;
&lt;p&gt;Setup: download the official iOS &lt;code&gt;.a&lt;/code&gt; from https://github.com/libjpeg-turbo/libjpeg-turbo/releases, add a bridging header with &lt;code&gt;turbojpeg.h&lt;/code&gt;, then call &lt;code&gt;tjCompress2&lt;/code&gt; with your CVPixelBuffer data and a literal integer quality. No calibration table needed.&lt;/p&gt;
&lt;p&gt;(Note: the integration steps above are based on the API docs — not personally verified end-to-end.)&lt;/p&gt;
&lt;h2&gt;An unexpected angle: forensics and liveness&lt;/h2&gt;
&lt;p&gt;Quick context if those words aren&apos;t your world. Image &lt;em&gt;forensics&lt;/em&gt; is figuring out an image&apos;s history from the file itself — what made it, whether it&apos;s been edited — without trusting any metadata. &lt;em&gt;Liveness&lt;/em&gt; is the check that decides a selfie is a real person in front of the camera right now, not a photo of a photo or an injected image. Both are how banking, KYC, and identity apps stop spoofing. And it turns out the boring compression detail above is one of their tools.&lt;/p&gt;
&lt;p&gt;Those DQT (Define Quantization Table) aren&apos;t just a quality knob — they&apos;re an encoder fingerprint. Apple&apos;s ImageIO and libjpeg quantize differently, so the tables alone often reveal what produced an image. Forensics tools use exactly this for source identification (which app or device made the file) and for tamper detection: a JPEG that&apos;s been edited and re-saved carries double-compression traces in its tables. The liveness angle is direct — a selfie straight off the camera sensor looks different from one that was saved, edited, or re-compressed before it reached your verification flow. It&apos;s a signal, not proof, but it&apos;s often the first thing that flags an injected or replayed image.&lt;/p&gt;
&lt;h2&gt;Tools&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://imagemagick.org/script/identify.php&quot;&gt;&lt;code&gt;identify&lt;/code&gt;&lt;/a&gt; (ImageMagick) — reads DQT tables and approximates Q-factor&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://exiftool.org&quot;&gt;&lt;code&gt;exiftool&lt;/code&gt;&lt;/a&gt; — file metadata: size, resolution, subsampling&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uiimage/jpegdata(compressionquality:)&quot;&gt;&lt;code&gt;UIImage.jpegData(compressionQuality:)&lt;/code&gt;&lt;/a&gt; — Apple docs (note: parameter is not a Q-factor)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://developer.apple.com/forums/tags/image-io&quot;&gt;Apple ImageIO forums&lt;/a&gt; — community discussion on ImageIO behavior&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://libjpeg-turbo.org/Documentation/Documentation&quot;&gt;libjpeg-turbo API docs&lt;/a&gt; — &lt;code&gt;tjCompress2&lt;/code&gt; reference&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;compressionQuality: 0.8&lt;/code&gt; → Q94, not Q80. The parameter is Apple-internal.&lt;/li&gt;
&lt;li&gt;The 0.75–1.0 range is nearly useless: all values collapse into Q93–Q100.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;identify&lt;/code&gt; approximates Q from the DQT tables with a sum-and-hash heuristic — expect ±2–5 units off for Apple-encoded files.&lt;/li&gt;
&lt;li&gt;Even at matching &lt;code&gt;identify&lt;/code&gt; Q, iOS files can be smaller than libjpeg — the two encoders aren&apos;t using the same quantization, and Apple&apos;s output compresses high-frequency content more aggressively.&lt;/li&gt;
&lt;li&gt;Need exact Q? Use the calibration table above, or link libjpeg-turbo for integer Q control.&lt;/li&gt;
&lt;/ul&gt;
</content:encoded><category>ios</category><category>jpeg</category><category>imageio</category><category>compression</category><category>swift</category></item><item><title>Turning the iPhone into a Laboratory Microscope with AI and BLE</title><link>https://amin.benarieb.com/posts/iphone-laboratory-microscope-ai-ble/</link><guid isPermaLink="true">https://amin.benarieb.com/posts/iphone-laboratory-microscope-ai-ble/</guid><description>Hardware + AI + BLE project that turns an iPhone into a working laboratory microscope. Full write-up on HackerNoon.</description><pubDate>Sat, 30 Aug 2025 13:11:49 GMT</pubDate><content:encoded>&lt;p&gt;I recently shared an exciting project where we transformed an iPhone into a fully functional laboratory microscope by combining hardware innovation with AI, if you&apos;re working on similar hardware + AI/BLE projects or interested in the technical details, check out the complete write-up on HackerNoon:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://hackernoon.com/how-we-turned-the-iphone-into-a-laboratory-microscope-with-ai-and-ble&quot;&gt;How We Turned the iPhone into a Laboratory Microscope with AI and BLE&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</content:encoded><category>edgeai</category><category>ios</category><category>microscopy</category><category>ble</category><category>computer-vision</category><category>technote</category></item><item><title>Dealing with Floating-Point Precision in Swift</title><link>https://amin.benarieb.com/posts/floating-precision/</link><guid isPermaLink="true">https://amin.benarieb.com/posts/floating-precision/</guid><description>Why Swift’s Double gives you 20.509999999999998 instead of 20.51, and when to reach for Foundation’s Decimal instead.</description><pubDate>Sun, 05 Jan 2025 13:57:18 GMT</pubDate><content:encoded>&lt;p&gt;During my junior years at university, we had a course dedicated to data structures and algorithms. One of the first tasks was working with large numbers, particularly floating-point numbers. At the time, I completed the task successfully. However, I eventually forgot the key lesson about the pitfalls of floating-point numbers and their rounding errors.&lt;/p&gt;
&lt;p&gt;Take a look at the following Swift code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let a = 12.31
let b = 8.2
print(a + b)
// Output: 20.509999999999998
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Surprisingly, instead of 20.51, you’re more likely to get &lt;code&gt;20.509999999999998&lt;/code&gt;.
And if you print values of &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; you also get something strange:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;a: Double = 12.31
b: Double = 8.1999999999999993
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Another example: if you sum 0.1 ten times (which should be equivalent to 0.1 * 10 = 1), you might get it wrong again:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var sum = 0.0
for i in 0..&amp;lt;10 {
  sum += 0.1
}
// Result: Double = 0.99999999999999988
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can play around with this behavior &lt;a href=&quot;https://swiftfiddle.com/mtt4kbx36bg57emyqzmzz4dt5q&quot;&gt;here on SwiftFiddle&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Why Does This Happen?&lt;/h4&gt;
&lt;p&gt;The reason for this behavior lies in how floating-point numbers are stored in memory. This is a common issue across many programming languages, not just Swift. Here’s a great &lt;a href=&quot;https://www.youtube.com/watch?v=8RvnnUuoHTA&amp;amp;ab_channel=EngineerMan&quot;&gt;video explanation&lt;/a&gt; along with an interactive &lt;a href=&quot;https://evanw.github.io/float-toy/&quot;&gt;playground&lt;/a&gt; that visually demonstrates how floating-point precision works.&lt;/p&gt;
&lt;h4&gt;When Does Precision Matter?&lt;/h4&gt;
&lt;p&gt;Floating-point precision errors might seem trivial, but they can have serious impacts depending on the field — especially in scientific, engineering, or financial applications.&lt;/p&gt;
&lt;p&gt;In iOS development, the most common cases involve billing, purchases, and paywalls. On the backend, even a rounding error of $0.01 per transaction can scale into significant financial discrepancies across thousands of users. On the mobile side, these errors can lead to misleading prices, poor user experience, and ultimately bad reviews or increased &lt;strong&gt;churn rates&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But generally, we don&apos;t care much about it: for UI animations, graphics, or games, floating-point errors are often negligible since small rounding errors won’t visibly affect the user experience.&lt;/p&gt;
&lt;h4&gt;How to Solve It&lt;/h4&gt;
&lt;p&gt;One solution is to store values as integers (e.g., by multiplying by 100 to avoid decimals). However, the solution I prefer is using &lt;a href=&quot;https://developer.apple.com/documentation/foundation/nsnumber/1407409-decimalvalue&quot;&gt;Decimal&lt;/a&gt; from Foundation.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Decimal&lt;/code&gt; is designed to handle precise decimal arithmetic, making it ideal for financial and high-accuracy calculations. Unlike &lt;code&gt;Double&lt;/code&gt;, which uses binary floating-point representation, &lt;code&gt;Decimal&lt;/code&gt; relies on a &lt;strong&gt;base-10 system&lt;/strong&gt;. This eliminates many common rounding errors that occur when working with currency or other exact values.&lt;/p&gt;
&lt;p&gt;That said, &lt;code&gt;Decimal&lt;/code&gt; does come with trade-offs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;slower performance compared to &lt;code&gt;Double&lt;/code&gt; for large-scale computations.&lt;/li&gt;
&lt;li&gt;higher memory usage.&lt;/li&gt;
&lt;li&gt;more verbose API, requiring additional steps for arithmetic operations.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a deeper dive into the differences between Decimal and Double, I recommend this great &lt;a href=&quot;https://www.jessesquires.com/blog/2022/02/01/decimal-vs-double/&quot;&gt;article by Jesse Squires&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>swift</category><category>decimal</category><category>floating-point</category></item><item><title>How to Debug Faster With ProcessInfo</title><link>https://amin.benarieb.com/posts/how-to-debug-faster-with-processinfo/</link><guid isPermaLink="true">https://amin.benarieb.com/posts/how-to-debug-faster-with-processinfo/</guid><description>Skip the rebuild loop: use ProcessInfo environment variables and launch arguments to inject test data and toggle debug behavior in your iOS app at runtime.</description><pubDate>Wed, 30 Oct 2024 11:19:20 GMT</pubDate><content:encoded>&lt;h1&gt;How to Debug Faster Without Rebuilding Your iOS App&lt;/h1&gt;
&lt;h2&gt;Problem&lt;/h2&gt;
&lt;p&gt;Building an iOS app can be time-consuming, especially for large projects with complex dependencies. Waiting for the app to rebuild every time you make a minor change or need to test with different data can significantly slow down your development process.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;To speed up debugging without rebuilding your app, you can leverage &lt;code&gt;ProcessInfo.processInfo.environment&lt;/code&gt; and &lt;code&gt;ProcessInfo.processInfo.arguments&lt;/code&gt;. These allow you to inject data and control app behavior at runtime, making it easier to test different scenarios quickly.&lt;/p&gt;
&lt;h3&gt;Using Environment Variables&lt;/h3&gt;
&lt;p&gt;Environment variables can be used to pass data into your app during runtime. For example, you can autofill the sign-in screen with test credentials when debugging:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func onAppear() {
    email.text = ProcessInfo.processInfo.environment[&quot;SIGNIN_EMAIL&quot;]
    password.text = ProcessInfo.processInfo.environment[&quot;SIGNIN_PASSWORD&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To set environment variables in Xcode:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Edit Scheme&lt;/strong&gt;: Go to your project in Xcode, select your target, and choose &lt;strong&gt;Edit Scheme&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Select Run&lt;/strong&gt;: In the scheme editor, select the &lt;strong&gt;Run&lt;/strong&gt; action from the sidebar.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Navigate to Arguments&lt;/strong&gt;: Click on the &lt;strong&gt;Arguments&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add Environment Variables&lt;/strong&gt;: Under &lt;strong&gt;Environment Variables&lt;/strong&gt;, click the plus button to add new variables.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./xcode-env-vars.png&quot; alt=&quot;Xcode scheme: Environment Variables panel&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Creating a Debug Configuration Interface&lt;/h3&gt;
&lt;p&gt;To manage environment variables more efficiently, you can create a utility that abstracts the access:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#if DEBUG
let environment: [String: String] = ProcessInfo.processInfo.environment
#else
let environment: [String: String] = [:]
#endif

public struct DebugConfiguration {
    public static func string(_ key: String) -&amp;gt; String? {
        return environment[key]
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, your code becomes cleaner:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func onAppear() {
    email.text = DebugConfiguration.string(&quot;SIGNIN_EMAIL&quot;)
    password.text = DebugConfiguration.string(&quot;SIGNIN_PASSWORD&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Using Launch Arguments&lt;/h3&gt;
&lt;p&gt;Launch arguments can be used to toggle features or behaviors without changing code. For instance, you might want to autofill the sign-up screen or automatically run onboarding steps during debugging.&lt;/p&gt;
&lt;p&gt;To add launch arguments:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Edit Scheme&lt;/strong&gt;: Select &lt;strong&gt;Edit Scheme&lt;/strong&gt; for your target.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Select Run&lt;/strong&gt;: Choose the &lt;strong&gt;Run&lt;/strong&gt; action.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Navigate to Arguments&lt;/strong&gt;: Go to the &lt;strong&gt;Arguments&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add Arguments&lt;/strong&gt;: Under &lt;strong&gt;Arguments Passed On Launch&lt;/strong&gt;, click the plus button&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;./xcode-launch-args.png&quot; alt=&quot;Xcode scheme: Arguments Passed On Launch&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Handling Debug Flags&lt;/h3&gt;
&lt;p&gt;Create an interface to check for these debug flags:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#if DEBUG
let arguments: [String] = ProcessInfo.processInfo.arguments
#else
let arguments: [String] = []
#endif

public enum DebugFlag: String {
    case signupAutofill = &quot;SIGNUP_AUTOFILL&quot;
    case onboardingAutorun = &quot;ONBOARDING_AUTORUN&quot;

    public static func isEnabled(_ flag: DebugFlag) -&amp;gt; Bool {
        return arguments.contains(flag.rawValue)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Applying Debug Flags in Your Code&lt;/h3&gt;
&lt;p&gt;Use these flags to conditionally execute code during debugging.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Autofill Sign-Up Screen:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func onAppear() {
    if DebugFlag.isEnabled(.signupAutofill) {
        email.text = &quot;newuser@example.com&quot;
        password.text = &quot;SecurePassword!&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Auto-Run Onboarding Steps:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func onAppear() {
    if DebugFlag.isEnabled(.onboardingAutorun) {
        onContinueButtonTap()
    }
}

func onContinueButtonTap() {
    // Proceed to the next onboarding step
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Mocking &lt;code&gt;UserDefaults&lt;/code&gt; Values&lt;/h3&gt;
&lt;p&gt;You can mock &lt;code&gt;UserDefaults&lt;/code&gt; to test specific scenarios, such as triggering a feature after a certain number of app launches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Set Up Mock Value via Launch Arguments:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Add the following to &lt;strong&gt;Arguments Passed On Launch&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-appLaunchCount&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;10&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;./xcode-userdefaults.png&quot; alt=&quot;Xcode scheme: mocking UserDefaults via launch args&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Modify Your Code to Use the Mock Value:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os

let logger = Logger(subsystem: &quot;com.example.app&quot;, category: &quot;AppLaunch&quot;)

func onAppear() {
    let appLaunchCount = UserDefaults.standard.integer(forKey: &quot;appLaunchCount&quot;)
    logger.debug(&quot;appLaunchCount = \(appLaunchCount)&quot;)
    if appLaunchCount &amp;gt;= 10 {
        logger.debug(&quot;Showing special feature on 10th launch&quot;)
        // Trigger the feature
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Sample Output:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[AppLaunch] appLaunchCount = 10
[AppLaunch] Showing special feature on 10th launch
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Further Thoughts&lt;/h2&gt;
&lt;p&gt;By utilizing environment variables and launch arguments, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Save Time&lt;/strong&gt;: Test different data inputs and scenarios without rebuilding or hardcoding values. Expecially to test layout changes, mock API responses, or inject localization strings&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enhance Flexibility&lt;/strong&gt;: Toggle features or behaviors dynamically during runtime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improve Collaboration&lt;/strong&gt;: Share custom schemes with your team to ensure consistent testing environments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expand Testing Capabilities&lt;/strong&gt;: Simulate edge cases, such as network delays or error states, more efficiently.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By integrating these practices into your development workflow, you can streamline testing and focus more on building great features.&lt;/p&gt;
&lt;h2&gt;Connect with Me&lt;/h2&gt;
&lt;p&gt;Have a comment or question? Feel free to reach out to me on &lt;a href=&quot;https://twitter.com/amin_benarieb&quot;&gt;Twitter&lt;/a&gt; or &lt;a href=&quot;https://linkedin.com/in/aminbenarieb&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;
</content:encoded></item><item><title>How to debug iOS deeplinks with local server</title><link>https://amin.benarieb.com/posts/how-to-debug-ios-deeplinks-with-local-server/</link><guid isPermaLink="true">https://amin.benarieb.com/posts/how-to-debug-ios-deeplinks-with-local-server/</guid><description>Spin up a tiny Python HTTP server that lists all the deep links you need to test, then tap through them in the iOS Simulator.</description><pubDate>Thu, 29 Jun 2023 13:27:48 GMT</pubDate><content:encoded>&lt;h1&gt;How to Debug iOS Deep Links with a Local Server&lt;/h1&gt;
&lt;h2&gt;Problem&lt;/h2&gt;
&lt;p&gt;Testing interactions within your iOS app that require a custom web page—such as engaging with payment options in a pop-up browser via universal links or deep links—can be challenging. Capturing and modifying URLs, handling deep links that close pop-up browsers, and ensuring that various types of links work seamlessly within your app can be time-consuming without the right tools.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;To streamline the testing process, you can set up a local server that hosts a custom HTML page containing all the links you need to test. This approach allows you to open the web page in the iOS Simulator and interact with your app&apos;s deep links in a controlled environment.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./deeplinks_menu.png&quot; alt=&quot;Deeplinks menu&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Setting Up a Local Server with Python&lt;/h3&gt;
&lt;p&gt;Python provides a simple way to set up a local HTTP server. Here&apos;s a Python script that serves an HTML file containing your test links:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from http.server import HTTPServer, BaseHTTPRequestHandler

class Serv(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == &apos;/&apos;:
            self.path = &apos;/test.html&apos;
        try:
            file_to_open = open(self.path[1:]).read()
            self.send_response(200)
        except FileNotFoundError:
            file_to_open = &quot;File not found&quot;
            self.send_response(404)
        self.end_headers()
        self.wfile.write(bytes(file_to_open, &apos;utf-8&apos;))

httpd = HTTPServer((&apos;localhost&apos;, 8081), Serv)
httpd.serve_forever()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Steps to Set Up:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Create Your HTML File:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Save your test links in an HTML file named &lt;code&gt;test.html&lt;/code&gt;. Here&apos;s an example of what &lt;code&gt;test.html&lt;/code&gt; might look like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;body&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;a href=&quot;yourapp://branch/Settings/FavoritePlaces&quot; class=&quot;button&quot;&amp;gt;Favorite Places&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;a href=&quot;yourapp://action/RestorePurchase&quot; class=&quot;button&quot;&amp;gt;Restore Purchases&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;a href=&quot;yourapp://branch/Payment/Point/330&quot; class=&quot;button&quot;&amp;gt;Branch Payment (330)&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;a href=&quot;yourapp://action/OpenLink?url=https%3A%2F%2Fstage-app.yourapp.com%2Fpayments%2Fplans%2Fpromo-special%2F%3Fcoupon%3DPRM_may2FREE%26require_token%3Dtrue&quot; class=&quot;button&quot;&amp;gt;Stage Coupon&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;a href=&quot;yourapp://action/payment_form/plans/promo-special?coupon=PRM_may2FREE&quot; class=&quot;button&quot;&amp;gt;Payment Form with Coupon&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;a href=&quot;yourapp://action/web_payment?status=success&quot; class=&quot;button&quot;&amp;gt;Web Payment Success&amp;lt;/a&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;!-- Add more links as needed --&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Replace &lt;code&gt;yourapp&lt;/code&gt; with your app&apos;s URL scheme.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Save the Python Script:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Save the Python script above in a file named &lt;code&gt;local_server.py&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Run the Local Server:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Open a terminal, navigate to the directory containing &lt;code&gt;local_server.py&lt;/code&gt;, and run:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python local_server.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This starts a local server on &lt;code&gt;localhost&lt;/code&gt; at port &lt;code&gt;8081&lt;/code&gt;. If port &lt;code&gt;8081&lt;/code&gt; is busy, you can change the port number in the script:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;httpd = HTTPServer((&apos;localhost&apos;, 8082), Serv)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Access the Test Page:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In the iOS Simulator, open Safari and navigate to &lt;code&gt;http://localhost:8081&lt;/code&gt;. You should see your &lt;code&gt;test.html&lt;/code&gt; page with all your test links.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Testing Deep Links&lt;/h3&gt;
&lt;p&gt;With your local server running and accessible from the iOS Simulator, you can now test how your app handles various deep links:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Click on the Links:&lt;/strong&gt; Interact with the links in &lt;code&gt;test.html&lt;/code&gt; to see how your app responds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify Navigation:&lt;/strong&gt; Ensure that your app opens and navigates to the correct screens.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test URL Parameters:&lt;/strong&gt; Modify URL parameters in &lt;code&gt;test.html&lt;/code&gt; to test different scenarios.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Handle Pop-Up Browsers:&lt;/strong&gt; If your app uses a pop-up browser (e.g., &lt;code&gt;SFSafariViewController&lt;/code&gt;), test how deep links from your web page close the browser and return control to the app.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Further Thoughts&lt;/h2&gt;
&lt;p&gt;Setting up a local server for testing deep links offers several advantages:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Efficiency:&lt;/strong&gt; Quickly test web interactions without deploying changes to a live server or rebuilding your app.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibility:&lt;/strong&gt; Easily add, remove, or modify test links in the &lt;code&gt;test.html&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolation:&lt;/strong&gt; Work in a controlled environment without affecting real users or requiring internet access.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Connect with Me&lt;/h2&gt;
&lt;p&gt;Have a comment or question? Feel free to reach out to me on &lt;a href=&quot;https://twitter.com/amin_benarieb&quot;&gt;Twitter&lt;/a&gt; or &lt;a href=&quot;https://linkedin.com/in/aminbenarieb&quot;&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>debug</category><category>deeplinks</category><category>localserver</category><category>debug</category></item></channel></rss>