Profile
Back to NewsBack
GitHub Trending 27 min
Reader Mode
andreapianidev/WalkieTalkie: Walkie Talkie app for iOS, macOS and Android

andreapianidev/WalkieTalkie: Walkie Talkie app for iOS, macOS and Android

6 hours ago

// Created by Andrea Piani - 22/05/26 - https://www.andreapiani.com - README.md

Talky: Walkie-Talkie & Internet Radio (Source-Available iOS + Android App)

Talky

Website</a> App Store</a> iOS</a> Android APK</a> macOS</a>-9aa0a6.svg?logo=apple&logoColor=white) Swift</a> Kotlin</a> Xcode</a> License: PolyForm NC 1.0.0</a> Commercial use: NOT permitted</a> ![Source available]()

Talky is a SwiftUI iOS app that combines offline peer-to-peer push-to-talk (Multipeer Connectivity), real live radio (HF shortwave and the CB 27 MHz band from public KiwiSDR receivers, public POC channels on Mumble, no fake audio anywhere), a global internet radio browser (330+ streaming stations across 80+ countries; the iPhone has no FM receiver, and as of 2.46 the app no longer implies it does), Live Activities + Dynamic Island controls (iOS 16.2+), a complete Pro tier (themes, animated backgrounds, equalizer, recording, sleep timer) and a production-grade AdMob monetization stack for the free tier.
⚠️ This project is source-available, NOT MIT/Apache/BSD/GPL. It is distributed under the PolyForm Noncommercial License 1.0.0: you may read, fork, modify and run it for personal/educational/non-profit purposes, but commercial use (including shipping a derived app on any app store, paid services, ad-supported services and consulting deliverables) is strictly prohibited without a separate written commercial license. See License for details and commercial-license contact.

Website: App Store: Talky: Walkie Talkie, Radio · Free · Bundle ID com.immaginet.talky Repository:


🆕 New in Talky 3.2: a real radio receiver in your pocket

Live CB, HF shortwave and POC, with the receiver's real spectrum and waterfall. Talky tunes public KiwiSDR receivers around the world, picks one near you, and draws what that antenna is hearing right now: carriers, voices, data bursts, the whole band moving down the screen. No fake audio, no recorded loops.

| CB 27 MHz, channel 21 | HF shortwave, Shannon VOLMET | World radio, live bars | Walkie, with the band selector | |---|---|---|---| | !Talky CB 27 MHz channel 21 with live spectrum and waterfall from a KiwiSDR in Fuerteventura | !Talky HF 5.505 MHz USB, Shannon VOLMET aviation weather from a receiver in Cobh, Ireland, with spectrum and waterfall | !Talky world radio, Antenne Bayern with ON AIR level bars | !Talky walkie-talkie screen with LOCAL, HF, CB and POC bands |

  • CB 27 MHz: all 40 channels plus the DX area (calling on 27.555 USB), AM, FM, LSB and USB, SCAN that stops when someone transmits, automatic squelch. The display shows the receiver, its distance from you, the S-meter and how many dB the signal sits above the noise.
  • HF shortwave: aviation weather (VOLMET), time signals, broadcasters and radio amateurs, tuned in 1 kHz steps or typed in.
  • POC: public Mumble voice channels where people are talking right now, with live level bars of what you hear.
  • World radio: 330+ stations from 80+ countries, now with their own screen and level bars that follow the audio, HLS streams included.
  • Widgets on the Home Screen and the Lock Screen (iOS 17+), and on the Mac desktop: your CB channel for free; with Talky Pro, band propagation with SFI, Kp, the 11/20/40/80 m bands and the Kp forecast, plus sunrise, sunset and the best band right now.
  • "Your Talky Pro": after buying, a page lists everything Pro unlocks and where to find it, in 14 languages.
  • 5 minutes of live listening are free every day; Talky Pro removes the limit and adds auto SCAN and listening with the screen off.
Screenshots taken on an iPhone 15 Plus on 3 November 2026, real receivers, real signals.

🙏 Thanks

Talky can show you a live band only because other people share theirs:

  • The radio amateurs who keep their KiwiSDR receivers online for anyone to listen to, at their own cost in power and bandwidth. Every spectrum and waterfall in these screenshots is one of their antennas.
  • John Seamons, who designed the KiwiSDR and keeps its software open.
  • Pierre Ynard, whose public list at rx.linkfanel.net is how Talky finds a receiver near you.
  • The people who run public Mumble servers, which is where the POC channels come from.
  • NOAA Space Weather Prediction Center, for the free solar flux, Kp and forecast data behind the propagation widgets.
  • @Joaov41, for the pull request on keeping the walkie-talkie alive in the background, and everyone who opened an issue or wrote a review.
If you run a KiwiSDR and would rather Talky did not use it, open an issue and it will be excluded.

The other screens

  • Talk: the frequency display, the channel, and the hold-to-talk key. The line under the key says whether anyone is connected.
  • Explore: a radar of the devices around you, with estimated distance and signal strength. Tap one to connect.
  • Connections: advertising and browsing state, the list of connected devices, and the two buttons that fix most problems: disconnect all, restart discovery.
  • Settings: notifications, haptics, auto-connect, and the Pro features (history, sleep timer, equalizer, recordings, private channels).
  • LOCAL / HF / CB / POC: the band selector above the display. LOCAL is the walkie between nearby phones; the other three are real radio, listen only (see Live Radio).

🖥 macTalky (macOS)

