diff --git a/examples/ios-example/ChatKittyExample.xcodeproj/project.pbxproj b/examples/ios-example/ChatKittyExample.xcodeproj/project.pbxproj deleted file mode 100644 index ab0438d5..00000000 --- a/examples/ios-example/ChatKittyExample.xcodeproj/project.pbxproj +++ /dev/null @@ -1,495 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 60; - objects = { - -/* Begin PBXBuildFile section */ - 3921AAF22DA3A7A900B3A465 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3921AAEE2DA3A7A900B3A465 /* LaunchScreen.xib */; }; - 3921AAF32DA3A7A900B3A465 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3921AAEB2DA3A7A900B3A465 /* Images.xcassets */; }; - 3921AAF42DA3A7A900B3A465 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3921AAEA2DA3A7A900B3A465 /* AppDelegate.swift */; }; - 3921AAF52DA3A7A900B3A465 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3921AAEF2DA3A7A900B3A465 /* ViewController.swift */; }; - 39FDA8A72DAB0F5800EFF94A /* ChatKitty in Frameworks */ = {isa = PBXBuildFile; productRef = 39FDA8A62DAB0F5800EFF94A /* ChatKitty */; }; - 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* Tests.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 607FACC81AFB9204008FA782 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 607FACCF1AFB9204008FA782; - remoteInfo = ChatKitty; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXFileReference section */ - 3921AAEA2DA3A7A900B3A465 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 3921AAEB2DA3A7A900B3A465 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - 3921AAEC2DA3A7A900B3A465 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 3921AAED2DA3A7A900B3A465 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; - 3921AAEF2DA3A7A900B3A465 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; - 607FACD01AFB9204008FA782 /* ChatKittyExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChatKittyExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 607FACE51AFB9204008FA782 /* ChatKittyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatKittyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 607FACEB1AFB9204008FA782 /* Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests.swift; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 607FACCD1AFB9204008FA782 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 39FDA8A72DAB0F5800EFF94A /* ChatKitty in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 607FACE21AFB9204008FA782 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 3921AAF02DA3A7A900B3A465 /* ChatKittyExample */ = { - isa = PBXGroup; - children = ( - 3921AAEA2DA3A7A900B3A465 /* AppDelegate.swift */, - 3921AAEB2DA3A7A900B3A465 /* Images.xcassets */, - 3921AAEC2DA3A7A900B3A465 /* Info.plist */, - 3921AAEE2DA3A7A900B3A465 /* LaunchScreen.xib */, - 3921AAEF2DA3A7A900B3A465 /* ViewController.swift */, - ); - path = ChatKittyExample; - sourceTree = ""; - }; - 397E5DDB2DA688BD00E42EC3 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; - 607FACC71AFB9204008FA782 = { - isa = PBXGroup; - children = ( - 3921AAF02DA3A7A900B3A465 /* ChatKittyExample */, - 607FACE81AFB9204008FA782 /* Tests */, - 397E5DDB2DA688BD00E42EC3 /* Frameworks */, - 607FACD11AFB9204008FA782 /* Products */, - ); - sourceTree = ""; - }; - 607FACD11AFB9204008FA782 /* Products */ = { - isa = PBXGroup; - children = ( - 607FACD01AFB9204008FA782 /* ChatKittyExample.app */, - 607FACE51AFB9204008FA782 /* ChatKittyTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 607FACE81AFB9204008FA782 /* Tests */ = { - isa = PBXGroup; - children = ( - 607FACEB1AFB9204008FA782 /* Tests.swift */, - 607FACE91AFB9204008FA782 /* Supporting Files */, - ); - path = Tests; - sourceTree = ""; - }; - 607FACE91AFB9204008FA782 /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 607FACEA1AFB9204008FA782 /* Info.plist */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 607FACCF1AFB9204008FA782 /* ChatKittyExample */ = { - isa = PBXNativeTarget; - buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "ChatKittyExample" */; - buildPhases = ( - 607FACCC1AFB9204008FA782 /* Sources */, - 607FACCD1AFB9204008FA782 /* Frameworks */, - 607FACCE1AFB9204008FA782 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = ChatKittyExample; - productName = ChatKittyExample; - productReference = 607FACD01AFB9204008FA782 /* ChatKittyExample.app */; - productType = "com.apple.product-type.application"; - }; - 607FACE41AFB9204008FA782 /* ChatKittyTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "ChatKittyTests" */; - buildPhases = ( - 607FACE11AFB9204008FA782 /* Sources */, - 607FACE21AFB9204008FA782 /* Frameworks */, - 607FACE31AFB9204008FA782 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 607FACE71AFB9204008FA782 /* PBXTargetDependency */, - ); - name = ChatKittyTests; - productName = Tests; - productReference = 607FACE51AFB9204008FA782 /* ChatKittyTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 607FACC81AFB9204008FA782 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0830; - LastUpgradeCheck = 0830; - ORGANIZATIONNAME = CocoaPods; - TargetAttributes = { - 607FACCF1AFB9204008FA782 = { - CreatedOnToolsVersion = 6.3.1; - LastSwiftMigration = 0900; - }; - 607FACE41AFB9204008FA782 = { - CreatedOnToolsVersion = 6.3.1; - DevelopmentTeam = ZW8X7G65H3; - LastSwiftMigration = 0900; - TestTargetID = 607FACCF1AFB9204008FA782; - }; - }; - }; - buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "ChatKittyExample" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; - hasScannedForEncodings = 0; - knownRegions = ( - English, - en, - Base, - ); - mainGroup = 607FACC71AFB9204008FA782; - packageReferences = ( - 39FDA8A82DACDA6200EFF94A /* XCLocalSwiftPackageReference "../../libraries/ios" */, - ); - productRefGroup = 607FACD11AFB9204008FA782 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 607FACCF1AFB9204008FA782 /* ChatKittyExample */, - 607FACE41AFB9204008FA782 /* ChatKittyTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 607FACCE1AFB9204008FA782 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 3921AAF22DA3A7A900B3A465 /* LaunchScreen.xib in Resources */, - 3921AAF32DA3A7A900B3A465 /* Images.xcassets in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 607FACE31AFB9204008FA782 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 607FACCC1AFB9204008FA782 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 3921AAF42DA3A7A900B3A465 /* AppDelegate.swift in Sources */, - 3921AAF52DA3A7A900B3A465 /* ViewController.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 607FACE11AFB9204008FA782 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 607FACEC1AFB9204008FA782 /* Tests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 607FACCF1AFB9204008FA782 /* ChatKittyExample */; - targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 3921AAEE2DA3A7A900B3A465 /* LaunchScreen.xib */ = { - isa = PBXVariantGroup; - children = ( - 3921AAED2DA3A7A900B3A465 /* Base */, - ); - name = LaunchScreen.xib; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 607FACED1AFB9204008FA782 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_SYMBOLS_PRIVATE_EXTERN = NO; - 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 = 9.3; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 607FACEE1AFB9204008FA782 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - 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_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - 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 = 9.3; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 607FACF01AFB9204008FA782 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = G2KJHX32XJ; - INFOPLIST_FILE = ChatKittyExample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MODULE_NAME = ChatKittyExample; - PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_STRICT_CONCURRENCY = minimal; - SWIFT_SUPPRESS_WARNINGS = NO; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 607FACF11AFB9204008FA782 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - DEVELOPMENT_TEAM = G2KJHX32XJ; - INFOPLIST_FILE = ChatKittyExample/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MODULE_NAME = ChatKittyExample; - PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_STRICT_CONCURRENCY = minimal; - SWIFT_SUPPRESS_WARNINGS = NO; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 607FACF31AFB9204008FA782 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - DEVELOPMENT_TEAM = ZW8X7G65H3; - FRAMEWORK_SEARCH_PATHS = ( - "$(PLATFORM_DIR)/Developer/Library/Frameworks", - "$(inherited)", - ); - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatKitty_Example.app/ChatKitty_Example"; - }; - name = Debug; - }; - 607FACF41AFB9204008FA782 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - DEVELOPMENT_TEAM = ZW8X7G65H3; - FRAMEWORK_SEARCH_PATHS = ( - "$(PLATFORM_DIR)/Developer/Library/Frameworks", - "$(inherited)", - ); - INFOPLIST_FILE = Tests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_SWIFT3_OBJC_INFERENCE = Default; - SWIFT_VERSION = 4.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatKitty_Example.app/ChatKitty_Example"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "ChatKittyExample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 607FACED1AFB9204008FA782 /* Debug */, - 607FACEE1AFB9204008FA782 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "ChatKittyExample" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 607FACF01AFB9204008FA782 /* Debug */, - 607FACF11AFB9204008FA782 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "ChatKittyTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 607FACF31AFB9204008FA782 /* Debug */, - 607FACF41AFB9204008FA782 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - -/* Begin XCLocalSwiftPackageReference section */ - 39FDA8A82DACDA6200EFF94A /* XCLocalSwiftPackageReference "../../libraries/ios" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = ../../libraries/ios; - }; -/* End XCLocalSwiftPackageReference section */ - -/* Begin XCSwiftPackageProductDependency section */ - 39FDA8A62DAB0F5800EFF94A /* ChatKitty */ = { - isa = XCSwiftPackageProductDependency; - productName = ChatKitty; - }; -/* End XCSwiftPackageProductDependency section */ - }; - rootObject = 607FACC81AFB9204008FA782 /* Project object */; -} diff --git a/examples/ios-example/ChatKittyExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/ios-example/ChatKittyExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index 0c67376e..00000000 --- a/examples/ios-example/ChatKittyExample.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/examples/ios-example/ChatKittyExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/examples/ios-example/ChatKittyExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 8389d20b..00000000 --- a/examples/ios-example/ChatKittyExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,51 +0,0 @@ -{ - "originHash" : "10e099b24537bcbdbba2721f066b019de6770edbb5faf0857e00be350d042430", - "pins" : [ - { - "identity" : "alamofire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/Alamofire.git", - "state" : { - "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", - "version" : "5.10.2" - } - }, - { - "identity" : "moya", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Moya/Moya.git", - "state" : { - "revision" : "c263811c1f3dbf002be9bd83107f7cdc38992b26", - "version" : "15.0.3" - } - }, - { - "identity" : "reactiveswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ReactiveCocoa/ReactiveSwift.git", - "state" : { - "revision" : "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c", - "version" : "6.7.0" - } - }, - { - "identity" : "rxswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/ReactiveX/RxSwift.git", - "state" : { - "revision" : "5dd1907d64f0d36f158f61a466bab75067224893", - "version" : "6.9.0" - } - }, - { - "identity" : "starscream", - "kind" : "remoteSourceControl", - "location" : "https://github.com/daltoniam/Starscream.git", - "state" : { - "revision" : "c6bfd1af48efcc9a9ad203665db12375ba6b145a", - "version" : "4.0.8" - } - } - ], - "version" : 3 -} diff --git a/examples/ios-example/ChatKittyExample.xcodeproj/xcshareddata/xcschemes/ChatUiExample.xcscheme b/examples/ios-example/ChatKittyExample.xcodeproj/xcshareddata/xcschemes/ChatUiExample.xcscheme deleted file mode 100644 index e28e4589..00000000 --- a/examples/ios-example/ChatKittyExample.xcodeproj/xcshareddata/xcschemes/ChatUiExample.xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/ios-example/ChatKittyExample/AppDelegate.swift b/examples/ios-example/ChatKittyExample/AppDelegate.swift deleted file mode 100644 index 16239bf1..00000000 --- a/examples/ios-example/ChatKittyExample/AppDelegate.swift +++ /dev/null @@ -1,44 +0,0 @@ -import UIKit - -@UIApplicationMain -class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - window = UIWindow(frame: UIScreen.main.bounds) - - let initialViewController = ViewController() - window?.rootViewController = initialViewController - - window?.makeKeyAndVisible() - - return true - } - - func applicationWillResignActive(_ application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. - } - - func applicationDidEnterBackground(_ application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - } - - func applicationWillEnterForeground(_ application: UIApplication) { - // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. - } - - func applicationDidBecomeActive(_ application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillTerminate(_ application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - - -} - diff --git a/examples/ios-example/ChatKittyExample/Base.lproj/LaunchScreen.xib b/examples/ios-example/ChatKittyExample/Base.lproj/LaunchScreen.xib deleted file mode 100644 index 6d100778..00000000 --- a/examples/ios-example/ChatKittyExample/Base.lproj/LaunchScreen.xib +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample.xcodeproj/project.pbxproj b/examples/ios-example/ChatKittyExample/ChatKittyExample.xcodeproj/project.pbxproj new file mode 100644 index 00000000..8484edae --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample.xcodeproj/project.pbxproj @@ -0,0 +1,645 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 39A0D1FE2EBC350700C4B96E /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 39A0D1FD2EBC350700C4B96E /* Moya */; }; + 39A0D2002EBC350A00C4B96E /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 39A0D1FF2EBC350A00C4B96E /* Starscream */; }; + 39A0D2022EBC351100C4B96E /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 39A0D2012EBC351100C4B96E /* RxSwift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 39A6FA272EBAC105004FE999 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 39A6FA112EBAC103004FE999 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 39A6FA182EBAC103004FE999; + remoteInfo = ChatKittyExample; + }; + 39A6FA312EBAC105004FE999 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 39A6FA112EBAC103004FE999 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 39A6FA182EBAC103004FE999; + remoteInfo = ChatKittyExample; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 39A6FA192EBAC103004FE999 /* ChatKittyExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ChatKittyExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 39A6FA262EBAC105004FE999 /* ChatKittyExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatKittyExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 39A6FA302EBAC105004FE999 /* ChatKittyExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ChatKittyExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 39A6FA1B2EBAC103004FE999 /* ChatKittyExample */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = ChatKittyExample; + sourceTree = ""; + }; + 39A6FA292EBAC105004FE999 /* ChatKittyExampleTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = ChatKittyExampleTests; + sourceTree = ""; + }; + 39A6FA332EBAC105004FE999 /* ChatKittyExampleUITests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = ChatKittyExampleUITests; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 39A6FA162EBAC103004FE999 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 39A0D2022EBC351100C4B96E /* RxSwift in Frameworks */, + 39A0D1FE2EBC350700C4B96E /* Moya in Frameworks */, + 39A0D2002EBC350A00C4B96E /* Starscream in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A6FA232EBAC105004FE999 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A6FA2D2EBAC105004FE999 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 39A0D1FC2EBC350700C4B96E /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 39A6FA102EBAC103004FE999 = { + isa = PBXGroup; + children = ( + 39A6FA1B2EBAC103004FE999 /* ChatKittyExample */, + 39A6FA292EBAC105004FE999 /* ChatKittyExampleTests */, + 39A6FA332EBAC105004FE999 /* ChatKittyExampleUITests */, + 39A0D1FC2EBC350700C4B96E /* Frameworks */, + 39A6FA1A2EBAC103004FE999 /* Products */, + ); + sourceTree = ""; + }; + 39A6FA1A2EBAC103004FE999 /* Products */ = { + isa = PBXGroup; + children = ( + 39A6FA192EBAC103004FE999 /* ChatKittyExample.app */, + 39A6FA262EBAC105004FE999 /* ChatKittyExampleTests.xctest */, + 39A6FA302EBAC105004FE999 /* ChatKittyExampleUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 39A6FA182EBAC103004FE999 /* ChatKittyExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 39A6FA3A2EBAC105004FE999 /* Build configuration list for PBXNativeTarget "ChatKittyExample" */; + buildPhases = ( + 39A6FA152EBAC103004FE999 /* Sources */, + 39A6FA162EBAC103004FE999 /* Frameworks */, + 39A6FA172EBAC103004FE999 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 39A6FA1B2EBAC103004FE999 /* ChatKittyExample */, + ); + name = ChatKittyExample; + packageProductDependencies = ( + 39A0D1FD2EBC350700C4B96E /* Moya */, + 39A0D1FF2EBC350A00C4B96E /* Starscream */, + 39A0D2012EBC351100C4B96E /* RxSwift */, + ); + productName = ChatKittyExample; + productReference = 39A6FA192EBAC103004FE999 /* ChatKittyExample.app */; + productType = "com.apple.product-type.application"; + }; + 39A6FA252EBAC105004FE999 /* ChatKittyExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 39A6FA3D2EBAC105004FE999 /* Build configuration list for PBXNativeTarget "ChatKittyExampleTests" */; + buildPhases = ( + 39A6FA222EBAC105004FE999 /* Sources */, + 39A6FA232EBAC105004FE999 /* Frameworks */, + 39A6FA242EBAC105004FE999 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 39A6FA282EBAC105004FE999 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 39A6FA292EBAC105004FE999 /* ChatKittyExampleTests */, + ); + name = ChatKittyExampleTests; + packageProductDependencies = ( + ); + productName = ChatKittyExampleTests; + productReference = 39A6FA262EBAC105004FE999 /* ChatKittyExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 39A6FA2F2EBAC105004FE999 /* ChatKittyExampleUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 39A6FA402EBAC105004FE999 /* Build configuration list for PBXNativeTarget "ChatKittyExampleUITests" */; + buildPhases = ( + 39A6FA2C2EBAC105004FE999 /* Sources */, + 39A6FA2D2EBAC105004FE999 /* Frameworks */, + 39A6FA2E2EBAC105004FE999 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 39A6FA322EBAC105004FE999 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 39A6FA332EBAC105004FE999 /* ChatKittyExampleUITests */, + ); + name = ChatKittyExampleUITests; + packageProductDependencies = ( + ); + productName = ChatKittyExampleUITests; + productReference = 39A6FA302EBAC105004FE999 /* ChatKittyExampleUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 39A6FA112EBAC103004FE999 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2600; + LastUpgradeCheck = 2600; + TargetAttributes = { + 39A6FA182EBAC103004FE999 = { + CreatedOnToolsVersion = 26.0.1; + }; + 39A6FA252EBAC105004FE999 = { + CreatedOnToolsVersion = 26.0.1; + TestTargetID = 39A6FA182EBAC103004FE999; + }; + 39A6FA2F2EBAC105004FE999 = { + CreatedOnToolsVersion = 26.0.1; + TestTargetID = 39A6FA182EBAC103004FE999; + }; + }; + }; + buildConfigurationList = 39A6FA142EBAC103004FE999 /* Build configuration list for PBXProject "ChatKittyExample" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 39A6FA102EBAC103004FE999; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 39A6FA752EBC32C0004FE999 /* XCRemoteSwiftPackageReference "Moya" */, + 39A6FA762EBC32D9004FE999 /* XCRemoteSwiftPackageReference "Starscream" */, + 39A6FA772EBC32F6004FE999 /* XCRemoteSwiftPackageReference "RxSwift" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 39A6FA1A2EBAC103004FE999 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 39A6FA182EBAC103004FE999 /* ChatKittyExample */, + 39A6FA252EBAC105004FE999 /* ChatKittyExampleTests */, + 39A6FA2F2EBAC105004FE999 /* ChatKittyExampleUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 39A6FA172EBAC103004FE999 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A6FA242EBAC105004FE999 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A6FA2E2EBAC105004FE999 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 39A6FA152EBAC103004FE999 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A6FA222EBAC105004FE999 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 39A6FA2C2EBAC105004FE999 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 39A6FA282EBAC105004FE999 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 39A6FA182EBAC103004FE999 /* ChatKittyExample */; + targetProxy = 39A6FA272EBAC105004FE999 /* PBXContainerItemProxy */; + }; + 39A6FA322EBAC105004FE999 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 39A6FA182EBAC103004FE999 /* ChatKittyExample */; + targetProxy = 39A6FA312EBAC105004FE999 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 39A6FA382EBAC105004FE999 /* 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 = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + 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; + }; + 39A6FA392EBAC105004FE999 /* 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 = 26.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 39A6FA3B2EBAC105004FE999 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + 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.chatkitty.ChatKittyExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 39A6FA3C2EBAC105004FE999 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + 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.chatkitty.ChatKittyExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 39A6FA3E2EBAC105004FE999 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.chatkitty.ChatKittyExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatKittyExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ChatKittyExample"; + }; + name = Debug; + }; + 39A6FA3F2EBAC105004FE999 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.chatkitty.ChatKittyExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ChatKittyExample.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ChatKittyExample"; + }; + name = Release; + }; + 39A6FA412EBAC105004FE999 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.chatkitty.ChatKittyExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ChatKittyExample; + }; + name = Debug; + }; + 39A6FA422EBAC105004FE999 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.chatkitty.ChatKittyExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = NO; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = ChatKittyExample; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 39A6FA142EBAC103004FE999 /* Build configuration list for PBXProject "ChatKittyExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 39A6FA382EBAC105004FE999 /* Debug */, + 39A6FA392EBAC105004FE999 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 39A6FA3A2EBAC105004FE999 /* Build configuration list for PBXNativeTarget "ChatKittyExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 39A6FA3B2EBAC105004FE999 /* Debug */, + 39A6FA3C2EBAC105004FE999 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 39A6FA3D2EBAC105004FE999 /* Build configuration list for PBXNativeTarget "ChatKittyExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 39A6FA3E2EBAC105004FE999 /* Debug */, + 39A6FA3F2EBAC105004FE999 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 39A6FA402EBAC105004FE999 /* Build configuration list for PBXNativeTarget "ChatKittyExampleUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 39A6FA412EBAC105004FE999 /* Debug */, + 39A6FA422EBAC105004FE999 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 39A6FA752EBC32C0004FE999 /* XCRemoteSwiftPackageReference "Moya" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Moya/Moya"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 15.0.3; + }; + }; + 39A6FA762EBC32D9004FE999 /* XCRemoteSwiftPackageReference "Starscream" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/daltoniam/Starscream"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.8; + }; + }; + 39A6FA772EBC32F6004FE999 /* XCRemoteSwiftPackageReference "RxSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactiveX/RxSwift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.9.1; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 39A0D1FD2EBC350700C4B96E /* Moya */ = { + isa = XCSwiftPackageProductDependency; + package = 39A6FA752EBC32C0004FE999 /* XCRemoteSwiftPackageReference "Moya" */; + productName = Moya; + }; + 39A0D1FF2EBC350A00C4B96E /* Starscream */ = { + isa = XCSwiftPackageProductDependency; + package = 39A6FA762EBC32D9004FE999 /* XCRemoteSwiftPackageReference "Starscream" */; + productName = Starscream; + }; + 39A0D2012EBC351100C4B96E /* RxSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 39A6FA772EBC32F6004FE999 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxSwift; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 39A6FA112EBAC103004FE999 /* Project object */; +} diff --git a/examples/ios-example/ChatKittyExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/ios-example/ChatKittyExample/ChatKittyExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from examples/ios-example/ChatKittyExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to examples/ios-example/ChatKittyExample/ChatKittyExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/ios-example/ChatKittyExample/ChatKittyExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/ios-example/ChatKittyExample/ChatKittyExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..23058801 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/Assets.xcassets/Contents.json b/examples/ios-example/ChatKittyExample/ChatKittyExample/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/ChatKittyExampleApp.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/ChatKittyExampleApp.swift new file mode 100644 index 00000000..3498960f --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/ChatKittyExampleApp.swift @@ -0,0 +1,17 @@ +// +// ChatKittyExampleApp.swift +// ChatKittyExample +// +// Created by Kevin Grafstrom on 2025-11-04. +// + +import SwiftUI + +@main +struct ChatKittyExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/ContentView.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/ContentView.swift new file mode 100644 index 00000000..cd1bd803 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/ContentView.swift @@ -0,0 +1,368 @@ +// +// ContentView.swift +// ChatKittyExample +// +// Created by Kevin Grafstrom on 2025-11-04. +// + +import SwiftUI +import WebKit + +struct ContentView: View { + var body: some View { + VStack { + ChatUi(widgetId: "5S7NNZQ9gwANwfYV", username: "kevingrafstrom", mode: "sandbox", apiKey: "2173cddb-5427-4a09-9742-1a1385cb6c46") + } + } +} + +struct ChatUi: UIViewRepresentable { + let widgetId: String + let username: String + let mode: String + let apiKey: String + + func makeUIView(context: Context) -> WKWebView { + let configuration = WKWebViewConfiguration() + + // IMPORTANT: Add the message handler BEFORE creating the script + configuration.userContentController.add(context.coordinator, name: "logging") + configuration.userContentController.add(context.coordinator, name: "flexMessage") + configuration.userContentController.add(context.coordinator, name: "onChatUiConnected") + + // Inject your custom object + let flexScript = """ + window.$flex = { + web: { + initialize: () => {}, + onMessage: () => {} + }, + callbacks: {}, + _resolveCallback: function(callbackId, result, error) { + if (window.$flex.callbacks[callbackId]) { + const { resolve, reject } = window.$flex.callbacks[callbackId]; + delete window.$flex.callbacks[callbackId]; + + if (error) { + reject(new Error(error)); + } else { + resolve(result); + } + } + }, + postMessage: function(message) { + return new Promise((resolve, reject) => { + if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.flexMessage) { + // Generate a unique callback ID + const callbackId = 'callback_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); + + // Store the resolve/reject functions + window.$flex.callbacks[callbackId] = { resolve, reject }; + + // Add callback ID to the message + const messageWithCallback = { + ...message, + _callbackId: callbackId + }; + + // Send the message + window.webkit.messageHandlers.flexMessage.postMessage(messageWithCallback); + + // Optional: Add a timeout + setTimeout(() => { + if (window.$flex.callbacks[callbackId]) { + delete window.$flex.callbacks[callbackId]; + reject(new Error('Message timeout')); + } + }, 30000); // 30 second timeout + } else { + reject(new Error('Message handler not available')); + } + }); + }, + onChatUiConnected: function() { + if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.onChatUiConnected) { + window.webkit.messageHandlers.onChatUiConnected.postMessage({}); + } + }, + onChatMounted: function() { + + } + }; + """ + + let flexUserScript = WKUserScript( + source: flexScript, + injectionTime: .atDocumentStart, + forMainFrameOnly: false + ) + configuration.userContentController.addUserScript(flexUserScript) + + // Inject JavaScript to capture console logs + let script = """ + (function() { + // Test if the message handler is available + if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.logging) { + window.webkit.messageHandlers.logging.postMessage({type: 'info', message: 'Console logger initialized successfully'}); + } + + // Capture unhandled errors + window.addEventListener('error', function(event) { + var errorMessage = 'Uncaught Error: ' + event.message + + ' at ' + event.filename + ':' + event.lineno + ':' + event.colno; + if (event.error && event.error.stack) { + errorMessage += '\\n' + event.error.stack; + } + if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.logging) { + window.webkit.messageHandlers.logging.postMessage({type: 'error', message: errorMessage}); + } + }); + + // Capture unhandled promise rejections + window.addEventListener('unhandledrejection', function(event) { + var errorMessage = 'Unhandled Promise Rejection: ' + event.reason; + if (event.reason && event.reason.stack) { + errorMessage += '\\n' + event.reason.stack; + } + if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.logging) { + window.webkit.messageHandlers.logging.postMessage({type: 'error', message: errorMessage}); + } + }); + + var originalLog = console.log; + var originalError = console.error; + var originalWarn = console.warn; + var originalInfo = console.info; + + const getMessage = (args) => { + const message = Array.from(args).map(function(arg) { + if (typeof arg === 'object') { + try { + return JSON.stringify(arg); + } catch(e) { + return String(arg); + } + } + return String(arg); + }).join(' '); + return message + } + + const postMessage = ({type, message}) => { + if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.logging) { + window.webkit.messageHandlers.logging.postMessage({type: type, message: message}); + } + } + + console.log = function() { + var message = getMessage(arguments) + postMessage({type: 'log', message: message}) + originalLog.apply(console, arguments); + }; + + console.error = function() { + var message = getMessage(arguments) + postMessage({type: 'error', message: message}) + originalError.apply(console, arguments); + }; + + console.warn = function() { + var message = getMessage(arguments) + postMessage({type: 'warn', message: message}) + originalWarn.apply(console, arguments); + }; + + console.info = function() { + var message = getMessage(arguments) + postMessage({type: 'info', message: message}) + originalInfo.apply(console, arguments); + }; + })(); + """ + + let userScript = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: false) + configuration.userContentController.addUserScript(userScript) + + // Optional: Enable inline media playback + configuration.allowsInlineMediaPlayback = true + + // Optional: Enable picture-in-picture + configuration.allowsPictureInPictureMediaPlayback = true + + let webView = WKWebView(frame: .zero, configuration: configuration) + + // Optional: Set navigation delegate for handling navigation events + webView.navigationDelegate = context.coordinator + + // Store the webView reference in the coordinator + context.coordinator.webView = webView + let stompXBridge = FlexStompXBridge(webView: webView) + context.coordinator.stompXBridge = stompXBridge + // Create and configure StompX instance + let stompXConfig = StompXConfiguration( + isSecure: false, + host: "10.0.0.54:9001", + isDebug: true + ) + + // CRITICAL: Store strong reference to prevent deallocation + let stompx = StompXImpl(configuration: stompXConfig) + context.coordinator.stompX = stompx + context.coordinator.chatUiStompXInteractor = ChatUIStompXInteractor(stompX: stompx, stompXBridge: stompXBridge) + + // Create the connect request + let connectRequest = StompXConnectRequest( + apiKey: apiKey, + username: username, + authParams: nil, + onConnected: { + print("βœ… StompX connected successfully") + }, + onConnectionLost: { + print("⚠️ StompX connection lost") + }, + onConnectionResumed: { + print("βœ… StompX connection resumed") + }, + onError: { error in + print("❌ StompX error: \(error.localizedDescription)") + } + ) + + // Connect to StompX + stompx.connect(request: connectRequest) + + // Enable debugging in Safari (iOS 16.4+) + if #available(iOS 16.4, *) { + webView.isInspectable = true + } + + return webView + } + + func updateUIView(_ webView: WKWebView, context: Context) { + guard let url = URL(string: "http://10.0.0.54:9002/chat?widget_id=\(widgetId)&username=\(username)&environment=development&mode=\(mode)") else { + return + } + let request = URLRequest(url: url) + webView.load(request) + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler { + let parent: ChatUi + var webView: WKWebView? + var stompXBridge: StompXBridge? + var chatUiStompXInteractor: ChatUIStompXInteractor? + + // Add strong reference to keep StompX alive + var stompX: StompX? + + init(_ parent: ChatUi) { + self.parent = parent + } + + func resolveFlexPromise(callbackId: String?) { + // Send success response back to JavaScript + if let callbackId = callbackId { + let response = """ + window.$flex._resolveCallback('\(callbackId)', {success: true, connected: true}, null); + """ + self.webView?.evaluateJavaScript(response, completionHandler: nil) + } + } + + // Handle console messages + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + + if message.name == "logging" { + if let body = message.body as? [String: Any] { + let type = body["type"] as? String ?? "log" + let logMessage = body["message"] as? String ?? "" + + let emoji: String + switch type { + case "error": emoji = "❌" + case "warn": emoji = "⚠️" + case "info": emoji = "ℹ️" + default: emoji = "πŸ“" + } + + print("\(emoji) [WebView \(type.uppercased())]: \(logMessage)") + } else { + print("πŸ“± Received message with unexpected body format: \(message.body)") + } + } else if message.name == "flexMessage" { + if let body = message.body as? [String: Any] { + let type = body["type"] as? String ?? "unknown" + let callbackId = body["_callbackId"] as? String + + do { + // Convert dictionary to JSON Data + let jsonData = try JSONSerialization.data(withJSONObject: body) + + // Decode to ChatUIMessage + let decoder = JSONDecoder() + let chatUIMessage = try decoder.decode(ChatUIMessage.self, from: jsonData) + + // Now you can use chatUIMessage + if let chatUiStompXInteractor = self.chatUiStompXInteractor { + DispatchQueue.main.async { + chatUiStompXInteractor.onReceiveMessage(event: chatUIMessage) + } + } + + } catch { + print("❌ Failed to decode ChatUIMessage: \(error)") + } + + self.resolveFlexPromise(callbackId: callbackId) + } + } else if message.name == "onChatUiConnected" { + if let body = message.body as? [String: Any] { + + // Call initialize with parameters + let initData = """ + { + "theme": "light", + "username": "\(parent.username)", + "clientSpecification": { + "connection": "shared", + "environment": "development", + "version": "1.0.0" + } + } + """ + + webView?.evaluateJavaScript("window.$flex.web.initialize(\(initData)); void 0;") { result, error in + if let error = error { + print("❌ Error calling initialize: \(error.localizedDescription)") + } else { + print("βœ… Successfully called initialize with data") + } + } + } + } + } + + // Optional: Handle navigation events + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + print("βœ… Page loaded successfully: \(webView.url?.absoluteString ?? "unknown")") + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + print("❌ Failed to load page: \(error.localizedDescription)") + } + + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { + print("πŸ”„ Started loading: \(webView.url?.absoluteString ?? "unknown")") + } + } +} + +#Preview { + ContentView() +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/api/AnyCodable.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/AnyCodable.swift new file mode 100644 index 00000000..39e15601 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/AnyCodable.swift @@ -0,0 +1,64 @@ +import Foundation +public struct AnyCodable { + var value: Codable? + + init(_ value: Codable?) { + self.value = value + } + + func printPrettyJson() throws { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] // Use .sortedKeys for consistent key ordering + + let data = try encoder.encode(self) + if let jsonString = String(data: data, encoding: .utf8) { + print(jsonString) + } + } +} + +extension AnyCodable: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + // Attempt to decode the value into different types + if let value = try? container.decode(Bool.self) { + self.value = value + } else if let value = try? container.decode(Int.self) { + self.value = value + } else if let value = try? container.decode(Double.self) { + self.value = value + } else if let value = try? container.decode(String.self) { + self.value = value + } else if let value = try? container.decode([AnyCodable].self) { + self.value = value + } else if let value = try? container.decode([String: AnyCodable].self) { + self.value = value + } else { + self.value = nil + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + // Attempt to encode the value into different types + switch value { + case let bool as Bool: + try container.encode(bool) + case let int as Int: + try container.encode(int) + case let double as Double: + try container.encode(double) + case let string as String: + try container.encode(string) + case let array as [AnyCodable]: + try container.encode(array) + case let dictionary as [String: AnyCodable]: + try container.encode(dictionary) + default: + try container.encodeNil() + } + } +} + diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/api/ChatUIMessage.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/ChatUIMessage.swift new file mode 100644 index 00000000..938bccb5 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/ChatUIMessage.swift @@ -0,0 +1,22 @@ +import Foundation + +final class ChatUIMessage: Codable { + let type: String + let id: String? + let payload: AnyCodable? +} + +extension Encodable { + func toJSONData() -> Data? { + try? JSONEncoder().encode(self) + } + + func toJSONString(prettyPrinted: Bool = false) -> String { + let encoder = JSONEncoder() + if prettyPrinted { + encoder.outputFormatting = .prettyPrinted + } + guard let jsonData = try? encoder.encode(self) else { return "" } + return String(data: jsonData, encoding: .utf8) ?? "" + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/api/ChatUiStompXInteractor.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/ChatUiStompXInteractor.swift new file mode 100644 index 00000000..2d61a8f9 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/ChatUiStompXInteractor.swift @@ -0,0 +1,115 @@ + +final class ChatUIStompXInteractor { + private let stompX: StompX + private let stompXBridge: StompXBridge + private var subscriptionMap: [String : () -> Void] = [:] + + init(stompX: StompX, stompXBridge: StompXBridge) { + self.stompX = stompX + self.stompXBridge = stompXBridge + } + + func onReceiveMessage(event: ChatUIMessage) { + switch event.type { + case "stompx:connect": + var writeGrant: String? = nil + var readGrant: String? = nil + + self.stompX.relayResource(request: StompXRelayResourceRequest( + destination: "/application/v1/user.relay", + onSuccess: { user in + self.stompX.relayResource(request: StompXRelayResourceRequest( + destination: "/application/v1/user.write_file_access_grant.relay", + onSuccess: { write in + writeGrant = write?.grant + self.stompX.relayResource(request: StompXRelayResourceRequest( + destination: "/application/v1/user.read_file_access_grant.relay", + onSuccess: { read in + readGrant = read?.grant + if let user { + self.stompXBridge.onMessage(id: nil, + type: .connectSuccess, + payload: ConnectPayload(user: user, + write: writeGrant, + read: readGrant)) + } + }, onError: { error in + self.stompXBridge.onMessage(id: nil, + type: .connectFailure, + payload: AnyCodable(nil)) + })) + }, onError: { error in + self.stompXBridge.onMessage(id: nil, + type: .connectFailure, + payload: AnyCodable(nil)) + })) + }, onError: { error in + self.stompXBridge.onMessage(id: nil, + type: .connectFailure, + payload: AnyCodable(nil)) + })) + case "stompx:resource.relay": + if let relayPayload = event.payload?.synthesize(to: StompXRelayPayload.self) { + self.stompX.relayResource(request: StompXRelayResourceRequest( + destination: relayPayload.destination, + parameters: relayPayload.parameters?.mapValues { $0 ? "true" : "false" } ?? [:], + onSuccess: { model in + guard let model else { + return + } + self.stompXBridge.onMessage(id: event.id, + type: .relaySuccess, + payload: StompXResource(resource: model)) + }, onError: { error in + self.stompXBridge.onMessage(id: event.id, + type: .relayError, + payload: StompXResource(resource: AnyCodable(nil))) + })) + } + case "stompx:topic.subscribe": + if let subscribePayload = event.payload?.synthesize(to: StompXSubscribePayload.self) { + let subscription = self.stompX.listenToTopic(request: StompXListenToTopicRequest(topic: subscribePayload.topic, onSuccess: { + self.stompXBridge.onMessage(id: event.id, + type: FlexStompXEventType.topicSubscribed) + })) + subscriptionMap[event.id ?? ""] = subscription + } + case "stompx:event.listen": + if let listenForEventPayload = event.payload?.synthesize(to: StompXListenForEventPayload.self) { + let subscription = self.stompX.listenForEvent(request: StompXListenForEventRequest(topic: listenForEventPayload.topic, + event: listenForEventPayload.event, + onNewData: { model in + self.stompXBridge.onMessage(id: event.id, + type: .eventPublished, + payload: StompXResource(resource: model)) + })) + + subscriptionMap[event.id ?? ""] = subscription + } + case "stompx:action.perform": + if let performPayload = event.payload?.synthesize(to: StompXPerformActionPayload.self) { + self.stompX.sendAction(request: StompXSendActionRequest(destination: performPayload.destination, + data: performPayload.body ?? AnyCodable(nil), onSent: { + self.stompXBridge.onMessage(id: event.id, + type: .actionSent, + payload: StompXResource(resource: AnyCodable(nil))) + }, onSuccess: { resource in + self.stompXBridge.onMessage(id: event.id, + type: .actionSuccess, + payload: StompXResource(resource: resource)) + })) + } + case "stompx:topic.unsubscribe", + "stompx:event.unsubscribe": + if let id = event.id { + subscriptionMap[id]?() + } + case "stompx:disconnect": + subscriptionMap.forEach { $0.value() } + subscriptionMap.removeAll() + default: + // TODO: Log to Sentry of an unhandled event + break + } + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/api/FlexStompXBridge.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/FlexStompXBridge.swift new file mode 100644 index 00000000..23a40cb8 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/FlexStompXBridge.swift @@ -0,0 +1,49 @@ +import WebKit +final class FlexStompXBridge: StompXBridge { + var webView: WKWebView + + init(webView: WKWebView) { + self.webView = webView + } + + func onMessage(id: String?, + type: FlexStompXEventType, + payload: T) { + + + + let message = FlexStompXMessage(id: id, + type: type.rawValue, + payload: payload) + let encoded = message.toJSONString().data(using: .utf8)?.base64EncodedString() + if encoded != nil { + if let encoded = encoded { + webView.evaluateJavaScript("window.$flex.web.onMessage('\(encoded)'); void 0;") { + result, error in + if let error = error { + print("❌ Error calling onMessage: \(error.localizedDescription)") + } + } + } + } + + + } + + func onMessage(id: String?, + type: FlexStompXEventType) { + let message = FlexStompXEmptyMessage(id: id, type: type.rawValue) + let encoded = message.toJSONString().data(using: .utf8)?.base64EncodedString() + if encoded != nil { + if let encoded = encoded { + webView.evaluateJavaScript("window.$flex.web.onMessage('\(encoded)'); void 0;") { + result, error in + if let error = error { + print("❌ Error calling onMessage: \(error.localizedDescription)") + } + } + } + } + } + +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/api/StompXBridge.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/StompXBridge.swift new file mode 100644 index 00000000..37e034a0 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/StompXBridge.swift @@ -0,0 +1,10 @@ +import Foundation + +protocol StompXBridge { + func onMessage(id: String?, + type: FlexStompXEventType, + payload: T) + + func onMessage(id: String?, + type: FlexStompXEventType) +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/api/models.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/models.swift new file mode 100644 index 00000000..339060e9 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/api/models.swift @@ -0,0 +1,99 @@ +import Foundation +final class FlexStompXMessage: Codable { + let id: String? + let type: String + let payload: T + let payloadBase64: String? + + init(id: String?, type: String, payload: T) { + self.id = id + self.type = type + self.payloadBase64 = Self.encodeToBase64(payload) + self.payload = payload + } + + private static func encodeToBase64(_ object: T) -> String? { + do { + let jsonData = try JSONEncoder().encode(object) + let base64String = jsonData.base64EncodedString() + return base64String + } catch { + return nil + } + } +} + +final class FlexStompXEmptyMessage: Codable { + let id: String? + let type: String + + init(id: String?, type: String) { + self.id = id + self.type = type + } +} + +enum FlexStompXEventType: String { + case connectSuccess = "stompx:connect.success" + case connectFailure = "stompx:connect.error" + case relaySuccess = "stompx:relay.success" + case relayError = "stompx:relay.error" + case eventPublished = "stompx:event.published" + case topicSubscribed = "stompx:topic.subscribed" + case actionSent = "stompx:action.sent" + case actionSuccess = "stompx:action.success" + case actionError = "stompx:action.error" + case streamSuccess = "stompx:stream.success" + case streamError = "stompx:stream.error" + case streamProgressStarted = "stompx:stream.progress.started" + case streamProgressPublished = "stompx:stream.progress.published" + case streamProgressCompleted = "stompx:stream.progress.completed" + case streamProgressFailed = "stompx:stream.progress.failed" + case streamProgressCancelled = "stompx:stream.progress.cancelled" +} + +final class EmptyResource: Codable { } + +final class StompXResource: Codable { + let resource: AnyCodable + + init(resource: AnyCodable) { + self.resource = resource + } +} + +final class StompXRelayPayload: Codable { + let parameters: [String : Bool]? + let destination: String +} + +final class StompXSubscribePayload: Codable { + let topic: String +} + +final class StompXPerformActionPayload: Codable { + let destination: String + let body: AnyCodable? +} + +final class StompXListenForEventPayload: Codable { + let topic: String + let event: String +} + +final class ConnectPayload: Codable { + let user: AnyCodable + let write: String? + let read: String? + + init(user: AnyCodable, write: String?, read: String?) { + self.user = user + self.write = write + self.read = read + } +} + +final class ChatKittyGrant: Codable { + let grant: String +} + diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/ChatKittyStream.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/ChatKittyStream.swift new file mode 100644 index 00000000..0b51020a --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/ChatKittyStream.swift @@ -0,0 +1,66 @@ +import Foundation +import UIKit +import Moya +internal import Alamofire + +enum StompXStream { + case uploadImages(url: URL, grant: String, images: [UIImage]) + case uploadFiles(url: URL, grant: String, data: [CreateDataFile]) +} + +extension StompXStream: TargetType { + var baseURL: URL { + switch self { + case let .uploadImages(url, _, _): + return url + case let .uploadFiles(url, _, _): + return url + } + } + + var path: String { + "" + } + + var method: Moya.Method { + switch self { + case .uploadImages, .uploadFiles: + return .post + } + } + + var task: Task { + switch self { + case let .uploadImages(_, _, images): + var array:[Moya.MultipartFormData] = [] + for image in images { + if let pngData = image.jpegData(compressionQuality: 1.0) { + let pngData = MultipartFormData(provider: .data(pngData), + name: "file", + fileName: "file.png", + mimeType: "image/png") + array.append(pngData) + } + } + return .uploadMultipart(array) + case let .uploadFiles(_, _, data): + var array:[Moya.MultipartFormData] = [] + for item in data { + let dataToUpload = MultipartFormData(provider: .data(item.data), + name: "file", + fileName: item.name ?? "file", + mimeType: item.contentType) + array.append(dataToUpload) + } + return .uploadMultipart(array) + } + } + + var headers: [String : String]? { + switch self { + case let .uploadImages(_, grant, _), let .uploadFiles(_, grant, _): + return ["Content-Type": "multipart/form-data", + "Grant": grant] + } + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/Data+JSON.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/Data+JSON.swift new file mode 100644 index 00000000..7fad0053 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/Data+JSON.swift @@ -0,0 +1,371 @@ +import Foundation + +extension Data { + func decode(to type: T.Type, logError: Bool = true) -> T? { + do { + let result = try JSONDecoder().decode(type, from: self) + return result + } catch { + StompXLogger.logError(String(describing: error)) + return nil + } + } + + var toDictionary: [String: Any]? { + do { + return try JSONSerialization.jsonObject(with: self, options: []) as? [String: Any] + } catch { + StompXLogger.logError(String(describing: error)) + } + return nil + } +} + +public extension Dictionary where Key: Hashable, Value: Any { + var jsonData: Data { + do { + return try JSONSerialization.data(withJSONObject: self, options: .prettyPrinted) + } catch { + return Data() + } + } + + func to(_ type: T.Type) -> T? { + jsonData.decode(to: type) + } +} + +extension Encodable { + var dictionary: [String: Any]? { + guard let data = try? JSONEncoder().encode(self) else { return nil } + return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] } + } +} + +class ObjectMapper { + static func writeValueAsString(dictionary: Dictionary) -> String? { + do { + if #available(iOS 13.0, *) { + let jsonData = try JSONSerialization.data(withJSONObject: dictionary, options: [.withoutEscapingSlashes]) + return String(data: jsonData, encoding: .utf8) + } else { + let jsonData = try JSONSerialization.data(withJSONObject: dictionary, options: []) + let jsonString = String(data: jsonData, encoding: .utf8)?.replacingOccurrences(of: "\\/", with: "/") + return jsonString + } + } catch { + return nil + } + } +} + +struct AnyCodingKey: CodingKey { + var stringValue: String + var intValue: Int? + + init?(stringValue: String) { + self.stringValue = stringValue + } + + init?(intValue: Int) { + self.intValue = intValue + self.stringValue = String(intValue) + } +} + +extension KeyedDecodingContainer { + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value + /// is not convertible to the requested type. + /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry + /// for the given key. + /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for + /// the given key. + public func decode(_ type: [Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [Any] { + var values = try nestedUnkeyedContainer(forKey: key) + return try values.decode(type) + } + + /// Decodes a value of the given type for the given key. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A value of the requested type, if present for the given key + /// and convertible to the requested type. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value + /// is not convertible to the requested type. + /// - throws: `DecodingError.keyNotFound` if `self` does not have an entry + /// for the given key. + /// - throws: `DecodingError.valueNotFound` if `self` has a null entry for + /// the given key. + public func decode(_ type: [String: Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [String: Any] { + let values = try nestedContainer(keyedBy: AnyCodingKey.self, forKey: key) + return try values.decode(type) + } + + /// Decodes a value of the given type for the given key, if present. + /// + /// This method returns `nil` if the container does not have a value + /// associated with `key`, or if the value is null. The difference between + /// these states can be distinguished with a `contains(_:)` call. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A decoded value of the requested type, or `nil` if the + /// `Decoder` does not have an entry associated with the given key, or if + /// the value is a null value. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value + /// is not convertible to the requested type. + public func decodeIfPresent(_ type: [Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [Any]? { + guard contains(key), + try decodeNil(forKey: key) == false else { return nil } + return try decode(type, forKey: key) + } + + /// Decodes a value of the given type for the given key, if present. + /// + /// This method returns `nil` if the container does not have a value + /// associated with `key`, or if the value is null. The difference between + /// these states can be distinguished with a `contains(_:)` call. + /// + /// - parameter type: The type of value to decode. + /// - parameter key: The key that the decoded value is associated with. + /// - returns: A decoded value of the requested type, or `nil` if the + /// `Decoder` does not have an entry associated with the given key, or if + /// the value is a null value. + /// - throws: `DecodingError.typeMismatch` if the encountered encoded value + /// is not convertible to the requested type. + public func decodeIfPresent(_ type: [String: Any].Type, forKey key: KeyedDecodingContainer.Key) throws -> [String: Any]? { + guard contains(key), + try decodeNil(forKey: key) == false else { return nil } + return try decode(type, forKey: key) + } +} + +private extension KeyedDecodingContainer { + func decode(_ type: [String: Any].Type) throws -> [String: Any] { + var dictionary: [String: Any] = [:] + for key in allKeys { + if try decodeNil(forKey: key) { + dictionary[key.stringValue] = NSNull() + } else if let bool = try? decode(Bool.self, forKey: key) { + dictionary[key.stringValue] = bool + } else if let string = try? decode(String.self, forKey: key) { + dictionary[key.stringValue] = string + } else if let int = try? decode(Int.self, forKey: key) { + dictionary[key.stringValue] = int + } else if let double = try? decode(Double.self, forKey: key) { + dictionary[key.stringValue] = double + } else if let dict = try? decode([String: Any].self, forKey: key) { + dictionary[key.stringValue] = dict + } else if let array = try? decode([Any].self, forKey: key) { + dictionary[key.stringValue] = array + } + } + return dictionary + } +} + +private extension UnkeyedDecodingContainer { + mutating func decode(_ type: [Any].Type) throws -> [Any] { + var elements: [Any] = [] + while !isAtEnd { + if try decodeNil() { + elements.append(NSNull()) + } else if let int = try? decode(Int.self) { + elements.append(int) + } else if let bool = try? decode(Bool.self) { + elements.append(bool) + } else if let double = try? decode(Double.self) { + elements.append(double) + } else if let string = try? decode(String.self) { + elements.append(string) + } else if let values = try? nestedContainer(keyedBy: AnyCodingKey.self), + let element = try? values.decode([String: Any].self) { + elements.append(element) + } else if var values = try? nestedUnkeyedContainer(), + let element = try? values.decode([Any].self) { + elements.append(element) + } + } + return elements + } +} + +extension KeyedEncodingContainer { + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + public mutating func encode(_ value: [String: Any], forKey key: KeyedEncodingContainer.Key) throws { + var container = nestedContainer(keyedBy: AnyCodingKey.self, forKey: key) + try container.encode(value) + } + + /// Encodes the given value for the given key. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + public mutating func encode(_ value: [Any], forKey key: KeyedEncodingContainer.Key) throws { + var container = nestedUnkeyedContainer(forKey: key) + try container.encode(value) + } + + /// Encodes the given value for the given key if it is not `nil`. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + public mutating func encodeIfPresent(_ value: [String: Any]?, forKey key: KeyedEncodingContainer.Key) throws { + if let value = value { + var container = nestedContainer(keyedBy: AnyCodingKey.self, forKey: key) + try container.encode(value) + } else { + try encodeNil(forKey: key) + } + } + + /// Encodes the given value for the given key if it is not `nil`. + /// + /// - parameter value: The value to encode. + /// - parameter key: The key to associate the value with. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + public mutating func encodeIfPresent(_ value: [Any]?, forKey key: KeyedEncodingContainer.Key) throws { + if let value = value { + var container = nestedUnkeyedContainer(forKey: key) + try container.encode(value) + } else { + try encodeNil(forKey: key) + } + } +} + +private extension KeyedEncodingContainer where K == AnyCodingKey { + mutating func encode(_ value: [String: Any]) throws { + for (k, v) in value { + let key = AnyCodingKey(stringValue: k)! + switch v { + case is NSNull: + try encodeNil(forKey: key) + case let string as String: + try encode(string, forKey: key) + case let int as Int: + try encode(int, forKey: key) + case let bool as Bool: + try encode(bool, forKey: key) + case let double as Double: + try encode(double, forKey: key) + case let dict as [String: Any]: + try encode(dict, forKey: key) + case let array as [Any]: + try encode(array, forKey: key) + default: + debugPrint("⚠️ Unsuported type!", v) + continue + } + } + } +} + +private extension UnkeyedEncodingContainer { + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + mutating func encode(_ value: [Any]) throws { + for v in value { + switch v { + case is NSNull: + try encodeNil() + case let string as String: + try encode(string) + case let int as Int: + try encode(int) + case let bool as Bool: + try encode(bool) + case let double as Double: + try encode(double) + case let dict as [String: Any]: + try encode(dict) + case let array as [Any]: + var values = nestedUnkeyedContainer() + try values.encode(array) + default: + debugPrint("⚠️ Unsuported type!", v) + } + } + } + + /// Encodes the given value. + /// + /// - parameter value: The value to encode. + /// - throws: `EncodingError.invalidValue` if the given value is invalid in + /// the current context for this format. + mutating func encode(_ value: [String: Any]) throws { + var container = self.nestedContainer(keyedBy: AnyCodingKey.self) + try container.encode(value) + } +} + +extension String { + + /// Assuming the current string is base64 encoded, this property returns a String + /// initialized by converting the current string into Unicode characters, encoded to + /// utf8. If the current string is not base64 encoded, nil is returned instead. + var base64Decoded: String? { + guard let base64 = Data(base64Encoded: self) else { return nil } + let utf8 = String(data: base64, encoding: .utf8) + return utf8 + } + + /// Returns a base64 representation of the current string, or nil if the + /// operation fails. + var base64Encoded: String? { + let utf8 = self.data(using: .utf8) + let base64 = utf8?.base64EncodedString() + return base64 + } + +} + + extension Date { + static func from(timeString: String) -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.timeZone = TimeZone(abbreviation: "UTC") + dateFormatter.dateFormat = "HH:mm'Z'" + return dateFormatter.date(from: timeString) + } + + static func from(isoString dateString: String) -> Date? { + let trimmedIsoString = dateString + .replacingOccurrences(of: "\\.\\d+", with: "", options: .regularExpression) + let formatter = ISO8601DateFormatter() + return formatter.date(from: trimmedIsoString) + } + } + + +extension Encodable { + func synthesize(to type: T.Type) -> T? { + do { + let jsonData = try JSONEncoder().encode(self) + let object = try JSONDecoder().decode(type, from: jsonData) + return object + } catch { + StompXLogger.logError(String(describing: error)) + return nil + } + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/File.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/File.swift new file mode 100644 index 00000000..3259852f --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/File.swift @@ -0,0 +1,64 @@ +import Foundation + +public final class ChatKittyFile: Codable { + public let type: String + public let url: String + public let name: String + public let contentType: String + public let size: Double + + public init(type: String, + url: String, + name: String, + contentType: String, + size: Double) { + self.type = type + self.url = url + self.name = name + self.contentType = contentType + self.size = size + } +} + +public enum File { + case data(CreateDataFile) + case external(CreateChatKittyExternalFileProperties) +} + +public final class CreateDataFile { + public let data: Data + public let contentType: String + public let name: String? + + public init(data: Data, + contentType: String, + name: String? = nil) { + self.data = data + self.contentType = contentType + self.name = name + } +} + +public final class CreateChatKittyExternalFileProperties: Codable { + public let url: String + public let name: String + public let contentType: String + public let size: Double + + public init(url: String, + name: String, + contentType: String, + size: Double + ) { + self.url = url + self.name = name + self.contentType = contentType + self.size = size + } +} + +public enum ChatKittyUploadResult { + case completed + case failed + case cancelled +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompX.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompX.swift new file mode 100644 index 00000000..a4f1534c --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompX.swift @@ -0,0 +1,23 @@ +import Foundation + +public protocol StompX { + var isConnected: Bool { get } + + func connect(request: StompXConnectRequest) + func disconnect(completion: @escaping () -> Void) + func relayResource(request: StompXRelayResourceRequest) + func relayResourceDictionary(request: StompXRelayResourceRequestDictionary) + func sendAction(request: StompXSendActionRequest) + func sendActionDictionary(request: StompXSendActionRequestDictionary) + func sendToStream(request: SendToStreamRequest) + func sendToStream(request: SendDataToStreamRequest) + func listenToTopic(request: StompXListenToTopicRequest) -> () -> Void + func listenForEvent(request: StompXListenForEventRequest) -> () -> Void + + func sendJSONMessage(destination: String, data: Data, headers: [String : String]) + func subscribe(destination: String, headers: [String : String]) -> String + func unsubscribe(subscriptionId: String) + + func sendFrame(_ frame: StompClientFrame) + func resignToClient(client: StompX) +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXConfiguration.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXConfiguration.swift new file mode 100644 index 00000000..dd9da54b --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXConfiguration.swift @@ -0,0 +1,7 @@ +import Foundation + +public struct StompXConfiguration { + public let isSecure: Bool + public let host: String + public let isDebug: Bool +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXImpl.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXImpl.swift new file mode 100644 index 00000000..32f2a77a --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXImpl.swift @@ -0,0 +1,852 @@ +import Foundation +import Starscream +import RxSwift +import Moya + +public typealias StompXImpl = WebsocketStompClient + +public final class WebsocketStompClient : StompX, WebSocketDelegate { + + private let service = MoyaProvider() + + public var isConnected: Bool = false + + private let specification: StompSpecification = StompSpecification() + + private var disconnectReceipt: String = "" + + private var frameQueue: FrameQueue = FrameQueue() + + private let configuration: StompXConfiguration + + private var _socket: WebSocket? + + private var socket: WebSocket { + guard let socket = _socket else { + fatalError("Socket not created!!!") + } + return socket + } + + private var url: URL! + private var stompXRequest: StompXConnectRequest! + + // MARK: Subscriptions + private let disposeBag = DisposeBag() + private let onConnectedSubject = PublishSubject() + private let onDisconnectedSubject = PublishSubject() + private let watchForReceiptSubject = PublishSubject() + private let watchForErrorsSubject = PublishSubject() + + private let pendingRelayRequests = ThreadSafeDictionary() + private let pendingActionRequests = ThreadSafeDictionary() + private let eventHandlers = ThreadSafeDictionary() + + private var subscriptionToDisposeBag = Dictionary() + + private var version: String { + if let bundle = Bundle.allFrameworks.filter({ $0.bundleIdentifier == "org.cocoapods.ChatKitty" }).first, + let version = bundle.infoDictionary?["CFBundleShortVersionString"] as? String { + return version + } + return "0.0.0" + } + + public init(configuration: StompXConfiguration) { + self.configuration = configuration + } + + public func connect(request: StompXConnectRequest) { + print("πŸš€ ===== CONNECT CALLED =====") + print("πŸš€ Configuration - isSecure: \(configuration.isSecure), host: \(configuration.host)") + + let wsScheme = configuration.isSecure ? "wss" : "ws" + let urlString = "\(wsScheme)://\(configuration.host)/rtm/websocket?api-key=\(request.apiKey)" + print("πŸš€ Connecting to URL: \(urlString)") + + guard let url = URL(string: urlString) else { + print("❌ FATAL: Invalid URL - \(urlString)") + fatalError("Invalid url") + } + + self.url = url + self.stompXRequest = request + + var urlRequest = URLRequest(url: url) + urlRequest.timeoutInterval = 10 // Add timeout + + if let authParams = request.authParams { + let authParamsString = ObjectMapper.writeValueAsString(dictionary: authParams)?.base64Encoded + urlRequest.setValue(authParamsString, forHTTPHeaderField: "StompX-Auth-Params") + print("πŸ” Auth params added to header") + } + + print("πŸ”Œ Creating WebSocket with request: \(urlRequest)") + _socket = WebSocket(request: urlRequest) + + guard let socket = _socket else { + print("❌ FATAL: Failed to create WebSocket instance") + return + } + + print("πŸ”Œ Setting delegate to self") + socket.delegate = self + print("πŸ”Œ Delegate set: \(socket.delegate != nil)") + + print("πŸ”Œ Calling socket.connect()...") + socket.connect() + print("πŸ”Œ socket.connect() returned. isConnected = \(isConnected)") + + onConnectedSubject.take(1).subscribe(onNext: { _ in + print("βœ… onConnectedSubject fired - calling request.onConnected()") + request.onConnected() + }).disposed(by: disposeBag) + + print("πŸš€ ===== CONNECT METHOD COMPLETED =====") + } + + public func send(message: SocketMessage) { + sendJSONMessage(destination: message.destination, data: message.data) + } + + public func sendJSONMessage(destination: String, data: Data, headers: [String : String] = [:]) { + do { + try sendFrame(specification.sendJSONMessage(destination: destination, data: data, headers: headers)) + } catch { + // TODO - parse error for receipt + } + } + + // MARK: STOMPX Implementation + + public func relayResourceDictionary(request: StompXRelayResourceRequestDictionary) { + let id = specification.generateSubscriptionId() + + pendingRelayRequests[id] = request + + sendFrame(specification.subscribe(id: id, + destination: request.destination, + headers: request.parameters ?? [:])) + } + + public func relayResource(request: StompXRelayResourceRequest) { + let id = specification.generateSubscriptionId() + + pendingRelayRequests[id] = request + + sendFrame(specification.subscribe(id: id, + destination: request.destination, + headers: request.parameters ?? [:])) + } + + public func sendActionDictionary(request: StompXSendActionRequestDictionary) where T : Decodable, T : Encodable { + let receipt = specification.generateReceipt() + + if let onSent = request.onSent { + watchForReceiptSubject + .filter{$0 == receipt} + .take(1) + .subscribe(onNext: { _ in + onSent() + }).disposed(by: disposeBag) + } + + pendingActionRequests[receipt] = request + + sendJSONMessage(destination: request.destination, + data: request.jsonData, + headers: ["receipt" : receipt]) + } + + public func sendAction(request: StompXSendActionRequest) where T : Decodable, P : Encodable { + let receipt = specification.generateReceipt() + + if let onSent = request.onSent { + watchForReceiptSubject + .filter{$0 == receipt} + .take(1) + .subscribe(onNext: { _ in + onSent() + }).disposed(by: disposeBag) + } + + pendingActionRequests[receipt] = request + + sendJSONMessage(destination: request.destination, + data: request.jsonData, + headers: ["receipt" : receipt]) + } + + public func sendToStream(request: SendToStreamRequest) where T : Decodable, T : Encodable { + service.request(.uploadImages(url: request.stream, + grant: request.grant, + images: request.images)) { result in + request.handleMoyaResult(result) + } + } + + public func sendToStream(request: SendDataToStreamRequest) where T : Decodable, T : Encodable { + service.request(.uploadFiles(url: request.stream, + grant: request.grant, + data: request.data)) { result in + request.handleMoyaResult(result) + } + } + + public func listenToTopic(request: StompXListenToTopicRequest) -> () -> Void { + let subscriptionReceipt = specification.generateReceipt() + if let onSuccess = request.onSuccess { + watchForReceiptSubject + .take(1) + .filter({ $0 == subscriptionReceipt}) + .subscribe(onNext: { _ in + onSuccess() + }).disposed(by: disposeBag) + } + let id = specification.generateSubscriptionId() + + sendFrame(specification.subscribe(id: id, destination: request.topic, headers: ["receipt" : subscriptionReceipt])) + + return { [weak self] in + guard let self = self else { return } + unsubscribe(subscriptionId: id) + } + } + + public func listenForEvent(request: StompXListenForEventRequest) -> () -> Void { + let handlers = eventHandlers[request.topic] ?? [] + + let handler = StompXEventHandler(event: request.event) { model in + request.onNewData?(model) + } + + eventHandlers[request.topic] = handlers + [handler] + + return { [weak self] in + guard let self = self else { return } + self.eventHandlers[request.topic]?.removeAll(where: { $0.id == handler.id }) + } + } + + // MARK: STOMP Implementation + + public func subscribe(destination: String, headers: [String : String] = [:]) -> String { + let id = specification.generateSubscriptionId() + + sendFrame(specification.subscribe(id: id, destination: destination, headers: headers)) + + return id + } + + public func unsubscribe(subscriptionId: String) { + sendFrame(specification.unsubscribe(subscriptionId: subscriptionId)) + } + + public func disconnect(completion: @escaping () -> Void) { + sendFramesToClient(client: self) + + disconnectReceipt = specification.generateReceipt() + + sendFrame(specification.disconnect(receipt: disconnectReceipt)) + + onDisconnectedSubject + .take(1) + .subscribe(onNext: { _ in + completion() + }).disposed(by: disposeBag) + } + + public func sendFrame(_ frame: StompClientFrame) { + if isConnected { + socket.write(string: frame.description) + print("\nπŸ“‘ Sent Frame: " + frame.description) + } else { + frameQueue.enqueue(frame) + } + } + + public func resignToClient(client: StompX) { + sendFramesToClient(client: client) + + socket.disconnect() + } + + private func websocketDidConnect(socket: WebSocketClient) { + isConnected = true + var stompXAuthParams: String? = nil + if let authParams = stompXRequest.authParams { + stompXAuthParams = ObjectMapper.writeValueAsString(dictionary: authParams)?.base64Encoded + } + sendFrame(specification.connect(host: url.host!, + stompXUser: stompXRequest.username, + stompXUserAgent: "ChatKitty-iOS/\(version)", + stompXAuthParams: stompXAuthParams)) + } + + private func websocketDidDisconnect(socket: WebSocketClient, error: Error?) { + isConnected = false + } + + public func didReceive(event: WebSocketEvent, client: WebSocketClient) { + switch event { + case .connected(_): + print("πŸ”Œ Did get event: connected") + websocketDidConnect(socket: client) + case .disconnected(_, _): + print("❌ Did get event: disconnected") + onDisconnectedSubject.onNext(()) + websocketDidDisconnect(socket: client, error: nil) + case .text(let text): + print("\nπŸ“¨ Received Frame:") + print(text) + websocketDidReceiveMessage(socket: client, text: text) + case .binary(let data): + print("πŸ“¦ Received Binary") + websocketDidReceiveData(socket: client, data: data) + case .ping(_): + print("πŸ“ Did get event: ping") + break + case .pong(_): + print("πŸ“ Did get event: pong") + break + case .viabilityChanged(_): + print("⚑️ Did get event: viability changed") + break + case .reconnectSuggested(_): + print("πŸ”„ Did get event: reconnect suggested") + break + case .cancelled: + print("πŸ›‘ Did get event: cancelled") + isConnected = false + case .error(_): + print("⚠️ Did get event: error") + isConnected = false + case .peerClosed: + print("πŸ‘‹ Did get event: peer closed") + isConnected = false + } + } + + public func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { + do { + let components = text.components(separatedBy: "\n") + + if components.first == "" { + sendHeartBeat() + } else { + let frame = try StompServerFrame(text: text) + + switch frame.command { + case .connected: + onConnectedSubject.onNext(()) + sendFramesToClient(client: self) + case .message: + var headers: [String : String] = [:] + + for header in frame.headers.values { + headers[header.key] = header.value + } + if frame.containsHeader("content-type") { + let contentType = frame.getHeader("content-type") + if contentType.contains("application/json") || contentType.contains("application/vnd.chatkitty.rtm+json") { + if let data = frame.body.data(using: .utf8) { + let id = frame.getHeader("subscription") + if let pendingRelayRequest = pendingRelayRequests[id] { + pendingRelayRequests.removeValue(forKey: id) + pendingRelayRequest.handleJSONMessage(client: self, + destination: frame.getHeader("destination"), subscriptionId: id, + data: data, headers: headers) + } + + if frame.containsHeader("destination") { + let destination = frame.getHeader("destination") + if let handlers = self.eventHandlers[destination] { + handlers.forEach { + $0.handleJSONMessage(data: data) + } + } + } + + if frame.containsHeader("receipt-id") { + let receiptId = frame.getHeader("receipt-id") + + if let pendingActionRequest = pendingActionRequests[receiptId] { + pendingActionRequests.removeValue(forKey: receiptId) + pendingActionRequest.handleJSONMessage(client: self, + destination: frame.getHeader("destination"), subscriptionId: id, + data: data, headers: headers) + } + } + } + } + } + case .receipt: + let receiptId = frame.getHeader("receipt-id") + watchForReceiptSubject.onNext(receiptId) + if receiptId == disconnectReceipt { + socket.disconnect() + } + case .error: + watchForErrorsSubject.onNext(NSError(domain: "com.chatkitty.ios.sockets", code: 1000, userInfo: [:])) + } + } + } catch { + watchForErrorsSubject.onNext(error) + } + } + + public func websocketDidReceiveData(socket: WebSocketClient, data: Data) { + // Not called in STOMP + } + + private func sendHeartBeat() { + socket.write(string: specification.generateHeartBeat()) + } + + private func sendFramesToClient(client: StompX) { + while !frameQueue.isEmpty { + client.sendFrame(frameQueue.dequeue()!) + } + } +} + +final class OfflineStompClient : StompX { + func sendToStream(request: SendDataToStreamRequest) where T : Decodable, T : Encodable { + // TODO + } + + func sendToStream(request: SendToStreamRequest) where T : Decodable, T : Encodable { + // TODO + } + + func sendActionDictionary(request: StompXSendActionRequestDictionary) where T : Decodable, T : Encodable { + // TODO + } + + func relayResourceDictionary(request: StompXRelayResourceRequestDictionary) { + // TODO + } + + func listenToTopic(request: StompXListenToTopicRequest) -> () -> Void { + // TODO + return {} + } + + + func sendAction(request: StompXSendActionRequest) where T : Decodable, T : Encodable, P : Encodable { + // TODO + } + + func relayResource(request: StompXRelayResourceRequest) where T : Decodable, T : Encodable { + // TODO + } + + public func listenForEvent(request: StompXListenForEventRequest) -> () -> Void { + return {} + } + + func disconnect(completion: @escaping () -> Void) { + // TODO + } + + + public var isConnected: Bool = false + + private let specification: StompSpecification = StompSpecification() + + private var frameQueue: FrameQueue = FrameQueue() + + public func connect(request: StompXConnectRequest) { } + + public func sendJSONMessage(destination: String, data: Data, headers: [String : String] = [:]) { + do { + try sendFrame(specification.sendJSONMessage(destination: destination, data: data)) + } catch {} + } + + public func subscribe(destination: String, headers: [String : String] = [:]) -> String { + let id = specification.generateSubscriptionId() + + sendFrame(specification.subscribe(id: id, destination: destination, headers: headers)) + + return id + } + + public func unsubscribe(subscriptionId: String) { + sendFrame(specification.unsubscribe(subscriptionId: subscriptionId)) + } + + public func sendFrame(_ frame: StompClientFrame) { + frameQueue.enqueue(frame) + } + + public func resignToClient(client: StompX) { + while !frameQueue.isEmpty { + client.sendFrame(frameQueue.dequeue()!) + } + } +} + +fileprivate struct FrameQueue { + private var array = [StompClientFrame?]() + private var head = 0 + + public var isEmpty: Bool { + return count == 0 + } + + public var count: Int { + return array.count - head + } + + public mutating func enqueue(_ element: StompClientFrame) { + array.append(element) + } + + public mutating func dequeue() -> StompClientFrame? { + guard head < array.count, let element = array[head] else { return nil } + + array[head] = nil + head += 1 + + let percentage = Double(head)/Double(array.count) + if array.count > 50 && percentage > 0.25 { + array.removeFirst(head) + head = 0 + } + + return element + } + + public var front: StompClientFrame? { + if isEmpty { + return nil + } else { + return array[head] + } + } +} + + +public struct StompClientFrame: CustomStringConvertible { + private(set) var command: StompClientCommand + private(set) var headers: Set + private(set) var body: String + + init(command: StompClientCommand, headers: Set = [], body: String = "") { + self.command = command + self.headers = headers + self.body = body + } + + public var description: String { + var string = command.rawValue + "\n" + + for header in headers { + string += header.key + ":" + header.value + "\n" + } + + string += "\n" + body + "\0" + + return string + } +} + +struct StompServerFrame: CustomStringConvertible { + private(set) var command: StompServerCommand + private(set) var headers: [String : StompHeader] + private(set) var body: String + + private init(command: StompServerCommand, headers: [String : StompHeader], body: String) { + self.command = command + self.headers = headers + self.body = body + } + + init(text: String) throws { + var components = text.components(separatedBy: "\n") + + if components.first == "" { + components.removeFirst() + } + + let command = try StompServerCommand(text: components.first!) + + var headers: [String:StompHeader] = [:] + var body = "" + var isBody = false + for index in 1 ..< components.count { + let component = components[index] + if isBody { + body += component + if body.hasSuffix("\0") { + body = body.replacingOccurrences(of: "\0", with: "") + } + } else { + if component == "" { + isBody = true + } else { + let parts = component.components(separatedBy: ":") + + guard let key = parts.first, let value = parts.last else { + continue + } + + headers[key] = StompHeader(key: key, value: value) + } + } + } + + self.init(command: command, headers: headers, body: body) + } + + var description: String { + var string = command.rawValue + "\n" + + for header in headers.values { + string += header.key + ":" + header.value + "\n" + } + + string += "\n" + body + "\0" + + return string + } + + + func containsHeader(_ header: String) -> Bool { + return headers[header]?.value != nil + } + + func getHeader(_ header: String) -> String { + return (headers[header]?.value)! + } +} + +enum StompClientCommand: String { + case send = "SEND" + case subscribe = "SUBSCRIBE" + case unsubscribe = "UNSUBSCRIBE" + case begin = "BEGIN" + case commit = "COMMIT" + case abort = "ABORT" + case ack = "ACK" + case nack = "NACK" + case disconnect = "DISCONNECT" + case connect = "CONNECT" + case stomp = "STOMP" + + + init(text: String) throws { + guard let command = StompClientCommand(rawValue: text) else { + throw NSError(domain: "com.chatkitty.ios.sockets", code: 1001, userInfo: [NSLocalizedDescriptionKey : "Sent command is undefined."]) + } + + self = command + } +} + +enum StompServerCommand: String { + case connected = "CONNECTED" + case message = "MESSAGE" + case receipt = "RECEIPT" + case error = "ERROR" + + init(text: String) throws { + guard let command = StompServerCommand(rawValue: text) else { + throw NSError(domain: "com.chatkitty.ios.sockets", code: 1002, userInfo: [NSLocalizedDescriptionKey : "Received command is undefined."]) + } + + self = command + } +} + + +nonisolated enum StompHeader: Hashable, Sendable { + case contentLength(length: Int) + case contentType(type: String) + case receipt(receipt: String) + + case acceptVersion(version: String) + case host(host: String) + case login(login: String) + case passcode(passcode: String) + case heartBeat(value: String) + + case version(version: String) + case session(session: String) + case server(server: String) + + case destination(destination: String) + case transaction(transaction: String) + + case id(id: String) + case ack(ack: String) + + case messageId(id: String) + case subscription(id: String) + + case receiptId(id: String) + + case custom(key: String, value: String) + + init(key: String, value: String) { + switch key { + case "content-length": + self = .contentLength(length: Int(value)!) + case "content-type": + self = .contentType(type: value) + case "receipt": + self = .receipt(receipt: value) + case "accept-version": + self = .acceptVersion(version: value) + case "host": + self = .host(host: value) + case "login": + self = .login(login: value) + case "passcode": + self = .passcode(passcode: value) + case "heart-beat": + self = .heartBeat(value: value) + case "version": + self = .version(version: value) + case "session": + self = .session(session: value) + case "server": + self = .server(server: value) + case "destination": + self = .destination(destination: value) + case "transaction": + self = .transaction(transaction: value) + case "id": + self = .id(id: value) + case "ack": + self = .ack(ack: value) + case "message-id": + self = .messageId(id: value) + case "subscription": + self = .subscription(id: value) + case "receipt-id": + self = .receiptId(id: value) + default: + self = .custom(key: key, value: value) + } + } + + var key: String { + switch self { + case .contentLength: + return "content-length" + case .contentType: + return "content-type" + case .receipt: + return "receipt" + case .acceptVersion: + return "accept-version" + case .host: + return "host" + case .login: + return "login" + case .passcode: + return "passcode" + case .heartBeat: + return "heart-beat" + case .version: + return "version" + case .session: + return "session" + case .server: + return "server" + case .destination: + return "destination" + case .transaction: + return "transaction" + case .id: + return "id" + case .ack: + return "ack" + case .messageId: + return "message-id" + case .subscription: + return "subscription" + case .receiptId: + return "receipt-id" + case .custom(let key, _): + return key + } + } + + var value: String { + switch self { + case .custom(_, let value): + return value + case .contentLength(let length): + return "\(length)" + case .contentType(let type): + return type + case .receipt(let receipt): + return receipt + case .acceptVersion(let version): + return version + case .host(let host): + return host + case .login(let login): + return login + case .passcode(let passcode): + return passcode + case .heartBeat(let value): + return value + case .version(let version): + return version + case .session(let session): + return session + case .server(let server): + return server + case .destination(let destination): + return destination + case .transaction(let transaction): + return transaction + case .id(let id): + return id + case .ack(let ack): + return ack + case .messageId(let id): + return id + case .subscription(let id): + return id + case .receiptId(let id): + return id + } + } + + func hash(into hasher: inout Hasher) { + hasher.combine(key.hashValue) + } + + static func ==(lhs: StompHeader, rhs: StompHeader) -> Bool { + return lhs.hashValue == rhs.hashValue + } +} + + +public struct SocketMessage { + public let destination: String + public let headers: [String : String] + public let data: Data + + public init(destination: String, + headers: [String : String] = [:], + data: Data = Data()) { + self.destination = destination + self.headers = headers + self.data = data + } + + public init(destination: String, headers: [String:String], object: Codable) { + do { + let data = try JSONSerialization.data(withJSONObject: object, options: JSONSerialization.WritingOptions()) + self.init(destination: destination, headers: headers, data: data) + } catch { + self.init(destination: destination, headers: headers) + } + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXLogger.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXLogger.swift new file mode 100644 index 00000000..1692c5aa --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXLogger.swift @@ -0,0 +1,11 @@ +import Foundation + +final class StompXLogger { + static func logDebug(_ text: String, file: String = #file, line: Int = #line) { + print("[STOMP-X][\(file)][\(line)] - \(text)") + } + + static func logError(_ text: String, file: String = #file, line: Int = #line) { + print("[STOMP-X][ERROR][\(file)][\(line)] - \(text)") + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXUploadProgressListener.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXUploadProgressListener.swift new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/StompXUploadProgressListener.swift @@ -0,0 +1 @@ + diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/ThreadSafeDictionary.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/ThreadSafeDictionary.swift new file mode 100644 index 00000000..d008093c --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/ThreadSafeDictionary.swift @@ -0,0 +1,63 @@ +import Foundation + +class ThreadSafeDictionary: Collection { + + private var dictionary: [V: T] + private let concurrentQueue = DispatchQueue(label: "Dictionary Barrier Queue", + attributes: .concurrent) + var startIndex: Dictionary.Index { + self.concurrentQueue.sync { + return self.dictionary.startIndex + } + } + + var endIndex: Dictionary.Index { + self.concurrentQueue.sync { + return self.dictionary.endIndex + } + } + + init(dict: [V: T] = [V:T]()) { + self.dictionary = dict + } + // this is because it is an apple protocol method + // swiftlint:disable identifier_name + func index(after i: Dictionary.Index) -> Dictionary.Index { + self.concurrentQueue.sync { + return self.dictionary.index(after: i) + } + } + // swiftlint:enable identifier_name + subscript(key: V) -> T? { + set(newValue) { + self.concurrentQueue.async(flags: .barrier) {[weak self] in + self?.dictionary[key] = newValue + } + } + get { + self.concurrentQueue.sync { + return self.dictionary[key] + } + } + } + + // has implicity get + subscript(index: Dictionary.Index) -> Dictionary.Element { + self.concurrentQueue.sync { + return self.dictionary[index] + } + } + + func removeValue(forKey key: V) { + self.concurrentQueue.async(flags: .barrier) {[weak self] in + self?.dictionary.removeValue(forKey: key) + } + } + + func removeAll() { + self.concurrentQueue.async(flags: .barrier) {[weak self] in + self?.dictionary.removeAll() + } + } + +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/RtmEvent.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/RtmEvent.swift new file mode 100644 index 00000000..f2095404 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/RtmEvent.swift @@ -0,0 +1,6 @@ +import Foundation + +final class RtmEvent: Codable { + let type: String + let resource: T? +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompEventHandlers.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompEventHandlers.swift new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompEventHandlers.swift @@ -0,0 +1 @@ + diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXError.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXError.swift new file mode 100644 index 00000000..41a91998 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXError.swift @@ -0,0 +1,24 @@ +import Moya +import Foundation +public enum StompXError: Error { + case parseError(String) + case serviceError(StompXServiceError) + case moyaError(MoyaError) + + public var localizedDescription: String { + switch self { + case .serviceError(let error): + return error.message + case .parseError(let error): + return error + case .moyaError(let error): + return error.localizedDescription + } + } +} + +public final class StompXServiceError: Codable & Sendable { + public let error: String + public let message: String + public let timestamp: String +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXEvent.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXEvent.swift new file mode 100644 index 00000000..3806a777 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXEvent.swift @@ -0,0 +1,6 @@ +import Foundation + +public class StompXEvent: Codable { + public let type: String + public let resource: T +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXEventHandler.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXEventHandler.swift new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXEventHandler.swift @@ -0,0 +1 @@ + diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXPage.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXPage.swift new file mode 100644 index 00000000..c7ae9682 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/model/StompXPage.swift @@ -0,0 +1,30 @@ +import Foundation + +final class StompXPage: Codable { + let _embedded: T? + let page: StompXPageMetadata + let _relays: StompXRelays? +} + +final class StompXPageMetadata: Codable { + let size: Int + let totalElements: Int? + let totalPages: Int? + let number: Int? +} + +final class StompXRelays: Codable { + let this: String + let first: String? + let prev: String? + let next: String? + let last: String? + + private enum CodingKeys : String, CodingKey { + case this = "self" + case first + case last + case prev + case next + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXConnectRequest.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXConnectRequest.swift new file mode 100644 index 00000000..baf4776a --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXConnectRequest.swift @@ -0,0 +1,10 @@ + +public struct StompXConnectRequest { + public let apiKey: String + public let username: String + public let authParams: Dictionary? + public let onConnected: () -> Void + public let onConnectionLost: () -> Void + public let onConnectionResumed: () -> Void + public let onError: (StompXError) -> Void +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXDisconnectRequest.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXDisconnectRequest.swift new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXDisconnectRequest.swift @@ -0,0 +1 @@ + diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXListenForEventRequest.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXListenForEventRequest.swift new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXListenForEventRequest.swift @@ -0,0 +1 @@ + diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXPerformActionRequest.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXPerformActionRequest.swift new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXPerformActionRequest.swift @@ -0,0 +1 @@ + diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXRelayResourceRequest.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXRelayResourceRequest.swift new file mode 100644 index 00000000..e8c2809f --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXRelayResourceRequest.swift @@ -0,0 +1,272 @@ +import Foundation +import UIKit +import Moya + +public protocol RelayResourceRequestable { + func handleJSONMessage(client: StompX, + destination: String, + subscriptionId: String, + data: Data, + headers: [String : String]) +} + +public struct StompXRelayResourceRequestDictionary: RelayResourceRequestable { + let destination: String + let parameters: Dictionary? + let onSuccess: (Dictionary) -> Void + let onError: (StompXError) -> Void + + init(destination: String, + parameters: Dictionary? = nil, + onSuccess: @escaping (Dictionary) -> Void, + onError: @escaping (StompXError) -> Void + ) { + self.destination = destination + self.parameters = parameters + self.onSuccess = onSuccess + self.onError = onError + } + + public func handleJSONMessage(client: StompX, + destination: String, + subscriptionId: String, + data: Data, + headers: [String : String]) { + if let dict = data.toDictionary?["resource"] as? [String: Any]{ + onSuccess(dict) + } else { + onError(StompXError.parseError("Unable to parse mode JSON to Dictionary for destination: \(destination)")) + } + } +} + +public struct StompXRelayResourceRequest: RelayResourceRequestable { + let destination: String + let parameters: Dictionary? + let onSuccess: (R?) -> Void + let onError: (StompXError) -> Void + + init(destination: String, + parameters: Dictionary? = nil, + onSuccess: @escaping (R?) -> Void, + onError: @escaping (StompXError) -> Void + ) { + self.destination = destination + self.parameters = parameters + self.onSuccess = onSuccess + self.onError = onError + } + + public func handleJSONMessage(client: StompX, + destination: String, + subscriptionId: String, + data: Data, + headers: [String : String]) { + if let model = data.decode(to: RtmEvent.self) { + onSuccess(model.resource) + } else { + onError(StompXError.parseError("Unable to parse model of type \(String(describing: R.self)) for destination: \(destination)")) + } + } +} + +public struct StompXListenToTopicRequest { + let topic: String + let onSuccess: (() -> Void)? + let onNewData: ((Data) -> Void)? + + init(topic: String, + onSuccess: (() -> Void)? = nil, + onNewData: ((Data) -> Void)? = nil) { + self.topic = topic + self.onSuccess = onSuccess + self.onNewData = onNewData + } +} + +public struct StompXListenForEventRequest { + let topic: String + let event: String + let onNewData: ((R) -> Void)? + + init(topic: String, + event: String, + onNewData: ((R) -> Void)? = nil) { + self.topic = topic + self.event = event + self.onNewData = onNewData + } +} + +public struct StompXSendActionRequestDictionary: RelayResourceRequestable { + let destination: String + let data: [String: Any] + let onSent: (() -> Void)? + let onSuccess: ((R) -> Void)? + let onError: ((StompXError) -> Void)? + + init(destination: String, + data: [String: Any], + onSent: (() -> Void)? = nil, + onSuccess: ((R) -> Void)? = nil, + onError: ((StompXError) -> Void)? = nil) { + self.destination = destination + self.data = data + self.onSent = onSent + self.onSuccess = onSuccess + self.onError = onError + } + + public func handleJSONMessage(client: StompX, + destination: String, + subscriptionId: String, + data: Data, + headers: [String : String]) { + // Actions only have errors + if let model = data.decode(to: R.self, logError: false) { + onSuccess?(model) + } else if let model = data.decode(to: StompXServiceError.self) { + onError?(StompXError.serviceError(model)) + } + } + + public var jsonData: Data { + do { + return try JSONSerialization.data(withJSONObject: data, options: .prettyPrinted) + } catch { + return Data() + } + } +} + +public struct SendToStreamRequest { + let stream: URL + let grant: String + let images: [UIImage] + let onSuccess: ((R) -> Void) + let onError: ((StompXError) -> Void) + + func handleMoyaResult(_ result: Result) { + switch result { + case let .success(moyaResponse): + let data = moyaResponse.data + if let model = data.decode(to: R.self) { + onSuccess(model) + } else { + onError(StompXError.parseError("Unable to parse model of type \(String(describing: R.self)) for destination: \(stream.absoluteString)")) + } + case let .failure(error): + onError(.moyaError(error)) + } + } +} + +public struct SendDataToStreamRequest { + let stream: URL + let grant: String + let data: [CreateDataFile] + let onSuccess: ((R) -> Void) + let onError: ((StompXError) -> Void) + + func handleMoyaResult(_ result: Result) { + switch result { + case let .success(moyaResponse): + let data = moyaResponse.data + if let model = data.decode(to: R.self) { + onSuccess(model) + } else { + onError(StompXError.parseError("Unable to parse model of type \(String(describing: R.self)) for destination: \(stream.absoluteString)")) + } + case let .failure(error): + onError(.moyaError(error)) + } + } +} + +public struct StompXSendActionRequest: RelayResourceRequestable { + let destination: String + let data: P + let onSent: (() -> Void)? + let onSuccess: ((R) -> Void)? + let onError: ((StompXError) -> Void)? + + init(destination: String, + data: P, + onSent: (() -> Void)? = nil, + onSuccess: ((R) -> Void)? = nil, + onError: ((StompXError) -> Void)? = nil) { + self.destination = destination + self.data = data + self.onSent = onSent + self.onSuccess = onSuccess + self.onError = onError + } + + public func handleJSONMessage(client: StompX, + destination: String, + subscriptionId: String, + data: Data, + headers: [String : String]) { + // Actions only have errors + if let model = data.decode(to: R.self, logError: false) { + onSuccess?(model) + } else if let model = data.decode(to: StompXServiceError.self) { + onError?(StompXError.serviceError(model)) + } + } + + public var jsonData: Data { + let encoder = JSONEncoder() + if let encoded = try? encoder.encode(data) { + return encoded + } + return Data() + } +} + +public struct StompXSubscription: RelayResourceRequestable { + private let subscriptionId: String + private let onNewMessage: ((Data) -> Void)? + + init(subscriptionId: String, onNewMessage: ((Data) -> Void)?) { + self.subscriptionId = subscriptionId + self.onNewMessage = onNewMessage + } + + + public func handleJSONMessage(client: StompX, + destination: String, + subscriptionId: String, + data: Data, + headers: [String : String]) { + onNewMessage?(data) + } + + + func unsubscribe(from client: StompX) { + client.unsubscribe(subscriptionId: subscriptionId) + } +} + +public protocol StompXEventHandlable { + var id: String { get } + func handleJSONMessage(data: Data) +} + +public struct StompXEventHandler: StompXEventHandlable { + public let id = UUID().uuidString.lowercased() + private let event: String + private let onNewMessage: ((R) -> Void)? + + init(event: String, + onNewMessage: ((R) -> Void)?) { + self.event = event + self.onNewMessage = onNewMessage + } + + public func handleJSONMessage(data: Data) { + if let model = data.decode(to: StompXEvent.self, logError: false), model.type == event{ + onNewMessage?(model.resource) + } + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXSendToStreamRequest.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXSendToStreamRequest.swift new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXSendToStreamRequest.swift @@ -0,0 +1 @@ + diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXSubscribeToTopicRequest.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXSubscribeToTopicRequest.swift new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/request/StompXSubscribeToTopicRequest.swift @@ -0,0 +1 @@ + diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/stomp/StompSpecification.swift b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/stomp/StompSpecification.swift new file mode 100644 index 00000000..e12da5cc --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExample/stompx/stomp/StompSpecification.swift @@ -0,0 +1,72 @@ +import Foundation + +final class StompSpecification { + public func connect(host: String, + stompXUser: String, + stompXUserAgent: String, + stompXAuthParams: String? + ) -> StompClientFrame { + var headers: Set = [.acceptVersion(version: "1.2"), + .host(host: host), + .heartBeat(value: "10000,10000"), + .custom(key: "StompX-User", value: stompXUser), + .custom(key: "StompX-User-Agent", value: stompXUserAgent)] + + if let stompXAuthParams { + headers.insert(.custom(key: "StompX-Auth-Params", value: stompXAuthParams)) + } + return StompClientFrame(command: .connect, headers: headers) + } + + public func subscribe(id: String, destination: String, headers: [String : String] = [:]) -> StompClientFrame { + var stompHeaders: Set = [.id(id: id), .destination(destination: destination), .ack(ack: "client-individual")] + + for (key, value) in headers { + stompHeaders.insert(.custom(key: key, value: value)) + } + + return StompClientFrame(command: .subscribe, headers: stompHeaders) + } + + public func sendJSONMessage(destination: String, data: Data, headers: [String : String] = [:]) throws -> StompClientFrame { + let message = String(data: data, encoding: String.Encoding.utf8)! + + var stompHeaders: Set = [.destination(destination: destination), .contentType(type: "application/json;charset=UTF-8"), .contentLength(length: message.utf8.count)] + + for (key, value) in headers { + stompHeaders.insert(.custom(key: key, value: value)) + } + + return StompClientFrame(command: .send, headers: stompHeaders, body: message) + } + + public func unsubscribe(subscriptionId: String) -> StompClientFrame { + let headers: Set = [.id(id: subscriptionId)] + + return StompClientFrame(command: .unsubscribe, headers: headers) + } + + public func ack(messageId: String) -> StompClientFrame { + let headers: Set = [.id(id: messageId)] + + return StompClientFrame(command: .ack, headers: headers) + } + + public func disconnect(receipt: String) -> StompClientFrame { + let headers: Set = [.receipt(receipt: receipt)] + + return StompClientFrame(command: .disconnect, headers: headers) + } + + public func generateReceipt() -> String { + return "receipt-" + UUID().uuidString.lowercased() + } + + public func generateSubscriptionId() -> String { + return "subscription-id-" + UUID().uuidString.lowercased() + } + + public func generateHeartBeat() -> String { + return "\n\n" + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExampleTests/ChatKittyExampleTests.swift b/examples/ios-example/ChatKittyExample/ChatKittyExampleTests/ChatKittyExampleTests.swift new file mode 100644 index 00000000..3375e4a4 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExampleTests/ChatKittyExampleTests.swift @@ -0,0 +1,17 @@ +// +// ChatKittyExampleTests.swift +// ChatKittyExampleTests +// +// Created by Kevin Grafstrom on 2025-11-04. +// + +import Testing +@testable import ChatKittyExample + +struct ChatKittyExampleTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExampleUITests/ChatKittyExampleUITests.swift b/examples/ios-example/ChatKittyExample/ChatKittyExampleUITests/ChatKittyExampleUITests.swift new file mode 100644 index 00000000..61bd433e --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExampleUITests/ChatKittyExampleUITests.swift @@ -0,0 +1,41 @@ +// +// ChatKittyExampleUITests.swift +// ChatKittyExampleUITests +// +// Created by Kevin Grafstrom on 2025-11-04. +// + +import XCTest + +final class ChatKittyExampleUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } +} diff --git a/examples/ios-example/ChatKittyExample/ChatKittyExampleUITests/ChatKittyExampleUITestsLaunchTests.swift b/examples/ios-example/ChatKittyExample/ChatKittyExampleUITests/ChatKittyExampleUITestsLaunchTests.swift new file mode 100644 index 00000000..cf903eb4 --- /dev/null +++ b/examples/ios-example/ChatKittyExample/ChatKittyExampleUITests/ChatKittyExampleUITestsLaunchTests.swift @@ -0,0 +1,33 @@ +// +// ChatKittyExampleUITestsLaunchTests.swift +// ChatKittyExampleUITests +// +// Created by Kevin Grafstrom on 2025-11-04. +// + +import XCTest + +final class ChatKittyExampleUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +} diff --git a/examples/ios-example/ChatKittyExample/Images.xcassets/AppIcon.appiconset/Contents.json b/examples/ios-example/ChatKittyExample/Images.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 7006c9ee..00000000 --- a/examples/ios-example/ChatKittyExample/Images.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "images" : [ - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "2x" - }, - { - "idiom" : "iphone", - "size" : "60x60", - "scale" : "3x" - }, - { - "idiom" : "ios-marketing", - "size" : "1024x1024", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/examples/ios-example/ChatKittyExample/Info.plist b/examples/ios-example/ChatKittyExample/Info.plist deleted file mode 100644 index 942b6a28..00000000 --- a/examples/ios-example/ChatKittyExample/Info.plist +++ /dev/null @@ -1,39 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - NSCameraUsageDescription - Your description here explaining why you need camera access - UILaunchStoryboardName - LaunchScreen - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - - - diff --git a/examples/ios-example/ChatKittyExample/ViewController.swift b/examples/ios-example/ChatKittyExample/ViewController.swift deleted file mode 100644 index 3f4cb8c5..00000000 --- a/examples/ios-example/ChatKittyExample/ViewController.swift +++ /dev/null @@ -1,47 +0,0 @@ -import UIKit -import ChatKitty - -class ViewController: UIViewController { - private lazy var chatUi: ChatUIView = { - let connection = ApiConnection(apiKey: "afaac908-1db3-4b5c-a7ae-c040b9684403") - let configuration = ChatUIConfiguration(widgetId: "UWiEkKvdAaUJ1xut", - username: "2989c53a-d0c5-4222-af8d-fbf7b0c74ec6", - connection: connection, // null to disable connection api - theme: .light) - - let components = ChatUIComponents( - onMounted: { context in - print("onMounted", context) - }, - onHeaderSelected: { channel in - print("onHeaderSelected", channel) - }, - onMenuActionSelected: { action in - print("onMenuActionSelected", action) - } - ) - - let view = ChatUIView(configuration: configuration, - components: components) - view.translatesAutoresizingMaskIntoConstraints = false - self.view.addSubview(view) - return view - }() - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .white - NSLayoutConstraint.activate([ - chatUi.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - chatUi.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), - chatUi.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), - chatUi.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor) - ]) - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } -} - diff --git a/examples/ios-example/ChatUiExample/.DS_Store b/examples/ios-example/ChatUiExample/.DS_Store deleted file mode 100644 index 1486f1cc..00000000 Binary files a/examples/ios-example/ChatUiExample/.DS_Store and /dev/null differ diff --git a/examples/ios-example/README.md b/examples/ios-example/README.md deleted file mode 100644 index 4bd28e64..00000000 --- a/examples/ios-example/README.md +++ /dev/null @@ -1 +0,0 @@ -# iOS Example diff --git a/examples/ios-example/Tests/Info.plist b/examples/ios-example/Tests/Info.plist deleted file mode 100644 index ba72822e..00000000 --- a/examples/ios-example/Tests/Info.plist +++ /dev/null @@ -1,24 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - BNDL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - - diff --git a/examples/ios-example/Tests/Tests.swift b/examples/ios-example/Tests/Tests.swift deleted file mode 100644 index 415e6f60..00000000 --- a/examples/ios-example/Tests/Tests.swift +++ /dev/null @@ -1,28 +0,0 @@ -import XCTest -import ChatKitty - -class Tests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testExample() { - // This is an example of a functional test case. - XCTAssert(true, "Pass") - } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure() { - // Put the code you want to measure the time of here. - } - } - -}