CounterTestApp is an iOS 15.6+ SwiftUI application that demonstrates two launch flows:
- A native casino-themed Counter game.
- An iGaming flow hosted in a production-ready
WKWebViewcontainer.
The project uses Coordinator + MVVM, protocol-based dependency injection, persistent storage abstractions, WebKit delegates, local notifications, analytics logging, and a compact critical test suite.
- Xcode 26 or newer
- iOS 15.6+
- Swift 5
- Internet access for the production WebView flow
The project uses Swift Package Manager to resolve KeychainAccess.
- Clone the repository.
- Open
CounterTestApp.xcodeprojin Xcode. - Wait for Swift Package Manager to resolve dependencies.
- Select the
CounterTestAppscheme and an iOS simulator or device. - Run with
Cmd+R.
Command-line build:
xcodebuild build \
-project CounterTestApp.xcodeproj \
-scheme CounterTestApp \
-destination 'generic/platform=iOS Simulator'Camera, microphone, and local-notification permissions are requested only when the associated feature needs them.
The application follows Coordinator + MVVM with protocol-driven services:
CounterTestAppApp (composition root)
└── AppCoordinator
├── SplashViewModel
├── CounterViewModel
├── WebViewViewModel
└── Services
├── InitialRouteProviding
├── StorageService
├── AnalyticsServiceProtocol
├── WebViewConfigurationProviding
├── ExternalURLOpening
├── DocumentPickerPresenting
├── NetworkMonitoring
└── NotificationServiceProtocol
Responsibilities:
CounterTestAppAppcreates concrete dependencies and contains no feature logic.AppCoordinatorowns the active.counteror.webViewroute and handles notification deep links.- Views render state and forward user actions.
- View models contain presentation and feature state.
- Services isolate storage, routing, analytics, WebKit configuration, network monitoring, external URLs, document pickers, and notifications.
Long-lived callbacks use weak captures. KVO observations are invalidated when the WebView is dismantled, WebKit delegates are cleared, async Splash tasks are cancelled, and navigation closures are unbound to avoid retain cycles.
MockRemoteConfigRoutingService simulates a remote-config request. The last successful destination is persisted through StorageService, and every cold launch alternates:
counter → webView → counter → webView → …
With empty storage, firstDestination controls the first flow and defaults to .counter. The simulated delay is configured through loadingDelayNanoseconds.
The routing strategy can be replaced through the InitialRouteProviding protocol without changing the coordinator or UI.
A single opaque SplashView is layered above AppCoordinatorView during startup.
- The mock configuration stage maps to 0–35%.
- For Counter, Splash completes after route resolution.
- For WebView, KVO-observed
WKWebView.estimatedProgressmaps to 35–100%. - After the initial WebView finishes, Splash remains visible for one additional second.
- Later WebView navigations use a linear SwiftUI
ProgressViewat the top of the page.
No opacity transition is applied to Splash.
The Counter module provides increment, decrement, reset, best-score persistence, animations, and accessibility identifiers.
CounterViewModel depends on StorageService, not on a concrete persistence API. Two implementations demonstrate SOLID dependency inversion:
UserDefaultsStorage— used by the composition root.KeychainStorage— interchangeable secure persistence backed byKeychainAccess.
The counter never falls below zero. A new best score is persisted immediately.
WebViewContainer wraps WKWebView with UIViewRepresentable. DefaultWebViewConfigurationProvider configures:
- persistent
WKWebsiteDataStore.default(); - JavaScript execution;
- JavaScript-created windows;
- persistent cookies, cache, and local storage;
- a custom application component in the user agent;
- viewport rules that prevent page and input-focus zoom;
- disabled text selection, callouts, magnifier behavior, and link previews.
The scroll view has bounce and pinch zoom disabled.
The production URL is:
https://lk.nsq.market/en/tools/testing
An idempotent WKUserScript is injected at atDocumentEnd into all frames. General interaction rules prevent viewport zoom, text selection, callouts, context menus, and drag-and-drop. Custom branding is guarded by an exact nsq.market/subdomain hostname check, so it is never applied to external pages.
For the testing page, the script:
- renames the
App testingheading toWhiteOutand makes it bold; - replaces the gray
#333333page color with the Counter game's deep violet#47147A, while preserving the site's repeating translucentbg.pngpattern; - changes the Bootstrap accent text in
.col.col-12.col-lg-4to the Counter game's gold#FFBF2Ewithout changing its background; - hides the non-functional mobile
#sidebarMenutoggle; - replaces the specific test YouTube embed (
5NV6Rdv1a3I) with Michael Jackson'sSmooth Criminalembed (h_D3VFfhvs4); - replaces the test OGG audio URL with Wikimedia's corresponding MP3 transcode and reloads its
<audio>element for iOS compatibility.
A MutationObserver reapplies these transformations when the site renders content dynamically. Every mutation is conditional, so already-customized elements are not rewritten repeatedly. These selectors and media IDs intentionally target this testing page and should be reviewed if its upstream markup changes.
WKNavigationDelegate handles action and response policies, lifecycle callbacks, redirects, authentication challenges, HTTP errors, downloads, and WebContent process termination.
- Same-site HTTP/HTTPS pages remain inside the WebView.
- Sibling subdomains such as
lk.nsq.marketandwww.nsq.marketare treated as the same site. - Explicit cross-site links open through
UIApplication.shared.open. - Custom schemes such as
tel:,mailto:, andtg:are handed to the system. - Server redirects remain transparent inside WebView so authentication and payment redirects are not interrupted.
- Internal
target="_blank"popups load in the current WebView. - External popups open through the system.
- JavaScript
alert,confirm, andpromptuse native iOS dialogs.
The dark navigation bar observes meaningful WKBackForwardList state. Fragment-only history entries are ignored so Back and Forward do not merely scroll between anchors.
- Camera and microphone requests are mapped to
AVCaptureDeviceauthorization and resolved throughWKPermissionDecision. - Required camera and microphone descriptions are present in the generated Info.plist.
- On iOS 18.4+, the public WebKit open-panel callback uses
UIDocumentPickerViewControllerexplicitly. - Earlier supported iOS versions use WKWebView's native Safari-compatible file-input picker because the public open-panel callback is unavailable there.
WKDownloadDelegatedetects attachments and unsupported MIME types, downloads to an isolated temporary directory, and presents a document exporter.- Temporary download files are removed after export cancellation, completion, or failure.
NWPathMonitor publishes connectivity changes through the NetworkMonitoring abstraction.
- Offline state is displayed immediately, without waiting for a WebKit timeout.
- Navigation failures, HTTP 4xx/5xx responses, and WebContent termination have dedicated error states.
- Expected cancellation and policy-change errors are ignored.
- Retry reloads the last main-frame request.
- Retry is disabled until connectivity returns.
NotificationService wraps UNUserNotificationCenter and acts as its delegate.
- Notification permission is requested after app startup.
- Entering inactive/background state schedules one notification after 10 seconds.
- Returning to active state cancels a pending reminder.
- A stable request identifier prevents duplicate notifications.
- Tapping the notification dismisses Splash, selects the WebView route, and loads:
https://www.apple.com/
This is a local-notification implementation; APNs and a backend are not required.
Features depend on AnalyticsServiceProtocol. The default ConsoleAnalyticsService uses unified logging (OSLog) and emits deterministic event names with sorted parameters.
Key event groups include:
- app startup, scene phase, route resolution, and route presentation;
- Splash start, success, and failure;
- Counter start, increment, decrement, reset, and new best score;
- WebView open, load lifecycle, redirects, cancellation, HTTP/network errors, retry, Back/Forward, deep links, and process termination;
- external URLs, popups, media permissions, file import, and downloads;
- network status changes;
- notification permission, scheduling, cancellation, response, and destination opening.
Production analytics can replace ConsoleAnalyticsService without changing feature code.
The suite intentionally covers only critical behavior.
Unit tests verify:
- alternating launch routing;
- Counter persistence and the zero lower bound;
- WebView error and retry state.
UI smoke tests verify:
- starting the Counter game and incrementing its value;
- presence of WebView Back and Forward controls.
UI tests launch with --ui-testing and either --force-counter or --force-webview. Test mode uses local HTML, an always-connected network monitor, and a no-op notification service, so tests do not depend on internet access or permission dialogs.
Run from Xcode with Product > Test, or:
xcodebuild test \
-project CounterTestApp.xcodeproj \
-scheme CounterTestApp \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro'CounterTestApp/
├── Core/
│ ├── Analytics/
│ ├── Navigation/
│ ├── Notifications/
│ ├── Routing/
│ └── Storage/
├── Features/
│ ├── Counter/
│ ├── Splash/
│ └── WebView/
├── Assets.xcassets/
└── Launch Screen.storyboard
CounterTestAppTests/
└── CriticalUnitTests.swift
CounterTestAppUITests/
└── CriticalFlowUITests.swift
- Only HTTPS production URLs are configured.
- Cookies and local storage persist because iGaming sessions require continuity.
- Camera and microphone access is requested on demand.
- Custom schemes are delegated to iOS instead of loaded by WebKit.
- Download filenames are sanitized and files are stored in unique temporary directories.
- Analytics currently logs URLs and filenames for demonstration; production builds should apply the product's privacy and redaction policy.