macTalky is the native macOS 26+ companion app, living in the macTalky/ folder as a third target of the same Xcode project. It speaks the same local TALKY1 protocol (Bonjour _walkie-talkie._tcp. + TCP + PCM16 mono 48 kHz) as Talky iOS and Talky Android, so a Mac, an iPhone and an Android phone on the same Wi-Fi all talk to each other.

⬇️ Download: grab macTalky.zip from GitHub Releases (macOS 26+, Apple Silicon). First launch: right-click → Open (the GitHub build is not notarized). In-app purchases work only in the Mac App Store version, currently in review on the same App Store record as Talky iOS (universal purchase).

| Walkie: Night Ops backdrop | World-band radio: 330+ stations | Walkie: Olive Drab backdrop | |---|---|---| | !macTalky Walkie, Night Ops | !macTalky Radio | !macTalky Walkie, Olive Drab |

  • Design: "tactical field console" UI, graphite panels, amber phosphor VFD typography, and a full Metal shader suite via SwiftUI colorEffect/layerEffect: 5 selectable tactical backdrops (Carbon, Night Ops, Olive Drab, Desert Fox, Naval Grey, grid, radar sweep, canvas weave, TX/RX-reactive tinting), a phosphor spectrum VU visualizer with peak-hold, a CRT effect (chromatic aberration + scanlines) on the VFD displays and radio pulse rings around the PTT key.
  • Push-to-talk: giant TALK key, hold with the mouse or hold the Space bar. 10 s max per transmission (TALKY1 frame cap), segmented VU meter, TX/RX/NET lamps.
  • Radio: the full worldwide browser (330+ stations) with search, filters, favorites and recents, Media-key/Now Playing integration.
  • Live receiver: HF shortwave, CB 27 MHz (40 channels, DX 27.555, scan, squelch, typed frequency) and POC channels, on the same engine as the iPhone (Shared/RealRadio/).
  • Private channels: same SHA256 password-derived channel IDs as iOS/Android (Pro).
  • Universal purchases: same bundle ID as the iOS app → the Talky Pro subscription and Themes Pack bought on iPhone unlock on the Mac and vice versa (StoreKit 2, no Firebase on macOS). Pro/Themes Pack also unlocks the three premium backdrops.
  • Build: xcodebuild -project WalkieTalkie.xcodeproj -scheme macTalky -configuration Release build (sandboxed, hardened runtime; network client/server + audio-input entitlements). The macTalky scheme is shared, ready for an Xcode Cloud "Archive - macOS" workflow.

Table of Contents


✨ Highlights

  • 🎙️ Push-to-Talk over peer-to-peer (no internet, no servers), up to 8 simultaneous peers
  • 📡 Real radio, no fake audio: HF shortwave and the CB 27 MHz band (all 40 channels plus the SSB DX area, calling on 27.555 USB) from public KiwiSDR receivers, and public POC channels on Mumble. Auto and manual scan, automatic squelch, typed frequency
  • 📻 330+ live radio stations across 80+ countries, with favorites, recents, nearby, search and grouping by country/genre
  • 🏝️ Live Activities + Dynamic Island (iOS 16.2+) for both radio and walkie modes, with iOS 17+ interactive controls (play/pause, next/previous from the lock screen and Dynamic Island)
  • 🎨 16 themes (5 free + 11 paid/Pro) including 2 fully-animated GPU backgrounds (Black Hole, Galaxy)
  • 🔊 5-band EQ, sleep timer, session recording and transmission history
  • 💎 Talky Pro subscription (weekly/yearly) with paywall, restore purchases and StoreKit 2
  • 📣 Full AdMob stack (App Open, Interstitial, Rewarded, Native Advanced) with UMP + ATT consent flow
  • 🌍 14 localizations: Italian, English, Spanish, French, German, Portuguese (Brazil), Japanese, Korean, Malay, Thai, Turkish, Vietnamese, Simplified and Traditional Chinese
  • 🔒 Privacy-first: walkie-talkie audio never leaves the device, no accounts, no cloud
  • 🧱 Clean MVVM + SwiftUI + Combine, singleton managers, Swift 6 strict concurrency-ready
  • ⚖️ Source-available under PolyForm Noncommercial 1.0.0: fork it, study it, learn from it; commercial reuse requires a separate license

🤖 Android (Beta)

Talky now ships an Android port too. It lives in Android/ inside this same repo and is distributed under the same PolyForm Noncommercial License as the iOS app.

⬇️ Download the latest Talky for Android (signed APK): Android 13/API 33 or newer. Android may ask you to authorize installation from your browser or file manager.
> The link points at the releases page, not at one file, so it cannot go stale
the way the old hard-coded v1.1 link did. Devices still on Android 11 or 12 can
install android-v1.2 from the same page. What changed in each version is in
CHANGELOG.md.
⚠️ Status: Beta. The Android app is a younger, leaner port of the iOS original. The core experience (push-to-talk + radio) is functional and builds a signed release APK, but it does not yet have feature parity on the Pro tier (no IAP/paywall, no Live Activities, single base theme).

What works today

  • 🎙️ Push-to-talk over the local network (CrossPlatformWalkieManager, Network Service Discovery / NSD) with an AudioManager PTT pipeline
  • 📻 330+ radio stations: the same catalogue as iOS, kept in sync from RadioManager.swift into Android/.../radio/RadioManager.kt
  • 📣 AdMob stack (banner / interstitial / rewarded / app-open) with UMP consent: uses Google test ad units by default (see note below)
  • 🔥 Firebase Analytics + Crashlytics
  • 🎨 Jetpack Compose UI (Material 3), dark "hardware radio" aesthetic

Tech stack

| | | |---|---| | Language | Kotlin | | UI | Jetpack Compose + Material 3 | | Min / Target SDK | 33 (Android 13) / 36 | | P2P | Network Service Discovery (NSD) | | Ads | Google Mobile Ads + User Messaging Platform | | Backend | Firebase (Analytics + Crashlytics) | | Build | Gradle (Kotlin DSL), R8 minify + resource shrinking |

Build the Android app

cd Android

Provide your signing credentials (see keystore.properties.sample)

cp keystore.properties.sample keystore.properties # then edit values, or remove to build unsigned

./gradlew assembleDebug # debug APK ./gradlew assembleRelease # signed, minified release APK → app/build/outputs/apk/release/

  • Requires the Android SDK and a local.properties with sdk.dir (Android Studio generates it automatically).
  • google-services.json is included for Firebase; replace it with yours or remove the Firebase plugins.
📣 Ad units note: for the open-source / sideload APK, AdConfig.kt intentionally uses Google's test ad units (and a test AdMob App ID in the manifest). This is the policy-compliant choice for an APK that is not distributed via the Play Store. Replace the LIVE_* IDs and the manifest App ID with your own real AdMob IDs before any Play Store release, the config switches to live IDs automatically once real (non-placeholder) values are present in a release build.

🚫 Not on Google Play (yet). This repo ships the Android source + a signed APK via GitHub Releases for sideloading. A Play Store listing would additionally require real AdMob IDs and a Play Console upload.

📊 Feature Matrix (Free vs Pro)

| Capability | Free | Talky Pro | |---|:--:|:--:| | Push-to-talk over Multipeer (up to 8 peers) | ✅ | ✅ | | Live HF, CB 27 MHz and POC listening (real receivers, listen only) | 5 min a day | ✅ unlimited | | Spectrum and waterfall of the receiver | ✅ | ✅ | | Widgets: Channel | ✅ | ✅ | | Widgets: Propagation, Sun and bands, Lock Screen | ❌ | ✅ | | CB channel step, fine tuning, mode, squelch, typed frequency | ✅ | ✅ | | Auto SCAN (CB and POC) | 24 h with a rewarded video (iPhone) | ✅ | | Live listening with the screen off | ❌ | ✅ | | Radio stations available | free selection | All 330+ | | Station browser (search / favorites / recents / nearby) | ✅ | ✅ | | Themes available | 5 base | All 16 | | Animated themes (Black Hole, Galaxy) | ❌ | ✅ | | 5-band Equalizer | ❌ | ✅ | | Session recording (RecordingsManager) | ❌ | ✅ | | Sleep timer | ✅ | ✅ | | Transmission history | ✅ | ✅ | | Ads (banner / interstitial / native / app-open) | Shown | Removed | | Rewarded "remove ads for 1 hour" | ✅ | n/a | | App Tracking Transparency prompt | Shown | Shown¹ |

¹ ATT is required by Apple regardless of Pro status; only ad delivery is disabled for Pro users.


🎙️ Walkie-Talkie Engine

Backed by MultipeerManager and AudioManager:

  • Service type: walkie-talkie (MCSession / MCNearbyServiceAdvertiser + Browser)
  • Cross-platform: MultipeerConnectivity only reaches other Apple devices. Mac
and Android are reached over TALKY1, the local protocol documented in docs/TALKY1.md, Bonjour _walkie-talkie._tcp., TCP, length-prefixed frames, PCM16 mono 48 kHz. That spec is everything you need to write a fourth implementation without reading this codebase.
  • No internet required: works over Wi-Fi, peer-to-peer Wi-Fi, or Bluetooth
  • Up to 8 simultaneous peers
  • Audio session .playAndRecord with .defaultToSpeaker, mixWithOthers off during PTT
  • Background audio entitlement for radio + incoming PTT alerts
  • Anti-spam notifications via NotificationManager cooldown
  • Haptic feedback on PTT press/release via HapticManager
  • Auto-reconnect with exponential backoff

📡 Live Radio: HF, CB 27 MHz, POC

Since 3.0 (iOS) and 2.0 (Mac) there is no recorded or simulated audio in Talky: the old fake frequencies (f1.mp3 … f24.mp3, the white noise) are gone. Next to LOCAL, the walkie has three bands that are real radio, listen only, from free public services with no keys and no accounts. The engine is shared by iPhone and Mac in Shared/RealRadio/.

| Band | Source | What you hear | |---|---|---| | HF | public KiwiSDR receivers | aviation weather (VOLMET), time signals, radio amateurs on 80/40/20 m, FT8 | | CB (3.1 / 2.1) | public KiwiSDR receivers | the citizens band on 27 MHz, live | | POC | public Mumble servers | push-to-talk channels with people in them right now; Talky joins muted |

CB 27 MHz (new in iOS 3.1 / Mac 2.1)

  • All 40 channels, the same in CEPT and FCC countries (channels 24 and 25
sit below 23, as they always have). AM on 1 to 35 and LSB on 36 to 40 by default, which is the operators' custom; AM, FM, LSB and USB are all one tap away.
  • The DX area above channel 40 (27.415 to 27.855 MHz) in USB, where the
11 m SSB stations work and call on 27.555. That is the DX calling frequency of the citizens band, not an amateur one: the 10 m amateur band starts at 28.000.
  • Manual: CH up/down, ±1 kHz fine tuning, or type the frequency
(27.555, 27,555 or 27555 all work).
  • Auto SCAN, like a CB rig: a moment on each channel, it stops while there
is a signal and moves on 3 s after it ends. It scans the part you are in, the 40 channels or the DX area on a 5 kHz raster.
  • Automatic squelch on the receiver's S-meter, so the hiss stays muted
between transmissions. Tuned on 2 Nov 2026 on two real receivers (Dordogna and Fuerteventura): it ignores the zero S-meter of a receiver's first packet, adapts the noise floor in seconds rather than packets (one receiver sends 6 packets a second, another 50), and treats a signal as a dead carrier only when both the S-meter and the audio stay flat for 4 s. The first version (3.1 / 2.1, build 103) looked at the S-meter alone and muted AM and FM stations after 4 s of talking, because in AM the S-meter follows the carrier, not the voice; fixed in build 104. See Squelch in CitizensBand.swift.
  • Talky listens through the free public receiver nearest to you, worked out
from your time zone without asking for your location (TimeZoneSpots.txt, from the tz database). CB is heard when propagation opens the band, usually in daytime; at night it is often quiet, and that is the real band, not a bug.

Spectrum and waterfall (iOS 3.2 / Mac 2.1)

In HF and CB the walkie's speaker grille becomes a panoramic screen: the spectrum around the dial on top, the waterfall of the last few seconds below, the passband you are hearing shaded, the CB channel numbers (or the kHz) on the scale, and the margin of the signal over the noise. It is the receiver's own waterfall, a second WebSocket to the same KiwiSDR (W/F, about nine rows a second); a receiver that refuses it keeps playing audio with an empty screen. See Panorama.swift.

Widgets (iOS 3.2)

Three WidgetKit widgets in TalkyWidgets/, all on real data: Talky channel (free) puts a chosen CB or HF channel on the walkie display with whether its band is open, and opens Talky tuned to it; Propagation and the lock screen Band conditions (Talky Pro) read solar flux, Kp and the R scale from NOAA SWPC and estimate 11 m, 20, 40 and 80 m from the sun's height where you are, worked out from the time zone. Talky Pro is checked in the extension with StoreKit 2, no App Group needed.

Talky widgets on iPhone: Talky channel CB 19, Sun and bands with sunset 18:15 and 20 m open, Propagation medium and large with solar flux 100, Kp 3.3 and the Kp bars of the last and next 24 hours from NOAA SWPC

iPad and iPhone Duo (iOS 3.2)

On a wide screen the walkie stops being a phone stretched to fit. When the window is regular in both directions and at least 700 points wide (iPad full screen, the inner display of iPhone Duo) the radio splits in two panels in Duo/RadioSplit.swift: the control head on the left (status, bands, display, keys), the screen on the right (spectrum and waterfall, level bars, talk button).

!Talky on iPad in landscape: control head on the left, HF 5.505 MHz Shannon VOLMET with live spectrum and waterfall from a receiver in Carlow, Ireland, on the right

  • iPad, landscape: the two panels side by side.
  • iPad, portrait: one column 720 points wide, and the scope takes the
height the phone does not have.
  • iPad in Split View: the width goes back to compact and so does the
layout, the single column of the phone.
  • iPhone Duo, open like a book: the gap between the panels is the hinge,
read from reservedRegions(kind: .division, options: .includeInactive), so the hinge is known even with the phone open flat and nothing moves when it folds.
  • iPhone Duo, standing on a table (horizontal hinge): the screen goes on
top, to be looked at, and the controls below, close to the fingers.

The hinge code is compiled only with the iOS 27.1 SDK and runs only on iOS 27.1; everywhere else Hinge.of answers .none and the iPad rules apply.

Free minutes and Talky Pro

Live listening is free for 5 minutes a day. After that the audio stops and Talky offers Talky Pro (unlimited listening, auto SCAN, screen off) or, on iPhone, a short video that unlocks the same for 24 hours. A new day, a purchase or the video resume listening on the same band by themselves.


📻 Radio Browser (330+ stations)

The radio side is a full streaming client (RadioManager + StationBrowserSheet):

  • 330+ live stations across 80+ countries (Italy, USA, UK, Spain, France, Germany, Japan, Brazil, Australia, Argentina, Mexico, Canada, Netherlands, Sweden, India, and 30+ more)
  • A free selection for everyone, the rest Pro-only (isPro per station; the original list uses id > 30)
  • Smart UX:
- 🔍 Full-text search by name / country / genre - ⭐ Favorites (persisted in UserDefaults) - 🕘 Recents (auto-tracked) - 📍 Nearby: surfaces stations matching deviceCountry (CoreTelephony / Locale) - 🌍 Grouping by country (with flag emoji) - 🎵 Grouping by genre
  • Native AdMob slot rendered between personal sections and country list (non-Pro only)
  • Background playback (radio keeps streaming when app is backgrounded)
  • Sleep timer (SleepTimerSheet + SleepTimerManager), 5/10/15/30/45/60 min or end-of-track
  • Pro paywall trigger when a free user taps a locked station

🏝️ Live Activities & Dynamic Island

Shipped in commit 4c0f54e, a full ActivityKit widget extension (TalkyLiveActivities/) plus a main-app LiveActivityManager that surfaces both modes on the Lock Screen and the Dynamic Island.

Targets & files

| Component | Path | |---|---| | Shared attributes (radio + walkie content state) | Shared/LiveActivityAttributes.swift | | Interactive intents (iOS 17+) | Shared/RadioActivityIntents.swift | | Widget extension bundle | TalkyLiveActivities/TalkyLiveActivitiesBundle.swift | | Radio widget UI (Lock Screen + Dynamic Island) | TalkyLiveActivities/RadioActivityWidget.swift | | Walkie widget UI | TalkyLiveActivities/WalkieActivityWidget.swift | | App-side orchestrator (start / update / end) | WalkieTalkie/LiveActivity/LiveActivityManager.swift | | Deep-link routing from LA → app | WalkieTalkie/LiveActivity/LiveActivityDeepLink.swift |

Radio Live Activity: content state

  • Station name, country, flag emoji, frequency, genre
  • isPlaying and isBuffering flags driving the play/pause glyph
  • Interactive controls (iOS 17+): ⏮ previous · ⏯ play/pause · ⏭ next, wired via LiveActivityIntent and broadcast back to the app through NotificationCenter (talkyRadioTogglePlayPause, talkyRadioNextStation, talkyRadioPreviousStation)
  • "Next/previous" prioritises favorites when the user has any, otherwise walks the full available station pool

Walkie Live Activity: content state

  • Connected peer count (badge in Dynamic Island compact view)
  • First 3 peer names (Lock Screen expanded view)
  • Current channel name
  • Optional talkerName: surfaced live when a peer is transmitting, so the user can see who's on the air without opening the app

Lifecycle & safety

  • @MainActor-isolated singleton (LiveActivityManager.shared), gated to iOS 16.2+
  • One-at-a-time semantics: starting a radio LA ends any walkie LA and vice-versa (modes are mutually exclusive in the UI)
  • Bootstrap on app launch reconnects to any in-flight Activity (e.g. app killed while LA is still on screen)
  • Respects user toggle in iOS Settings → Talky → Live Activities (ActivityAuthorizationInfo().areActivitiesEnabled)
  • Mirrors the data already pushed into MPNowPlayingInfoCenter, so older devices (< iOS 16.2) keep the existing Now-Playing behaviour
  • No push token / no pushType: nil: pure local updates from the app
Design spec lives in docs/ (see commit 23e97ac). The two widget files contain the full SwiftUI layout for the Lock Screen card, the compact/minimal/expanded Dynamic Island regions, and the iOS 17 interactive button rows.

🎨 Theme System (16 themes)

Themes are organised into 3 pluggable packs registered into a central ThemeRegistry:

🎨 Color Pack: Free (5 themes)

| Theme | Accent | Icon | |---|---|---| | Default (Talky Yellow) | #FFCC00 | radio | | Ocean | Cyan | water.waves | | Forest | Green | leaf.fill | | Sunset | Orange | sun.horizon.fill | | Midnight | Indigo | moon.stars.fill |

🎭 Identity Pack: included in Pro, or in the All Themes Pack (9 themes)

  • Military · Retro 80s · Vintage Radio · Cyberpunk · Stealth · Aurora · Submarine · Ham Radio · Festival
Each Identity theme bundles:
  • Custom accentColor
  • Optional custom PostScript font (e.g. PressStart2P-Regular for Retro 80s) via FontManager
  • Optional themed sound pack (e.g. morse, sonar, glitch) via ThemeSoundManager

🌌 Animated Pack: included in Pro, or in the All Themes Pack (2 themes)

  • Black Hole: GPU shader gravitational lensing background
  • Galaxy: Procedural starfield
Rendered by AnimatedBackgroundView using TimelineView + Canvas for 60 fps.
Correction (20 Sep 2026): individual theme purchases do not exist. This section claimed €0.99 and €1.99 per theme, but ProductID.allIDs only ever requests ProWeeklyWT, ProAnnualWT, the All Themes Pack (€5.99) and Talky Pro Lifetime (€39.99), so StoreKit is never asked for a per-theme product. A leftover app.immaginet.talky.theme.military record does sit on App Store Connect in MISSING_METADATA, unreferenced by the app. Themes unlock via Pro or the All Themes Pack, enforced by ThemeManager reading IAPManager.shared.ownedProducts.

💎 Pro Tier: IAP & Paywall

Backed by StoreKit 2 in IAP/IAPManager.swift with product IDs declared in IAP/IAPProducts.swift.

Subscriptions (auto-renewing)

| Product ID | Plan | |---|---| | app.immaginet.talky.pro.weekly | Weekly | | app.immaginet.talky.pro.yearly | Yearly (best value) |

One-shot theme purchases (non-consumable)

  • app.immaginet.talky.theme.: one per Identity / Animated theme

Paywall: IAP/PaywallView.swift

  • Trigger-aware (station_browser, theme_locked, equalizer, …) for analytics
  • Restore Purchases
  • Receipt validation via Apple's Transaction.currentEntitlements
  • Fast-boot Pro flag cached in UserDefaults("fastboot_isProUser") to avoid UI flicker on cold launch

📣 AdMob Monetization

A complete, policy-compliant AdMob stack lives under Ads/:

| Format | Coordinator | Where it shows | |---|---|---| | App Open | AppOpenAdManager | Cold launch + return-from-background | | Interstitial | InterstitialAdCoordinator | Natural breaks (frequency-capped: 5/day, 180s min interval) | | Rewarded | RewardedAdCoordinator | Two opt-in rewards: "remove ads for 1 hour", and (2.46) "unlock this Pro station for 24 hours" from the station browser | | ~~Native Advanced~~ | NativeAdCoordinator + NativeAdCardView | Not wired. Removed from AdManager after 14 days at $0.00 with 9 impressions. The files stay for a future placement; the nativowalkie unit on AdMob receives no requests. |

Implementation notes:

  • SDK: Google Mobile Ads SDK 13.x (modern types: InterstitialAd, Request, MobileAds.shared.start, etc., no GAD prefix)
  • DEBUG: always uses Google's test ad unit IDs (no risk of policy strikes during development)
  • Release: production unit IDs under account ca-app-pub-1193280742171051
  • Native ad card is styled with the app's design tokens (Color("SurfaceColor"), Color("PrimaryTextColor")) so it visually matches the surrounding station rows; the AdMob "Ad" badge is always visible (policy compliance)
  • Frequency capping: AdConfig.FrequencyCap enforces interstitial cool-downs + rewarded-ads remove-ads duration
  • Pro / adsRemoved users: every coordinator guards on IAPManager.shared.isProUser and AdManager.shared.adsRemoved: Pro users never see an ad

Bootstrap order (Ads/AdManager.swift)

1. UMP consent flow   ──► gatherConsent()
  1. ATT prompt ──► requestATTIfNeeded()
  2. SDK init ──► MobileAds.shared.start
  3. Preload ──► appOpen only, and only if it can still be shown
this session (not Pro, no active reward, paywall not already claiming the session)

Interstitial and rewarded are not preloaded at launch any more. Until 2.46 they were, and because the interstitial only ever shows on exit from radio mode and the rewarded only on an explicit tap, most sessions asked Google for an ad, got one, and threw it away: 13% show rate on the interstitial, 5% on the rewarded. They now load at the moment the opportunity appears (prepareInterstitialForRadioSession when playback starts, prepareRewardedIfNeeded when a rewarded CTA is on screen).

This order is mandatory for both Apple review and AdMob eCPM (Google must see the consent + IDFA signal before any ad request).


🔒 Privacy & Consent (UMP + ATT)

Ads/ConsentManager.swift orchestrates the full GDPR + ATT flow:

  • UMP SDK: shows the AdMob consent form to EEA/UK users; gates ad requests on canRequestAds
  • ATT (App Tracking Transparency): prompted after UMP resolution and only once UI is active (Apple requirement for iOS 14.5+)
  • Privacy policy, terms and support at ,
/terms/ and /support/, in English, Italian and Spanish
  • What we don't collect:
- Walkie-talkie audio: it goes device to device on the local network and is never sent to us. It is not always encrypted (Multipeer runs with encryptionPreference: .optional, the cross-platform TALKY1 link is plain TCP), so the policy says so - Location: no GPS. Receivers and the sun's height come from the time zone - The names of the devices you talk with: since build 125 they are no longer attached to the device_connection analytics events
  • What we do collect (iPhone and iPad only, the Mac app has neither):
- Anonymous Firebase Analytics events and Crashlytics crash reports. Collection is on from launch, it is not gated by the consent form - AdMob ads for free users; personalisation only after UMP consent, the IDFA only after ATT

🌍 Localization

14 full translations under WalkieTalkie/.lproj/Localizable.strings:

| Locale | Language | |---|---| | it | 🇮🇹 Italiano | | en | 🇬🇧 English | | es | 🇪🇸 Español | | fr | 🇫🇷 Français | | de | 🇩🇪 Deutsch | | pt-BR | 🇧🇷 Português (Brasil) | | ja | 🇯🇵 日本語 | | ko | 🇰🇷 한국어 | | ms | 🇲🇾 Bahasa Melayu | | th | 🇹🇭 ไทย | | tr | 🇹🇷 Türkçe | | vi | 🇻🇳 Tiếng Việt | | zh-Hans | 🇨🇳 简体中文 (Simplified Chinese) | | zh-Hant | 🇹🇼 繁體中文 (Traditional Chinese) |

All UI strings flow through String+Localization.swift (.localized). i18n keys are kept in sync across languages, see commit 6ba6d06 for the latest dedup audit.


🏗️ Architecture

Pattern: MVVM + Singleton Managers + Combine for reactive UI.

┌───────────────────────────────────────────────────────────┐
│ SwiftUI Views (@StateObject / @ObservedObject)            │
│  ContentView · ExploreView · ConnectionsView · Settings   │
│  StationBrowserSheet · PaywallView · ThemeSelectorView    │
└──────────────────────────┬────────────────────────────────┘
                           │  @Published / Combine
┌──────────────────────────▼────────────────────────────────┐
│ Singleton Managers (.shared)                              │
│  AudioManager · MultipeerManager · RadioManager           │
│  NotificationManager · SettingsManager · PowerManager     │
│  HapticManager · FirebaseManager · ThemeManager           │
│  IAPManager · AdManager · ConsentManager                  │
│  EqualizerManager · RecordingsManager · SleepTimerManager │
│  LiveActivityManager (iOS 16.2+ · ActivityKit)            │
└──────────────────────────┬────────────────────────────────┘
                           │
┌──────────────────────────▼────────────────────────────────┐
│ Apple frameworks                                          │
│  MultipeerConnectivity · AVFoundation · StoreKit 2        │
│  UserNotifications · Combine · CoreHaptics                │
│  AppTrackingTransparency · ActivityKit · WidgetKit        │
│  AppIntents (iOS 17+ Live Activity controls)              │
└──────────────────────────┬────────────────────────────────┘
                           │
