Add runnable iOS SwiftUI demo app with encapsulated SocketManager#5
Add runnable iOS SwiftUI demo app with encapsulated SocketManager#5dustturtle merged 2 commits intomainfrom
Conversation
- Create Examples/iOSDemo/ with complete SwiftUI iOS app - Add SocketManager.swift encapsulating all NWAsyncSocket operations - Add demo views for StreamBuffer, SSEParser, UTF-8 Safety, Socket Connection - Create Xcode project with local NWAsyncSocket SPM dependency - Update .gitignore to allow demo xcodeproj - Update README.md with iOS demo documentation Co-authored-by: dustturtle <2305214+dustturtle@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds a runnable SwiftUI iOS demo app to showcase NWAsyncSocket features with an ObservableObject-based SocketManager, complementing the existing CLI demos.
Changes:
- Introduces
Examples/iOSDemo/Xcode project + SwiftUI app with multiple feature demo views. - Adds
SocketManagerwrapper to encapsulate connection, send/receive, TLS, SSE parsing, and streaming-text behaviors for SwiftUI binding. - Updates repo docs and gitignore to include and describe the new iOS demo project.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents the new iOS demo app and updates the repo tree + demo section. |
| .gitignore | Un-ignores the new Examples/iOSDemo/iOSDemo.xcodeproj so it can be committed. |
| Examples/iOSDemo/iOSDemo/iOSDemoApp.swift | SwiftUI app entry point. |
| Examples/iOSDemo/iOSDemo/ContentView.swift | Navigation menu linking to the component demo screens. |
| Examples/iOSDemo/iOSDemo/SocketManager.swift | ObservableObject wrapper around NWAsyncSocket with published state and delegate handling. |
| Examples/iOSDemo/iOSDemo/Views/StreamBufferDemoView.swift | UI-driven StreamBuffer demonstrations. |
| Examples/iOSDemo/iOSDemo/Views/SSEParserDemoView.swift | UI-driven SSE parsing demonstrations. |
| Examples/iOSDemo/iOSDemo/Views/UTF8SafetyDemoView.swift | UI-driven UTF-8 boundary safety demonstrations. |
| Examples/iOSDemo/iOSDemo/Views/SocketConnectionDemoView.swift | Interactive connect/send/receive UI using SocketManager. |
| Examples/iOSDemo/iOSDemo/Assets.xcassets/Contents.json | Asset catalog metadata. |
| Examples/iOSDemo/iOSDemo/Assets.xcassets/AppIcon.appiconset/Contents.json | App icon asset metadata. |
| Examples/iOSDemo/iOSDemo/Assets.xcassets/AccentColor.colorset/Contents.json | Accent color asset metadata. |
| Examples/iOSDemo/iOSDemo.xcodeproj/project.pbxproj | Xcode project defining the iOS demo target and local SPM dependency. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| /// Disconnect the current socket. | ||
| func disconnect() { |
There was a problem hiding this comment.
disconnect() nils out socket but leaves isConnected unchanged until the async delegate callback arrives. This can temporarily show the wrong connection status in SwiftUI. Consider setting isConnected = false (and optionally resetting per-connection state) synchronously when initiating a disconnect.
| func disconnect() { | |
| func disconnect() { | |
| // Update published connection state immediately so SwiftUI reflects | |
| // the disconnect without waiting for async delegate callbacks. | |
| isConnected = false |
| let portNum = UInt16(port) ?? 443 | ||
| manager.connect(host: host, port: portNum, | ||
| useTLS: useTLS, | ||
| enableSSE: enableSSE, | ||
| enableStreaming: enableStreaming) |
There was a problem hiding this comment.
If the user enters port "0", UInt16(port) succeeds and the app will attempt to connect to port 0, which will crash inside NWAsyncSocket.connect (it force-unwraps NWEndpoint.Port(rawValue:)). Validate that the parsed port is in 1...65535 and surface an error to the user instead of proceeding.
| ### Swift Demo | ||
| ### iOS App Demo (Recommended) | ||
|
|
||
| A complete SwiftUI iOS app is included at `Examples/iOSDemo/`. Open `Examples/iOSDemo/iOSDemo.xcodeproj` in Xcode and run on a simulator or device. |
There was a problem hiding this comment.
The README says the iOS demo can be run “on a simulator or device”, but the Xcode project has an empty DEVELOPMENT_TEAM, so running on a physical device will require the user to set a signing team in Xcode first. Consider adding a short note here to avoid confusion.
| A complete SwiftUI iOS app is included at `Examples/iOSDemo/`. Open `Examples/iOSDemo/iOSDemo.xcodeproj` in Xcode and run on a simulator or device. | |
| A complete SwiftUI iOS app is included at `Examples/iOSDemo/`. Open `Examples/iOSDemo/iOSDemo.xcodeproj` in Xcode and run on a simulator or device (for a physical device, first select your Apple Developer team under **Signing & Capabilities** in Xcode). |
| func socket(_ sock: NWAsyncSocket, didConnectToHost host: String, port: UInt16) { | ||
| isConnected = true | ||
| appendLog("✅ Connected to \(host):\(port)") | ||
| // Start reading | ||
| sock.readData(withTimeout: -1, tag: readTag) | ||
| readTag += 1 | ||
| } |
There was a problem hiding this comment.
Delegate callbacks can arrive from a previous socket instance after connect() creates a new one (because disconnect() is async). Right now any late didConnect/didRead will still mutate this manager’s state. Consider ignoring callbacks unless sock matches the current socket (e.g., identity check) to avoid mixing state across reconnections.
| func socketDidDisconnect(_ sock: NWAsyncSocket, withError error: Error?) { | ||
| isConnected = false | ||
| if let error = error { | ||
| appendLog("🔴 Disconnected: \(error.localizedDescription)") | ||
| } else { | ||
| appendLog("🔴 Disconnected") | ||
| } | ||
| } |
There was a problem hiding this comment.
socketDidDisconnect unconditionally sets isConnected = false. If a disconnect from an older socket instance is delivered after a newer socket has connected, this will incorrectly flip the UI back to disconnected. Add a guard so only the active/current socket can update connection state.
Current demos are CLI-only (
main.swift/main.m). This adds a proper iOS app demo with UI and a reusable manager class wrapping all socket operations.New:
Examples/iOSDemo/SocketManager—ObservableObjectfacade overNWAsyncSocketwith@Publishedstate (isConnected,logs,receivedText,sseEvents). Single-callconnect(host:port:useTLS:enableSSE:enableStreaming:)+send(_:)/disconnect().iOSDemo.xcodeproj— references../../as a local SPM package dependency. Open in Xcode and run.Other changes
.gitignore— added!Examples/iOSDemo/iOSDemo.xcodeprojexceptionREADME.md— added iOS demo section, updated file structure tree💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.