English | 简体中文
A Swift logging library for iOS with a built-in web viewer. View live app logs from a browser on the same Wi-Fi, without a USB connection. Powered by Glider.
Licensed under MIT. The GitHub repository is public. The project has not been published to the public CocoaPods registry.
- Four log levels: error, warning, info and debug, with tags and structured context.
- Automatic call-site file name and line number in console, formatted web logs and plain-text logs.
- Search messages, tags, source locations and context; combine level and tag filters.
- Collapsible filters, a fixed-height log area, and smart auto-scroll: scroll more than 100px from the bottom to pause following, then return within 100px to resume on the next update.
- Plain-text view, text selection, and copying all filtered logs, including a fallback for local HTTP pages.
- English and Simplified Chinese interface text. The app follows the system language; the web viewer supports a saved language choice.
- Up to 2000 recent entries in memory, with reconnect synchronization and deduplication.
- Preferred HTTP port 8848. If occupied, the service selects an available port and displays a notice with the actual address.
- iOS 15 or later; Swift 5 language mode; CocoaPods.
- Phone and computer on the same Wi-Fi. For uninterrupted log delivery, keep the app in the foreground.
git clone --branch 0.1.2 https://github.com/dangercheng/SparkNetLoger.git
cd SparkNetLoger/Example
pod install
open SparkNetLogerDemo.xcworkspaceSelect your development team and device in Xcode, then run the Debug configuration. In the demo, open Settings · Live Logs, enable logging, and open the displayed HTTP address in a browser. The demo can emit all four levels or generate logs once per second. After installing the app, the USB cable may be disconnected.
Install version 0.1.2 directly from the public GitHub repository using HTTPS. No GitHub login or SSH configuration is required:
platform :ios, '15.0'
use_frameworks!
target 'YourApp' do
pod 'SparkNetLoger', :git => 'https://github.com/dangercheng/SparkNetLoger.git',
:tag => '0.1.2'
endRun pod install and keep Podfile.lock in version control. The library is not published to CocoaPods trunk, so keep the explicit Git URL and tag.
For local development, replace only the SparkNetLoger declaration with the path to your checkout:
pod 'SparkNetLoger', :path => '../SparkNetLoger'GliderLogger is installed automatically through SparkNetLoger’s ~> 2 dependency (>= 2.0 and < 3.0); no separate declaration is needed. This release is validated with the public CocoaPods version 2.0.0.
Logging is globally disabled by default. Enable it explicitly at app startup:
import SparkNetLoger
// Build configuration is the host app's choice, not a restriction in the library.
#if DEBUG
SparkNetLoger.configure(enabled: true)
#else
SparkNetLoger.configure(enabled: false)
#endifconfigure(enabled: false) stops the service, clears in-memory history and disables console output. It preserves the saved live-logging preference; enabling the library restores that preference without requiring the settings screen to be opened.
SparkNetLoger.error("Network", "Request failed", context: ["statusCode": 500])
SparkNetLoger.warning("Cache", "Cache expiring", context: ["expiresAt": Date()])
SparkNetLoger.info("Player", "Playback started", context: ["song": ["id": 123]])
SparkNetLoger.debug("UI", "Screen loaded")The first argument is a required tag. Blank tags become Default; context defaults to an empty dictionary. All four methods capture file: String = #fileID and line: UInt = #line. Only the file name and line number are displayed; full paths and function names are not emitted. Forward these parameters when wrapping the API:
func appLog(_ message: String, file: String = #fileID, line: UInt = #line) {
SparkNetLoger.info("App", message, file: file, line: line)
}The library does not intercept print or NSLog. Log messages, tags and user context are preserved in their original language; interface language changes do not translate them.
Context is converted to an independent JSON snapshot on the calling thread. Dates use ISO 8601, URLs become strings, and other objects use their descriptions. Non-finite numbers become strings. Nesting is limited to 8 levels, messages and context to 32 KiB each, and tags to 1 KiB; oversized content is marked as truncated. Do not modify mutable containers while passing them into a log call.
When disabled, the message autoclosure is not evaluated and context is not converted. Swift still evaluates the tag and context argument expressions before entering the method. Avoid logging passwords or tokens.
navigationController?.pushViewController(
SparkNetLogerSettingsViewController(), animated: true
)For custom interfaces:
SparkNetLoger.startLiveLogging()
SparkNetLoger.stopLiveLogging()
let state = SparkNetLoger.state
// Keep the returned token in an instance property.
observation = SparkNetLoger.observeState { state in
// Called on the main thread.
print(state.phase, state.webURL as Any, state.connectionCount)
print(state.noticeMessage as Any, state.errorMessage as Any)
}Phases distinguish disabled, off, waiting for Wi-Fi, starting, running and failed. The legacy paused case remains for source compatibility but is no longer emitted on background entry. noticeMessage reports nonfatal events such as port changes; errorMessage reports failures. The switch preference is saved in UserDefaults, initially off. Background suspension and connection failures do not change it. History is cleared when live logging is explicitly stopped, the library is disabled, or the process exits.
Service control is dispatched to the main thread. State queries synchronously return a main-thread snapshot, so do not block the main thread while waiting for another thread to query state. Releasing the observation token or calling cancel() ends observation.
- App and demo: follow the first system preferred language.
zhvariants use Simplified Chinese; English and unsupported languages use English. Restart the app after changing its system language. - Web viewer: initially follows the browser language. The English / 中文 selector changes all UI text without clearing logs, search or filter selections. The choice is saved in browser local storage for that origin; a new IP or port has separate storage. If storage is unavailable, switching still works for the current page.
- Native translations are centralized in
Sources/Localization.swift; web translations are inResources/Web/app.js. System-generated errors retain the language supplied by the operating system.
Use the address shown in the app, usually http://PHONE_IP:8848/. Each startup tries 8848 first. On a bind conflict, a system-assigned port is used and the settings screen shows the new address. Glider WebSocket uses 49152...49251; the webpage obtains its actual port automatically.
The service listens on Wi-Fi only. It uses no cloud service, Bonjour discovery or LAN scanning. All web resources are packaged with the Pod. HTTP accepts only GET requests for fixed resources, not arbitrary local files. The viewer is intended for trusted development networks; it does not provide authentication or TLS.
Entering the background or locking the phone does not explicitly stop the HTTP service or disconnect WebSocket clients. iOS may still suspend or terminate the app, so continuous background delivery is not guaranteed. Returning to the foreground rechecks service availability and restarts it if needed, preserving healthy connections; use the displayed address if the IP or port changed. Browsers reconnect after 1, 2, 4, 8 and then 10 seconds; explicitly stopping live logging ends retries.
Level/tag selections are OR within each group and AND across groups. Search is case-insensitive. Formatted entries expand to show context. Plain-text entries include time, level, tag, source location, message and nonempty context. Copy all copies the current filtered results. Clear logs affects only the current browser, not the phone's history. Pausing auto-scroll does not pause reception; Scroll to bottom does not change the auto-scroll switch.
Protocol version 1 uses historyStart, logs, historyEnd, closed and suspended. Entries contain sessionID, sequence, version, timestamp, level, tag, message, context, file and line. Older entries without source locations remain displayable.
Glider 2.0.0 has an empty WebSocketPeer.stop() implementation. Previously used servers are retained until process exit to avoid upstream callbacks accessing released objects, so repeated restarts can accumulate instances. Stopping listeners does not guarantee immediate physical disconnection or release of all connection resources.
The wrapper does not eliminate Glider's internal concurrency risks or enforce a hard slow-client memory bound. Logs are sent in batches of up to 20, normally every 100ms. Test heavy workloads, long sessions and lifecycle behavior on real devices. Simulator loopback tests do not replace phone-to-computer Wi-Fi verification.
node --test Tests/web.test.js
ruby Scripts/validate_pod.rb
cd Example
xcodebuild -workspace SparkNetLogerDemo.xcworkspace -scheme SparkNetLogerDemo \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' testChoose an available simulator on your machine. Scripts/generate_project.rb is only for creating the initial project and refuses to overwrite an existing one. VALIDATION.md contains historical validation notes in Chinese; consult current test output for the latest results.
Copyright (c) 2026 chengdengjian. SparkNetLoger is licensed under the MIT License. It is not published to CocoaPods trunk. Third-party dependencies retain their own licenses; Glider uses MIT.