┌──────────────────────────▼────────────────────────────────┐
│ Third-party (SPM)                                         │
│  GoogleMobileAds (13.x) · UserMessagingPlatform           │
│  Firebase (Analytics + Crashlytics + Performance)         │
└───────────────────────────────────────────────────────────┘

📁 Project Structure

WalkieTalkie/                            ← Repo root (Xcode project + Android port)
├── Android/                             ← 🤖 Android app (Kotlin + Jetpack Compose)
│   ├── app/src/main/java/com/immaginet/talky/
│   │   ├── MainActivity.kt              ← Compose UI (radio + walkie)
│   │   ├── net/CrossPlatformWalkieManager.kt  ← P2P PTT over NSD
│   │   ├── radio/RadioManager.kt        ← 335 stations (synced from iOS)
│   │   ├── audio/ · ads/ · firebase/ · protocol/
│   ├── keystore.properties.sample       ← signing config template
│   └── build.gradle.kts                 ← R8 minify + signing
├── WalkieTalkie.xcodeproj/
├── LICENSE                              ← PolyForm Noncommercial 1.0.0
├── README.md
├── Shared/                              ← Code shared between app + widget extension
│   ├── LiveActivityAttributes.swift     ← ActivityAttributes for radio + walkie
│   └── RadioActivityIntents.swift       ← Interactive intents (iOS 17+)
│
├── TalkyLiveActivities/                 ← Widget extension target
│   ├── TalkyLiveActivitiesBundle.swift  ← @main WidgetBundle
│   ├── RadioActivityWidget.swift        ← Lock Screen + Dynamic Island UI (radio)
│   ├── WalkieActivityWidget.swift       ← Lock Screen + Dynamic Island UI (walkie)
│   └── Info.plist
│
├── WalkieTalkie/                        ← App source
│   ├── WalkieTalkieApp.swift            ← @main + AppDelegate (Firebase, ad bootstrap)
│   ├── ContentView.swift                ← Main UI: radio/walkie tabs
│   ├── OnboardingView.swift             ← First-run permissions + intro
│   ├── ExploreView.swift                ← Peer discovery
│   ├── ConnectionsView.swift            ← Active peer list
│   ├── SettingsView.swift               ← Preferences + Pro upsell
│   │
│   ├── LiveActivity/                    ← App-side orchestration
│   │   ├── LiveActivityManager.swift    ← Start/update/end + intent handling
│   │   └── LiveActivityDeepLink.swift   ← LA tap → app deep-link routing
│   │
│   ├── Ads/                             ← AdMob stack (5 files)
│   │   ├── AdConfig.swift               ← Ad unit IDs (DEBUG/Release split)
│   │   ├── AdManager.swift              ← Bootstrap + lifecycle
│   │   ├── ConsentManager.swift         ← UMP + ATT
│   │   ├── AppOpenAdManager.swift
│   │   ├── InterstitialAdCoordinator.swift
│   │   ├── RewardedAdCoordinator.swift
│   │   ├── NativeAdCoordinator.swift    ← NEW: Native Advanced loader
│   │   ├── NativeAdCardView.swift       ← NEW: SwiftUI card UI
│   │   └── AdViewControllerRepresentable.swift
│   │
│   ├── IAP/                             ← StoreKit 2
│   │   ├── IAPProducts.swift            ← Subscription + theme product IDs
│   │   ├── IAPManager.swift             ← Purchase / restore / entitlements
│   │   └── PaywallView.swift            ← Paywall UI with trigger analytics
│   │
│   ├── Theme/                           ← 16-theme engine
│   │   ├── Theme.swift                  ← enum Theme (16 cases)
│   │   ├── ThemeMetadata.swift          ← Tier + accent + font + sound
│   │   ├── ThemeColorPack.swift         ← 5 free themes
│   │   ├── ThemeIdentityPack.swift      ← 9 identity themes (€0.99)
│   │   ├── ThemeAnimatedPack.swift      ← 2 animated themes (€1.99)
│   │   ├── ThemeManager.swift           ← Active theme + persistence
│   │   ├── ThemeSelectorView.swift      ← Theme picker
│   │   ├── ThemePurchaseSheet.swift     ← Single-theme purchase flow
│   │   ├── ThemeSoundManager.swift      ← Themed sound packs
│   │   ├── FontManager.swift            ← Custom PostScript fonts
│   │   └── AnimatedBackgroundView.swift ← GPU animated backgrounds
│   │
│   ├── Radio/                           ← Radio browser
│   │   ├── StationBrowserSheet.swift    ← Search + favorites + recents + groups
│   │   ├── SleepTimerSheet.swift        ← Timer UI
│   │   └── SleepTimerManager.swift
│   │
│   ├── Audio/                           ← Pro audio features
│   │   ├── EqualizerManager.swift       ← 5-band EQ (AVAudioUnitEQ)
│   │   ├── EqualizerView.swift
│   │   ├── RecordingsManager.swift      ← Persist + replay sessions
│   │   └── RecordingsListView.swift
│   │
│   ├── History/
│   │   └── TransmissionHistoryView.swift
│   │
│   ├── Managers (core)/
│   │   ├── AudioManager.swift           ← AVAudioSession + PTT pipeline
│   │   ├── MultipeerManager.swift       ← MCSession + peer discovery
│   │   ├── RadioManager.swift           ← AVPlayer + 330+ stations
│   │   ├── NotificationManager.swift    ← Anti-spam local notifications
│   │   ├── SettingsManager.swift        ← UserDefaults wrapper
│   │   ├── PowerManager.swift           ← Battery monitoring
│   │   ├── HapticManager.swift          ← CoreHaptics
│   │   ├── FirebaseManager.swift        ← Analytics + Crashlytics
│   │   ├── PerformanceMonitor.swift     ← FPS / memory profiling
│   │   └── Logger.swift                 ← Categorised logs
│   │
│   ├── String+Localization.swift
│   ├── FirstTimeEventTracker.swift      ← Onboarding analytics
│   ├── FirstRunCoachView.swift
│   │
│   ├── Audio resources/
│   │   └── f1.mp3 … f24.mp3, radio2.mp3, radio3.mp3, radio4.mp3
│   │
│   ├── Assets.xcassets/                 ← App icons, color sets
│   │
│   ├── it.lproj · en.lproj · es.lproj · ms.lproj · zh-Hant.lproj
│   │
│   └── Info.plist · GoogleService-Info.plist
│
└── README.md · AppStore_Description.md · plan.md

