From 493d01038cb0e41187936540acdf4a1e2098fa33 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:50:13 +0000 Subject: [PATCH 1/2] Initial plan From 0ac6edf251a8e8336d7f35004e28be06af806d9e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 05:56:47 +0000 Subject: [PATCH 2/2] Add iOS SwiftUI demo app with encapsulated SocketManager - 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> --- .gitignore | 1 + .../iOSDemo/iOSDemo.xcodeproj/project.pbxproj | 384 ++++++++++++++++++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + .../iOSDemo/Assets.xcassets/Contents.json | 6 + Examples/iOSDemo/iOSDemo/ContentView.swift | 32 ++ Examples/iOSDemo/iOSDemo/SocketManager.swift | 162 ++++++++ .../iOSDemo/Views/SSEParserDemoView.swift | 143 +++++++ .../Views/SocketConnectionDemoView.swift | 124 ++++++ .../iOSDemo/Views/StreamBufferDemoView.swift | 135 ++++++ .../iOSDemo/Views/UTF8SafetyDemoView.swift | 116 ++++++ Examples/iOSDemo/iOSDemo/iOSDemoApp.swift | 10 + README.md | 26 +- 13 files changed, 1160 insertions(+), 3 deletions(-) create mode 100644 Examples/iOSDemo/iOSDemo.xcodeproj/project.pbxproj create mode 100644 Examples/iOSDemo/iOSDemo/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/iOSDemo/iOSDemo/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/iOSDemo/iOSDemo/Assets.xcassets/Contents.json create mode 100644 Examples/iOSDemo/iOSDemo/ContentView.swift create mode 100644 Examples/iOSDemo/iOSDemo/SocketManager.swift create mode 100644 Examples/iOSDemo/iOSDemo/Views/SSEParserDemoView.swift create mode 100644 Examples/iOSDemo/iOSDemo/Views/SocketConnectionDemoView.swift create mode 100644 Examples/iOSDemo/iOSDemo/Views/StreamBufferDemoView.swift create mode 100644 Examples/iOSDemo/iOSDemo/Views/UTF8SafetyDemoView.swift create mode 100644 Examples/iOSDemo/iOSDemo/iOSDemoApp.swift diff --git a/.gitignore b/.gitignore index f34cbe3..d6e05f0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc *.xcodeproj +!Examples/iOSDemo/iOSDemo.xcodeproj _codeql_detected_source_root diff --git a/Examples/iOSDemo/iOSDemo.xcodeproj/project.pbxproj b/Examples/iOSDemo/iOSDemo.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9dc6036 --- /dev/null +++ b/Examples/iOSDemo/iOSDemo.xcodeproj/project.pbxproj @@ -0,0 +1,384 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + A1000001 /* iOSDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000001 /* iOSDemoApp.swift */; }; + A1000002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000002 /* ContentView.swift */; }; + A1000003 /* SocketManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000003 /* SocketManager.swift */; }; + A1000004 /* StreamBufferDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000004 /* StreamBufferDemoView.swift */; }; + A1000005 /* SSEParserDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000005 /* SSEParserDemoView.swift */; }; + A1000006 /* UTF8SafetyDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000006 /* UTF8SafetyDemoView.swift */; }; + A1000007 /* SocketConnectionDemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2000007 /* SocketConnectionDemoView.swift */; }; + A1000008 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A2000008 /* Assets.xcassets */; }; + A1000009 /* NWAsyncSocket in Frameworks */ = {isa = PBXBuildFile; productRef = A6000001 /* NWAsyncSocket */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A2000001 /* iOSDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSDemoApp.swift; sourceTree = ""; }; + A2000002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + A2000003 /* SocketManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketManager.swift; sourceTree = ""; }; + A2000004 /* StreamBufferDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamBufferDemoView.swift; sourceTree = ""; }; + A2000005 /* SSEParserDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSEParserDemoView.swift; sourceTree = ""; }; + A2000006 /* UTF8SafetyDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTF8SafetyDemoView.swift; sourceTree = ""; }; + A2000007 /* SocketConnectionDemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketConnectionDemoView.swift; sourceTree = ""; }; + A2000008 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A2000010 /* iOSDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A3000001 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A1000009 /* NWAsyncSocket in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A4000001 /* Root */ = { + isa = PBXGroup; + children = ( + A4000002 /* iOSDemo */, + A4000004 /* Products */, + ); + sourceTree = ""; + }; + A4000002 /* iOSDemo */ = { + isa = PBXGroup; + children = ( + A2000001 /* iOSDemoApp.swift */, + A2000002 /* ContentView.swift */, + A2000003 /* SocketManager.swift */, + A4000003 /* Views */, + A2000008 /* Assets.xcassets */, + ); + path = iOSDemo; + sourceTree = ""; + }; + A4000003 /* Views */ = { + isa = PBXGroup; + children = ( + A2000004 /* StreamBufferDemoView.swift */, + A2000005 /* SSEParserDemoView.swift */, + A2000006 /* UTF8SafetyDemoView.swift */, + A2000007 /* SocketConnectionDemoView.swift */, + ); + path = Views; + sourceTree = ""; + }; + A4000004 /* Products */ = { + isa = PBXGroup; + children = ( + A2000010 /* iOSDemo.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A5000001 /* iOSDemo */ = { + isa = PBXNativeTarget; + buildConfigurationList = A8000003 /* Build configuration list for PBXNativeTarget "iOSDemo" */; + buildPhases = ( + A3000002 /* Sources */, + A3000001 /* Frameworks */, + A3000003 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iOSDemo; + packageProductDependencies = ( + A6000001 /* NWAsyncSocket */, + ); + productName = iOSDemo; + productReference = A2000010 /* iOSDemo.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A7000001 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + A5000001 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = A8000001 /* Build configuration list for PBXProject "iOSDemo" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A4000001 /* Root */; + packageReferences = ( + A6000002 /* XCLocalSwiftPackageReference "../../" */, + ); + productRefGroup = A4000004 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A5000001 /* iOSDemo */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A3000003 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A1000008 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A3000002 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A1000001 /* iOSDemoApp.swift in Sources */, + A1000002 /* ContentView.swift in Sources */, + A1000003 /* SocketManager.swift in Sources */, + A1000004 /* StreamBufferDemoView.swift in Sources */, + A1000005 /* SSEParserDemoView.swift in Sources */, + A1000006 /* UTF8SafetyDemoView.swift in Sources */, + A1000007 /* SocketConnectionDemoView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + A9000001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A9000002 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A9000003 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nwasyncsocket.iOSDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A9000004 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.nwasyncsocket.iOSDemo; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A8000001 /* Build configuration list for PBXProject "iOSDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A9000001 /* Debug */, + A9000002 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A8000003 /* Build configuration list for PBXNativeTarget "iOSDemo" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A9000003 /* Debug */, + A9000004 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + A6000002 /* XCLocalSwiftPackageReference "../../" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + A6000001 /* NWAsyncSocket */ = { + isa = XCSwiftPackageProductDependency; + productName = NWAsyncSocket; + }; +/* End XCSwiftPackageProductDependency section */ + + }; + rootObject = A7000001 /* Project object */; +} diff --git a/Examples/iOSDemo/iOSDemo/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/iOSDemo/iOSDemo/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Examples/iOSDemo/iOSDemo/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/iOSDemo/iOSDemo/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/iOSDemo/iOSDemo/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/Examples/iOSDemo/iOSDemo/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/iOSDemo/iOSDemo/Assets.xcassets/Contents.json b/Examples/iOSDemo/iOSDemo/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Examples/iOSDemo/iOSDemo/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/iOSDemo/iOSDemo/ContentView.swift b/Examples/iOSDemo/iOSDemo/ContentView.swift new file mode 100644 index 0000000..52fc26e --- /dev/null +++ b/Examples/iOSDemo/iOSDemo/ContentView.swift @@ -0,0 +1,32 @@ +import SwiftUI + +struct ContentView: View { + var body: some View { + NavigationView { + List { + Section(header: Text("Core Components")) { + NavigationLink(destination: StreamBufferDemoView()) { + Label("StreamBuffer", systemImage: "arrow.left.arrow.right") + } + NavigationLink(destination: SSEParserDemoView()) { + Label("SSE Parser", systemImage: "antenna.radiowaves.left.and.right") + } + NavigationLink(destination: UTF8SafetyDemoView()) { + Label("UTF-8 Safety", systemImage: "textformat") + } + } + Section(header: Text("Socket")) { + NavigationLink(destination: SocketConnectionDemoView()) { + Label("Socket Connection", systemImage: "network") + } + } + } + .navigationTitle("NWAsyncSocket Demo") + } + .navigationViewStyle(.stack) + } +} + +#Preview { + ContentView() +} diff --git a/Examples/iOSDemo/iOSDemo/SocketManager.swift b/Examples/iOSDemo/iOSDemo/SocketManager.swift new file mode 100644 index 0000000..1bb31b1 --- /dev/null +++ b/Examples/iOSDemo/iOSDemo/SocketManager.swift @@ -0,0 +1,162 @@ +import Foundation +import NWAsyncSocket + +/// A manager that encapsulates NWAsyncSocket operations for use in SwiftUI. +/// +/// Provides high-level methods for connecting, sending, receiving data, and +/// enables TLS, SSE parsing, and streaming text modes. Published properties +/// allow SwiftUI views to react to connection state and incoming data. +/// +/// Usage: +/// ```swift +/// let manager = SocketManager() +/// manager.connect(host: "example.com", port: 443, useTLS: true) +/// manager.send("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") +/// ``` +final class SocketManager: NSObject, ObservableObject { + + // MARK: - Published State + + @Published var isConnected = false + @Published var logs: [LogEntry] = [] + @Published var receivedData: Data = Data() + @Published var receivedText: String = "" + @Published var sseEvents: [SSEEvent] = [] + + struct LogEntry: Identifiable { + let id = UUID() + let timestamp = Date() + let message: String + } + + // MARK: - Private + + private var socket: NWAsyncSocket? + private var readTag: Int = 0 + + // MARK: - Connection + + /// Connect to a host and port. Optionally enable TLS, SSE parsing, and streaming text. + func connect(host: String, port: UInt16, useTLS: Bool = false, + enableSSE: Bool = false, enableStreaming: Bool = false) { + disconnect() + + let sock = NWAsyncSocket(delegate: self, delegateQueue: .main) + + if useTLS { + sock.enableTLS() + } + if enableSSE { + sock.enableSSEParsing() + } + if enableStreaming { + sock.enableStreamingText() + } + + socket = sock + appendLog("Connecting to \(host):\(port)...") + + do { + try sock.connect(toHost: host, onPort: port, withTimeout: 15) + } catch { + appendLog("❌ Connect error: \(error.localizedDescription)") + } + } + + /// Disconnect the current socket. + func disconnect() { + socket?.disconnect() + socket = nil + } + + /// Send a string as UTF-8 data. + func send(_ text: String) { + guard let data = text.data(using: .utf8) else { + appendLog("❌ Failed to encode text") + return + } + send(data) + } + + /// Send raw data. + func send(_ data: Data) { + guard let socket = socket else { + appendLog("❌ Not connected") + return + } + let tag = readTag + readTag += 1 + socket.write(data, withTimeout: 30, tag: tag) + appendLog("πŸ“€ Sent \(data.count) bytes (tag: \(tag))") + } + + /// Request to read available data. + func readData() { + guard let socket = socket else { + appendLog("❌ Not connected") + return + } + let tag = readTag + readTag += 1 + socket.readData(withTimeout: 30, tag: tag) + } + + /// Clear all logs and received data. + func clearAll() { + logs.removeAll() + receivedData = Data() + receivedText = "" + sseEvents.removeAll() + readTag = 0 + } + + // MARK: - Private + + private func appendLog(_ message: String) { + logs.append(LogEntry(message: message)) + } +} + +// MARK: - NWAsyncSocketDelegate + +extension SocketManager: NWAsyncSocketDelegate { + + 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 + } + + func socket(_ sock: NWAsyncSocket, didRead data: Data, withTag tag: Int) { + receivedData.append(data) + appendLog("πŸ“₯ Received \(data.count) bytes (tag: \(tag))") + // Continue reading + sock.readData(withTimeout: -1, tag: readTag) + readTag += 1 + } + + func socket(_ sock: NWAsyncSocket, didWriteDataWithTag tag: Int) { + appendLog("βœ… Write complete (tag: \(tag))") + } + + func socketDidDisconnect(_ sock: NWAsyncSocket, withError error: Error?) { + isConnected = false + if let error = error { + appendLog("πŸ”΄ Disconnected: \(error.localizedDescription)") + } else { + appendLog("πŸ”΄ Disconnected") + } + } + + func socket(_ sock: NWAsyncSocket, didReceiveSSEEvent event: SSEEvent) { + sseEvents.append(event) + appendLog("πŸ“‘ SSE Event: type=\(event.event), data=\(event.data.prefix(100))") + } + + func socket(_ sock: NWAsyncSocket, didReceiveString string: String) { + receivedText += string + appendLog("πŸ“ Text chunk: \(string.prefix(100))") + } +} diff --git a/Examples/iOSDemo/iOSDemo/Views/SSEParserDemoView.swift b/Examples/iOSDemo/iOSDemo/Views/SSEParserDemoView.swift new file mode 100644 index 0000000..331ad84 --- /dev/null +++ b/Examples/iOSDemo/iOSDemo/Views/SSEParserDemoView.swift @@ -0,0 +1,143 @@ +import SwiftUI +import NWAsyncSocket + +struct SSEParserDemoView: View { + @State private var results: [DemoResult] = [] + + struct DemoResult: Identifiable { + let id = UUID() + let title: String + let detail: String + let success: Bool + } + + var body: some View { + List { + if results.isEmpty { + Section { + Text("Tap \"Run All\" to demonstrate SSE parsing: single events, multiple events, LLM streaming simulation, and special fields.") + .foregroundColor(.secondary) + } + } + ForEach(results) { result in + Section(header: Text(result.title)) { + Text(result.detail) + .font(.system(.body, design: .monospaced)) + HStack { + Image(systemName: result.success ? "checkmark.circle.fill" : "xmark.circle.fill") + .foregroundColor(result.success ? .green : .red) + Text(result.success ? "Passed" : "Failed") + } + } + } + } + .navigationTitle("SSE Parser") + .toolbar { + Button("Run All") { runAllDemos() } + } + } + + private func runAllDemos() { + results.removeAll() + demoSingleEvent() + demoMultipleEvents() + demoLLMStreaming() + demoIdRetry() + demoMultiLineData() + } + + private func demoSingleEvent() { + let parser = SSEParser() + let data = "event: chat\ndata: Hello from the server!\n\n".data(using: .utf8)! + let events = parser.parse(data) + + let success = events.count == 1 && events.first?.event == "chat" + let detail = """ + Input: "event: chat\\ndata: Hello from the server!\\n\\n" + Parsed \(events.count) event(s): + \(events.map { " type: \($0.event), data: \($0.data)" }.joined(separator: "\n")) + """ + results.append(DemoResult(title: "Single SSE Event", detail: detail, success: success)) + } + + private func demoMultipleEvents() { + let parser = SSEParser() + let data = "data: first\n\ndata: second\n\nevent: custom\ndata: third\n\n".data(using: .utf8)! + let events = parser.parse(data) + + let success = events.count == 3 + let detail = """ + Parsed \(events.count) events from one chunk: + \(events.enumerated().map { " [\($0.offset + 1)] type: \($0.element.event), data: \($0.element.data)" }.joined(separator: "\n")) + """ + results.append(DemoResult(title: "Multiple Events in One Chunk", detail: detail, success: success)) + } + + private func demoLLMStreaming() { + let parser = SSEParser() + let chunks = [ + "data: {\"tok", + "en\": \"Hel\"}\n", + "\ndata: {\"token\"", + ": \"lo\"}\n\ndata", + ": {\"token\": \" World\"}\n\n" + ] + + var allEvents: [SSEEvent] = [] + var chunkDetails: [String] = [] + for (i, chunk) in chunks.enumerated() { + let parsed = parser.parse(chunk) + allEvents.append(contentsOf: parsed) + let display = chunk.replacingOccurrences(of: "\n", with: "\\n") + chunkDetails.append(" Chunk \(i + 1): \"\(display)\" β†’ \(parsed.count) event(s)") + } + + let success = allEvents.count == 3 + let detail = """ + Fed \(chunks.count) partial chunks: + \(chunkDetails.joined(separator: "\n")) + Total events: \(allEvents.count) + \(allEvents.enumerated().map { " [\($0.offset + 1)] \($0.element.data)" }.joined(separator: "\n")) + """ + results.append(DemoResult(title: "LLM Streaming Simulation", detail: detail, success: success)) + } + + private func demoIdRetry() { + let parser = SSEParser() + let data = "id: 42\nretry: 3000\nevent: update\ndata: payload\n\n".data(using: .utf8)! + let events = parser.parse(data) + + let event = events.first + let success = event?.id == "42" && event?.retry == 3000 && event?.event == "update" + let detail = """ + Input: "id: 42\\nretry: 3000\\nevent: update\\ndata: payload\\n\\n" + type: \(event?.event ?? "nil") + data: \(event?.data ?? "nil") + id: \(event?.id ?? "nil") + retry: \(event?.retry.map(String.init) ?? "nil") + lastEventId: \(parser.lastEventId ?? "nil") + """ + results.append(DemoResult(title: "ID and Retry Fields", detail: detail, success: success)) + } + + private func demoMultiLineData() { + let parser = SSEParser() + let data = "data: line one\ndata: line two\ndata: line three\n\n".data(using: .utf8)! + let events = parser.parse(data) + + let event = events.first + let success = event?.data == "line one\nline two\nline three" + let detail = """ + Input: 3 data fields in one event + data: "\(event?.data ?? "nil")" + Contains newlines: \(event?.data.contains("\n") == true ? "yes" : "no") + """ + results.append(DemoResult(title: "Multi-Line Data", detail: detail, success: success)) + } +} + +#Preview { + NavigationView { + SSEParserDemoView() + } +} diff --git a/Examples/iOSDemo/iOSDemo/Views/SocketConnectionDemoView.swift b/Examples/iOSDemo/iOSDemo/Views/SocketConnectionDemoView.swift new file mode 100644 index 0000000..f88038b --- /dev/null +++ b/Examples/iOSDemo/iOSDemo/Views/SocketConnectionDemoView.swift @@ -0,0 +1,124 @@ +import SwiftUI +import NWAsyncSocket + +struct SocketConnectionDemoView: View { + @StateObject private var manager = SocketManager() + @State private var host = "example.com" + @State private var port = "443" + @State private var useTLS = true + @State private var enableSSE = false + @State private var enableStreaming = true + @State private var messageToSend = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n" + + var body: some View { + List { + Section(header: Text("Connection Settings")) { + HStack { + Text("Host") + Spacer() + TextField("Host", text: $host) + .multilineTextAlignment(.trailing) + .autocapitalization(.none) + .disableAutocorrection(true) + } + HStack { + Text("Port") + Spacer() + TextField("Port", text: $port) + .multilineTextAlignment(.trailing) + .keyboardType(.numberPad) + } + Toggle("TLS", isOn: $useTLS) + Toggle("SSE Parsing", isOn: $enableSSE) + Toggle("Streaming Text", isOn: $enableStreaming) + } + + Section(header: Text("Actions")) { + if manager.isConnected { + Button(role: .destructive) { + manager.disconnect() + } label: { + Label("Disconnect", systemImage: "xmark.circle") + } + } else { + Button { + let portNum = UInt16(port) ?? 443 + manager.connect(host: host, port: portNum, + useTLS: useTLS, + enableSSE: enableSSE, + enableStreaming: enableStreaming) + } label: { + Label("Connect", systemImage: "play.circle") + } + } + } + + if manager.isConnected { + Section(header: Text("Send Data")) { + TextEditor(text: $messageToSend) + .font(.system(.caption, design: .monospaced)) + .frame(minHeight: 80) + Button { + manager.send(messageToSend) + } label: { + Label("Send", systemImage: "paperplane") + } + } + } + + if !manager.receivedText.isEmpty { + Section(header: Text("Received Text")) { + ScrollView { + Text(manager.receivedText.prefix(2000)) + .font(.system(.caption, design: .monospaced)) + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxHeight: 200) + } + } + + if !manager.sseEvents.isEmpty { + Section(header: Text("SSE Events (\(manager.sseEvents.count))")) { + ForEach(Array(manager.sseEvents.enumerated()), id: \.offset) { idx, event in + VStack(alignment: .leading, spacing: 4) { + Text("[\(idx + 1)] type: \(event.event)") + .font(.system(.caption, design: .monospaced)) + Text("data: \(event.data.prefix(200))") + .font(.system(.caption, design: .monospaced)) + .foregroundColor(.secondary) + } + } + } + } + + Section(header: Text("Logs (\(manager.logs.count))")) { + if manager.logs.isEmpty { + Text("No activity yet") + .foregroundColor(.secondary) + } else { + Button("Clear") { + manager.clearAll() + } + ForEach(manager.logs.reversed()) { entry in + Text(entry.message) + .font(.system(.caption, design: .monospaced)) + } + } + } + } + .navigationTitle("Socket Connection") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Circle() + .fill(manager.isConnected ? Color.green : Color.red) + .frame(width: 12, height: 12) + } + } + } +} + +#Preview { + NavigationView { + SocketConnectionDemoView() + } +} diff --git a/Examples/iOSDemo/iOSDemo/Views/StreamBufferDemoView.swift b/Examples/iOSDemo/iOSDemo/Views/StreamBufferDemoView.swift new file mode 100644 index 0000000..1059dae --- /dev/null +++ b/Examples/iOSDemo/iOSDemo/Views/StreamBufferDemoView.swift @@ -0,0 +1,135 @@ +import SwiftUI +import NWAsyncSocket + +struct StreamBufferDemoView: View { + @State private var results: [DemoResult] = [] + + struct DemoResult: Identifiable { + let id = UUID() + let title: String + let detail: String + let success: Bool + } + + var body: some View { + List { + if results.isEmpty { + Section { + Text("Tap \"Run All\" to demonstrate StreamBuffer capabilities: sticky-packet splitting, split-packet reassembly, delimiter-based reads, and read-all.") + .foregroundColor(.secondary) + } + } + ForEach(results) { result in + Section(header: Text(result.title)) { + Text(result.detail) + .font(.system(.body, design: .monospaced)) + HStack { + Image(systemName: result.success ? "checkmark.circle.fill" : "xmark.circle.fill") + .foregroundColor(result.success ? .green : .red) + Text(result.success ? "Passed" : "Failed") + } + } + } + } + .navigationTitle("StreamBuffer") + .toolbar { + Button("Run All") { runAllDemos() } + } + } + + private func runAllDemos() { + results.removeAll() + demoStickyPacket() + demoSplitPacket() + demoDelimiterRead() + demoReadAll() + } + + private func demoStickyPacket() { + let buffer = StreamBuffer() + let data = "Hello\r\nWorld\r\nFoo\r\n".data(using: .utf8)! + buffer.append(data) + + let delimiter = "\r\n".data(using: .utf8)! + var messages: [String] = [] + while let chunk = buffer.readData(toDelimiter: delimiter) { + if let text = String(data: chunk, encoding: .utf8) { + messages.append(text) + } + } + + let success = messages.count == 3 + let detail = """ + Input: "Hello\\r\\nWorld\\r\\nFoo\\r\\n" + Parsed \(messages.count) messages: + \(messages.enumerated().map { " [\($0.offset + 1)] \($0.element.replacingOccurrences(of: "\r\n", with: "\\r\\n"))" }.joined(separator: "\n")) + Remaining: \(buffer.count) bytes + """ + results.append(DemoResult(title: "Sticky Packet (η²˜εŒ…)", detail: detail, success: success)) + } + + private func demoSplitPacket() { + let buffer = StreamBuffer() + buffer.append("Hel".data(using: .utf8)!) + let first = buffer.readData(toLength: 11) + + buffer.append("lo World".data(using: .utf8)!) + let second = buffer.readData(toLength: 11) + + let text = second.flatMap { String(data: $0, encoding: .utf8) } + let success = first == nil && text == "Hello World" + let detail = """ + Part 1: "Hel" β†’ read 11 bytes: \(first == nil ? "nil (waiting)" : "got data") + Part 2: "lo World" β†’ read 11 bytes: "\(text ?? "nil")" + """ + results.append(DemoResult(title: "Split Packet (ζ‹†εŒ…)", detail: detail, success: success)) + } + + private func demoDelimiterRead() { + let buffer = StreamBuffer() + buffer.append("key1=value1&key2=value2&key3=value3".data(using: .utf8)!) + + let amp = "&".data(using: .utf8)! + var pairs: [String] = [] + while let data = buffer.readData(toDelimiter: amp) { + if let text = String(data: data, encoding: .utf8) { + pairs.append(text) + } + } + let remaining = buffer.readAllData() + if !remaining.isEmpty, let text = String(data: remaining, encoding: .utf8) { + pairs.append(text) + } + + let success = pairs.count == 3 + let detail = """ + Input: "key1=value1&key2=value2&key3=value3" + Parsed \(pairs.count) pairs: + \(pairs.map { " \($0)" }.joined(separator: "\n")) + """ + results.append(DemoResult(title: "Delimiter-Based Read", detail: detail, success: success)) + } + + private func demoReadAll() { + let buffer = StreamBuffer() + buffer.append("Part A ".data(using: .utf8)!) + buffer.append("Part B ".data(using: .utf8)!) + buffer.append("Part C".data(using: .utf8)!) + let all = buffer.readAllData() + let text = String(data: all, encoding: .utf8) ?? "" + + let success = text == "Part A Part B Part C" && buffer.isEmpty + let detail = """ + Appended: "Part A " + "Part B " + "Part C" + readAllData: "\(text)" + Buffer empty: \(buffer.isEmpty) + """ + results.append(DemoResult(title: "Read All Data", detail: detail, success: success)) + } +} + +#Preview { + NavigationView { + StreamBufferDemoView() + } +} diff --git a/Examples/iOSDemo/iOSDemo/Views/UTF8SafetyDemoView.swift b/Examples/iOSDemo/iOSDemo/Views/UTF8SafetyDemoView.swift new file mode 100644 index 0000000..e594064 --- /dev/null +++ b/Examples/iOSDemo/iOSDemo/Views/UTF8SafetyDemoView.swift @@ -0,0 +1,116 @@ +import SwiftUI +import NWAsyncSocket + +struct UTF8SafetyDemoView: View { + @State private var results: [DemoResult] = [] + + struct DemoResult: Identifiable { + let id = UUID() + let title: String + let detail: String + let success: Bool + } + + var body: some View { + List { + if results.isEmpty { + Section { + Text("Tap \"Run All\" to demonstrate UTF-8 boundary safety: complete multi-byte characters, incomplete boundary detection, and safe byte counting.") + .foregroundColor(.secondary) + } + } + ForEach(results) { result in + Section(header: Text(result.title)) { + Text(result.detail) + .font(.system(.body, design: .monospaced)) + HStack { + Image(systemName: result.success ? "checkmark.circle.fill" : "xmark.circle.fill") + .foregroundColor(result.success ? .green : .red) + Text(result.success ? "Passed" : "Failed") + } + } + } + } + .navigationTitle("UTF-8 Safety") + .toolbar { + Button("Run All") { runAllDemos() } + } + } + + private func runAllDemos() { + results.removeAll() + demoCompleteMultiByte() + demoIncompleteBoundary() + demoSafeByteCount() + } + + private func demoCompleteMultiByte() { + let buffer = StreamBuffer() + let emoji = "Hello πŸŒπŸš€".data(using: .utf8)! + buffer.append(emoji) + let str = buffer.readUTF8SafeString() + + let success = str == "Hello πŸŒπŸš€" + let detail = """ + Input: "Hello πŸŒπŸš€" (\(emoji.count) bytes) + UTF-8 safe read: "\(str ?? "nil")" + """ + results.append(DemoResult(title: "Complete Multi-Byte Characters", detail: detail, success: success)) + } + + private func demoIncompleteBoundary() { + let buffer = StreamBuffer() + let chinese = "δ½ ε₯½δΈ–η•Œ".data(using: .utf8)! // 12 bytes (3 per char) + let partial = Data(chinese.prefix(10)) // Cuts 4th character + buffer.append(partial) + + let safeCount = StreamBuffer.utf8SafeByteCount(buffer.data) + let str1 = buffer.readUTF8SafeString() + let remaining1 = buffer.count + + // Complete the character + buffer.append(Data(chinese.suffix(from: 10))) + let str2 = buffer.readUTF8SafeString() + + let success = safeCount == 9 && str1 == "δ½ ε₯½δΈ–" && str2 == "η•Œ" && buffer.isEmpty + let detail = """ + "δ½ ε₯½δΈ–η•Œ" = \(chinese.count) bytes (3 bytes/char) + Truncated to 10 bytes: + Safe byte count: \(safeCount) (3 chars Γ— 3 bytes) + First read: "\(str1 ?? "nil")" + Remaining: \(remaining1) byte(s) + After appending final \(chinese.count - 10) bytes: + Second read: "\(str2 ?? "nil")" + Buffer empty: \(buffer.isEmpty) + """ + results.append(DemoResult(title: "Incomplete Boundary Detection", detail: detail, success: success)) + } + + private func demoSafeByteCount() { + // 2-byte character (Γ© = 0xC3 0xA9) + let cafe = "cafΓ©".data(using: .utf8)! + let truncated2 = Data(cafe.prefix(cafe.count - 1)) + let safe2 = StreamBuffer.utf8SafeByteCount(truncated2) + + // 4-byte character (𝕳 = U+1D573) + let fourByte = "A𝕳B".data(using: .utf8)! + let truncated4 = Data(fourByte.prefix(3)) + let safe4 = StreamBuffer.utf8SafeByteCount(truncated4) + + let success = safe2 == cafe.count - 2 && safe4 == 1 + let detail = """ + "cafΓ©" β†’ \(cafe.count) bytes, truncated to \(truncated2.count): + Safe count: \(safe2) (excludes incomplete Γ©) + + "A𝕳B" β†’ \(fourByte.count) bytes, truncated to 3: + Safe count: \(safe4) (only 'A' is complete) + """ + results.append(DemoResult(title: "utf8SafeByteCount", detail: detail, success: success)) + } +} + +#Preview { + NavigationView { + UTF8SafetyDemoView() + } +} diff --git a/Examples/iOSDemo/iOSDemo/iOSDemoApp.swift b/Examples/iOSDemo/iOSDemo/iOSDemoApp.swift new file mode 100644 index 0000000..39189f9 --- /dev/null +++ b/Examples/iOSDemo/iOSDemo/iOSDemoApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSDemoApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/README.md b/README.md index f6b8d68..ccb4932 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,14 @@ NWAsyncSocket/ β”‚ β”œβ”€β”€ StreamBuffer.swift # Byte buffer with UTF-8 safety β”‚ β”œβ”€β”€ SSEParser.swift # SSE event parser β”‚ └── ReadRequest.swift # Read request queue model -β”œβ”€β”€ Examples/SwiftDemo/ # Swift interactive demo +β”œβ”€β”€ Examples/iOSDemo/ # iOS SwiftUI demo app +β”‚ β”œβ”€β”€ iOSDemo.xcodeproj # Open in Xcode to run +β”‚ └── iOSDemo/ +β”‚ β”œβ”€β”€ iOSDemoApp.swift # App entry point +β”‚ β”œβ”€β”€ ContentView.swift # Main navigation +β”‚ β”œβ”€β”€ SocketManager.swift # Encapsulated socket operations +β”‚ └── Views/ # Feature demo views +β”œβ”€β”€ Examples/SwiftDemo/ # Swift interactive demo (CLI) β”‚ └── main.swift # Run: swift run SwiftDemo β”œβ”€β”€ ObjC/NWAsyncSocketObjC/ # Objective-C version β”‚ β”œβ”€β”€ include/ # Public headers @@ -257,9 +264,22 @@ Add the ObjC source and test files to an Xcode project and run the XCTest test s ## Demo -Interactive demos are provided for both Swift and Objective-C to help you verify all core components. +Interactive demos are provided to help you verify all core components. -### 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. + +The app demonstrates all core components with an interactive UI: + +- **StreamBuffer** β€” sticky-packet / split-packet handling, delimiter-based reads +- **SSE Parser** β€” single/multi/split SSE events, LLM streaming simulation +- **UTF-8 Safety** β€” multi-byte character boundary detection +- **Socket Connection** β€” connect/disconnect, send/receive, TLS, SSE, streaming text + +The app includes a `SocketManager` class that encapsulates all NWAsyncSocket operations into a clean, SwiftUI-friendly `ObservableObject` with `@Published` properties. + +### Swift Demo (CLI) Run the interactive Swift demo via SPM: