// Created by Andrea Piani - 22/05/26 - https://www.andreapiani.com - README.md
Talky — Walkie-Talkie & Internet Radio (Source-Available iOS + Android App)
Talky is a SwiftUI iOS app that combines offline peer-to-peer push-to-talk (Multipeer Connectivity) with a global internet radio browser (135 streaming stations across 50+ 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: com.immaginet.talky
Repository:
📱 Talky on iPhone
| Talk — hold to transmit | Explore — nearby devices | Connections | Settings | |---|---|---|---| | !Talky Talk screen, push to talk | !Talky Explore, radar of nearby devices | !Talky Connections, advertising and browsing state | !Talky Settings, white noise and haptics |
- Talk — the frequency display, the channel, and the hold-to-talk key. The dot
- Explore — a radar of the devices around you, with estimated distance and
- Connections — advertising and browsing state, the list of connected devices,
- Settings — notifications, white noise and its volume, haptics, auto-connect.
These are the App Store screenshots of the current build (2.46). They were shot
in July 2025 and still show the older yellow look: the radio, the 16 themes and
the Live Activities are not in them. Worth re-shooting.
🖥 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 — 343 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 343-station worldwide browser with search, filters, favorites and recents, Media-key/Now Playing integration.
- 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). ThemacTalkyscheme is shared, ready for an Xcode Cloud "Archive – macOS" workflow.
Table of Contents
- Highlights
- Android (Beta)
- Feature Matrix (Free vs Pro)
- Walkie-Talkie Engine
- TALKY1 protocol specification
- Radio Browser (135 stations)
- Live Activities & Dynamic Island
- Theme System (16 themes)
- Pro Tier — IAP & Paywall
- AdMob Monetization
- Privacy & Consent (UMP + ATT)
- Localization
- Architecture
- Project Structure
- Build & Run
- Configuration
- Testing
- Performance & Battery
- Contributing
- License
- Credits & Links
✨ Highlights
- 🎙️ Push-to-Talk over peer-to-peer (no internet, no servers) — up to 8 simultaneous peers
- 📻 135 verified live radio stations across 50+ 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
- 🌍 5 localizations: 🇮🇹 Italian · 🇬🇧 English · 🇪🇸 Spanish · 🇲🇾 Malay · 🇹🇼 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 Talky for Android v1.1 (signed APK) — Android 11/API 30 or newer. Android may ask you to authorize installation from your browser or file manager.
⚠️ 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 anAudioManagerPTT pipeline - 📻 343 radio stations — the same catalogue as iOS, kept in sync from
RadioManager.swiftintoAndroid/.../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 | 30 (Android 11) / 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.propertieswithsdk.dir(Android Studio generates it automatically). google-services.jsonis included for Firebase; replace it with yours or remove the Firebase plugins.
📣 Ad units note — for the open-source / sideload APK,AdConfig.ktintentionally 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 theLIVE_*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) | ✅ | ✅ | | 24 frequency channels (f1.mp3 … f24.mp3) | ✅ | ✅ | | Radio stations available | 30 free | All 135 | | 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
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
.playAndRecordwith.defaultToSpeaker, mixWithOthers off during PTT - Background audio entitlement for radio + incoming PTT alerts
- Anti-spam notifications via
NotificationManagercooldown - Haptic feedback on PTT press/release via
HapticManager - Auto-reconnect with exponential backoff
- 24 frequency channels for quick audible signalling (
f1.mp3…f24.mp3)
📻 Radio Browser (135 stations)
The radio side is a full streaming client (RadioManager + StationBrowserSheet):
- 135 live stations across 50+ countries (Italy, USA, UK, Spain, France, Germany, Japan, Brazil, Australia, Argentina, Mexico, Canada, Netherlands, Sweden, India, and 30+ more)
- 30 stations free · 105 stations Pro-only (
isPro = id > 30) - Smart UX:
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
isPlayingandisBufferingflags driving the play/pause glyph- Interactive controls (iOS 17+): ⏮ previous · ⏯ play/pause · ⏭ next — wired via
LiveActivityIntentand broadcast back to the app throughNotificationCenter(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 indocs/(see commit23e97ac). 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
- Custom
accentColor - Optional custom PostScript font (e.g.
PressStart2P-Regularfor Retro 80s) viaFontManager - Optional themed sound pack (e.g.
morse,sonar,glitch) viaThemeSoundManager
🌌 Animated Pack — included in Pro, or in the All Themes Pack (2 themes)
- Black Hole — GPU shader gravitational lensing background
- Galaxy — Procedural starfield
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, butProductID.allIDsonly ever requestsProWeeklyWT,ProAnnualWT, the All Themes Pack (€5.99) and Talky Pro Lifetime (€39.99), so StoreKit is never asked for a per-theme product. A leftoverapp.immaginet.talky.theme.militaryrecord does sit on App Store Connect inMISSING_METADATA, unreferenced by the app. Themes unlock via Pro or the All Themes Pack, enforced byThemeManagerreadingIAPManager.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. — noGADprefix) - 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.FrequencyCapenforces interstitial cool-downs + rewarded-ads remove-ads duration - Pro /
adsRemovedusers: every coordinator guards onIAPManager.shared.isProUserandAdManager.shared.adsRemoved— Pro users never see an ad
Bootstrap order (Ads/AdManager.swift)
1. UMP consent flow ──► gatherConsent()
- ATT prompt ──► requestATTIfNeeded()
- SDK init ──► MobileAds.shared.start
- 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 hosted at
- What we DON'T collect:
- What we DO collect (only with consent):
🌍 Localization
5 full translations under WalkieTalkie/:
| Locale | Language |
|---|---|
| it | 🇮🇹 Italiano |
| en | 🇬🇧 English |
| es | 🇪🇸 Español |
| ms | 🇲🇾 Bahasa Melayu |
| 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 ← 343 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 + 135 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.plistwith 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
- Open the project in Xcode
- Target WalkieTalkie → Signing & Capabilities → choose your Team
- Set a unique Bundle Identifier (the default
com.immaginet.talkyis owned by the original developer) - Run on a physical device with Cmd+R
Swift Package Manager dependencies (auto-resolved)
swift-package-manager-google-mobile-ads(Google Mobile Ads SDK)firebase-ios-sdk(Analytics + Crashlytics + Performance)- UMP SDK is bundled with Google Mobile Ads
⚙️ 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 recordingNSLocalNetworkUsageDescription— Multipeer Connectivity discoveryNSBonjourServices—_walkie-talkie._tcp,_walkie-talkie._udpNSUserTrackingUsageDescription— ATT prompt copyGADApplicationIdentifier— AdMob app IDSKAdNetworkItems— see Info.plist (full list of partner SKAdNetwork IDs)
Audio session
- Category
.playAndRecordwith.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 →
ThemePurchaseSheetshould open with the correct price
🔋 Performance & Battery
PowerManagerwatchesUIDevice.batteryLevel+ProcessInfo.isLowPowerModeEnabledand reduces radio pollingPerformanceMonitorexposes a debug overlay with FPS / memory / active peers (gated by a hidden build flag)- AVAudioSession is deactivated when both modes are idle to free the audio hardware
- All Combine subscriptions store
cancellablesand are torn down indeinitto avoid retain cycles MultipeerManagerautomatically disconnects idle peers after a configurable timeout to save battery- AdMob ads are preloaded once and reused; frequency caps prevent ad-storm scenarios
🤝 Contributing
Contributions are welcome! Please:
- Fork the repo and create a feature branch (
feat/orfix/) - Match the existing code style — Swift API Design Guidelines, no force-unwraps, prefer
@MainActorandnonisolatedcorrectness over@unchecked Sendable - Update localizations if you change any UI string (
it,en,es,ms,zh-Hantmust stay in sync) - Build & run on a physical device before opening the PR
- Open a PR with a clear description, screenshots for UI changes, and reference any related issue
feat:, fix:, chore:, refactor:, docs:, style:.
Contributor license agreement
By opening a pull request you agree that your contribution is licensed under the same PolyForm Noncommercial License 1.0.0 as the rest of the project, and that you grant Andrea Piani a perpetual, worldwide, royalty-free right to relicense your contribution under any other license (including commercial) as part of the official Talky distribution. This is the standard inbound=outbound model used by most source-available projects and is required to keep the commercial-license path viable.
Good first issues
- Add more radio stations (extend
RadioManager.radioStations) - Add additional language translations (clone an existing
.lprojfolder) - New themes — add a new file under
Theme/and register intoThemeRegistry - Apple Watch companion app
- Home Screen / Lock Screen widgets for quick PTT (separate from the Live Activity)
- StandBy mode large-radio widget (iOS 17+)
📄 License
PolyForm Noncommercial License 1.0.0 — © 2025–2026 Andrea Piani · NIE 02915190306-Z · El Paso, Santa Cruz de Tenerife · Islas Canarias. All rights reserved.
🚫 This is NOT an MIT/Apache/BSD/GPL project. It is source-available, not "open-source" in the OSI sense. Commercial use is strictly prohibited without a separate written license signed by the copyright holder.
✅ What you ARE allowed to do (free of charge)
- Read, study and learn from the code — this repo exists primarily as an educational reference for production-grade SwiftUI / Multipeer / StoreKit / AdMob / ActivityKit integration.
- Fork and modify for personal, academic, research, journalism, hobby or non-profit purposes.
- Run your modified copy on your own devices.
- Share the source or your patches, as long as the
LICENSEfile and the copyright notice stay intact. - Use by educational institutions, public-research organisations, NGOs, charities and government bodies is explicitly permitted (see PolyForm §"Noncommercial Organizations").
🚫 What you ARE NOT allowed to do without a commercial license
- ❌ Publish, distribute or sell this app — or any app substantially derived from this codebase — on the Apple App Store, Google Play, AltStore, Setapp, or any other paid or ad-supported channel.
- ❌ Use this code, in whole or in part, in any product, service, SaaS, enterprise deployment, paid app, ad-supported service, sponsored build, consulting deliverable or any activity primarily directed toward commercial advantage or monetary compensation.
- ❌ Train commercial machine-learning models on this codebase.
- ❌ Re-license the code under more permissive terms (MIT, Apache-2.0, BSD, GPL, …) — this is explicitly forbidden.
- ❌ Remove, alter, or obscure the copyright notice, the
LICENSEfile, the in-app "About" attribution, the project name, or the identifying branding from any distributed copy. - ❌ Pretend the code is yours.
💼 Commercial license
If you want to use Talky (or a derivative) in any commercial context — including shipping it on an app store, bundling it inside another paid product, or offering it as a service — you must obtain a separate written commercial license from the copyright holder.
📧 Commercial licensing contact: [email protected] 🌐 https://www.andreapiani.com
Reasonable commercial terms are available for individuals, startups and enterprises. Just write — we'll work it out.
🛡️ Enforcement & DMCA
Violations of this license — including unauthorised App Store submissions, paid-app derivatives and trademark-passing-off — will be enforced through App Store / Play Store DMCA take-down requests, GitHub DMCA, and where appropriate legal action under Italian and EU copyright law (Law 633/1941 as amended) and the Berne Convention. Please don't make us do that — just email instead.
📜 Full legal text
The authoritative legal terms are in the LICENSE file at the root of this repository (PolyForm Noncommercial License 1.0.0, verbatim text). The license is hosted at
The plain-language summary above is informational only and does not replace the legal text.
🏔️ Credits & Links
- Developer: Andrea Piani · NIE 02915190306-Z · El Paso, Santa Cruz de Tenerife · Islas Canarias
- Website:
- Privacy policy:
- Support coffee ☕:
Sister app — Peak (GPS Altimeter + Walkie)
The walkie-talkie engine from Talky also powers Peak — GPS Altimeter Barometer, an outdoor companion app with professional altimetry and built-in PTT.💛 If Talky helped you, ship something cool with it, or just learn how to build a freemium SwiftUI app — drop a ⭐ on GitHub and consider a coffee. Pull requests, station packs and theme contributions are always welcome.