🚀 Build & Run

Prerequisites

  • macOS with Xcode 16.0+ (Xcode 17 fully supported)
  • iOS 15.6+ target device (physical device required, Multipeer Connectivity does not run in the Simulator)
  • Apple Developer account for code signing
  • (Optional) Firebase project: replace GoogleService-Info.plist with yours or remove Firebase frameworks
  • (Optional) AdMob account: replace production unit IDs in Ads/AdConfig.swift; DEBUG builds always use Google's test IDs

Clone & open

git clone https://github.com/andreapianidev/WalkieTalkie.git
cd WalkieTalkie
open WalkieTalkie/WalkieTalkie.xcodeproj

Command-line build

# Debug build
xcodebuild -project WalkieTalkie/WalkieTalkie.xcodeproj \
           -scheme WalkieTalkie -configuration Debug build

Release archive

xcodebuild -project WalkieTalkie/WalkieTalkie.xcodeproj \ -scheme WalkieTalkie -configuration Release archive

Code-signing

  1. Open the project in Xcode
  2. Target WalkieTalkie → Signing & Capabilities → choose your Team
  3. Set a unique Bundle Identifier (the default com.immaginet.talky is owned by the original developer)
  4. Run on a physical device with Cmd+R

Swift Package Manager dependencies (auto-resolved)


⚙️ Configuration

AdMob unit IDs: Ads/AdConfig.swift

#if DEBUG
// Google's official test ad units, safe during development.
static let appOpenAdUnitID       = "ca-app-pub-3940256099942544/5575463023"
static let interstitialAdUnitID  = "ca-app-pub-3940256099942544/4411468910"
static let rewardedAdUnitID      = "ca-app-pub-3940256099942544/1712485313"
static let nativeStationAdUnitID = "ca-app-pub-3940256099942544/3986624511"
#else
// Replace with YOUR AdMob account IDs before shipping.
static let appOpenAdUnitID       = "ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX"
static let interstitialAdUnitID  = "ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX"
static let rewardedAdUnitID      = "ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX"
static let nativeStationAdUnitID = "ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX"
#endif

Frequency capping

enum FrequencyCap {
    static let appOpenMaxAge: TimeInterval = 4 * 3600      // 4h
    static let interstitialMinInterval: TimeInterval = 180 // 3min between interstitials
    static let interstitialDailyMax: Int = 5
    static let removeAdsRewardDuration: TimeInterval = 3600 // 1h
}

Info.plist permission strings

  • NSMicrophoneUsageDescription: walkie-talkie recording
  • NSLocalNetworkUsageDescription: Multipeer Connectivity discovery
  • NSBonjourServices: _walkie-talkie._tcp, _walkie-talkie._udp
  • NSUserTrackingUsageDescription: ATT prompt copy
  • GADApplicationIdentifier: AdMob app ID
  • SKAdNetworkItems: see Info.plist (full list of partner SKAdNetwork IDs)

Audio session

  • Category .playAndRecord with .defaultToSpeaker, .allowBluetooth, .mixWithOthers
  • 44.1 kHz, 16-bit PCM, mono for PTT / stereo for radio
  • Background audio entitlement enabled

🧪 Testing

⚠️ Multipeer Connectivity does not work in the iOS Simulator. You need at least one physical device (two for PTT testing).

Recommended test matrix:

  • PTT: 2+ physical iPhones on the same Wi-Fi → discover → connect → press-to-talk
  • Radio: pick a station from each of the 14 region groups, verify audio + sleep timer + favorite/unfavorite
  • IAP: create a StoreKit configuration file with the product IDs in IAPProducts.swift, test the paywall + restore on a sandbox account
  • Ads: launch a DEBUG build → the AdMob test creatives must appear (app-open, interstitial after 5 mode switches, native card inside station browser)
  • Consent: change device region to EU → UMP consent form must appear on first launch, then ATT prompt
  • Theme purchase: try to apply a locked theme → ThemePurchaseSheet should open with the correct price
There is no automated test target in this repo, contributions welcome.


🔋 Performance & Battery

  • PowerManager watches UIDevice.batteryLevel + ProcessInfo.isLowPowerModeEnabled and reduces radio polling
  • PerformanceMonitor exposes a debug overlay with FPS / memory / active peers (gated by a hidden build flag)
  • AVAudioSession is deactivated when
... (README truncated for length)
Chat with me