diff --git a/.gitignore b/.gitignore index 1e354d7aa..d3e6ebd12 100755 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,8 @@ examples/* # exception to the rule !examples/.gitkeep -.DS_Store +**/.DS_Store +templates/swift/example/.build +templates/swift/example/Example.xcodeproj/project.xcworkspace/xcuserdata +templates/swift/example/Example.xcodeproj/xcuserdata +**/xcuserdata diff --git a/example.php b/example.php index 0890091c0..d45ae01fb 100644 --- a/example.php +++ b/example.php @@ -15,6 +15,7 @@ use Appwrite\SDK\Language\Deno; use Appwrite\SDK\Language\HTTP; use Appwrite\SDK\Language\Swift; +use Appwrite\SDK\Language\SwiftClient; use Appwrite\SDK\Language\DotNet; use Appwrite\SDK\Language\Flutter; use Appwrite\SDK\Language\Android; @@ -269,7 +270,7 @@ function getSSLPage($url) { $sdk->generate(__DIR__ . '/examples/go'); - // Swift + // Swift (Server) $sdk = new SDK(new Swift(), new Swagger2($spec)); $sdk @@ -291,7 +292,31 @@ function getSSLPage($url) { ]) ; - $sdk->generate(__DIR__ . '/examples/swift'); + $sdk->generate(__DIR__ . '/examples/swift-server'); + + // Swift (Client) + $sdk = new SDK(new SwiftClient(), new Swagger2($spec)); + + $sdk + ->setName('NAME') + ->setDescription('Repo description goes here') + ->setShortDescription('Repo short description goes here') + ->setURL('https://example.com') + ->setLogo('https://appwrite.io/v1/images/console.png') + ->setLicenseContent('test test test') + ->setWarning('**WORK IN PROGRESS - NOT READY FOR USAGE**') + ->setChangelog('**CHANGELOG**') + ->setVersion('0.0.1') + ->setGitUserName('repoowner') + ->setGitRepoName('reponame') + ->setTwitter('appwrite_io') + ->setDiscord('564160730845151244', 'https://appwrite.io/discord') + ->setDefaultHeaders([ + 'X-Appwrite-Response-Format' => '0.7.0', + ]) + ; + + $sdk->generate(__DIR__ . '/examples/swift-client'); // DotNet $sdk = new SDK(new DotNet(), new Swagger2($spec)); diff --git a/src/SDK/Language/Swift.php b/src/SDK/Language/Swift.php index 27eb62652..95216806a 100644 --- a/src/SDK/Language/Swift.php +++ b/src/SDK/Language/Swift.php @@ -119,18 +119,18 @@ public function getFiles() 'template' => 'swift/Package.swift.twig', 'minify' => false, ], - [ - 'scope' => 'service', - 'destination' => 'docs/{{service.name | caseLower}}.md', - 'template' => 'swift/docs/service.md.twig', - 'minify' => false, - ], [ 'scope' => 'method', 'destination' => 'docs/examples/{{service.name | caseLower}}/{{method.name | caseDash}}.md', 'template' => 'swift/docs/example.md.twig', 'minify' => false, ], + [ + 'scope' => 'default', + 'destination' => '/Tests/{{ spec.title | caseUcfirst}}Tests/Tests.swift', + 'template' => 'swift/Tests/Tests.swift.twig', + 'minify' => false, + ], [ 'scope' => 'default', 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Client.swift', @@ -139,16 +139,130 @@ public function getFiles() ], [ 'scope' => 'default', - 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Service.swift', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/{{ spec.title | caseUcfirst}}Error.swift', + 'template' => '/swift/Sources/Models/Error.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/File.swift', + 'template' => 'swift/Sources/Models/File.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/Codable+JSON.swift', + 'template' => 'swift/Sources/Extensions/Codable+JSON.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/Cookie+Codable.swift', + 'template' => 'swift/Sources/Extensions/Cookie+Codable.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Extensions/HTTPClientRequest+Cookies.swift', + 'template' => 'swift/Sources/Extensions/HTTPClientRequest+Cookies.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/StreamingDelegate.swift', + 'template' => 'swift/Sources/StreamingDelegate.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Services/Service.swift', 'template' => 'swift/Sources/Service.swift.twig', 'minify' => false, ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/iOS/iOSDeviceInfo.swift', + 'template' => 'swift/Sources/DeviceInfo/iOS/iOSDeviceInfo.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/iOS/UIDevice+ModelName.swift', + 'template' => 'swift/Sources/DeviceInfo/iOS/UIDevice+ModelName.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/Linux/LinuxDeviceInfo.swift', + 'template' => 'swift/Sources/DeviceInfo/Linux/LinuxDeviceInfo.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/MacOS/MacOSDeviceInfo.swift', + 'template' => 'swift/Sources/DeviceInfo/MacOS/MacOSDeviceInfo.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/MacOS/CwlSysCtl.swift', + 'template' => 'swift/Sources/DeviceInfo/MacOS/CwlSysCtl.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/Windows/WindowsDeviceInfo.swift', + 'template' => 'swift/Sources/DeviceInfo/Windows/WindowsDeviceInfo.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/DeviceInfo/OSDeviceInfo.swift', + 'template' => 'swift/Sources/DeviceInfo/OSDeviceInfo.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/PackageInfo/Apple/PackageInfo+Apple.swift', + 'template' => 'swift/Sources/PackageInfo/Apple/PackageInfo+Apple.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/PackageInfo/Linux/PackageInfo+Linux.swift', + 'template' => 'swift/Sources/PackageInfo/Linux/PackageInfo+Linux.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/PackageInfo/Windows/PackageInfo+Windows.swift', + 'template' => 'swift/Sources/PackageInfo/Windows/PackageInfo+Windows.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/PackageInfo/OSPackageInfo.swift', + 'template' => 'swift/Sources/PackageInfo/OSPackageInfo.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/PackageInfo/PackageInfo.swift', + 'template' => 'swift/Sources/PackageInfo/PackageInfo.swift', + 'minify' => false, + ], [ 'scope' => 'service', 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Services/{{service.name | caseUcfirst}}.swift', 'template' => 'swift/Sources/Services/Service.swift.twig', 'minify' => false, ], + [ + 'scope' => 'definition', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}Models/{{ definition.name | caseUcfirst }}.swift', + 'template' => '/swift/Sources/Models/Model.swift.twig', + 'minify' => false, + ], ]; } @@ -161,19 +275,18 @@ public function getTypeName($type) switch ($type) { case self::TYPE_INTEGER: return 'Int'; - break; + case self::TYPE_NUMBER: + return 'Double'; case self::TYPE_STRING: return 'String'; - break; case self::TYPE_FILE: - return 'Array'; - break; + return 'File'; case self::TYPE_BOOLEAN: return 'Bool'; - break; case self::TYPE_ARRAY: - return "Array"; - break; + return '[Any]'; + case self::TYPE_OBJECT: + return 'Any'; } return $type; @@ -197,12 +310,12 @@ public function getParamDefault(array $param) if(empty($default) && $default !== 0 && $default !== false) { switch ($type) { - case self::TYPE_NUMBER: case self::TYPE_INTEGER: + case self::TYPE_NUMBER: $output = "0"; break; case self::TYPE_STRING: - $output .= "\"\""; + $output .= '""'; break; case self::TYPE_BOOLEAN: $output .= 'false'; @@ -210,16 +323,20 @@ public function getParamDefault(array $param) case self::TYPE_ARRAY: $output .= '[]'; break; + case self::TYPE_OBJECT: + $output .= 'nil'; + break; + default: + echo $type; } } else { switch ($type) { - case self::TYPE_NUMBER: case self::TYPE_INTEGER: $output .= $default; break; - case self::TYPE_ARRAY: - $output .= $default; + case self::TYPE_NUMBER: + $output .= sprintf("%.1f",$default); break; case self::TYPE_BOOLEAN: $output .= ($default) ? 'true' : 'false'; @@ -227,6 +344,10 @@ public function getParamDefault(array $param) case self::TYPE_STRING: $output .= "\"{$default}\""; break; + case self::TYPE_ARRAY: + case self::TYPE_OBJECT: + $output .= 'nil'; + break; } } @@ -248,7 +369,7 @@ public function getParamExample(array $param) if(empty($example) && $example !== 0 && $example !== false) { switch ($type) { case self::TYPE_FILE: - $output .= "nil"; + $output .= 'File(name: "image.jpg", buffer: yourByteBuffer)'; break; case self::TYPE_NUMBER: case self::TYPE_INTEGER: diff --git a/src/SDK/Language/SwiftClient.php b/src/SDK/Language/SwiftClient.php new file mode 100644 index 000000000..f54069c91 --- /dev/null +++ b/src/SDK/Language/SwiftClient.php @@ -0,0 +1,310 @@ + 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/OAuth/WebAuthComponent.swift', + 'template' => '/swift/Sources/OAuth/WebAuthComponent.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/OAuth/View+OAuth.swift', + 'template' => '/swift/Sources/OAuth/View+OAuth.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Models/RealtimeModels.swift', + 'template' => '/swift/Sources/Models/RealtimeModels.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/Services/Realtime.swift', + 'template' => '/swift/Sources/Services/Realtime.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/WebSockets/HTTPHandler.swift', + 'template' => '/swift/Sources/WebSockets/HTTPHandler.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/WebSockets/MessageHandler.swift', + 'template' => '/swift/Sources/WebSockets/MessageHandler.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/WebSockets/WebSocketClient.swift', + 'template' => '/swift/Sources/WebSockets/WebSocketClient.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/WebSockets/WebSocketClientDelegate.swift', + 'template' => '/swift/Sources/WebSockets/WebSocketClientDelegate.swift.twig', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/Sources/{{ spec.title | caseUcfirst}}/WebSockets/WebSocketClientError.swift', + 'template' => '/swift/Sources/WebSockets/WebSocketClientError.swift.twig', + 'minify' => false, + ], + // Config for project example-swiftui + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/iOS/Info.plist', + 'template' => '/swift/example-swiftui/iOS/Info.plist', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/iOS/ImagePicker+iOS.swift', + 'template' => '/swift/example-swiftui/iOS/ImagePicker+iOS.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/iOS/Keyboard.swift', + 'template' => '/swift/example-swiftui/iOS/Keyboard.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/macOS/ImagePicker+macOS.swift', + 'template' => '/swift/example-swiftui/macOS/ImagePicker+macOS.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/macOS/Info.plist', + 'template' => '/swift/example-swiftui/macOS/Info.plist', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/macOS/macOS.entitlements', + 'template' => '/swift/example-swiftui/macOS/macOS.entitlements', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Shared/Assets.xcassets/AccentColor.colorset/Contents.json', + 'template' => '/swift/example-swiftui/Shared/Assets.xcassets/AccentColor.colorset/Contents.json', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json', + 'template' => '/swift/example-swiftui/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Shared/Assets.xcassets/Contents.json', + 'template' => '/swift/example-swiftui/Shared/Assets.xcassets/Contents.json', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Shared/Image/OSImage.swift', + 'template' => '/swift/example-swiftui/Shared/Image/OSImage.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Shared/ExampleApp.swift', + 'template' => '/swift/example-swiftui/Shared/ExampleApp.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Shared/ExampleView.swift', + 'template' => '/swift/example-swiftui/Shared/ExampleView.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Shared/ExampleViewModel.swift', + 'template' => '/swift/example-swiftui/Shared/ExampleViewModel.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Shared/ImagePicker.swift', + 'template' => '/swift/example-swiftui/Shared/ImagePicker.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', + 'template' => '/swift/example-swiftui/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Example.xcodeproj/project.pbxproj', + 'template' => '/swift/example-swiftui/Example.xcodeproj/project.pbxproj', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata', + 'template' => '/swift/example-swiftui/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Tests iOS/Info.plist', + 'template' => '/swift/example-swiftui/Tests iOS/Info.plist', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Tests iOS/Tests_iOS.swift', + 'template' => '/swift/example-swiftui/Tests iOS/Tests_iOS.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Tests macOS/Info.plist', + 'template' => '/swift/example-swiftui/Tests macOS/Info.plist', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-swiftui/Tests macOS/Tests_macOS.swift', + 'template' => '/swift/example-swiftui/Tests macOS/Tests_macOS.swift', + 'minify' => false, + ], + // Config for project example-uikit + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample/Assets.xcassets/AccentColor.colorset/Contents.json', + 'template' => '/swift/example-uikit/UIKitExample/Assets.xcassets/AccentColor.colorset/Contents.json', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json', + 'template' => '/swift/example-uikit/UIKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample/Assets.xcassets/Contents.json', + 'template' => '/swift/example-uikit/UIKitExample/Assets.xcassets/Contents.json', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample/Base.lproj/LaunchScreen.storyboard', + 'template' => '/swift/example-uikit/UIKitExample/Base.lproj/LaunchScreen.storyboard', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample/Base.lproj/Main.storyboard', + 'template' => '/swift/example-uikit/UIKitExample/Base.lproj/Main.storyboard', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample/AppDelegate.swift', + 'template' => '/swift/example-uikit/UIKitExample/AppDelegate.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample/ImagePicker.swift', + 'template' => '/swift/example-uikit/UIKitExample/ImagePicker.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample/Info.plist', + 'template' => '/swift/example-uikit/UIKitExample/Info.plist', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample/SceneDelegate.swift', + 'template' => '/swift/example-uikit/UIKitExample/SceneDelegate.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample/ViewController.swift', + 'template' => '/swift/example-uikit/UIKitExample/ViewController.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved', + 'template' => '/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', + 'template' => '/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample.xcodeproj/project.pbxproj', + 'template' => '/swift/example-uikit/UIKitExample.xcodeproj/project.pbxproj', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata', + 'template' => '/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExampleTests/Info.plist', + 'template' => '/swift/example-uikit/UIKitExampleTests/Info.plist', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExampleTests/UIKitExampleTests.swift', + 'template' => '/swift/example-uikit/UIKitExampleTests/UIKitExampleTests.swift', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExampleUITests/Info.plist', + 'template' => '/swift/example-uikit/UIKitExampleUITests/Info.plist', + 'minify' => false, + ], + [ + 'scope' => 'default', + 'destination' => '/example-uikit/UIKitExampleUITests/UIKitExampleUITests.swift', + 'template' => '/swift/example-uikit/UIKitExampleUITests/UIKitExampleUITests.swift', + 'minify' => false, + ], + ]); + } +} \ No newline at end of file diff --git a/src/SDK/SDK.php b/src/SDK/SDK.php index dea9b17ed..eb1cc384a 100644 --- a/src/SDK/SDK.php +++ b/src/SDK/SDK.php @@ -160,6 +160,13 @@ public function __construct(Language $language, Spec $spec) } return implode("\n", $value); }, ['is_safe' => ['html']])); + $this->twig->addFilter(new TwigFilter('swiftComment', function ($value) { + $value = explode("\n", $value); + foreach ($value as $key => $line) { + $value[$key] = " /// " . wordwrap($value[$key], 75, "\n /// "); + } + return implode("\n", $value); + }, ['is_safe' => ['html']])); $this->twig->addFilter(new TwigFilter('escapeDollarSign', function ($value) { return str_replace('$', '\$', $value); }, ['is_safe'=>['html']])); diff --git a/templates/swift/.github/workflows/publish.yml b/templates/swift/.github/workflows/publish.yml new file mode 100644 index 000000000..fa98b6b18 --- /dev/null +++ b/templates/swift/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +name: Publish to Cocoapods + +on: + release: + types: [released] + +jobs: + pod: + name: Lint and publish to Cocoapods + runs-on: macos-latest + steps: + - name: Setup Swift + uses: fwal/setup-swift@v1 + with: + swift-version: "5.4" + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install cocoapods + run: gem install cocoapods + + - name: Set library version from tag + run: echo "POD_LIB_VERSION=${{github.event.release.tag_name }}" >> $GITHUB_ENV + + - name: Deploy to Cocoapods + run: | + set -eo pipefail + pod install + pod lib lint --allow-warnings + pod trunk push --allow-warnings + env: + COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} \ No newline at end of file diff --git a/templates/swift/.travis.yml b/templates/swift/.travis.yml new file mode 100644 index 000000000..567f78035 --- /dev/null +++ b/templates/swift/.travis.yml @@ -0,0 +1,13 @@ +language: swift +osx_image: xcode12.5 +before_install: + - gem install cocoapods + - pod install + +jobs: + include: + - stage: Pod Lint + script: + - pod lib lint --allow-warnings + on: + tags: true \ No newline at end of file diff --git a/templates/swift/Package.swift.twig b/templates/swift/Package.swift.twig index 4e1471f4c..4b5d36f08 100644 --- a/templates/swift/Package.swift.twig +++ b/templates/swift/Package.swift.twig @@ -1,35 +1,37 @@ // swift-tools-version:5.1 -// The swift-tools-version declares the minimum version of Swift required to build this package. -// -// Created by Armino -// GitHub: https://github.com/armino-dev/sdk-generator -// import PackageDescription let package = Package( name: "{{spec.title | caseUcfirst}}", products: [ - // Products define the executables and libraries produced by a package, - // and make them visible to other packages. .library( name: "{{spec.title | caseUcfirst}}", - targets: ["{{spec.title | caseUcfirst}}"]), + targets: ["{{spec.title | caseUcfirst}}", "{{spec.title | caseUcfirst}}Models"] + ), ], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-nio.git", from: "2.32.0") ], targets: [ - // Targets are the basic building blocks of a package. - // A target can define a module or a test suite. - // Targets can depend on other targets in this package, - // and on products in packages which this package depends on. .target( name: "{{spec.title | caseUcfirst}}", - dependencies: []), + dependencies: [ + .product(name: "AsyncHTTPClient", package: "async-http-client"), + .product(name: "NIOWebSocket", package: "swift-nio"), + "{{spec.title | caseUcfirst}}Models" + ] + ), + .target( + name: "{{spec.title | caseUcfirst}}Models" + ), .testTarget( name: "{{spec.title | caseUcfirst}}Tests", - dependencies: [{{spec.title | caseUcfirst}}]), - ] -) + dependencies: [ + "{{ spec.title | caseUcfirst }}" + ] + ) + ], + swiftLanguageVersions: [.v5] +) \ No newline at end of file diff --git a/templates/swift/README.md.twig b/templates/swift/README.md.twig index bbf8c34a0..181ef35e9 100644 --- a/templates/swift/README.md.twig +++ b/templates/swift/README.md.twig @@ -1,6 +1,7 @@ # {{ spec.title }} {{sdk.name}} SDK -![License](https://img.shields.io/github/license/{{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }}.svg?style=flat-square) +![Swift Package Manager](https://img.shields.io/github/v/release/{{ sdk.gitUserName }}/{{ sdk.gitRepoName }}.svg?color=green&style=flat-square) +![License](https://img.shields.io/github/license/{{ sdk.gitUserName | url_encode }}/{{ sdk.gitRepoName|url_encode }}.svg?style=flat-square) ![Version](https://img.shields.io/badge/api%20version-{{ spec.version|url_encode }}-blue.svg?style=flat-square) [![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator) {% if sdk.twitterHandle %} @@ -22,10 +23,36 @@ ## Installation +### Xcode with Swift Package Manager + +The Appwrite Swift SDK is available via multiple package managers, including Swift Package Manager. In order to use the Appwrite Swift SDK from Xcode, select File > Swift Packages > **Add Package Dependency** + +In the dialog that appears, enter the Appwrite Swift SDK [package URL]({{ sdk.gitRepo }}) and click **Next**. + +Once the repository information is loaded, add your version rules and click **Next** again. + +On the final screen, make sure you see `Appwrite` as a product selected for your target: + +### Swift Package Manager + +Add the package to your `Package.swift` dependencies: + +```swift + dependencies: [ + .package(url: "{{ sdk.gitRepo }}", from: "{{ sdk.version }}"), + ], ``` - git clone {{ sdk.gitUserName|url_encode }}/{{ sdk.gitRepoName|url_encode }} - cd {{ sdk.gitRepoName }} - swift run + +Then add it to your target: + +```swift + targets: [ + .target( + name: "YourAppTarget", + dependencies: [ + .product(name: "{{ spec.name | caseUcfirst }}", package: "{{ sdk.gitRepoName }}") + ] + ), ``` {% if sdk.gettingStarted %} @@ -33,6 +60,10 @@ {{ sdk.gettingStarted|raw }} {% endif %} +## Contribution + +This library is auto-generated by Appwrite custom [SDK Generator](https://github.com/appwrite/sdk-generator). To learn more about how you can help us improve this SDK, please check the [contribution guide](https://github.com/appwrite/sdk-generator/blob/master/CONTRIBUTING.md) before sending a pull-request. + ## License -Please see the [{{spec.licenseName}} license]({{spec.licenseURL}}) file for more information. +Please see the [{{spec.licenseName}} license]({{spec.licenseURL}}) file for more information. \ No newline at end of file diff --git a/templates/swift/Sources/Client.swift.twig b/templates/swift/Sources/Client.swift.twig index 90cf9a26e..c18c8f079 100644 --- a/templates/swift/Sources/Client.swift.twig +++ b/templates/swift/Sources/Client.swift.twig @@ -1,201 +1,492 @@ -// -// Client.swift -// -// Created by Armino -// GitHub: https://github.com/armino-dev/sdk-generator -// - +import NIO +import NIOCore +import NIOFoundationCompat +import NIOSSL import Foundation +import AsyncHTTPClient +@_exported import {{spec.title | caseUcfirst}}Models + +let DASHDASH = "--" +let CRLF = "\r\n" open class Client { // MARK: Properties - open var selfSigned = false + open var endPoint = "{{spec.endpoint}}" - open var endpoint = "{{spec.endpoint}}" + open var endPointRealtime: String? = nil open var headers: [String: String] = [ - "content-type": "", - "x-sdk-version": "{{spec.title | caseDash}}:{{ language.name | caseLower }}:{{ sdk.version }}"{% if spec.global.defaultHeaders | length > 0 %},{% endif %} - + "content-type": "", + "x-sdk-version": "{{spec.title | caseDash}}:{{ language.name | caseLower }}:{{ sdk.version }}"{% if spec.global.defaultHeaders | length > 0 %},{% endif %} {% for key,header in spec.global.defaultHeaders %} - "{{key}}" : "{{header}}"{% if not loop.last %},{% endif %} + "{{key}}": "{{header}}"{% if not loop.last %},{% endif %} {% endfor %} - + ] + open var config: [String: String] = [:] + + open var http: HTTPClient + + private static let boundaryChars = + "abcdefghijklmnopqrstuvwxyz1234567890" + + private static var eventLoopGroupProvider = + HTTPClient.EventLoopGroupProvider.createNew // MARK: Methods - // default constructor public init() { + http = Client.createHTTP() + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + addUserAgent() + #endif } - {% for header in spec.global.headers %} + private static func createHTTP( + selfSigned: Bool = false, + maxRedirects: Int = 5, + alloweRedirectCycles: Bool = false, + connectTimeout: TimeAmount = .seconds(30), + readTimeout: TimeAmount = .seconds(30) + ) -> HTTPClient { + let timeout = HTTPClient.Configuration.Timeout( + connect: connectTimeout, + read: readTimeout + ) + let redirect = HTTPClient.Configuration.RedirectConfiguration.follow( + max: 5, + allowCycles: false + ) + var tls = TLSConfiguration + .makeClientConfiguration() + + if selfSigned { + tls.certificateVerification = .none + } + + return HTTPClient( + eventLoopGroupProvider: eventLoopGroupProvider, + configuration: HTTPClient.Configuration( + tlsConfiguration: tls, + redirectConfiguration: redirect, + timeout: timeout, + decompression: .enabled(limit: .none) + ) + ) + + } + + deinit { + do { + try http.syncShutdown() + } catch { + print(error) + } + } + +{% for header in spec.global.headers %} /// /// Set {{header.key | caseUcfirst}} /// - {% if header.description %} +{% if header.description %} /// {{header.description}} /// - {% endif %} +{% endif %} /// @param String value /// /// @return Client /// - open func set{{header.key | caseUcfirst}}(value: String) -> Client { - - self.addHeader(key: "{{header.name}}", value: value) + open func set{{ header.key | caseUcfirst }}(_ value: String) -> Client { + config["{{ header.key | caseLower }}"] = value + _ = addHeader(key: "{{header.name}}", value: value) return self } - {% endfor %} +{% endfor %} + /// + /// Set self signed /// /// @param Bool status + /// + /// @return Client + /// + open func setSelfSigned(_ status: Bool = true) -> Client { + try! http.syncShutdown() + http = Client.createHTTP(selfSigned: status) + return self + } + + /// + /// Set endpoint + /// + /// @param String endPoint + /// /// @return Client /// - open func setSelfSigned(status: Bool = true) -> Client { + open func setEndpoint(_ endPoint: String) -> Client { + self.endPoint = endPoint + + if (self.endPointRealtime == nil && endPoint.starts(with: "http")) { + self.endPointRealtime = endPoint + .replacingOccurrences(of: "http://", with: "ws://") + .replacingOccurrences(of: "https://", with: "wss://") + } - self.selfSigned = status return self } /// - /// @param String endpoint + /// Set realtime endpoint. + /// + /// @param String endPoint + /// /// @return Client /// - open func setEndpoint(endpoint: String) -> Client { + open func setEndpointRealtime(_ endPoint: String) -> Client { + self.endPointRealtime = endPoint - self.endpoint = endpoint return self } + /// + /// Add header /// /// @param String key /// @param String value /// + /// @return Client + /// open func addHeader(key: String, value: String) -> Client { - - self.headers[key.lowercased()] = value - + self.headers[key] = value return self } - /// - open func httpBuildQuery(params: [String: Any], prefix: String = "") -> String { - var output: String = "" - for (key, value) in params { - let finalKey: String = prefix.isEmpty ? key : (prefix + "[" + key + "]") - if (value is AnyCollection) { - output += self.httpBuildQuery(params: value as! [String : Any], prefix: finalKey) - } else { - output += "\(value)" - } - output += "&" - } - return output - } + /// + /// Builds a query string from parameters + /// + /// @param Dictionary params + /// @param String prefix + /// + /// @return String + /// + open func parametersToQueryString(params: [String: Any?]) -> String { + var output: String = "" + + func appendWhenNotLast(_ index: Int, ofTotal count: Int, outerIndex: Int? = nil, outerCount: Int? = nil) { + if (index != count - 1 || (outerIndex != nil + && outerCount != nil + && index == count - 1 + && outerIndex! != outerCount! - 1)) { + output += "&" + } + } + + for (parameterIndex, element) in params.enumerated() { + switch element.value { + case nil: + break + case is Array: + let list = element.value as! Array + for (nestedIndex, item) in list.enumerated() { + output += "\(element.key)[]=\(item!)" + appendWhenNotLast(nestedIndex, ofTotal: list.count, outerIndex: parameterIndex, outerCount: params.count) + } + appendWhenNotLast(parameterIndex, ofTotal: params.count) + default: + output += "\(element.key)=\(element.value!)" + appendWhenNotLast(parameterIndex, ofTotal: params.count) + } + } + + return output.addingPercentEncoding( + withAllowedCharacters: .urlHostAllowed + ) ?? "" + } /// /// Make an API call /// /// @param String method /// @param String path - /// @param Array params - /// @param Array headers - /// @return Array|String + /// @param Dictionary params + /// @param Dictionary headers + /// @return Response /// @throws Exception /// - func call(method:String, path:String = "", headers:[String: String] = [:], params:[String: Any] = [:]) -> Any { + open func call( + method: String, + path: String = "", + headers: [String: String] = [:], + params: [String: Any?] = [:], + sink: ((ByteBuffer) -> Void)? = nil, + convert: (([String: Any]) -> T)? = nil, + completion: ((Result) -> Void)? = nil + ) { + self.headers.merge(headers) { (_, new) in + new + } - self.headers.merge(headers){(_, new) in new} - let targetURL:URL = URL(string: self.endpoint + path + (( method == HTTPMethod.get.rawValue && !params.isEmpty ) ? "?" + httpBuildQuery(params: params) : ""))! + let validParams = params.filter { $0.value != nil } - var query: String = "" + let queryParameters = method == "GET" && !validParams.isEmpty + ? "?" + parametersToQueryString(params: validParams) + : "" - var responseStatus: Int = HTTPStatus.unknown.rawValue - var responseType: String = "" - var responseBody: Any = "" + let targetURL = URL(string: endPoint + path + queryParameters)! - switch (self.headers["content-type"]) { - case "application/json": - do { - let json = try JSONSerialization.data(withJSONObject:params, options: []) - query = String( data: json, encoding: String.Encoding.utf8)! - } catch { - print("Failed to parse json: \(error.localizedDescription)") - } - break - default: - query = self.httpBuildQuery(params: params) - break + var request: HTTPClient.Request + do { + request = try HTTPClient.Request( + url: targetURL, + method: .RAW(value: method) + ) + } catch { + completion?(Result.failure({{ spec.title | caseUcfirst }}Error(message: error.localizedDescription))) + return + } + + addHeaders(to: &request) + request.addDomainCookies() + + if "GET" == method { + execute(request, convert: convert, completion: completion) + return } - var request = URLRequest(url: targetURL) - let session = URLSession.shared + do { + try buildBody(for: &request, with: validParams) + } catch let error { + completion?(Result.failure({{ spec.title | caseUcfirst}}Error(message: error.localizedDescription))) + return + } + execute(request, withSink: sink, convert: convert, completion: completion) + } + + private func addHeaders(to request: inout HTTPClient.Request) { for (key, value) in self.headers { - request.setValue(value, forHTTPHeaderField: key) + request.headers.add(name: key, value: value) } + } - request.httpMethod = method - if (method.uppercased() == "POST") { - request.httpBody = query.data(using: .utf8) + private func buildBody( + for request: inout HTTPClient.Request, + with params: [String: Any?] + ) throws { + if request.headers["content-type"][0] == "multipart/form-data" { + buildMultipart(&request, with: params) + } else { + try buildJSON(&request, with: params) } + } - let semaphore = DispatchSemaphore(value: 0) + private func execute( + _ request: HTTPClient.Request, + withSink bufferSink: ((ByteBuffer) -> Void)? = nil, + convert: (([String: Any]) -> T)? = nil, + completion: ((Result) -> Void)? = nil + ) { + if bufferSink == nil { + http.execute( + request: request, + delegate: ResponseAccumulator(request: request) + ).futureResult.whenComplete( { result in + complete(with: result) + }) + return + } - session.dataTask(with: request) { data, response, error in - if (error != nil) { - print(error!) + http.execute( + request: request, + delegate: StreamingDelegate(request: request, sink: bufferSink) + ).futureResult.whenComplete { result in + complete(with: result) + } + + func complete(with result: Result) { + guard let completion = completion else { return } - do { - let httpResponse = response as! HTTPURLResponse - responseStatus = httpResponse.statusCode - if (responseStatus == HTTPStatus.internalServerError.rawValue) { - print(responseStatus) - return + switch result { + case .failure(let error): print(error) + case .success(var response): + switch response.status.code { + case 0..<400: + if response.cookies.count > 0 { + UserDefaults.standard.set( + try! response.cookies.toJson(), + forKey: "\(response.host)-cookies" + ) + } + switch T.self { + case is Bool.Type: + completion(.success(true as! T)) + case is ByteBuffer.Type: + completion(.success(response.body! as! T)) + default: + if response.body == nil { + completion(.success(true as! T)) + return + } + let dict = try! JSONSerialization + .jsonObject(with: response.body!) as? [String: Any] + + completion(.success(convert?(dict!) ?? dict! as! T)) + } + default: + var message = "" + + do { + let dict = try JSONSerialization + .jsonObject(with: response.body!) as? [String: Any] + + message = dict?["message"] as? String + ?? response.status.reasonPhrase + } catch { + message = response.body!.readString(length: response.body!.readableBytes)! + } + + let error = AppwriteError( + message: message, + code: Int(response.status.code) + ) + + completion(.failure(error)) } + } + } + } + + private func randomBoundary() -> String { + var string = "" + for _ in 0..<16 { + string.append(Client.boundaryChars.randomElement()!) + } + return string + } - responseType = httpResponse.mimeType ?? "" + private func buildJSON( + _ request: inout HTTPClient.Request, + with params: [String: Any?] = [:] + ) throws { + let json = try JSONSerialization.data(withJSONObject: params, options: []) - if (responseType == "application/json") { - let json = try JSONSerialization.jsonObject(with: data!, options: []) - responseBody = json - } else { - responseBody = String(data: data!, encoding: String.Encoding.utf8)! - } - } catch { - print(error) + request.body = .data(json) + } + + private func buildMultipart( + _ request: inout HTTPClient.Request, + with params: [String: Any?] = [:] + ) { + func addPart(name: String, value: Any) { + bodyBuffer.writeString(DASHDASH) + bodyBuffer.writeString(boundary) + bodyBuffer.writeString(CRLF) + bodyBuffer.writeString("Content-Disposition: form-data; name=\"\(name)\"") + + if let file = value as? File { + bodyBuffer.writeString("; filename=\"\(file.name)\"") + bodyBuffer.writeString(CRLF) + bodyBuffer.writeString("Content-Length: \(bodyBuffer.readableBytes)") + bodyBuffer.writeString(CRLF+CRLF) + bodyBuffer.writeBuffer(&file.buffer) + bodyBuffer.writeString(CRLF) + return } - semaphore.signal() - }.resume() + let string = String(describing: value) + bodyBuffer.writeString(CRLF) + bodyBuffer.writeString("Content-Length: \(string.count)") + bodyBuffer.writeString(CRLF+CRLF) + bodyBuffer.writeString(string) + bodyBuffer.writeString(CRLF) + } + + let boundary = randomBoundary() + var bodyBuffer = ByteBuffer() + + for (key, value) in params { + switch key { + case "file": + addPart(name: key, value: value!) + default: + if let list = value as? [Any] { + for listValue in list { + addPart(name: "\(key)[]", value: listValue) + } + continue + } + addPart(name: key, value: value!) + } + } - _ = semaphore.wait(wallTimeout: .distantFuture) + bodyBuffer.writeString(DASHDASH) + bodyBuffer.writeString(boundary) + bodyBuffer.writeString(DASHDASH) + bodyBuffer.writeString(CRLF) - return responseBody + request.headers.remove(name: "content-type") + request.headers.add(name: "Content-Length", value: bodyBuffer.readableBytes.description) + request.headers.add(name: "Content-Type", value: "multipart/form-data;boundary=\"\(boundary)\"") + request.body = .byteBuffer(bodyBuffer) } + private func addUserAgent() { + let packageInfo = OSPackageInfo.get() + let deviceInfo = OSDeviceInfo() + var device = ""; + var operatingSystem = "" + + #if os(iOS) + let iosinfo = deviceInfo.iOSInfo + device = "\(iosinfo!.modelIdentifier) iOS/\(iosinfo!.systemVersion)"; + operatingSystem = "ios" + #elseif os(tvOS) + let iosinfo = deviceInfo.iOSInfo + device = "\(iosinfo!.systemInfo.machine) tvOS/\(iosinfo!.systemVersion)"; + operatingSystem = "tvos" + #elseif os(watchOS) + let iosinfo = deviceInfo.iOSInfo + device = "\(iosinfo!.systemInfo.machine) watchOS/\(iosinfo!.systemVersion)"; + operatingSystem = "watchos" + #elseif os(macOS) + let macinfo = deviceInfo.macOSInfo + device = "(Macintosh; \(macinfo!.model))" + operatingSystem = "macos" + #elseif os(Linux) + let lininfo = deviceInfo.linuxInfo + device = "(Linux; U; \(lininfo!.id) \(lininfo!.version))" + operatingSystem = "linux" + #elseif os(Windows) + let wininfo = deviceInfo.windowsInfo + device = "(Windows NT; \(wininfo!.computerName))" + operatingSystem = "windows" + #endif + + #if !os(Linux) && !os(Windows) + _ = addHeader( + key: "user-agent", + value: "\(packageInfo.packageName)/\(packageInfo.version) \(device)" + ) + #endif + } } extension Client { public enum HTTPStatus: Int { case unknown = -1 - case ok = 200 case created = 201 case accepted = 202 - case movedPermanently = 301 case found = 302 - case badRequest = 400 case notAuthorized = 401 case paymentRequired = 402 @@ -203,25 +494,7 @@ extension Client { case notFound = 404 case methodNotAllowed = 405 case notAcceptable = 406 - case internalServerError = 500 case notImplemented = 501 } - - public enum HTTPMethod: String { - case get - - case post - case put - case patch - - case delete - - case head - case options - case connect - case trace - } - - } diff --git a/templates/swift/Sources/DeviceInfo/Linux/LinuxDeviceInfo.swift b/templates/swift/Sources/DeviceInfo/Linux/LinuxDeviceInfo.swift new file mode 100644 index 000000000..cd29eae69 --- /dev/null +++ b/templates/swift/Sources/DeviceInfo/Linux/LinuxDeviceInfo.swift @@ -0,0 +1,100 @@ +#if os(Linux) +import Foundation + +class LinuxDeviceInfo : DeviceInfo { + + let name: String + let version: String + let id: String + let idLike: [String] + let versionCodename: String + let versionId: String + let prettyName: String + let buildId: String + let variant: String + let variantId: String + let machineId: String + + internal init( + name: String, + version: String, + id: String, + idLike: [String], + versionCodename: String, + versionId: String, + prettyName: String, + buildId: String, + variant: String, + variantId: String, + machineId: String + ) { + self.name = name + self.version = version + self.id = id + self.idLike = idLike + self.versionCodename = versionCodename + self.versionId = versionId + self.prettyName = prettyName + self.buildId = buildId + self.variant = variant + self.variantId = variantId + self.machineId = machineId + } + + public static func get() -> LinuxDeviceInfo { + let os = getOsRelease() + let lsb = getLsbRelease() + let machineId = getMachineId() + + return LinuxDeviceInfo( + name: os["NAME"] ?? "Linux", + version: os["VERSION"] ?? lsb["LSB_VERSION"] ?? "", + id: os["ID"] ?? lsb["DISTRIB_ID"] ?? "linux", + idLike: os["ID_LIKE"]?.split(separator: " ").map { String($0) } ?? [], + versionCodename: os["VERSION_CODENAME"] ?? lsb["DISTRIB_CODENAME"] ?? "", + versionId: os["VERSION_ID"] ?? lsb["DISTRIB_RELEASE"] ?? "", + prettyName: os["PRETTY_NAME"] ?? lsb["DISTRIB_DESCRIPTION"] ?? "Linux", + buildId: os["BUILD_ID"] ?? "", + variant: os["VARIANT"] ?? "", + variantId: os["VARIANT_ID"] ?? "", + machineId: machineId + ) + } + + private static func getOsRelease() -> [String: String] { + return tryReadKeyValues(path: "/etc/os-release") + } + + private static func getLsbRelease() -> [String: String] { + return tryReadKeyValues(path: "/etc/lsb-release") + } + + private static func getMachineId() -> String { + return tryReadValue(path: "/etc/machine-id")! + } + + private static func tryReadValue(path: String) -> String? { + let url = URL(fileURLWithPath: path) + return try! String(contentsOf: url, encoding: .utf8) + } + + private static func tryReadKeyValues(path: String) -> [String: String] { + let url = URL(fileURLWithPath: path) + let string = try! String(contentsOf: url, encoding: .utf8) + let lines = string.components(separatedBy: .newlines) + + var dict = [String: String]() + for line in lines { + let splits = line.split(separator: "=") + if splits.count > 1 { + let key = String(splits[0]) + let value = String(splits[1]) + + dict[key] = value + } + } + + return dict + } +} +#endif \ No newline at end of file diff --git a/templates/swift/Sources/DeviceInfo/MacOS/CwlSysCtl.swift b/templates/swift/Sources/DeviceInfo/MacOS/CwlSysCtl.swift new file mode 100644 index 000000000..55d1134d1 --- /dev/null +++ b/templates/swift/Sources/DeviceInfo/MacOS/CwlSysCtl.swift @@ -0,0 +1,171 @@ +// +// CwlSysctl.swift +// CwlUtils +// +// Created by Matt Gallagher on 2016/02/03. +// Copyright © 2016 Matt Gallagher ( https://www.cocoawithlove.com ). All rights reserved. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +#if os(macOS) +import Foundation + +/// A "static"-only namespace around a series of functions that operate on buffers returned from the `Darwin.sysctl` function +public struct Sysctl { + /// Possible errors. + public enum Error: Swift.Error { + case unknown + case malformedUTF8 + case invalidSize + case posixError(POSIXErrorCode) + } + + /// Access the raw data for an array of sysctl identifiers. + public static func data(for keys: [Int32]) throws -> [Int8] { + return try keys.withUnsafeBufferPointer() { keysPointer throws -> [Int8] in + // Preflight the request to get the required data size + var requiredSize = 0 + let preFlightResult = Darwin.sysctl(UnsafeMutablePointer(mutating: keysPointer.baseAddress), UInt32(keys.count), nil, &requiredSize, nil, 0) + if preFlightResult != 0 { + throw POSIXErrorCode(rawValue: errno).map { + print($0.rawValue) + return Error.posixError($0) + } ?? Error.unknown + } + + // Run the actual request with an appropriately sized array buffer + let data = Array(repeating: 0, count: requiredSize) + let result = data.withUnsafeBufferPointer() { dataBuffer -> Int32 in + return Darwin.sysctl(UnsafeMutablePointer(mutating: keysPointer.baseAddress), UInt32(keys.count), UnsafeMutableRawPointer(mutating: dataBuffer.baseAddress), &requiredSize, nil, 0) + } + if result != 0 { + throw POSIXErrorCode(rawValue: errno).map { Error.posixError($0) } ?? Error.unknown + } + + return data + } + } + + /// Convert a sysctl name string like "hw.memsize" to the array of `sysctl` identifiers (e.g. [CTL_HW, HW_MEMSIZE]) + public static func keys(for name: String) throws -> [Int32] { + var keysBufferSize = Int(CTL_MAXNAME) + var keysBuffer = Array(repeating: 0, count: keysBufferSize) + try keysBuffer.withUnsafeMutableBufferPointer { (lbp: inout UnsafeMutableBufferPointer) throws in + try name.withCString { (nbp: UnsafePointer) throws in + guard sysctlnametomib(nbp, lbp.baseAddress, &keysBufferSize) == 0 else { + throw POSIXErrorCode(rawValue: errno).map { Error.posixError($0) } ?? Error.unknown + } + } + } + if keysBuffer.count > keysBufferSize { + keysBuffer.removeSubrange(keysBufferSize..(ofType: T.Type, forKeys keys: [Int32]) throws -> T { + let buffer = try data(for: keys) + if buffer.count != MemoryLayout.size { + throw Error.invalidSize + } + return try buffer.withUnsafeBufferPointer() { bufferPtr throws -> T in + guard let baseAddress = bufferPtr.baseAddress else { throw Error.unknown } + return baseAddress.withMemoryRebound(to: T.self, capacity: 1) { $0.pointee } + } + } + + /// Invoke `sysctl` with an array of identifers, interpreting the returned buffer as the specified type. This function will throw `Error.invalidSize` if the size of buffer returned from `sysctl` fails to match the size of `T`. + public static func value(ofType type: T.Type, forKeys keys: Int32...) throws -> T { + return try value(ofType: type, forKeys: keys) + } + + /// Invoke `sysctl` with the specified name, interpreting the returned buffer as the specified type. This function will throw `Error.invalidSize` if the size of buffer returned from `sysctl` fails to match the size of `T`. + public static func value(ofType type: T.Type, forName name: String) throws -> T { + return try value(ofType: type, forKeys: keys(for: name)) + } + + /// Invoke `sysctl` with an array of identifers, interpreting the returned buffer as a `String`. This function will throw `Error.malformedUTF8` if the buffer returned from `sysctl` cannot be interpreted as a UTF8 buffer. + public static func string(for keys: [Int32]) throws -> String { + let optionalString = try data(for: keys).withUnsafeBufferPointer() { dataPointer -> String? in + dataPointer.baseAddress.flatMap { String(validatingUTF8: $0) } + } + guard let s = optionalString else { + throw Error.malformedUTF8 + } + return s + } + + /// Invoke `sysctl` with an array of identifers, interpreting the returned buffer as a `String`. This function will throw `Error.malformedUTF8` if the buffer returned from `sysctl` cannot be interpreted as a UTF8 buffer. + public static func string(for keys: Int32...) throws -> String { + return try string(for: keys) + } + + /// Invoke `sysctl` with the specified name, interpreting the returned buffer as a `String`. This function will throw `Error.malformedUTF8` if the buffer returned from `sysctl` cannot be interpreted as a UTF8 buffer. + public static func string(for name: String) throws -> String { + return try string(for: keys(for: name)) + } + + /// e.g. "MyComputer.local" (from System Preferences -> Sharing -> Computer Name) or + /// "My-Name-iPhone" (from Settings -> General -> About -> Name) + public static var hostName: String { return try! Sysctl.string(for: [CTL_KERN, KERN_HOSTNAME]) } + + /// e.g. "x86_64" or "N71mAP" + /// NOTE: this is *corrected* on iOS devices to fetch hw.model + public static var machine: String { + #if os(iOS) && !arch(x86_64) && !arch(i386) + return try! Sysctl.string(for: [CTL_HW, HW_MODEL]) + #else + return try! Sysctl.string(for: [CTL_HW, HW_MACHINE]) + #endif + } + + /// e.g. "MacPro4,1" or "iPhone8,1" + /// NOTE: this is *corrected* on iOS devices to fetch hw.machine + public static var model: String { + #if os(iOS) && !arch(x86_64) && !arch(i386) + return try! Sysctl.string(for: [CTL_HW, HW_MACHINE]) + #else + return try! Sysctl.string(for: [CTL_HW, HW_MODEL]) + #endif + } + + /// e.g. "8" or "2" + public static var activeCPUs: Int32 { return try! Sysctl.value(ofType: Int32.self, forKeys: [CTL_HW, HW_AVAILCPU]) } + + /// e.g. "15.3.0" or "15.0.0" + public static var osRelease: String { return try! Sysctl.string(for: [CTL_KERN, KERN_OSRELEASE]) } + + /// e.g. "Darwin" or "Darwin" + public static var osType: String { return try! Sysctl.string(for: [CTL_KERN, KERN_OSTYPE]) } + + /// e.g. "15D21" or "13D20" + public static var osVersion: String { return try! Sysctl.string(for: [CTL_KERN, KERN_OSVERSION]) } + + /// e.g. "Darwin Kernel Version 15.3.0: Thu Dec 10 18:40:58 PST 2015; root:xnu-3248.30.4~1/RELEASE_X86_64" or + /// "Darwin Kernel Version 15.0.0: Wed Dec 9 22:19:38 PST 2015; root:xnu-3248.31.3~2/RELEASE_ARM64_S8000" + public static var version: String { return try! Sysctl.string(for: [CTL_KERN, KERN_VERSION]) } + + #if os(macOS) + /// e.g. 199506 (not available on iOS) + public static var osRev: Int32 { return try! Sysctl.value(ofType: Int32.self, forKeys: [CTL_KERN, KERN_OSREV]) } + + /// e.g. 2659000000 (not available on iOS) + public static var cpuFreq: Int64 { return try! Sysctl.value(ofType: Int64.self, forName: "hw.cpufrequency") } + + /// e.g. 25769803776 (not available on iOS) + public static var memSize: UInt64 { return try! Sysctl.value(ofType: UInt64.self, forKeys: [CTL_HW, HW_MEMSIZE]) } + #endif +} +#endif diff --git a/templates/swift/Sources/DeviceInfo/MacOS/MacOSDeviceInfo.swift b/templates/swift/Sources/DeviceInfo/MacOS/MacOSDeviceInfo.swift new file mode 100644 index 000000000..fe9610a12 --- /dev/null +++ b/templates/swift/Sources/DeviceInfo/MacOS/MacOSDeviceInfo.swift @@ -0,0 +1,17 @@ +#if os(macOS) +import Foundation + +class MacOSDeviceInfo : DeviceInfo { + let computerName = Sysctl.hostName + let hostName = Sysctl.osType + let arch = Sysctl.machine + let model = Sysctl.model + let kernelVersion = Sysctl.version + let osRelease = Sysctl.osRelease + let activeCPUs = Sysctl.activeCPUs + + public static func get() -> MacOSDeviceInfo { + return MacOSDeviceInfo() + } +} +#endif diff --git a/templates/swift/Sources/DeviceInfo/OSDeviceInfo.swift b/templates/swift/Sources/DeviceInfo/OSDeviceInfo.swift new file mode 100644 index 000000000..09509e110 --- /dev/null +++ b/templates/swift/Sources/DeviceInfo/OSDeviceInfo.swift @@ -0,0 +1,29 @@ +import Foundation + +protocol DeviceInfo {} + +class OSDeviceInfo { + + #if os(iOS) || os(watchOS) || os(tvOS) + var iOSInfo: iOSDeviceInfo? + #elseif os(macOS) + var macOSInfo: MacOSDeviceInfo? + #elseif os(Linux) + var linuxInfo: LinuxDeviceInfo? + #elseif os(Windows) + var windowsInfo: WindowsDeviceInfo? + #endif + + init() { + #if os(iOS) || os(watchOS) || os(tvOS) + self.iOSInfo = iOSDeviceInfo.get() + #elseif os(macOS) + self.macOSInfo = MacOSDeviceInfo.get() + #elseif os(Linux) + self.linuxInfo = LinuxDeviceInfo.get() + #elseif os(Windows) + self.windowsInfo = LinuxDeviceInfo.get() + #endif + } +} + diff --git a/templates/swift/Sources/DeviceInfo/Windows/WindowsDeviceInfo.swift b/templates/swift/Sources/DeviceInfo/Windows/WindowsDeviceInfo.swift new file mode 100644 index 000000000..d43b8a9fc --- /dev/null +++ b/templates/swift/Sources/DeviceInfo/Windows/WindowsDeviceInfo.swift @@ -0,0 +1,31 @@ +#if os(Windows) +import Foundation + +class WindowsDeviceInfo : DeviceInfo { + + let numberOfCores: String + let computerName: String + let systemMemoryInMegabytes: UInt64 + + public init( + numberOfCores: String, + computerName: String, + systemMemoryInMegabytes: UInt64 + ) { + self.numberOfCores = numberOfCores + self.computerName = computerName + self.systemMemoryInMegabytes = systemMemoryInMegabytes + } + + public static func get() -> WindowsDeviceInfo { + let memory = ProcessInfo.processInfo.physicalMemory / 1000 / 1000 // Bytes to MB + + return WindowsDeviceInfo( + numberOfCores: ProcessInfo.processInfo.processorCount.description, + computerName: Host.current().localizedName ?? "", + systemMemoryInMegabytes: memory + ) + } +} +#endif + diff --git a/templates/swift/Sources/DeviceInfo/iOS/UIDevice+ModelName.swift b/templates/swift/Sources/DeviceInfo/iOS/UIDevice+ModelName.swift new file mode 100644 index 000000000..18c8e41bd --- /dev/null +++ b/templates/swift/Sources/DeviceInfo/iOS/UIDevice+ModelName.swift @@ -0,0 +1,101 @@ +#if os(iOS) || os(tvOS) || os(watchOS) +import Foundation +import UIKit + +public extension UIDevice { + + static let modelName: String = { + var systemInfo = utsname() + uname(&systemInfo) + let machineMirror = Mirror(reflecting: systemInfo.machine) + let identifier = machineMirror.children.reduce("") { identifier, element in + guard let value = element.value as? Int8, value != 0 else { return identifier } + return identifier + String(UnicodeScalar(UInt8(value))) + } + + func mapToDevice(identifier: String) -> String { // swiftlint:disable:this cyclomatic_complexity + #if os(iOS) + switch identifier { + case "iPod5,1": return "iPod touch (5th generation)" + case "iPod7,1": return "iPod touch (6th generation)" + case "iPod9,1": return "iPod touch (7th generation)" + case "iPhone3,1", "iPhone3,2", "iPhone3,3": return "iPhone 4" + case "iPhone4,1": return "iPhone 4s" + case "iPhone5,1", "iPhone5,2": return "iPhone 5" + case "iPhone5,3", "iPhone5,4": return "iPhone 5c" + case "iPhone6,1", "iPhone6,2": return "iPhone 5s" + case "iPhone7,2": return "iPhone 6" + case "iPhone7,1": return "iPhone 6 Plus" + case "iPhone8,1": return "iPhone 6s" + case "iPhone8,2": return "iPhone 6s Plus" + case "iPhone8,4": return "iPhone SE" + case "iPhone9,1", "iPhone9,3": return "iPhone 7" + case "iPhone9,2", "iPhone9,4": return "iPhone 7 Plus" + case "iPhone10,1", "iPhone10,4": return "iPhone 8" + case "iPhone10,2", "iPhone10,5": return "iPhone 8 Plus" + case "iPhone10,3", "iPhone10,6": return "iPhone X" + case "iPhone11,2": return "iPhone XS" + case "iPhone11,4", "iPhone11,6": return "iPhone XS Max" + case "iPhone11,8": return "iPhone XR" + case "iPhone12,1": return "iPhone 11" + case "iPhone12,3": return "iPhone 11 Pro" + case "iPhone12,5": return "iPhone 11 Pro Max" + case "iPhone12,8": return "iPhone SE (2nd generation)" + case "iPhone13,1": return "iPhone 12 mini" + case "iPhone13,2": return "iPhone 12" + case "iPhone13,3": return "iPhone 12 Pro" + case "iPhone13,4": return "iPhone 12 Pro Max" + case "iPhone14,4": return "iPhone 13 mini" + case "iPhone14,5": return "iPhone 13" + case "iPhone14,2": return "iPhone 13 Pro" + case "iPhone14,3": return "iPhone 13 Pro Max" + case "iPad2,1", "iPad2,2", "iPad2,3", "iPad2,4": return "iPad 2" + case "iPad3,1", "iPad3,2", "iPad3,3": return "iPad (3rd generation)" + case "iPad3,4", "iPad3,5", "iPad3,6": return "iPad (4th generation)" + case "iPad6,11", "iPad6,12": return "iPad (5th generation)" + case "iPad7,5", "iPad7,6": return "iPad (6th generation)" + case "iPad7,11", "iPad7,12": return "iPad (7th generation)" + case "iPad11,6", "iPad11,7": return "iPad (8th generation)" + case "iPad12,1", "iPad12,2": return "iPad (9th generation)" + case "iPad4,1", "iPad4,2", "iPad4,3": return "iPad Air" + case "iPad5,3", "iPad5,4": return "iPad Air 2" + case "iPad11,3", "iPad11,4": return "iPad Air (3rd generation)" + case "iPad13,1", "iPad13,2": return "iPad Air (4th generation)" + case "iPad2,5", "iPad2,6", "iPad2,7": return "iPad mini" + case "iPad4,4", "iPad4,5", "iPad4,6": return "iPad mini 2" + case "iPad4,7", "iPad4,8", "iPad4,9": return "iPad mini 3" + case "iPad5,1", "iPad5,2": return "iPad mini 4" + case "iPad11,1", "iPad11,2": return "iPad mini (5th generation)" + case "iPad14,1", "iPad14,2": return "iPad mini (6th generation)" + case "iPad6,3", "iPad6,4": return "iPad Pro (9.7-inch)" + case "iPad7,3", "iPad7,4": return "iPad Pro (10.5-inch)" + case "iPad8,1", "iPad8,2", "iPad8,3", "iPad8,4": return "iPad Pro (11-inch) (1st generation)" + case "iPad8,9", "iPad8,10": return "iPad Pro (11-inch) (2nd generation)" + case "iPad13,4", "iPad13,5", "iPad13,6", "iPad13,7": return "iPad Pro (11-inch) (3rd generation)" + case "iPad6,7", "iPad6,8": return "iPad Pro (12.9-inch) (1st generation)" + case "iPad7,1", "iPad7,2": return "iPad Pro (12.9-inch) (2nd generation)" + case "iPad8,5", "iPad8,6", "iPad8,7", "iPad8,8": return "iPad Pro (12.9-inch) (3rd generation)" + case "iPad8,11", "iPad8,12": return "iPad Pro (12.9-inch) (4th generation)" + case "iPad13,8", "iPad13,9", "iPad13,10", "iPad13,11":return "iPad Pro (12.9-inch) (5th generation)" + case "AppleTV5,3": return "Apple TV" + case "AppleTV6,2": return "Apple TV 4K" + case "AudioAccessory1,1": return "HomePod" + case "AudioAccessory5,1": return "HomePod mini" + case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "iOS"))" + default: return identifier + } + #elseif os(tvOS) + switch identifier { + case "AppleTV5,3": return "Apple TV 4" + case "AppleTV6,2": return "Apple TV 4K" + case "i386", "x86_64": return "Simulator \(mapToDevice(identifier: ProcessInfo().environment["SIMULATOR_MODEL_IDENTIFIER"] ?? "tvOS"))" + default: return identifier + } + #endif + } + + return mapToDevice(identifier: identifier) + }() + +} +#endif diff --git a/templates/swift/Sources/DeviceInfo/iOS/iOSDeviceInfo.swift b/templates/swift/Sources/DeviceInfo/iOS/iOSDeviceInfo.swift new file mode 100644 index 000000000..c091e8d74 --- /dev/null +++ b/templates/swift/Sources/DeviceInfo/iOS/iOSDeviceInfo.swift @@ -0,0 +1,47 @@ +#if os(iOS) || os(tvOS) || os(watchOS) +import Foundation +import UIKit + +class iOSDeviceInfo : DeviceInfo { + + let name: String + let systemName: String + let systemVersion: String + let model: String + let localizedModel: String + let identifierForVendor: String + let modelIdentifier: String + + internal init( + name: String, + systemName: String, + systemVersion: String, + model: String, + localizedModel: String, + identifierForVendor: String, + modelIdentifier: String + ) { + self.name = name + self.systemName = systemName + self.systemVersion = systemVersion + self.model = model + self.localizedModel = localizedModel + self.identifierForVendor = identifierForVendor + self.modelIdentifier = modelIdentifier + } + + public static func get() -> iOSDeviceInfo { + let device = UIDevice.current + + return iOSDeviceInfo( + name: device.name, + systemName: device.systemName, + systemVersion: device.systemVersion, + model: device.model, + localizedModel: device.localizedModel, + identifierForVendor: device.identifierForVendor?.uuidString ?? "", + modelIdentifier: UIDevice.modelName + ) + } +} +#endif diff --git a/templates/swift/Sources/Extensions/Codable+JSON.swift.twig b/templates/swift/Sources/Extensions/Codable+JSON.swift.twig new file mode 100644 index 000000000..523eb8355 --- /dev/null +++ b/templates/swift/Sources/Extensions/Codable+JSON.swift.twig @@ -0,0 +1,27 @@ +import Foundation + +let jsonEncoder = JSONEncoder() +let jsonDecoder = JSONDecoder() + +extension Encodable { + func toJson() throws -> String { + return String(data: try jsonEncoder.encode(self), encoding: .utf8)! + } +} + +extension String { + func fromJson(to model: T.Type) throws -> T { + return try jsonDecoder.decode(model, from: self.data(using: .utf8)!) + } +} + +protocol JsonConvert : Encodable { + func jsonCast() -> T +} + +extension JsonConvert { + func jsonCast(to model: T.Type) throws -> T { + let string = try (self as Encodable).toJson() + return try string.fromJson(to: model) + } +} \ No newline at end of file diff --git a/templates/swift/Sources/Extensions/Cookie+Codable.swift.twig b/templates/swift/Sources/Extensions/Cookie+Codable.swift.twig new file mode 100644 index 000000000..8f0d329d5 --- /dev/null +++ b/templates/swift/Sources/Extensions/Cookie+Codable.swift.twig @@ -0,0 +1,42 @@ +import Foundation +import AsyncHTTPClient + +extension HTTPClient.Cookie : Codable { + + enum CodingKeys: String, CodingKey { + case name + case value + case path + case domain + case expires + case maxAge + case httpOnly + case secure + } + + public init(from decoder: Decoder) throws { + self.init(name: "", value: "") + + let values = try decoder.container(keyedBy: CodingKeys.self) + name = try values.decode(String.self, forKey: .name) + value = try values.decode(String.self, forKey: .value) + path = try values.decodeIfPresent(String.self, forKey: .path) ?? "" + domain = try values.decodeIfPresent(String.self, forKey: .domain) ?? "" + expires = try values.decodeIfPresent(Date.self, forKey: .expires) ?? Date() + maxAge = try values.decodeIfPresent(Int.self, forKey: .maxAge) ?? nil + httpOnly = try values.decodeIfPresent(Bool.self, forKey: .httpOnly) ?? false + secure = try values.decodeIfPresent(Bool.self, forKey: .secure) ?? false + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(value, forKey: .value) + try container.encodeIfPresent(path, forKey: .path) + try container.encodeIfPresent(domain, forKey: .domain) + try container.encodeIfPresent(expires, forKey: .expires) + try container.encodeIfPresent(maxAge, forKey: .maxAge) + try container.encodeIfPresent(httpOnly, forKey: .httpOnly) + try container.encodeIfPresent(secure, forKey: .secure) + } +} diff --git a/templates/swift/Sources/Extensions/HTTPClientRequest+Cookies.swift.twig b/templates/swift/Sources/Extensions/HTTPClientRequest+Cookies.swift.twig new file mode 100644 index 000000000..40176c040 --- /dev/null +++ b/templates/swift/Sources/Extensions/HTTPClientRequest+Cookies.swift.twig @@ -0,0 +1,22 @@ +import AsyncHTTPClient +import Foundation +import NIO +import NIOHTTP1 + +extension HTTPClient.Request { + public mutating func addDomainCookies() { + headers.addDomainCookies(for: url.host!) + } +} + +extension HTTPHeaders { + public mutating func addDomainCookies(for domain: String) { + let cookieJson = UserDefaults.standard.string(forKey: "\(domain)-cookies") + let cookies: [HTTPClient.Cookie?]? = try? cookieJson?.fromJson(to: [HTTPClient.Cookie].self) + ?? [(try? cookieJson?.fromJson(to: HTTPClient.Cookie.self))] + + if let authCookie = cookies?.first(where: { $0?.name.starts(with: "a_session_") == true } ) { + add(name: "cookie", value: "\(authCookie!.name)=\(authCookie!.value)") + } + } +} \ No newline at end of file diff --git a/templates/swift/Sources/Models/Error.swift.twig b/templates/swift/Sources/Models/Error.swift.twig new file mode 100644 index 000000000..d79340cdb --- /dev/null +++ b/templates/swift/Sources/Models/Error.swift.twig @@ -0,0 +1,12 @@ +import Foundation + +open class {{ spec.title | caseUcfirst}}Error : Swift.Error, Decodable { + + public let message: String + public let code: Int? + + init(message: String, code: Int? = nil) { + self.message = message + self.code = code + } +} diff --git a/templates/swift/Sources/Models/File.swift.twig b/templates/swift/Sources/Models/File.swift.twig new file mode 100644 index 000000000..46c355d1d --- /dev/null +++ b/templates/swift/Sources/Models/File.swift.twig @@ -0,0 +1,13 @@ +import Foundation +import NIO + +open class File { + + public let name: String + public var buffer: ByteBuffer + + public init(name: String, buffer: ByteBuffer) { + self.name = name + self.buffer = buffer + } +} diff --git a/templates/swift/Sources/Models/Model.swift.twig b/templates/swift/Sources/Models/Model.swift.twig new file mode 100644 index 000000000..245ab7002 --- /dev/null +++ b/templates/swift/Sources/Models/Model.swift.twig @@ -0,0 +1,75 @@ +{% macro sub_schema(property) %}{% if property.sub_schema %}{% if property.type == 'array' %}[{{property.sub_schema | caseUcfirst}}]{% else %}{{property.sub_schema | caseUcfirst}}{% endif %}{% else %}{{property.type | typeName}}{% endif %}{% endmacro %} + +/// {{ definition.description }} +public class {{ definition.name | caseUcfirst }} { + +{% for property in definition.properties %} + /// {{ property.description }} + public let {{ property.name | escapeKeyword | removeDollarSign }}: {{_self.sub_schema(property)}}{% if not property.required %}?{% endif %} + + +{% endfor %} +{% if definition.additionalProperties %} + let data: [String: Any] + +{% endif %} + init( +{% for property in definition.properties %} + {{ property.name | escapeKeyword | removeDollarSign }}: {{_self.sub_schema(property)}}{% if not property.required %}? = {{ property.default }}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + +{% endfor %} +{% if definition.additionalProperties %} + data: [String: Any] +{% endif %} + ) { +{% for property in definition.properties %} + self.{{ property.name | escapeKeyword | removeDollarSign }} = {{ property.name | escapeKeyword | removeDollarSign }} +{% endfor %} +{% if definition.additionalProperties %} + self.data = data +{% endif %} + } + + public static func from(map: [String: Any]) -> {{ definition.name | caseUcfirst }} { + return {{ definition.name | caseUcfirst }}( +{% for property in definition.properties %} + {{ property.name | escapeKeyword | removeDollarSign }}: {% if property.sub_schema %}{% if property.type == 'array' %}(map["{{property.name }}"] as! [[String: Any]]).map { {{property.sub_schema | caseUcfirst}}.from(map: $0) }{% else %}{{property.sub_schema | caseUcfirst}}.from(map: map["{{property.name }}"] as! [String: Any]){% endif %}{% else %}map["{{property.name }}"] as{% if property.required %}!{% else %}?{% endif %} {{ _self.sub_schema(property) }}{% endif %}{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + +{% endfor %} +{% if definition.additionalProperties %} + data: map +{% endif %} + ) + } + + public func toMap() -> [String: Any] { + return [ +{% for property in definition.properties %} + "{{ property.name | escapeKeyword }}": {% if property.sub_schema %}{% if property.type == 'array' %}{{property.name | escapeKeyword | removeDollarSign}}.map { $0.toMap() }{% else %}{{property.name | escapeKeyword | removeDollarSign}}.toMap(){% endif %}{% else %}{{property.name | escapeKeyword | removeDollarSign}}{% endif %} as Any{% if not loop.last or (loop.last and definition.additionalProperties) %},{% endif %} + +{% endfor %} +{% if definition.additionalProperties %} + "data": data +{% endif %} + ] + } +{% if definition.additionalProperties %} + + public func convertTo(fromJson: ([String: Any]) -> T) -> T { + return fromJson(data) + } + {% endif %} + {% for property in definition.properties %} + {% if property.sub_schema %} + {% for def in spec.definitions %} + {% if def.name == property.sub_schema and def.additionalProperties and property.type == 'array' %} + + public func convertTo(fromJson: ([String: Any]) -> T) -> [T] { + {{property.name | removeDollarSign}}.map { d in d.convertTo(fromJson: fromJson) } + } +{% endif %} +{% endfor %} +{% endif %} +{% endfor %} + +} \ No newline at end of file diff --git a/templates/swift/Sources/Models/RealtimeModels.swift.twig b/templates/swift/Sources/Models/RealtimeModels.swift.twig new file mode 100644 index 000000000..f80108cab --- /dev/null +++ b/templates/swift/Sources/Models/RealtimeModels.swift.twig @@ -0,0 +1,41 @@ +import Foundation + +public class RealtimeSubscription { + public var close: () -> Void + + init(close: @escaping () -> Void) { + self.close = close + } +} + +public class RealtimeCallback { + public let channels: Set + public let callback: (RealtimeResponseEvent) -> Void + + init( + for channels: Set, + and callback: @escaping (RealtimeResponseEvent) -> Void + ) { + self.channels = channels + self.callback = callback + } +} + +public class RealtimeResponseEvent { + public let event: String? + public let channels: [String]? + public let timestamp: Int64? + public var payload: [String: Any]? + + init( + event: String, + channels: [String], + timestamp: Int64, + payload: [String: Any] + ) { + self.event = event + self.channels = channels + self.timestamp = timestamp + self.payload = payload + } +} diff --git a/templates/swift/Sources/OAuth/View+OAuth.swift.twig b/templates/swift/Sources/OAuth/View+OAuth.swift.twig new file mode 100644 index 000000000..7d6686364 --- /dev/null +++ b/templates/swift/Sources/OAuth/View+OAuth.swift.twig @@ -0,0 +1,37 @@ +#if os(macOS) +typealias OSApplication = NSApplication +typealias OSViewController = NSViewController +let notificationType = NSApplication.willBecomeActiveNotification +#elseif os(iOS) || os(tvOS) || os(watchOS) +typealias OSApplication = UIApplication +typealias OSViewController = UIViewController +let notificationType = UIApplication.willEnterForegroundNotification +#endif + +#if canImport(SwiftUI) +import SwiftUI +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension View { + public func registerOAuthHandler() -> some View { + onOpenURL { url in + WebAuthComponent.handleIncomingCookie(from: url) + }.onReceive(NotificationCenter.default.publisher(for: notificationType)) { _ in + WebAuthComponent.onCallback() + } + } +} +#endif + +#if canImport(OSViewController) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension OSViewController { + public func registerOAuthHandler() { + #if os(macOS) + typealias OSHostingController = NSHostingController + #elseif os(iOS) || os(tvOS) || os(watchOS) + typealias OSHostingController = UIHostingController + #endif + self.addChild(OSHostingController(rootView: EmptyView().registerOAuthHandler())) + } +} +#endif diff --git a/templates/swift/Sources/OAuth/WebAuthComponent.swift.twig b/templates/swift/Sources/OAuth/WebAuthComponent.swift.twig new file mode 100644 index 000000000..666755767 --- /dev/null +++ b/templates/swift/Sources/OAuth/WebAuthComponent.swift.twig @@ -0,0 +1,102 @@ +import AsyncHTTPClient +import Foundation +import NIO + +#if canImport(SwiftUI) +import SwiftUI +#endif + +/// +/// Used to authenticate with external OAuth2 providers. Launches browser windows and handles +/// suspension until the user completes the process or otherwise returns to the app. +/// +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public class WebAuthComponent { + +#if canImport(SwiftUI) + @Environment(\.openURL) + private static var openURL +#endif + + private static var callbacks = [String: (Result) -> Void]() + + /// + /// Authenticate Session with OAuth2 + /// + /// Launches a chrome custom tab from the given activity and directs to the given url, + /// suspending until the user returns to the app, at which point the given [onComplete] callback + /// will run, passing the callback url from the intent used to launch the [CallbackActivity], + /// or an [IllegalStateException] in the case the user closed the window or returned to the + /// app without passing through the [CallbackActivity]. + /// + /// - Parameters: + /// - url: The url to launch + /// - callbackScheme: The callback url scheme used to key the given callback + /// - onComplete: The callback to run when a result (success or failure) is received + /// + internal static func authenticate( + url: URL, + callbackScheme: String, + onComplete: @escaping (Result) -> Void + ) { + callbacks[callbackScheme] = onComplete + + #if canImport(SwiftUI) + openURL(url) + #endif + } + + /// + /// Handle an incoming cooke from a URL, saving it to use for future requests. + /// + /// - Parameters: + /// - url: The URL containing the cookie + /// + public static func handleIncomingCookie(from url: URL) { + let components = URLComponents(string: url.absoluteString)! + + let cookieParts = [String: String](uniqueKeysWithValues: components.queryItems!.map { + ($0.name, $0.value!) + }) + + var domain = cookieParts["domain"]! + domain.remove(at: domain.startIndex) + + let cookie = HTTPClient.Cookie( + name: cookieParts["key"]!, + value: cookieParts["secret"]! + ) + let cookieJson = try! cookie.toJson() + UserDefaults.standard.set(cookieJson, forKey: "\(domain)-cookies") + + WebAuthComponent.onCallback( + scheme: components.scheme!, + url: components.url! + ) + } + + /// + /// Trigger a web auth callback + /// + /// Attempts to find a callback for the given [scheme] and if found, invokes it, passing the + /// given [url]. Calling this method stops auth suspension, so any calls to [authenticate] + /// will continue execution from their suspension points immediately after this method + /// is called. + /// + /// - Parameters: + /// - scheme: The scheme to match to a callback's key + /// - url: The url received through intent data from the [CallbackActivity] + /// + public static func onCallback(scheme: String? = nil, url: URL? = nil) { + if let scheme = scheme, let url = url { + callbacks.removeValue(forKey: scheme)?(.success(url.absoluteString)) + } + cleanUp() + } + + private static func cleanUp() { + callbacks.forEach { (_, callback) in + callback(.failure({{ spec.title | caseUcfirst}}Error(message: "User cancelled login."))) + } + } +} diff --git a/templates/swift/Sources/PackageInfo/Apple/PackageInfo+Apple.swift b/templates/swift/Sources/PackageInfo/Apple/PackageInfo+Apple.swift new file mode 100644 index 000000000..2138da426 --- /dev/null +++ b/templates/swift/Sources/PackageInfo/Apple/PackageInfo+Apple.swift @@ -0,0 +1,23 @@ +import Foundation + +extension PackageInfo { + + public static func getApplePackage() -> PackageInfo { + let bundle = Bundle.main + + let appName = bundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String + ?? bundle.object(forInfoDictionaryKey: "CFBundleName") as? String + ?? "" + + let packageName = bundle.bundleIdentifier ?? "" + let version = bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "" + let build = bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "" + + return PackageInfo( + appName: appName, + version: version, + buildNumber: build, + packageName: packageName + ) + } +} diff --git a/templates/swift/Sources/PackageInfo/Linux/PackageInfo+Linux.swift b/templates/swift/Sources/PackageInfo/Linux/PackageInfo+Linux.swift new file mode 100644 index 000000000..a12f7edf5 --- /dev/null +++ b/templates/swift/Sources/PackageInfo/Linux/PackageInfo+Linux.swift @@ -0,0 +1,24 @@ +import Foundation + +extension PackageInfo { + + public static func getLinuxPackage() -> PackageInfo { + let version = getVersionJson() + + return PackageInfo( + appName: version["app_name"] as! String, + version: version["version"] as! String, + buildNumber: version["build_number"] as! String, + packageName: "", + buildSignature: "" + ) + } + + private static func getVersionJson() -> [String: Any] { + let exePath = URL(fileURLWithPath: "/proc/self/exe").resolvingSymlinksInPath() + let appPath = exePath.deletingLastPathComponent() + let jsonPath = appPath.appendingPathComponent("version.json") + return try! JSONSerialization + .jsonObject(with: Data(contentsOf: jsonPath)) as! [String: Any] + } +} diff --git a/templates/swift/Sources/PackageInfo/OSPackageInfo.swift b/templates/swift/Sources/PackageInfo/OSPackageInfo.swift new file mode 100644 index 000000000..12d441f2f --- /dev/null +++ b/templates/swift/Sources/PackageInfo/OSPackageInfo.swift @@ -0,0 +1,14 @@ +import Foundation + +class OSPackageInfo { + + public static func get() -> PackageInfo { + #if os(iOS) || os(watchOS) || os(tvOS) || os(macOS) + return PackageInfo.getApplePackage() + #elseif os(Linux) + return PackageInfo.getLinuxPackage() + #elseif os(Windows) + return PackageInfo.getWindowsPackage() + #endif + } +} diff --git a/templates/swift/Sources/PackageInfo/PackageInfo.swift b/templates/swift/Sources/PackageInfo/PackageInfo.swift new file mode 100644 index 000000000..47e8d5a47 --- /dev/null +++ b/templates/swift/Sources/PackageInfo/PackageInfo.swift @@ -0,0 +1,24 @@ +import Foundation + +class PackageInfo { + + let appName: String + let version: String + let buildNumber: String + let packageName: String + let buildSignature: String? + + internal init( + appName: String, + version: String, + buildNumber: String, + packageName: String, + buildSignature: String? = nil + ) { + self.appName = appName + self.version = version + self.buildNumber = buildNumber + self.packageName = packageName + self.buildSignature = buildSignature + } +} diff --git a/templates/swift/Sources/PackageInfo/Windows/PackageInfo+Windows.swift b/templates/swift/Sources/PackageInfo/Windows/PackageInfo+Windows.swift new file mode 100644 index 000000000..2398f4fae --- /dev/null +++ b/templates/swift/Sources/PackageInfo/Windows/PackageInfo+Windows.swift @@ -0,0 +1,13 @@ +import Foundation + +class WindowsPackageInfo : PackageInfo { + + public static func getWindowsPackage() -> PackageInfo { + return WindowsPackageInfo( + appName: "", + version: "", + buildNumber: "", + packageName: "" + ) + } +} diff --git a/templates/swift/Sources/Service.swift.twig b/templates/swift/Sources/Service.swift.twig index b11a067a5..3cf3d97b1 100644 --- a/templates/swift/Sources/Service.swift.twig +++ b/templates/swift/Sources/Service.swift.twig @@ -1,15 +1,8 @@ -// -// Service.swift -// -// Created by Armino -// GitHub: https://github.com/armino-dev/sdk-generator -// - open class Service { - open var client: Client; + internal var client: Client - public init(client: Client) + public init(_ client: Client) { self.client = client } diff --git a/templates/swift/Sources/Services/Realtime.swift.twig b/templates/swift/Sources/Services/Realtime.swift.twig new file mode 100644 index 000000000..76b8f480e --- /dev/null +++ b/templates/swift/Sources/Services/Realtime.swift.twig @@ -0,0 +1,216 @@ +import Foundation +import AsyncHTTPClient +import NIO +import NIOHTTP1 + +open class Realtime : Service { + + private let TYPE_ERROR = "error" + private let TYPE_EVENT = "event" + private let DEBOUNCE_MILLIS = 1 + + private var socketClient: WebSocketClient? = nil + private var activeChannels = Set() + private var activeSubscriptions = [Int: RealtimeCallback]() + + let connectSync = DispatchQueue(label: "ConnectSync") + let callbackSync = DispatchQueue(label: "CallbackSync") + + private var subCallDepth = 0 + private var reconnectAttempts = 0 + private var subscriptionsCounter = 0 + private var reconnect = true + + private func createSocket() { + guard activeChannels.count > 0 else { + return + } + + var queryParams = "project=\(client.config["project"]!)" + + for channel in activeChannels { + queryParams += "&channels[]=\(channel)" + } + + let url = "\(client.endPointRealtime!)/realtime?\(queryParams)" + + if (socketClient != nil) { + reconnect = false + closeSocket() + } else { + socketClient = WebSocketClient(url, delegate: self)! + } + + try! socketClient?.connect() + } + + private func closeSocket() { + socketClient?.close() + //socket?.close(RealtimeCode.POLICY_VIOLATION.value, null) + } + + private func getTimeout() -> Int { + switch reconnectAttempts { + case 0..<5: return 1000 + case 5..<15: return 5000 + case 15..<100: return 10000 + default: return 60000 + } + } + + public func subscribe( + channel: String, + callback: @escaping (RealtimeResponseEvent) -> Void + ) -> RealtimeSubscription { + return subscribe( + channels: [channel], + payloadType: String.self, + callback: callback + ) + } + + public func subscribe( + channels: Set, + callback: @escaping (RealtimeResponseEvent) -> Void + ) -> RealtimeSubscription { + return subscribe( + channels: channels, + payloadType: String.self, + callback: callback + ) + } + + public func subscribe( + channel: String, + payloadType: T.Type, + callback: @escaping (RealtimeResponseEvent) -> Void + ) -> RealtimeSubscription { + return subscribe( + channels: [channel], + payloadType: T.self, + callback: callback + ) + } + + public func subscribe( + channels: Set, + payloadType: T.Type, + callback: @escaping (RealtimeResponseEvent) -> Void + ) -> RealtimeSubscription { + subscriptionsCounter += 1 + let counter = subscriptionsCounter + + channels.forEach { + activeChannels.insert($0) + } + + activeSubscriptions[counter] = RealtimeCallback( + for: Set(channels), + and: callback + ) + + connectSync.sync { + subCallDepth+=1 + } + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(DEBOUNCE_MILLIS)) { + if (self.subCallDepth == 1) { + self.createSocket() + } + self.connectSync.sync { + self.subCallDepth-=1 + } + } + + return RealtimeSubscription { + self.activeSubscriptions[counter] = nil + self.cleanUp(channels: channels) + self.createSocket() + } + } + + func cleanUp(channels: Set) { + activeChannels = activeChannels.filter { channel in + guard channels.contains(channel) else { + return true + } + let subsWithChannel = activeSubscriptions.filter { callback in + return callback.value.channels.contains(channel) + } + return subsWithChannel.isEmpty + } + } +} + +extension Realtime: WebSocketClientDelegate { + + public func onOpen(channel: Channel) { + self.reconnectAttempts = 0 + } + + public func onMessage(text: String) { + let data = Data(text.utf8) + if let json = try! JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { + if let type = json["type"] as? String { + switch type { + case TYPE_ERROR: try! handleResponseError(from: json) + case TYPE_EVENT: handleResponseEvent(from: json) + default: break + } + } + } + } + + public func onClose(channel: Channel, data: Data) { + if (!reconnect) { + reconnect = true + return + } + + let timeout = getTimeout() + + print("Realtime disconnected. Re-connecting in \(timeout / 1000) seconds.") + + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(timeout)) { + self.reconnectAttempts += 1 + self.createSocket() + } + } + + public func onError(error: Swift.Error?, status: HTTPResponseStatus?) { + print(error?.localizedDescription ?? "Unknown error") + } + + func handleResponseError(from json: [String: Any]) throws { + throw {{ spec.title | caseUcfirst }}Error(message: json["message"] as? String ?? "Unknown error") + } + + func handleResponseEvent(from json: [String: Any]) { + guard let data = json["data"] as? [String: Any] else { + return + } + guard let channels = data["channels"] as? Array else { + return + } + guard let payload = data["payload"] as? [String: Any] else { + return + } + guard channels.contains(where: { channel in + activeChannels.contains(channel) + }) else { + return + } + + for subscription in activeSubscriptions { + if channels.contains(where: { subscription.value.channels.contains($0) }) { + let response = RealtimeResponseEvent( + event: data["event"] as! String, + channels: channels, + timestamp: data["timestamp"] as! Int64, + payload: payload + ) + subscription.value.callback(response) + } + } + } +} diff --git a/templates/swift/Sources/Services/Service.swift.twig b/templates/swift/Sources/Services/Service.swift.twig index 816fad25f..fd92b28cd 100644 --- a/templates/swift/Sources/Services/Service.swift.twig +++ b/templates/swift/Sources/Services/Service.swift.twig @@ -1,57 +1,112 @@ +{% macro methodNeedsSecurityParameters(method) %} +{% if (method.type == "webAuth" or method.type == "location") and method.security|length > 0 %}{{ true }}{% else %}{{false}}{% endif %} +{% endmacro %} +{% macro resultType(spec, method) %} +{% if method.type == "webAuth" %}Bool{% elseif method.type == "location" %}ByteBuffer{% elseif not method.responseModel or method.responseModel == 'any' %}Any{% else %}{{spec.title | caseUcfirst}}Models.{{method.responseModel | caseUcfirst}}{% endif %} +{% endmacro %} +import AsyncHTTPClient +import Foundation +import NIO +import {{spec.title | caseUcfirst}}Models - -class {{ service.name | caseUcfirst }}: Service -{ +open class {{ service.name | caseUcfirst }}: Service { {% for method in service.methods %} - /** - * {{ method.title }} - * + /// + /// {{ method.title }} + /// {% if method.description %} -{{ method.description|comment1 }} - * +{{ method.description | swiftComment }} + /// {% endif %} {% for parameter in method.parameters.all %} - * @param {{ parameter.type | typeName | raw}} _{{ parameter.name | caseCamel }} + /// @param {{ parameter.type | typeName | raw}} {{ parameter.name | caseCamel }} {% endfor %} - * @throws Exception - * @return array - */ - - func {{ method.name | caseCamel }}({% for parameter in method.parameters.all %}_{{ parameter.name | caseCamel }}: {{ parameter.type | typeName | raw }}{{ parameter | paramDefault }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Array { - {% if method.parameters.path %}var{% else %}let{% endif %} path: String = "{{ method.path }}" + /// @throws Exception + /// @return array + /// +{% if method.type == "webAuth" %} + @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +{% endif %} + open func {{ method.name | caseCamel }}( +{% for parameter in method.parameters.all %} + {{ parameter.name | caseCamel | escapeKeyword }}: {{ parameter.type | typeName | raw }}{% if not parameter.required %}? = nil{% endif %}, +{% endfor %} + completion: ((Result<{{ _self.resultType(spec, method) }}, {{ spec.title | caseUcfirst}}Error>) -> Void)? = nil + ) { +{% if method.parameters.path %} var{% else %} let{% endif %} path: String = "{{ method.path }}" {% for parameter in method.parameters.path %} path = path.replacingOccurrences( of: "{{ '{' }}{{ parameter.name | caseCamel }}{{ '}' }}", - with: {% if method.parameters.path %}_{{ parameter.name | caseCamel }}{% else %}""{% endif %} - + with: {% if method.parameters.path %}{{ parameter.name | caseCamel | escapeKeyword }}{% else %}""{% endif %} ) -{% endfor %} - - {% if method.parameters.query or method.parameters.body or method.parameters.formData %} - var params: [String: Any] = [:] - {% else %} - let params: [String: Any] = [:] - {% endif %} -{% for parameter in method.parameters.query %} - params["{{ parameter.name }}"] = _{{ parameter.name | caseCamel }} -{% endfor %} -{% for parameter in method.parameters.body %} - params["{{ parameter.name }}"] = _{{ parameter.name | caseCamel }} {% endfor %} -{% for parameter in method.parameters.formData %} - params["{{ parameter.name }}"] = _{{ parameter.name | caseCamel }} +{% if method.parameters.query or method.parameters.body or method.parameters.formData or _self.methodNeedsSecurityParameters(method) %} + let params: [String: Any?] = [ +{% else %} + let params: [String: Any?] = [:] +{% endif %} +{% for parameter in method.parameters.query | merge(method.parameters.body) %} + "{{ parameter.name }}": {{ parameter.name | caseCamel | escapeKeyword }}{% if not loop.last or _self.methodNeedsSecurityParameters(method) %},{% endif %} + {% endfor %} +{% if _self.methodNeedsSecurityParameters(method) %} +{% for node in method.security %} +{% for key,header in node|keys %} + "{{header|caseLower}}": client.config["{{header|caseLower}}"]{% if not loop.last %},{% endif %} - return [self.client.call(method: Client.HTTPMethod.{{ method.method }}.rawValue, path: path, headers: [ -{% for parameter in method.parameters.header %} - "{{ parameter.name }}": {{ parameter.name | caseCamel }}, {% endfor %} -{% for key, header in method.headers %} - "{{ key }}": "{{ header }}", {% endfor %} - ], params: params)]; +{% endif %} +{% if method.parameters.query or method.parameters.body or method.parameters.formData or _self.methodNeedsSecurityParameters(method) %} + ] +{% endif %} + +{% if method.type == 'webAuth' %} + let query = "?\(client.parametersToQueryString(params: params))" + let url = URL(string: client.endPoint + path + query)! + let callbackScheme = "appwrite-callback-\(client.config["project"] ?? "")" + + WebAuthComponent.authenticate(url: url, callbackScheme: callbackScheme) { result in + guard let completion = completion else { + return + } + completion(result.map { _ in true }) + } +{% elseif method.type == 'location' %} + client.call( + method: "{{ method.method | caseUpper }}", + path: path, + params: params, + completion: completion + ) +{% else %} + let headers: [String: String] = [ +{{ method.headers|map((header, key) => " \"#{key}\": \"#{header}\"")|join(',\n')|raw }} + ] + +{% if method.responseModel %} + let convert: ([String: Any]) -> {{ _self.resultType(spec, method) }} = { dict in +{% if method.responseModel == 'any' %} + return dict +{% else %} + return {{ spec.title | caseUcfirst}}Models.{{method.responseModel | caseUcfirst}}.from(map: dict) +{% endif %} + } + +{% endif %} + client.call( + method: "{{ method.method | caseUpper }}", + path: path, + headers: headers, + params: params, +{% if method.responseModel %} + convert: convert, +{% endif %} + completion: completion + ) +{% endif %} } {% endfor %} diff --git a/templates/swift/Sources/StreamingDelegate.swift.twig b/templates/swift/Sources/StreamingDelegate.swift.twig new file mode 100644 index 000000000..c80dc4eda --- /dev/null +++ b/templates/swift/Sources/StreamingDelegate.swift.twig @@ -0,0 +1,124 @@ +import NIO +import NIOHTTP1 +import AsyncHTTPClient + +class StreamingDelegate: HTTPClientResponseDelegate { + + public typealias Response = HTTPClient.Response + + enum State { + case idle + case head(HTTPResponseHead) + case body(HTTPResponseHead) + case end + case error(Swift.Error) + } + + var state = State.idle + let request: HTTPClient.Request + + public struct Progress { + public var totalBytes: Int? + public var transferredBytes: Int + } + + private var progress = Progress(totalBytes: nil, transferredBytes: 0) + + private let reportHead: ((HTTPResponseHead) -> Void)? + private let reportProgress: ((Progress) -> Void)? + private var reportError: ((AppwriteError) -> Void)? + private var sink: ((ByteBuffer) -> Void)? + private var completion: (() -> Void)? + + public init( + request: HTTPClient.Request, + sink: ((ByteBuffer) -> Void)? = nil, + reportHead: ((HTTPResponseHead) -> Void)? = nil, + reportProgress: ((Progress) -> Void)? = nil, + reportError: ((AppwriteError) -> Void)? = nil + ) { + self.request = request + self.sink = sink + self.reportHead = reportHead + self.reportProgress = reportProgress + self.reportError = reportError + } + + func didSendRequestHead(task: HTTPClient.Task, _ head: HTTPRequestHead) { + if let totalBytesString = head.headers.first(name: "Content-Length"), + let totalBytes = Int(totalBytesString) { + progress.totalBytes = totalBytes + } + } + + func didSendRequestPart(task: HTTPClient.Task, _ part: IOData) { + progress.transferredBytes += part.readableBytes + reportProgress?(progress) + } + + func didReceiveHead( + task: HTTPClient.Task, + _ head: HTTPResponseHead + ) -> EventLoopFuture { + switch state { + case .idle: + state = .head(head) + if let totalBytesString = head.headers.first(name: "content-length"), + let totalBytes = Int(totalBytesString) { + progress.totalBytes = totalBytes + } + case .head: + preconditionFailure("Head already set") + case .body: + preconditionFailure("No head received before body") + case .end: + preconditionFailure("Request already processed") + case .error: + break + } + return task.eventLoop.makeSucceededFuture(()) + } + + func didReceiveBodyPart( + task: HTTPClient.Task, + _ part: ByteBuffer + ) -> EventLoopFuture { + switch state { + case .idle: + preconditionFailure("No head received before body") + case .head(let head): + state = .body(head) + case .body(let head): + state = .end + progress.transferredBytes += part.readableBytes + reportProgress?(progress) + sink?(part) + state = .body(head) + case .end: + preconditionFailure("Request already processed") + case .error: + break + } + return task.eventLoop.makeSucceededFuture(()) + } + + func didFinishRequest(task: HTTPClient.Task) throws -> Response { + switch state { + case .idle: + preconditionFailure("No head received before end") + case .head(let head): + return Response(host: request.host, status: head.status, version: head.version, headers: head.headers, body: nil) + case .body(let head): + return Response(host: request.host, status: head.status, version: head.version, headers: head.headers, body: nil) + case .end: + preconditionFailure("Request already processed") + case .error(let error): + throw error + } + } + + func didReceiveError(task: HTTPClient.Task, _ error: Swift.Error) { + state = .error(error) + reportError?(AppwriteError(message: error.localizedDescription)) + } +} \ No newline at end of file diff --git a/templates/swift/Sources/WebSockets/HTTPHandler.swift.twig b/templates/swift/Sources/WebSockets/HTTPHandler.swift.twig new file mode 100644 index 000000000..5a9305cbe --- /dev/null +++ b/templates/swift/Sources/WebSockets/HTTPHandler.swift.twig @@ -0,0 +1,90 @@ +import NIO +import NIOHTTP1 +import Foundation + +/// Handles the HTTP pipeline for opening a WebSocket connection. +/// +/// Adds the required headers to the outbound upgrade connection request and handles success and failures responses. +class HTTPHandler { + + unowned let client: WebSocketClient + + let headers: HTTPHeaders + + init(client: WebSocketClient, headers: HTTPHeaders) { + self.client = client + self.headers = headers + } + + func upgradeFailure(status: HTTPResponseStatus) { + if let delegate = client.delegate { + switch status { + case .badRequest: + try! delegate.onError(error: WebSocketClientError.badRequest, status: status) + case .notFound: + try! delegate.onError(error: WebSocketClientError.notFound, status: status) + default: + break + } + } else { + switch status { + case .badRequest: + client.onError(WebSocketClientError.badRequest, status) + case .notFound: + client.onError(WebSocketClientError.notFound, status) + default: + break + } + } + } +} + +extension HTTPHandler : ChannelInboundHandler, RemovableChannelHandler { + + public typealias InboundIn = HTTPClientResponsePart + public typealias OutboundOut = HTTPClientRequestPart + + func channelActive(context: ChannelHandlerContext) { + var headers = HTTPHeaders() + + headers.add(name: "Host", value: "\(client.host):\(client.port)") + headers.add(name: "Content-Type", value: "text/plain") + headers.add(name: "Content-Length", value: "\(1)") + headers.add(contentsOf: self.headers) + headers.addDomainCookies(for: client.host) + let requestHead = HTTPRequestHead( + version: .http1_1, + method: .GET, + uri: "\(client.uri)?\(client.query)", + headers: headers + ) + + context.write(wrapOutboundOut(.head(requestHead)), promise: nil) + context.write(wrapOutboundOut(.body(.byteBuffer(ByteBuffer(string: "\r\n")))), promise: nil) + context.writeAndFlush(wrapOutboundOut(.end(nil)), promise: nil) + } + + func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let response = unwrapInboundIn(data) + + switch response { + case .head(let header): + print(String(describing: response)) + upgradeFailure(status: header.status) + break + case .body(var body): + print(body.readString(length: body.readableBytes)!) + break + case .end(_): + break + } + } + + func errorCaught(context: ChannelHandlerContext, error: Swift.Error) { + if client.delegate != nil { + try! client.delegate?.onError(error: error, status: nil) + } else { + client.onError(error, nil) + } + } +} diff --git a/templates/swift/Sources/WebSockets/MessageHandler.swift.twig b/templates/swift/Sources/WebSockets/MessageHandler.swift.twig new file mode 100644 index 000000000..330b526b9 --- /dev/null +++ b/templates/swift/Sources/WebSockets/MessageHandler.swift.twig @@ -0,0 +1,137 @@ +import Foundation +import NIO +import NIOHTTP1 +import NIOWebSocket + +/// Handles the HTTP pipeline for opening a WebSocket connection. +/// +/// Adds the required headers to the outbound upgrade connection request and handles success and failures responses. +class MessageHandler { + + private let client: WebSocketClient + private var buffer: ByteBuffer + private var binaryBuffer: Data = Data() + private var isText: Bool = false + private var string: String = "" + + public init(client: WebSocketClient) { + self.client = client + self.buffer = ByteBufferAllocator().buffer(capacity: 0) + } + + private func unmaskedData(frame: WebSocketFrame) -> ByteBuffer { + var frameData = frame.data + if let maskingKey = frame.maskKey { + frameData.webSocketUnmask(maskingKey) + } + return frameData + } +} + +extension MessageHandler: ChannelInboundHandler, RemovableChannelHandler { + + typealias InboundIn = WebSocketFrame + + public func channelRead(context: ChannelHandlerContext, data: NIOAny) { + let frame = self.unwrapInboundIn(data) + switch frame.opcode { + case .text: + let data = unmaskedData(frame: frame) + if frame.fin { + guard let text = data.getString(at: 0, length: data.readableBytes) else { + return + } + if let delegate = client.delegate { + try! delegate.onMessage(text: text) + } else { + client.onTextMessage(text) + } + } else { + isText = true + guard let text = data.getString(at: 0, length: data.readableBytes) else { + return + } + string = text + } + case .binary: + let data = unmaskedData(frame: frame) + if frame.fin { + guard let binaryData = data.getData(at: 0, length: data.readableBytes) else { + return + } + if let delegate = client.delegate { + try! delegate.onMessage(data: binaryData) + } else { + client.onBinaryMessage(binaryData) + } + } else { + guard let binaryData = data.getData(at: 0, length: data.readableBytes) else { + return + } + binaryBuffer = binaryData + } + case .continuation: + let data = unmaskedData(frame: frame) + if isText { + if frame.fin { + guard let text = data.getString(at: 0, length: data.readableBytes) else { + return + } + string.append(text) + isText = false + if let delegate = client.delegate { + try! delegate.onMessage(text: string) + } else { + client.onTextMessage(string) + } + } else { + guard let text = data.getString(at: 0, length: data.readableBytes) else { + return + } + string.append(text) + } + } else { + if frame.fin { + guard let binaryData = data.getData(at: 0, length: data.readableBytes) else { + return + } + binaryBuffer.append(binaryData) + if let delegate = client.delegate { + try! delegate.onMessage(data: binaryBuffer) + } else { + client.onBinaryMessage(binaryBuffer) + } + } else { + guard let binaryData = data.getData(at: 0, length: data.readableBytes) else { + return + } + binaryBuffer.append(binaryData) + } + } + case .connectionClose: + guard frame.fin else { + return + } + let data = frame.data + if !client.closeSent { + client.close(data: frame.data.getData(at: 0, length: frame.data.readableBytes) ?? Data()) + } + if let delegate = client.delegate { + delegate.onClose(channel: context.channel, data: data.getData(at: 0, length: data.readableBytes)!) + } else { + client.onClose(context.channel, data.getData(at: 0, length: data.readableBytes)!) + } + default: + break + } + } + + public func errorCaught(context: ChannelHandlerContext, error: Swift.Error) { + if client.delegate != nil { + try! client.delegate?.onError(error: error, status: nil) + } else { + client.onError(error, nil) + } + client.close() + } +} diff --git a/templates/swift/Sources/WebSockets/WebSocketClient.swift.twig b/templates/swift/Sources/WebSockets/WebSocketClient.swift.twig new file mode 100644 index 000000000..67cc39652 --- /dev/null +++ b/templates/swift/Sources/WebSockets/WebSocketClient.swift.twig @@ -0,0 +1,405 @@ +import Foundation +import NIO +import NIOHTTP1 +import NIOWebSocket +import Dispatch +import NIOFoundationCompat +import NIOSSL + +public let WEBSOCKET_LOCKER_QUEUE = "SyncLocker" + +/// Creates and manages connections to a WebSocket server. +/// +/// Creates a connection to the remote host and allows setting callbacks for messages sent from the WebSocket server. +public class WebSocketClient { + + // MARK: - Properties + let host: String + let port: Int + let uri: String + let query: String + let headers: HTTPHeaders + let frameKey: String + + public private(set) var maxFrameSize: Int + + var channel: Channel? = nil + var tlsEnabled: Bool = false + var closeSent: Bool = false + + let upgradedSignalled = DispatchSemaphore(value: 0) + let locker = DispatchQueue(label: WEBSOCKET_LOCKER_QUEUE) + let threadGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + + weak var delegate: WebSocketClientDelegate? = nil + + /// Is this client currently connected to a WebSocket + public var isConnected: Bool { + channel?.isActive ?? false + } + + // MARK: - Stored callbacks + + private var _openCallback: (Channel) -> Void = { _ in } + var onOpen: (Channel) -> Void { + get { + return locker.sync { + return _openCallback + } + } + set { + locker.sync { + _openCallback = newValue + } + } + } + + private var _closeCallback: (Channel, Data) -> Void = { _,_ in } + var onClose: (Channel, Data) -> Void { + get { + return locker.sync { + return _closeCallback + } + } + set { + locker.sync { + _closeCallback = newValue + } + } + } + + private var _textCallback: (String) -> Void = { _ in } + var onTextMessage: (String) -> Void { + get { + return locker.sync { + return _textCallback + } + } + set { + locker.sync { + _textCallback = newValue + } + } + } + + private var _binaryCallback: (Data) -> Void = { _ in } + var onBinaryMessage: (Data) -> Void { + get { + return locker.sync { + return _binaryCallback + } + } + set { + locker.sync { + _binaryCallback = newValue + } + } + } + + private var _errorCallBack: (Swift.Error?, HTTPResponseStatus?) -> Void = { _,_ in } + var onError: (Swift.Error?, HTTPResponseStatus?) -> Void { + get { + return locker.sync { + return _errorCallBack + } + } + set { + locker.sync { + _errorCallBack = newValue + } + } + } + + // MARK: - Callback setters + + /// Set a callback to be fired when a WebSocket connection is opened. + /// + /// - parameters: + /// - callback: Callback to fie when a WebSocket connection is opened + public func onOpen(_ callback: @escaping (Channel) -> Void) { + onOpen = callback + } + + /// Set a callback to be fired when a WebSocket text message is received. + /// + /// - parameters: + /// - callback: Callback to fie when a WebSocket text message is received + public func onMessage(_ callback: @escaping (String) -> Void) { + onTextMessage = callback + } + + /// Set a callback to be fired when a WebSocket binary message is received. + /// + /// - parameters: + /// - callback: Callback to fie when a WebSocket binary message is received + public func onMessage(_ callback: @escaping (Data) -> Void) { + onBinaryMessage = callback + } + + /// Set a callback to be fired when a WebSocket close message is received. + /// + /// - parameters: + /// - callback: Callback to fie when a WebSocket close message is received + public func onClose(_ callback: @escaping (Channel, Data) -> Void) { + onClose = callback + } + + /// Set a callback to be fired when a WebSocket error occurs. + /// + /// - parameters: + /// - callback: Callback to fie when a WebSocket error occurs + public func onError(_ callback: @escaping (Swift.Error?, HTTPResponseStatus?) -> Void) { + onError = callback + } + + // MARK: - Constructors + + /// Create a new `WebSocketClient`. + /// + /// - parameters: + /// - host: Host name of the remote server + /// - port: Port number on which the remote server is listening + /// - uri: The "Request-URI" of the GET method, it is used to identify the endpoint of the WebSocket connection + /// - frameKey: The key sent by client which server has to include while building it's response. + /// - maxFrameSize: Maximum allowable frame size of WebSocket client is configured using this parameter. + /// - tlsEnabled: Is TLS enabled for this client. + /// - delegate: Delegate to handle message and error callbacks + public init?( + host: String, + port: Int, + uri: String, + query: String, + frameKey: String, + headers: HTTPHeaders = HTTPHeaders(), + maxFrameSize: Int = 14, + tlsEnabled: Bool = false, + delegate: WebSocketClientDelegate? = nil + ) { + self.host = host + self.port = port + self.uri = uri + self.query = query + self.headers = headers + self.frameKey = frameKey + self.maxFrameSize = maxFrameSize + self.tlsEnabled = tlsEnabled + self.delegate = delegate + } + + /// Create a new `WebSocketClient`. + /// + /// - parameters: + /// - url: The absolute URL of the GET method used to identify the WebSocket endpoint + /// - delegate: Delegate to handle message and error callbacks. + public init?( + _ url: String, + headers: HTTPHeaders = HTTPHeaders(), + delegate: WebSocketClientDelegate? = nil + ) { + let rawUrl = URL(string: url) + let hasTLS = rawUrl?.scheme == "wss" || rawUrl?.scheme == "https" + self.frameKey = "tergregfgbsfdgfdsfgdbv==" + self.host = rawUrl?.host ?? "localhost" + self.port = rawUrl?.port ?? (hasTLS ? 443 : 80) + self.uri = rawUrl?.path ?? "/" + self.query = rawUrl?.query ?? "" + self.headers = headers + self.maxFrameSize = 24 + self.tlsEnabled = hasTLS + self.delegate = delegate + } + + deinit { + try! threadGroup.syncShutdownGracefully() + } + + // MARK: - Open connection + + /// Open a connection to the configured host and attempt to upgrade the connection to a WebSocket. If successful the `onOpen` callback will fire, otherwise a connection error will be thrown from here. + public func connect() throws { + let socketOptions = ChannelOptions.socket( + SocketOptionLevel(SOL_SOCKET), + SO_REUSEPORT + ) + + let bootstrap = ClientBootstrap(group: threadGroup) + .channelOption(socketOptions, value: 1) + .channelInitializer(self.openChannel) + + _ = try bootstrap + .connect(host: self.host, port: self.port) + .wait() + + self.upgradedSignalled.wait() + } + + private func openChannel(channel: Channel) -> EventLoopFuture { + let httpHandler = HTTPHandler(client: self, headers: headers) + + let basicUpgrader = NIOWebSocketClientUpgrader( + requestKey: self.frameKey, + upgradePipelineHandler: self.upgradePipelineHandler + ) + + let config: NIOHTTPClientUpgradeConfiguration = (upgraders: [basicUpgrader], completionHandler: { context in + context.channel.pipeline.removeHandler(httpHandler, promise: nil) + }) + + return channel.pipeline.addHTTPClientHandlers(withClientUpgrade: config).flatMap { _ in + return channel.pipeline.addHandler(httpHandler).flatMap { _ in + if self.tlsEnabled { + let tlsConfig = TLSConfiguration.makeClientConfiguration() + let sslContext = try! NIOSSLContext(configuration: tlsConfig) + let sslHandler = try! NIOSSLClientHandler(context: sslContext, serverHostname: self.host) + return channel.pipeline.addHandler(sslHandler, position: .first) + } else { + return channel.eventLoop.makeSucceededFuture(()) + } + } + } + } + + private func upgradePipelineHandler(channel: Channel, response: HTTPResponseHead) -> EventLoopFuture { + self.onOpen(channel) + + let handler = MessageHandler(client: self) + + if response.status == .switchingProtocols { + self.channel = channel + self.upgradedSignalled.signal() + } + + return channel.pipeline.addHandler(handler) + } + + // MARK: - Close connection + + /// Closes the connection + /// + /// - parameters: + /// - data: Close frame payload + public func close(data: Data = Data()) { + closeSent = true + + var buffer = ByteBufferAllocator() + .buffer(capacity: data.count) + + buffer.writeBytes(data) + + send( + data: buffer, + opcode: .connectionClose, + finalFrame: true + ) + } + + // MARK: - Send data + + /// Sends binary-formatted data to the connected server in multiple frames. + /// + /// - parameters: + /// - raw: Raw data to be sent in the frame + /// - opcode: Websocket opcode indicating type of the frame + /// - finalFrame: Whether the frame to be sent is the last one + public func send( + data: Data, + opcode: WebSocketOpcode, + finalFrame: Bool = true + ) { + var buffer = ByteBufferAllocator() + .buffer(capacity: data.count) + + buffer.writeBytes(data) + + if opcode == .connectionClose { + self.closeSent = true + } + + send( + data: buffer, + opcode: opcode, + finalFrame: finalFrame + ) + } + + /// Sends text-formatted data to the connected server in multiple frames. + /// + /// - parameters: + /// - raw: Raw text to be sent in the frame + /// - opcode: Websocket opcode indicating type of the frame + /// - finalFrame: Whether the frame to be sent is the last one + public func send( + text: String, + opcode: WebSocketOpcode = .text, + finalFrame: Bool = true + ) { + var buffer = ByteBufferAllocator() + .buffer(capacity: text.count) + + buffer.writeString(text) + + send( + data: buffer, + opcode: opcode, + finalFrame: finalFrame + ) + } + + + /// Sends the JSON representation of the given model to the connected server in multiple frames. + /// + /// - parameters: + /// - model: The model to encode and send + /// - opcode: Websocket opcode indicating type of the frame + /// - finalFrame: Whether the frame to be sent is the last one + public func send( + model: T, + opcode: WebSocketOpcode = .text, + finalFrame: Bool = true + ) { + let jsonEncoder = JSONEncoder() + do { + let jsonData = try jsonEncoder.encode(model) + let string = String(data: jsonData, encoding: .utf8)! + var buffer = ByteBufferAllocator() + .buffer(capacity: string.count) + + buffer.writeString(string) + + send( + data: buffer, + opcode: opcode, + finalFrame: finalFrame + ) + } catch let error { + print(error) + } + } + + /// Sends buffered bytes to the connected server in multiple frames. + /// + /// - parameters: + /// - model: The model to encode and send + /// - opcode: Websocket opcode indicating type of the frame + /// - finalFrame: Whether the frame to be sent is the last one + public func send( + data: ByteBuffer, + opcode: WebSocketOpcode, + finalFrame: Bool + ) { + let frame = WebSocketFrame( + fin: finalFrame, + opcode: opcode, + maskKey: nil, + data: data + ) + guard let channel = channel else { + return + } + if finalFrame { + channel.writeAndFlush(frame, promise: nil) + } else { + channel.write(frame, promise: nil) + } + } +} diff --git a/templates/swift/Sources/WebSockets/WebSocketClientDelegate.swift.twig b/templates/swift/Sources/WebSockets/WebSocketClientDelegate.swift.twig new file mode 100644 index 000000000..d1556acb9 --- /dev/null +++ b/templates/swift/Sources/WebSockets/WebSocketClientDelegate.swift.twig @@ -0,0 +1,26 @@ +import Foundation +import NIO +import NIOHTTP1 + +/// Handles messages received by a connected WebSocket server. +public protocol WebSocketClientDelegate : AnyObject { + func onOpen(channel: Channel) + func onMessage(text: String) throws + func onMessage(data: Data) throws + func onClose(channel: Channel, data: Data) + func onError(error: Swift.Error?, status: HTTPResponseStatus?) throws +} + +// Add empty default implementations +extension WebSocketClientDelegate { + public func onOpen(channel: Channel) { + } + public func onMessage(text: String) { + } + public func onMessage(data: Data) { + } + public func onClose(channel: Channel, data: Data) { + } + public func onError(error: Swift.Error?, status: HTTPResponseStatus?) throws { + } +} diff --git a/templates/swift/Sources/WebSockets/WebSocketClientError.swift.twig b/templates/swift/Sources/WebSockets/WebSocketClientError.swift.twig new file mode 100644 index 000000000..d9445a9e1 --- /dev/null +++ b/templates/swift/Sources/WebSockets/WebSocketClientError.swift.twig @@ -0,0 +1,14 @@ +/// Error mapped HTTP codes +enum WebSocketClientError: UInt, Swift.Error { + case notFound = 404 + case badRequest = 400 + + func code() -> UInt? { + switch self { + case .notFound: + return 404 + case .badRequest: + return 400 + } + } +} diff --git a/templates/swift/Tests/Tests.swift.twig b/templates/swift/Tests/Tests.swift.twig new file mode 100644 index 000000000..80a577efa --- /dev/null +++ b/templates/swift/Tests/Tests.swift.twig @@ -0,0 +1,5 @@ +import XCTest + +class Tests: XCTestCase { + +} \ No newline at end of file diff --git a/templates/swift/docs/example.md.twig b/templates/swift/docs/example.md.twig index 86d8941d7..1127cda4e 100644 --- a/templates/swift/docs/example.md.twig +++ b/templates/swift/docs/example.md.twig @@ -1,18 +1,31 @@ -/// Swift {{spec.title | caseUcfirst}} SDK -/// Produced by {{spec.title | caseUcfirst}} SDK Generator -/// +import Appwrite - -var client: Client = Client() - -client - .setEndpoint(endpoint: "https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint +func main() { + let client = Client() +{% if method.security|length > 0 %} + .setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint {% for node in method.security %} {% for key,header in node|keys %} - .set{{header}}(value: "{{node[header]['x-appwrite']['demo']}}") // {{node[header].description}} + .set{{header | caseUcfirst}}("{{node[header]['x-appwrite']['demo']}}") // {{node[header].description}} {% endfor %} {% endfor %} +{% endif %} -var {{ service.name | caseCamel }}: {{ service.name | caseUcfirst }} = {{ service.name | caseUcfirst }}(client: client); + let {{ service.name | caseCamel }} = {{ service.name | caseUcfirst }}(client) + {{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% if method.parameters.all | filter((param) => param.required) | length == 0 %}) { result in{% endif %} -var result = {{ service.name | caseCamel }}.{{ method.name | caseCamel }}({% for parameter in method.parameters.all %}{% if parameter.required %}{% if not loop.first %}, {% endif %}_{{ parameter.name | caseCamel }}: {{ parameter | paramExample }}{% endif %}{% endfor %}); +{% for parameter in method.parameters.all | filter((param) => param.required) %} + {{parameter.name}}: {{ parameter | paramExample | escapeKeyword }}{% if not loop.last %},{% endif %} + +{% if loop.last %} + ) { result in +{% endif %} +{% endfor %} + switch result { + case .failure(let error): + print(error.message) + case .success(let {% if method.type == 'webAuth' %}success{% elseif method.type == 'location' %}byteBuffer{% else %}{{ method.responseModel | caseCamel | escapeKeyword }}{% endif %}): + print(String(describing: {% if method.type == 'webAuth' %}success{% elseif method.type == 'location' %}byteBuffer{% else %}{{ method.responseModel | caseCamel | escapeKeyword }}{% endif %}) + } + } +} diff --git a/templates/swift/docs/service.md.twig b/templates/swift/docs/service.md.twig deleted file mode 100644 index 6c6ecc4a6..000000000 --- a/templates/swift/docs/service.md.twig +++ /dev/null @@ -1,37 +0,0 @@ -/// Swift {{spec.title | caseUcfirst}} SDK -/// Produced by {{spec.title | caseUcfirst}} SDK Generator -/// - -# {{ service.name | caseUcfirst }} Service - -{% for method in service.methods %} -## {{ method.title }} - -```http request -{{ method.method | caseUpper }} {{ spec.endpoint }}{{ method.path }} -``` -{% if method.description %} - -** {{ method.description }} ** -{% endif %} - -{% if method.parameters.all is not empty %} -### Parameters - -| Field Name | Type | Description | Default | -| --- | --- | --- | --- | -{% for parameter in method.parameters.path %} -| {{ parameter.name }} | {{ parameter.type }} | {% if parameter.required == 1 %}**Required** {% endif %}{{ parameter.description }} | {{ parameter.default }} | -{% endfor %} -{% for parameter in method.parameters.query %} -| {{ parameter.name }} | {{ parameter.type }} | {% if parameter.required == 1 %}**Required** {% endif %}{{ parameter.description }} | {{ parameter.default }} | -{% endfor %} -{% for parameter in method.parameters.body %} -| {{ parameter.name }} | {{ parameter.type }} | {{ parameter.description }} | {{ parameter.default }} | -{% endfor %} -{% for parameter in method.parameters.formData %} -| {{ parameter.name }} | {{ parameter.type }} | {{ parameter.description }} | {{ parameter.default }} | -{% endfor %} - -{% endif %} -{% endfor %} diff --git a/templates/swift/example-swiftui/Example.xcodeproj/project.pbxproj b/templates/swift/example-swiftui/Example.xcodeproj/project.pbxproj new file mode 100644 index 000000000..a17b1a5e2 --- /dev/null +++ b/templates/swift/example-swiftui/Example.xcodeproj/project.pbxproj @@ -0,0 +1,729 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 1D0E30BC26B9430D002ECB40 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0E30BB26B9430D002ECB40 /* ExampleApp.swift */; }; + 1D0E30BD26B9430D002ECB40 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D0E30BB26B9430D002ECB40 /* ExampleApp.swift */; }; + 1D34C2532702E4A000D1DA8D /* ExampleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D34C2522702E4A000D1DA8D /* ExampleViewModel.swift */; }; + 1D34C2552702E4B500D1DA8D /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D34C2542702E4B500D1DA8D /* ExampleView.swift */; }; + 1D43900826FC8B2500C71E3E /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43900726FC8B2500C71E3E /* ImagePicker.swift */; }; + 1DC4093C26BA74470025A85C /* Appwrite in Frameworks */ = {isa = PBXBuildFile; productRef = 1DC4093B26BA74470025A85C /* Appwrite */; }; + 1DC4093E26BA744D0025A85C /* Appwrite in Frameworks */ = {isa = PBXBuildFile; productRef = 1DC4093D26BA744D0025A85C /* Appwrite */; }; + 2358B1EE270AC9DC0016EFBA /* ExampleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D34C2522702E4A000D1DA8D /* ExampleViewModel.swift */; }; + 2358B1EF270AC9E30016EFBA /* ExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D34C2542702E4B500D1DA8D /* ExampleView.swift */; }; + 2358B1F0270AC9E80016EFBA /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43900726FC8B2500C71E3E /* ImagePicker.swift */; }; + 23DDF5932709A457006EFAFA /* ImagePicker+iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DDF5922709A457006EFAFA /* ImagePicker+iOS.swift */; }; + 23DDF5952709A46A006EFAFA /* ImagePicker+macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DDF5942709A46A006EFAFA /* ImagePicker+macOS.swift */; }; + 23DDF598270AC2DA006EFAFA /* OSImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DDF597270AC2DA006EFAFA /* OSImage.swift */; }; + 23DDF599270AC2DA006EFAFA /* OSImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23DDF597270AC2DA006EFAFA /* OSImage.swift */; }; + 4B11518505BC6CF6BEBD216B /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B11557E0F213BC84ED01382 /* Tests_iOS.swift */; }; + 4B1153AF12216F7E7AC0CC99 /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B11562301B29DB7DF72AE29 /* Tests_macOS.swift */; }; + 4B115544F254539A96D47868 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B115883E8645C18835765B2 /* Assets.xcassets */; }; + 4B1157C67CF5E569FBCB65DD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B115883E8645C18835765B2 /* Assets.xcassets */; }; + 4B115FDF5335FA3B14FAA66B /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B115338FA3067342367ADA9 /* Keyboard.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 4B11523A6CB24B030BAD7AC4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4B115A97BA5CA0A597279577 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4B115947BBA7BA3BBD6D5A40; + remoteInfo = "test (macOS)"; + }; + 4B115801AEE2B6439C7AC84E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4B115A97BA5CA0A597279577 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4B1155899FD4139AFC990F38; + remoteInfo = "test (iOS)"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 1D0E30BB26B9430D002ECB40 /* ExampleApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExampleApp.swift; sourceTree = ""; }; + 1D34C2522702E4A000D1DA8D /* ExampleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleViewModel.swift; sourceTree = ""; }; + 1D34C2542702E4B500D1DA8D /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = ""; }; + 1D43900726FC8B2500C71E3E /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; + 1DC4093A26BA73C50025A85C /* swift-client */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "swift-client"; path = "../../../examples/swift-client"; sourceTree = ""; }; + 23DDF5922709A457006EFAFA /* ImagePicker+iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImagePicker+iOS.swift"; sourceTree = ""; }; + 23DDF5942709A46A006EFAFA /* ImagePicker+macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ImagePicker+macOS.swift"; sourceTree = ""; }; + 23DDF597270AC2DA006EFAFA /* OSImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSImage.swift; sourceTree = ""; }; + 4B11505C9899942E695B59FF /* test.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = test.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4B115338FA3067342367ADA9 /* Keyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keyboard.swift; sourceTree = ""; }; + 4B11555C2BF19042AB9E45DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; + 4B11557E0F213BC84ED01382 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = ""; }; + 4B11562301B29DB7DF72AE29 /* Tests_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOS.swift; sourceTree = ""; }; + 4B115883E8645C18835765B2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4B1159CEF53EC1ED2744C870 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; + 4B115D1FE1A2FDEB30700CA6 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; + 4B115D7F73BEBBFC0952D71E /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 4B115D8A84F1369173AA102E /* Tests macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + 4B115DA4916DAA4E8F13734F /* test.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = test.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 4B115EF92A96C13057984C77 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; + 4B115F66DF72EB7706C69816 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4B11576DE1A4F215102AF056 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B1158C372E36CA75988B039 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B11595BC781600EC3573196 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1DC4093E26BA744D0025A85C /* Appwrite in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B11596DCF77D21EBB9749E1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1DC4093C26BA74470025A85C /* Appwrite in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1DC4093526BA6E7A0025A85C /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 23DDF596270AC2B7006EFAFA /* Image */ = { + isa = PBXGroup; + children = ( + 23DDF597270AC2DA006EFAFA /* OSImage.swift */, + ); + path = Image; + sourceTree = ""; + }; + 4B1152396B94D0F596CEBB52 = { + isa = PBXGroup; + children = ( + 1DC4093A26BA73C50025A85C /* swift-client */, + 4B115B49441350FF784C7745 /* Products */, + 4B11566FD1E1FA1ABD88E438 /* Shared */, + 4B115E925CD74030DFF6498C /* iOS */, + 4B115909955A1ABEA1A8A456 /* macOS */, + 4B115C8820DA716164AC3081 /* Tests iOS */, + 4B11573D106E5B7B7D944C97 /* Tests macOS */, + 1DC4093526BA6E7A0025A85C /* Frameworks */, + ); + sourceTree = ""; + }; + 4B11566FD1E1FA1ABD88E438 /* Shared */ = { + isa = PBXGroup; + children = ( + 23DDF596270AC2B7006EFAFA /* Image */, + 1D0E30BB26B9430D002ECB40 /* ExampleApp.swift */, + 4B115883E8645C18835765B2 /* Assets.xcassets */, + 1D43900726FC8B2500C71E3E /* ImagePicker.swift */, + 1D34C2522702E4A000D1DA8D /* ExampleViewModel.swift */, + 1D34C2542702E4B500D1DA8D /* ExampleView.swift */, + ); + path = Shared; + sourceTree = ""; + }; + 4B11573D106E5B7B7D944C97 /* Tests macOS */ = { + isa = PBXGroup; + children = ( + 4B115F66DF72EB7706C69816 /* Info.plist */, + 4B11562301B29DB7DF72AE29 /* Tests_macOS.swift */, + ); + path = "Tests macOS"; + sourceTree = ""; + }; + 4B115909955A1ABEA1A8A456 /* macOS */ = { + isa = PBXGroup; + children = ( + 4B115EF92A96C13057984C77 /* Info.plist */, + 4B115D1FE1A2FDEB30700CA6 /* macOS.entitlements */, + 23DDF5942709A46A006EFAFA /* ImagePicker+macOS.swift */, + ); + path = macOS; + sourceTree = ""; + }; + 4B115B49441350FF784C7745 /* Products */ = { + isa = PBXGroup; + children = ( + 4B115DA4916DAA4E8F13734F /* test.app */, + 4B11505C9899942E695B59FF /* test.app */, + 4B115D7F73BEBBFC0952D71E /* Tests iOS.xctest */, + 4B115D8A84F1369173AA102E /* Tests macOS.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 4B115C8820DA716164AC3081 /* Tests iOS */ = { + isa = PBXGroup; + children = ( + 4B1159CEF53EC1ED2744C870 /* Info.plist */, + 4B11557E0F213BC84ED01382 /* Tests_iOS.swift */, + ); + path = "Tests iOS"; + sourceTree = ""; + }; + 4B115E925CD74030DFF6498C /* iOS */ = { + isa = PBXGroup; + children = ( + 4B11555C2BF19042AB9E45DB /* Info.plist */, + 4B115338FA3067342367ADA9 /* Keyboard.swift */, + 23DDF5922709A457006EFAFA /* ImagePicker+iOS.swift */, + ); + path = iOS; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4B1155899FD4139AFC990F38 /* test (iOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B11590F5ECAF18BA0C4D004 /* Build configuration list for PBXNativeTarget "test (iOS)" */; + buildPhases = ( + 4B115FBE71381666C548843D /* Sources */, + 4B11596DCF77D21EBB9749E1 /* Frameworks */, + 4B115C97AF566C4A78327A9C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "test (iOS)"; + packageProductDependencies = ( + 1DC4093B26BA74470025A85C /* Appwrite */, + ); + productName = "test (iOS)"; + productReference = 4B115DA4916DAA4E8F13734F /* test.app */; + productType = "com.apple.product-type.application"; + }; + 4B115947BBA7BA3BBD6D5A40 /* test (macOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B1150C5B56EC2962831BA91 /* Build configuration list for PBXNativeTarget "test (macOS)" */; + buildPhases = ( + 4B1151AF07540208EF7F3E5C /* Sources */, + 4B11595BC781600EC3573196 /* Frameworks */, + 4B1159A021B783B33063CDA9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "test (macOS)"; + packageProductDependencies = ( + 1DC4093D26BA744D0025A85C /* Appwrite */, + ); + productName = "test (macOS)"; + productReference = 4B11505C9899942E695B59FF /* test.app */; + productType = "com.apple.product-type.application"; + }; + 4B115DB67D85396641A5CF4E /* Tests macOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B115781429F2FBF265033C9 /* Build configuration list for PBXNativeTarget "Tests macOS" */; + buildPhases = ( + 4B1152E054592E9234AACE5F /* Sources */, + 4B11576DE1A4F215102AF056 /* Frameworks */, + 4B115E3D34C64A5E0F28D7B3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4B115F6DD7CD037808A2BD84 /* PBXTargetDependency */, + ); + name = "Tests macOS"; + productName = "Tests macOS"; + productReference = 4B115D8A84F1369173AA102E /* Tests macOS.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 4B115F7D5C1CD078034D7A09 /* Tests iOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B11546D7E0E252413856637 /* Build configuration list for PBXNativeTarget "Tests iOS" */; + buildPhases = ( + 4B115BD3A33CB57011542F4D /* Sources */, + 4B1158C372E36CA75988B039 /* Frameworks */, + 4B1156C121304167FFE7C362 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4B11572E06C4F4CE6D776194 /* PBXTargetDependency */, + ); + name = "Tests iOS"; + productName = "Tests iOS"; + productReference = 4B115D7F73BEBBFC0952D71E /* Tests iOS.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4B115A97BA5CA0A597279577 /* Project object */ = { + isa = PBXProject; + attributes = { + TargetAttributes = { + 4B115DB67D85396641A5CF4E = { + TestTargetID = 4B115947BBA7BA3BBD6D5A40; + }; + 4B115F7D5C1CD078034D7A09 = { + TestTargetID = 4B1155899FD4139AFC990F38; + }; + }; + }; + buildConfigurationList = 4B1156B059331CBF2AFC6F05 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + ); + mainGroup = 4B1152396B94D0F596CEBB52; + productRefGroup = 4B115B49441350FF784C7745 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4B1155899FD4139AFC990F38 /* test (iOS) */, + 4B115947BBA7BA3BBD6D5A40 /* test (macOS) */, + 4B115F7D5C1CD078034D7A09 /* Tests iOS */, + 4B115DB67D85396641A5CF4E /* Tests macOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4B1156C121304167FFE7C362 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B1159A021B783B33063CDA9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B115544F254539A96D47868 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B115C97AF566C4A78327A9C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B1157C67CF5E569FBCB65DD /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B115E3D34C64A5E0F28D7B3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4B1151AF07540208EF7F3E5C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2358B1F0270AC9E80016EFBA /* ImagePicker.swift in Sources */, + 2358B1EF270AC9E30016EFBA /* ExampleView.swift in Sources */, + 2358B1EE270AC9DC0016EFBA /* ExampleViewModel.swift in Sources */, + 1D0E30BD26B9430D002ECB40 /* ExampleApp.swift in Sources */, + 23DDF5952709A46A006EFAFA /* ImagePicker+macOS.swift in Sources */, + 23DDF599270AC2DA006EFAFA /* OSImage.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B1152E054592E9234AACE5F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B1153AF12216F7E7AC0CC99 /* Tests_macOS.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B115BD3A33CB57011542F4D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B11518505BC6CF6BEBD216B /* Tests_iOS.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B115FBE71381666C548843D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1D34C2532702E4A000D1DA8D /* ExampleViewModel.swift in Sources */, + 1D0E30BC26B9430D002ECB40 /* ExampleApp.swift in Sources */, + 23DDF598270AC2DA006EFAFA /* OSImage.swift in Sources */, + 1D43900826FC8B2500C71E3E /* ImagePicker.swift in Sources */, + 4B115FDF5335FA3B14FAA66B /* Keyboard.swift in Sources */, + 1D34C2552702E4B500D1DA8D /* ExampleView.swift in Sources */, + 23DDF5932709A457006EFAFA /* ImagePicker+iOS.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 4B11572E06C4F4CE6D776194 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4B1155899FD4139AFC990F38 /* test (iOS) */; + targetProxy = 4B115801AEE2B6439C7AC84E /* PBXContainerItemProxy */; + }; + 4B115F6DD7CD037808A2BD84 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4B115947BBA7BA3BBD6D5A40 /* test (macOS) */; + targetProxy = 4B11523A6CB24B030BAD7AC4 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 4B1152AEF1E176405C2195FF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "Tests macOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "jake.Tests-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = "test (macOS)"; + }; + name = Release; + }; + 4B1152BA2EA7E9A4F393B278 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = "Tests iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "jake.Tests-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "test (iOS)"; + }; + name = Debug; + }; + 4B11556FCDC6A28B50E25A82 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; + COMBINE_HIDPI_IMAGES = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = macOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_BUNDLE_IDENTIFIER = io.appwrite.mac; + PRODUCT_NAME = test; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 4B1155C082B6FD1F61732197 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = iOS/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.appwrite.ios; + PRODUCT_NAME = test; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4B11578A13DBD767753EAEDD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = "Tests macOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + "@loader_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "jake.Tests-macOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = "test (macOS)"; + }; + name = Debug; + }; + 4B1158A65D94F6A293B7E01F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = iOS/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.appwrite.ios; + PRODUCT_NAME = test; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4B115A9034682E45066FDA66 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; + COMBINE_HIDPI_IMAGES = YES; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = macOS/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + PRODUCT_BUNDLE_IDENTIFIER = io.appwrite.mac; + PRODUCT_NAME = test; + SDKROOT = macosx; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 4B115CEAFB65559F89E9D4F6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 4B115D30022E3635A25B16D4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = "Tests iOS/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "jake.Tests-iOS"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = "test (iOS)"; + }; + name = Release; + }; + 4B115EB2225D57BEE90DA49E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4B1150C5B56EC2962831BA91 /* Build configuration list for PBXNativeTarget "test (macOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B11556FCDC6A28B50E25A82 /* Debug */, + 4B115A9034682E45066FDA66 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B11546D7E0E252413856637 /* Build configuration list for PBXNativeTarget "Tests iOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B1152BA2EA7E9A4F393B278 /* Debug */, + 4B115D30022E3635A25B16D4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B1156B059331CBF2AFC6F05 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B115EB2225D57BEE90DA49E /* Debug */, + 4B115CEAFB65559F89E9D4F6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B115781429F2FBF265033C9 /* Build configuration list for PBXNativeTarget "Tests macOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B11578A13DBD767753EAEDD /* Debug */, + 4B1152AEF1E176405C2195FF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B11590F5ECAF18BA0C4D004 /* Build configuration list for PBXNativeTarget "test (iOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B1158A65D94F6A293B7E01F /* Debug */, + 4B1155C082B6FD1F61732197 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 1DC4093B26BA74470025A85C /* Appwrite */ = { + isa = XCSwiftPackageProductDependency; + productName = Appwrite; + }; + 1DC4093D26BA744D0025A85C /* Appwrite */ = { + isa = XCSwiftPackageProductDependency; + productName = Appwrite; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 4B115A97BA5CA0A597279577 /* Project object */; +} diff --git a/templates/swift/example-swiftui/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/templates/swift/example-swiftui/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/templates/swift/example-swiftui/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/templates/swift/example-swiftui/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/templates/swift/example-swiftui/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/templates/swift/example-swiftui/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/templates/swift/example-swiftui/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/templates/swift/example-swiftui/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..d3f54eff1 --- /dev/null +++ b/templates/swift/example-swiftui/Example.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,61 @@ +{ + "object": { + "pins": [ + { + "package": "async-http-client", + "repositoryURL": "https://github.com/swift-server/async-http-client.git", + "state": { + "branch": null, + "revision": "8e4d51908dd49272667126403bf977c5c503f78f", + "version": "1.5.0" + } + }, + { + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", + "state": { + "branch": null, + "revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", + "version": "1.4.2" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "fb48bdd8279799f655da5f8b4e0a21430eca6012", + "version": "2.32.3" + } + }, + { + "package": "swift-nio-extras", + "repositoryURL": "https://github.com/apple/swift-nio-extras.git", + "state": { + "branch": null, + "revision": "f72c4688f89c28502105509186eadc49a49cb922", + "version": "1.10.0" + } + }, + { + "package": "swift-nio-ssl", + "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", + "state": { + "branch": null, + "revision": "044f90dfa0a7015446b40f5e578b06343ae1affe", + "version": "2.16.0" + } + }, + { + "package": "swift-nio-transport-services", + "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", + "state": { + "branch": null, + "revision": "39587bceccda72780e2a8a8c5e857e42a9df2fa8", + "version": "1.11.0" + } + } + ] + }, + "version": 1 +} diff --git a/templates/swift/example-swiftui/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/templates/swift/example-swiftui/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/templates/swift/example-swiftui/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/templates/swift/example-swiftui/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json b/templates/swift/example-swiftui/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..c136eaff7 --- /dev/null +++ b/templates/swift/example-swiftui/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,148 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/templates/swift/example-swiftui/Shared/Assets.xcassets/Contents.json b/templates/swift/example-swiftui/Shared/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/templates/swift/example-swiftui/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/templates/swift/example-swiftui/Shared/ExampleApp.swift b/templates/swift/example-swiftui/Shared/ExampleApp.swift new file mode 100644 index 000000000..19c5592e8 --- /dev/null +++ b/templates/swift/example-swiftui/Shared/ExampleApp.swift @@ -0,0 +1,12 @@ +import SwiftUI +import Appwrite +import NIO + +@main +struct ExampleApp: App { + var body: some Scene { + WindowGroup { + ExampleView(viewModel: ExampleView.ViewModel()) + } + } +} diff --git a/templates/swift/example-swiftui/Shared/ExampleView.swift b/templates/swift/example-swiftui/Shared/ExampleView.swift new file mode 100644 index 000000000..801cb0c19 --- /dev/null +++ b/templates/swift/example-swiftui/Shared/ExampleView.swift @@ -0,0 +1,68 @@ +import SwiftUI +import Appwrite +import NIO + +struct ExampleView: View { + + @ObservedObject var viewModel: ViewModel + + @State var imageToUpload = OSImage() + + var body: some View { + + VStack(spacing: 8) { + + viewModel.downloadedImage? + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 200) + + TextEditor(text: $viewModel.response) + .padding() + + Button("Login") { + viewModel.login() + } + + Button("Login with Facebook") { + viewModel.loginWithFacebook() + } + + Button("Register") { + viewModel.register() + } + + Button("Download image") { + viewModel.download() + } + + Button("Upload image") { + viewModel.isShowPhotoLibrary = true + } + + Button("Subscribe") { + viewModel.subscribe() + } + } + .onChange(of: viewModel.isShowPhotoLibrary) { showing in + #if os(macOS) + ImagePicker.present() + #endif + } + .sheet(isPresented: $viewModel.isShowPhotoLibrary) { + #if !os(macOS) + ImagePicker(selectedImage: $imageToUpload) + #endif + } + .onChange(of: imageToUpload) { img in + viewModel.upload(image: img) + } + .registerOAuthHandler() + } +} + +struct ExampleView_Previews: PreviewProvider { + static var previews: some View { + ExampleView(viewModel: ExampleView.ViewModel()) + } +} diff --git a/templates/swift/example-swiftui/Shared/ExampleViewModel.swift b/templates/swift/example-swiftui/Shared/ExampleViewModel.swift new file mode 100644 index 000000000..ac79ff965 --- /dev/null +++ b/templates/swift/example-swiftui/Shared/ExampleViewModel.swift @@ -0,0 +1,118 @@ +import Foundation +import SwiftUI +import Appwrite +import NIO + +let host = "https://localhost/v1" +let projectId = "60f6a0d6e2a52" + +extension ExampleView { + + class ViewModel : ObservableObject { + + let client = Client() + .setEndpoint(host) + .setProject(projectId) + + lazy var account = Account(client) + lazy var storage = Storage(client) + lazy var realtime = Realtime(client) + + @Published var downloadedImage: Image? = nil + + @Published public var username: String = "test@test.test" + @Published public var password: String = "password" + @Published public var fileId: String = "60f7a0178c3e5" + @Published public var collectionId: String = "6155742223662" + @Published public var isShowPhotoLibrary = false + @Published public var response: String = "" + + func register() { + account.create(email: username, password: password) { result in + DispatchQueue.main.async { + switch result { + case .failure(let error): + self.response = error.message + case .success(let response): + self.response = response.email + } + } + } + + + } + + func login() { + account.createSession(email: username, password: password) { result in + DispatchQueue.main.async { + switch result { + case .failure(let error): + self.response = error.message + case .success(let response): + self.response = response.userId + } + } + } + } + + func loginWithFacebook() { + account.createOAuth2Session( + provider: "facebook", + success: "\(host)/auth/oauth2/success", + failure: "\(host)/auth/oauth2/failure" + ) { result in + switch result { + case .failure: self.response = "false" + case .success(let response): self.response = response.description + } + } + } + + func download() { + storage.getFileDownload(fileId: fileId) { result in + DispatchQueue.main.async { + switch result { + case .failure(let error): self.response = error.message + case .success(let response): + self.downloadedImage = Image(data: Data(buffer: response)) + } + } + } + } + + func upload(image: OSImage) { + let imageBuffer = ByteBufferAllocator() + .buffer(data: image.data) + + #if os(macOS) + let fileName = "file.tiff" + #else + let fileName = "file.png" + #endif + + let file = File( + name: fileName, + buffer: imageBuffer + ) + + storage.createFile(file: file) { result in + DispatchQueue.main.async { + switch result { + case .failure(let error): + self.response = error.message + case .success(let response): + self.response = response.name + } + } + } + } + + func subscribe() { + _ = realtime.subscribe(channels: ["collections.\(collectionId).documents"]) { response in + DispatchQueue.main.async { + self.response = String(describing: response.payload!) + } + } + } + } +} diff --git a/templates/swift/example-swiftui/Shared/Image/OSImage.swift b/templates/swift/example-swiftui/Shared/Image/OSImage.swift new file mode 100644 index 000000000..11b8d73d8 --- /dev/null +++ b/templates/swift/example-swiftui/Shared/Image/OSImage.swift @@ -0,0 +1,35 @@ +// +// OSImage.swift +// Example +// +// Created by Jake on 4/10/21. +// +import SwiftUI + +#if os(macOS) +import AppKit +public typealias OSImage = NSImage +#elseif os(iOS) || os(tvOS) || os(watchOS) +import UIKit +public typealias OSImage = UIImage +#endif + +extension Image { + public init(data: Data) { + #if os(macOS) + self.init(nsImage: NSImage(data: data)!) + #elseif os(iOS) || os(tvOS) || os(watchOS) + self.init(uiImage: UIImage(data: data)!) + #endif + } +} + +extension OSImage { + public var data: Data { + #if os(macOS) + return self.tiffRepresentation! + #elseif os(iOS) || os(tvOS) || os(watchOS) + return self.pngData()! + #endif + } +} diff --git a/templates/swift/example-swiftui/Shared/ImagePicker.swift b/templates/swift/example-swiftui/Shared/ImagePicker.swift new file mode 100644 index 000000000..827356ffd --- /dev/null +++ b/templates/swift/example-swiftui/Shared/ImagePicker.swift @@ -0,0 +1,7 @@ +import SwiftUI + +struct ImagePicker { + @Binding var selectedImage: OSImage + + @Environment(\.presentationMode) var presentationMode +} diff --git a/templates/swift/example-swiftui/Tests iOS/Info.plist b/templates/swift/example-swiftui/Tests iOS/Info.plist new file mode 100644 index 000000000..64d65ca49 --- /dev/null +++ b/templates/swift/example-swiftui/Tests iOS/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/templates/swift/example-swiftui/Tests iOS/Tests_iOS.swift b/templates/swift/example-swiftui/Tests iOS/Tests_iOS.swift new file mode 100644 index 000000000..fe781c08e --- /dev/null +++ b/templates/swift/example-swiftui/Tests iOS/Tests_iOS.swift @@ -0,0 +1,35 @@ +import XCTest + +class Tests_iOS: 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. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/templates/swift/example-swiftui/Tests macOS/Info.plist b/templates/swift/example-swiftui/Tests macOS/Info.plist new file mode 100644 index 000000000..64d65ca49 --- /dev/null +++ b/templates/swift/example-swiftui/Tests macOS/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/templates/swift/example-swiftui/Tests macOS/Tests_macOS.swift b/templates/swift/example-swiftui/Tests macOS/Tests_macOS.swift new file mode 100644 index 000000000..79762cf27 --- /dev/null +++ b/templates/swift/example-swiftui/Tests macOS/Tests_macOS.swift @@ -0,0 +1,35 @@ +import XCTest + +class Tests_macOS: 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. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/templates/swift/example-swiftui/iOS/ImagePicker+iOS.swift b/templates/swift/example-swiftui/iOS/ImagePicker+iOS.swift new file mode 100644 index 000000000..afa731632 --- /dev/null +++ b/templates/swift/example-swiftui/iOS/ImagePicker+iOS.swift @@ -0,0 +1,46 @@ +import UIKit +import SwiftUI + +extension ImagePicker : UIViewControllerRepresentable { + + var sourceType: UIImagePickerController.SourceType { + get { + return .photoLibrary + } + } + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController { + + let imagePicker = UIImagePickerController() + imagePicker.allowsEditing = false + imagePicker.sourceType = sourceType + imagePicker.delegate = context.coordinator + + return imagePicker + } + + func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext) { + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + + var parent: ImagePicker + + init(_ parent: ImagePicker) { + self.parent = parent + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + + if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { + parent.selectedImage = image + } + + parent.presentationMode.wrappedValue.dismiss() + } + } +} diff --git a/templates/swift/example-swiftui/iOS/Info.plist b/templates/swift/example-swiftui/iOS/Info.plist new file mode 100644 index 000000000..1dda4880a --- /dev/null +++ b/templates/swift/example-swiftui/iOS/Info.plist @@ -0,0 +1,65 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Appwrite iOS + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + io.appwrite + CFBundleURLSchemes + + appwrite-callback-60f6a0d6e2a52 + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/templates/swift/example-swiftui/iOS/Keyboard.swift b/templates/swift/example-swiftui/iOS/Keyboard.swift new file mode 100644 index 000000000..037ffad11 --- /dev/null +++ b/templates/swift/example-swiftui/iOS/Keyboard.swift @@ -0,0 +1,34 @@ +// +// Created by Jake Barnby on 4/08/21. +// + +import SwiftUI +import Combine + +final class Keyboard: ObservableObject { + + @Published var height: CGFloat = 0 + + var cancellables: Set = [] + + init() { + NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification) + .compactMap({ (notification) -> CGFloat? in + guard let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { + return nil + } + if frame.origin.y == UIScreen.main.bounds.height { + return 0 + } + return frame.size.height + }) + .assign(to: \.height, on: self) + .store(in: &cancellables) + } + + deinit { + cancellables.forEach { + $0.cancel() + } + } +} \ No newline at end of file diff --git a/templates/swift/example-swiftui/macOS/ImagePicker+macOS.swift b/templates/swift/example-swiftui/macOS/ImagePicker+macOS.swift new file mode 100644 index 000000000..114c7e0d3 --- /dev/null +++ b/templates/swift/example-swiftui/macOS/ImagePicker+macOS.swift @@ -0,0 +1,28 @@ +import AppKit + +extension ImagePicker { + static func present() { + let dialog = NSOpenPanel(); + + dialog.title = "Choose an image" + dialog.showsResizeIndicator = true + dialog.showsHiddenFiles = false + dialog.allowsMultipleSelection = false + dialog.canChooseDirectories = false + dialog.allowedFileTypes = ["png", "jpg", "jpeg", "gif", "tiff"] + + if (dialog.runModal() == NSApplication.ModalResponse.OK) { + guard let result = dialog.url else { + return + } + + + // path contains the file path e.g + // /Users/ourcodeworld/Desktop/tiger.jpeg + + } else { + // User clicked on "Cancel" + return + } + } +} diff --git a/templates/swift/example-swiftui/macOS/Info.plist b/templates/swift/example-swiftui/macOS/Info.plist new file mode 100644 index 000000000..d749829b7 --- /dev/null +++ b/templates/swift/example-swiftui/macOS/Info.plist @@ -0,0 +1,39 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + io.appwrite + CFBundleURLSchemes + + appwrite-callback-60f6a0d6e2a52 + + + + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + + diff --git a/templates/swift/example-swiftui/macOS/macOS.entitlements b/templates/swift/example-swiftui/macOS/macOS.entitlements new file mode 100644 index 000000000..40b639e46 --- /dev/null +++ b/templates/swift/example-swiftui/macOS/macOS.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/templates/swift/example-uikit/UIKitExample.xcodeproj/project.pbxproj b/templates/swift/example-uikit/UIKitExample.xcodeproj/project.pbxproj new file mode 100644 index 000000000..edf703f67 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample.xcodeproj/project.pbxproj @@ -0,0 +1,592 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 1D05AC7426F1E3C70018CE49 /* Appwrite in Frameworks */ = {isa = PBXBuildFile; productRef = 1D05AC7326F1E3C70018CE49 /* Appwrite */; }; + 1D8C054726E603A200646CFC /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8C054626E603A200646CFC /* ImagePicker.swift */; }; + 4B1151B5D92BD55833AD2A54 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B115E35390385E1BB5197A8 /* LaunchScreen.storyboard */; }; + 4B1152F1B5ED8D14FA2730FE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B115A2E92E19A32F4D71704 /* AppDelegate.swift */; }; + 4B115847F32FD75449111E64 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B11553806456B6FA77CE45A /* Assets.xcassets */; }; + 4B1158A99DA9247E84D91381 /* UIKitExampleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B115BFBF112445FB40F14B1 /* UIKitExampleUITests.swift */; }; + 4B115972B0A6EAC084FE3152 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B1154D4D49EEBB77357E461 /* Main.storyboard */; }; + 4B115B5435095F951B4B84C0 /* UIKitExampleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B115317377938CDB4FF518B /* UIKitExampleTests.swift */; }; + 4B115BA5B6725B85BD1C7493 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B115D4FCDFFFE6F7E217C82 /* SceneDelegate.swift */; }; + 4B115DDE8CB687298EF8E34F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B115CB3B2AC1512228488E5 /* ViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 4B115388F0F34068B6B04925 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4B115F609FCCA2464CAF6D99 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4B115B98FE1D2B6A92194433; + remoteInfo = UIKitExample; + }; + 4B115C6D56B30DE3CAF87184 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4B115F609FCCA2464CAF6D99 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4B115B98FE1D2B6A92194433; + remoteInfo = UIKitExample; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 1D53246626CA414A00D5382C /* swift-client */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "swift-client"; path = "../../../examples/swift-client"; sourceTree = ""; }; + 1D8C054626E603A200646CFC /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; + 4B1151B0FC3E5D1AD1405D6D /* UIKitExampleUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIKitExampleUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4B115317377938CDB4FF518B /* UIKitExampleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitExampleTests.swift; sourceTree = ""; }; + 4B1154366EE3D6AD624FC76C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; + 4B11553806456B6FA77CE45A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 4B1155B516711F85AE292AE8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 4B115965CB401A8C8E7E6225 /* UIKitExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIKitExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 4B115A2E92E19A32F4D71704 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 4B115B649DC85B3E27773877 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 4B115BC2A83914775080F701 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; + 4B115BFBF112445FB40F14B1 /* UIKitExampleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitExampleUITests.swift; sourceTree = ""; }; + 4B115C2267F0B8F83B9C1CF3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; + 4B115CB3B2AC1512228488E5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 4B115D4FCDFFFE6F7E217C82 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 4B115F7604FCAA687C1B01F4 /* UIKitExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UIKitExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 4B11519DBD7D95C2D53DEA2F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B1156E0B8313E61A12CB905 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B115F5293F77A1C684976FF /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1D05AC7426F1E3C70018CE49 /* Appwrite in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1D40B4F526C26A4C00412625 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 4B11584FFD11E0EC8AE1D05E = { + isa = PBXGroup; + children = ( + 1D53246626CA414A00D5382C /* swift-client */, + 4B115EA639EF1F575013EFA6 /* Products */, + 4B115E17FC70E956925F85AF /* UIKitExample */, + 4B115A1762CECE9495C57394 /* UIKitExampleTests */, + 4B1159107834D7E391C94100 /* UIKitExampleUITests */, + 1D40B4F526C26A4C00412625 /* Frameworks */, + ); + sourceTree = ""; + }; + 4B1159107834D7E391C94100 /* UIKitExampleUITests */ = { + isa = PBXGroup; + children = ( + 4B115BC2A83914775080F701 /* Info.plist */, + 4B115BFBF112445FB40F14B1 /* UIKitExampleUITests.swift */, + ); + path = UIKitExampleUITests; + sourceTree = ""; + }; + 4B115A1762CECE9495C57394 /* UIKitExampleTests */ = { + isa = PBXGroup; + children = ( + 4B115C2267F0B8F83B9C1CF3 /* Info.plist */, + 4B115317377938CDB4FF518B /* UIKitExampleTests.swift */, + ); + path = UIKitExampleTests; + sourceTree = ""; + }; + 4B115E17FC70E956925F85AF /* UIKitExample */ = { + isa = PBXGroup; + children = ( + 4B1154366EE3D6AD624FC76C /* Info.plist */, + 4B11553806456B6FA77CE45A /* Assets.xcassets */, + 4B115A2E92E19A32F4D71704 /* AppDelegate.swift */, + 4B115E35390385E1BB5197A8 /* LaunchScreen.storyboard */, + 4B115D4FCDFFFE6F7E217C82 /* SceneDelegate.swift */, + 4B115CB3B2AC1512228488E5 /* ViewController.swift */, + 4B1154D4D49EEBB77357E461 /* Main.storyboard */, + 1D8C054626E603A200646CFC /* ImagePicker.swift */, + ); + path = UIKitExample; + sourceTree = ""; + }; + 4B115EA639EF1F575013EFA6 /* Products */ = { + isa = PBXGroup; + children = ( + 4B115F7604FCAA687C1B01F4 /* UIKitExample.app */, + 4B115965CB401A8C8E7E6225 /* UIKitExampleTests.xctest */, + 4B1151B0FC3E5D1AD1405D6D /* UIKitExampleUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 4B115486ADC45594BFC6F4E0 /* UIKitExampleTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B115AF5D436CDBECFC180F4 /* Build configuration list for PBXNativeTarget "UIKitExampleTests" */; + buildPhases = ( + 4B1151C8195D61FE8E56873F /* Sources */, + 4B11519DBD7D95C2D53DEA2F /* Frameworks */, + 4B11552D4B82EAEFCC83BFEB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4B11527E9CE0B345600A3192 /* PBXTargetDependency */, + ); + name = UIKitExampleTests; + productName = UIKitExampleTests; + productReference = 4B115965CB401A8C8E7E6225 /* UIKitExampleTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 4B115B98FE1D2B6A92194433 /* UIKitExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B115DA818369B82D74923DE /* Build configuration list for PBXNativeTarget "UIKitExample" */; + buildPhases = ( + 4B115349ABA13773BE390058 /* Sources */, + 4B115F5293F77A1C684976FF /* Frameworks */, + 4B115A21FF6FC5EAF6B50337 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = UIKitExample; + packageProductDependencies = ( + 1D05AC7326F1E3C70018CE49 /* Appwrite */, + ); + productName = UIKitExample; + productReference = 4B115F7604FCAA687C1B01F4 /* UIKitExample.app */; + productType = "com.apple.product-type.application"; + }; + 4B115F83B10DBB6464EEB40F /* UIKitExampleUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4B1152EC126A217DE3AC63E4 /* Build configuration list for PBXNativeTarget "UIKitExampleUITests" */; + buildPhases = ( + 4B115ABDFD0B2F83FC79F7B7 /* Sources */, + 4B1156E0B8313E61A12CB905 /* Frameworks */, + 4B115061785AB5D436DE4929 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4B115CDBDA62CFF14C8BA666 /* PBXTargetDependency */, + ); + name = UIKitExampleUITests; + productName = UIKitExampleUITests; + productReference = 4B1151B0FC3E5D1AD1405D6D /* UIKitExampleUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4B115F609FCCA2464CAF6D99 /* Project object */ = { + isa = PBXProject; + attributes = { + }; + buildConfigurationList = 4B1156AD21F2FF81D746C9B6 /* Build configuration list for PBXProject "UIKitExample" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + English, + en, + Base, + ); + mainGroup = 4B11584FFD11E0EC8AE1D05E; + productRefGroup = 4B115EA639EF1F575013EFA6 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 4B115B98FE1D2B6A92194433 /* UIKitExample */, + 4B115486ADC45594BFC6F4E0 /* UIKitExampleTests */, + 4B115F83B10DBB6464EEB40F /* UIKitExampleUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 4B115061785AB5D436DE4929 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B11552D4B82EAEFCC83BFEB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B115A21FF6FC5EAF6B50337 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B115847F32FD75449111E64 /* Assets.xcassets in Resources */, + 4B1151B5D92BD55833AD2A54 /* LaunchScreen.storyboard in Resources */, + 4B115972B0A6EAC084FE3152 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 4B1151C8195D61FE8E56873F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B115B5435095F951B4B84C0 /* UIKitExampleTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B115349ABA13773BE390058 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B1152F1B5ED8D14FA2730FE /* AppDelegate.swift in Sources */, + 1D8C054726E603A200646CFC /* ImagePicker.swift in Sources */, + 4B115BA5B6725B85BD1C7493 /* SceneDelegate.swift in Sources */, + 4B115DDE8CB687298EF8E34F /* ViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4B115ABDFD0B2F83FC79F7B7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B1158A99DA9247E84D91381 /* UIKitExampleUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 4B11527E9CE0B345600A3192 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4B115B98FE1D2B6A92194433 /* UIKitExample */; + targetProxy = 4B115C6D56B30DE3CAF87184 /* PBXContainerItemProxy */; + }; + 4B115CDBDA62CFF14C8BA666 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4B115B98FE1D2B6A92194433 /* UIKitExample */; + targetProxy = 4B115388F0F34068B6B04925 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 4B1154D4D49EEBB77357E461 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4B115B649DC85B3E27773877 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 4B115E35390385E1BB5197A8 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 4B1155B516711F85AE292AE8 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 4B1151973C795CC8662CFD2A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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 = 14.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 4B1154F3BF9FE54378ED0231 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + INFOPLIST_FILE = UIKitExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.appwrite.ios; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 4B1156117D0EF00204FFB013 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = UIKitExampleUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = appwrite.io.UIKitExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = UIKitExample; + }; + name = Debug; + }; + 4B1156F74BD7CA3877BE0431 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/UIKitExample.app/UIKitExample"; + INFOPLIST_FILE = UIKitExampleTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = appwrite.io.UIKitExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUNDLE_LOADER)"; + }; + name = Debug; + }; + 4B1157700023BB1132A121F7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + INFOPLIST_FILE = UIKitExampleUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = appwrite.io.UIKitExampleUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = UIKitExample; + }; + name = Release; + }; + 4B115A76CDE70E5B18B5DC79 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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 = 14.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 4B115C7AB63AB20FCE5CD241 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/UIKitExample.app/UIKitExample"; + INFOPLIST_FILE = UIKitExampleTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = appwrite.io.UIKitExampleTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUNDLE_LOADER)"; + }; + name = Release; + }; + 4B115E80ECDBE5FE141F3AE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + INFOPLIST_FILE = UIKitExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = io.appwrite.ios; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4B1152EC126A217DE3AC63E4 /* Build configuration list for PBXNativeTarget "UIKitExampleUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B1156117D0EF00204FFB013 /* Debug */, + 4B1157700023BB1132A121F7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B1156AD21F2FF81D746C9B6 /* Build configuration list for PBXProject "UIKitExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B115A76CDE70E5B18B5DC79 /* Debug */, + 4B1151973C795CC8662CFD2A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B115AF5D436CDBECFC180F4 /* Build configuration list for PBXNativeTarget "UIKitExampleTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B1156F74BD7CA3877BE0431 /* Debug */, + 4B115C7AB63AB20FCE5CD241 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4B115DA818369B82D74923DE /* Build configuration list for PBXNativeTarget "UIKitExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4B115E80ECDBE5FE141F3AE5 /* Debug */, + 4B1154F3BF9FE54378ED0231 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + 1D05AC7326F1E3C70018CE49 /* Appwrite */ = { + isa = XCSwiftPackageProductDependency; + productName = Appwrite; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 4B115F609FCCA2464CAF6D99 /* Project object */; +} diff --git a/templates/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/templates/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/templates/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/templates/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/templates/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/templates/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..776422177 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,61 @@ +{ + "object": { + "pins": [ + { + "package": "async-http-client", + "repositoryURL": "https://github.com/swift-server/async-http-client.git", + "state": { + "branch": null, + "revision": "8e4d51908dd49272667126403bf977c5c503f78f", + "version": "1.5.0" + } + }, + { + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", + "state": { + "branch": null, + "revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", + "version": "1.4.2" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "f2705f9655ede35399b12040e892cf653126de98", + "version": "2.32.2" + } + }, + { + "package": "swift-nio-extras", + "repositoryURL": "https://github.com/apple/swift-nio-extras.git", + "state": { + "branch": null, + "revision": "f72c4688f89c28502105509186eadc49a49cb922", + "version": "1.10.0" + } + }, + { + "package": "swift-nio-ssl", + "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", + "state": { + "branch": null, + "revision": "2e74773972bd6254c41ceeda827f229bccbf1c0f", + "version": "2.15.0" + } + }, + { + "package": "swift-nio-transport-services", + "repositoryURL": "https://github.com/apple/swift-nio-transport-services.git", + "state": { + "branch": null, + "revision": "39587bceccda72780e2a8a8c5e857e42a9df2fa8", + "version": "1.11.0" + } + } + ] + }, + "version": 1 +} diff --git a/templates/swift/example-uikit/UIKitExample/AppDelegate.swift b/templates/swift/example-uikit/UIKitExample/AppDelegate.swift new file mode 100644 index 000000000..597ee24e4 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample/AppDelegate.swift @@ -0,0 +1,28 @@ +import Appwrite +import UIKit + + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } +} diff --git a/templates/swift/example-uikit/UIKitExample/Assets.xcassets/AccentColor.colorset/Contents.json b/templates/swift/example-uikit/UIKitExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/templates/swift/example-uikit/UIKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/templates/swift/example-uikit/UIKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..9221b9bb1 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/templates/swift/example-uikit/UIKitExample/Assets.xcassets/Contents.json b/templates/swift/example-uikit/UIKitExample/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/templates/swift/example-uikit/UIKitExample/Base.lproj/LaunchScreen.storyboard b/templates/swift/example-uikit/UIKitExample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..1cfdc7e9d --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/swift/example-uikit/UIKitExample/Base.lproj/Main.storyboard b/templates/swift/example-uikit/UIKitExample/Base.lproj/Main.storyboard new file mode 100644 index 000000000..cbe57ff72 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample/Base.lproj/Main.storyboard @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/swift/example-uikit/UIKitExample/ImagePicker.swift b/templates/swift/example-uikit/UIKitExample/ImagePicker.swift new file mode 100644 index 000000000..0c0522d60 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample/ImagePicker.swift @@ -0,0 +1,55 @@ +import UIKit + +public protocol ImagePickerDelegate: AnyObject { + func didSelect(image: UIImage?) +} + +open class ImagePicker: NSObject { + + private let pickerController: UIImagePickerController + private weak var presentationController: UIViewController? + private weak var delegate: ImagePickerDelegate? + + public init(presentationController: UIViewController, delegate: ImagePickerDelegate) { + self.pickerController = UIImagePickerController() + + super.init() + + self.presentationController = presentationController + self.delegate = delegate + + self.pickerController.delegate = self + self.pickerController.allowsEditing = false + self.pickerController.sourceType = .photoLibrary + self.pickerController.mediaTypes = ["public.image"] + } + + public func present() { + self.presentationController?.present(self.pickerController, animated: true) + } + + private func pickerController(_ controller: UIImagePickerController, didSelect image: UIImage?) { + controller.dismiss(animated: true, completion: nil) + + self.delegate?.didSelect(image: image) + } +} + +extension ImagePicker: UIImagePickerControllerDelegate { + + public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + self.pickerController(picker, didSelect: nil) + } + + public func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + guard let image = info[.originalImage] as? UIImage else { + return self.pickerController(picker, didSelect: nil) + } + self.pickerController(picker, didSelect: image) + } +} + +extension ImagePicker: UINavigationControllerDelegate { + +} diff --git a/templates/swift/example-uikit/UIKitExample/Info.plist b/templates/swift/example-uikit/UIKitExample/Info.plist new file mode 100644 index 000000000..22808e498 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample/Info.plist @@ -0,0 +1,81 @@ + + + + + NSPhotoLibraryUsageDescription + Required to upload a photo. + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + io.appwrite + CFBundleURLSchemes + + appwrite-callback-60f6a0d6e2a52 + + + + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/templates/swift/example-uikit/UIKitExample/SceneDelegate.swift b/templates/swift/example-uikit/UIKitExample/SceneDelegate.swift new file mode 100644 index 000000000..525b434a6 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample/SceneDelegate.swift @@ -0,0 +1,16 @@ +import Appwrite +import UIKit + + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url, + url.absoluteString.contains("appwrite-callback") else { + return + } + WebAuthComponent.handleIncomingCookie(from: url) + } +} diff --git a/templates/swift/example-uikit/UIKitExample/ViewController.swift b/templates/swift/example-uikit/UIKitExample/ViewController.swift new file mode 100644 index 000000000..3cabb1e8a --- /dev/null +++ b/templates/swift/example-uikit/UIKitExample/ViewController.swift @@ -0,0 +1,166 @@ +import UIKit +import NIO +import Appwrite + +let host = "https://demo.appwrite.io/v1" + +class ViewController: UIViewController { + + @IBOutlet weak var text: UITextView! + @IBOutlet weak var register: UIButton! + @IBOutlet weak var loginButton: UIButton! + @IBOutlet weak var loginWithFacebook: UIButton! + @IBOutlet weak var downloadButton: UIButton! + @IBOutlet weak var uploadButton: UIButton! + @IBOutlet weak var image: UIImageView! + + let client = Client() + .setEndpoint(host) + .setProject("60f6a0d6e2a52") + + let COLLECTION_ID = "6155742223662" + + lazy var account = Account(client) + lazy var storage = Storage(client) + lazy var realtime = Realtime(client) + lazy var database = Database(client) + + var picker: ImagePicker? + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func viewDidLoad() { + super.viewDidLoad() + + picker = ImagePicker(presentationController: self, delegate: self) + } + + @IBAction func registerClick(_ sender: Any) { + let disptch = DispatchGroup() + disptch.enter() + var string: String = "" + + account.create(email: "jake@appwrite.io", password: "password") { result in + switch result { + case .failure(let error): string = error.message + case .success(var response): string = response.body!.readString(length: response.body!.readableBytes) ?? "" + } + disptch.leave() + } + disptch.wait() + self.text.text = string + } + + @IBAction func loginClick(_ sender: Any) { + account.createSession(email: "jake@appwrite.io", password: "password") { result in + var string: String = "" + + switch result { + case .failure(let error): string = error.message + case .success(var response): + string = response.body!.readString(length: response.body!.readableBytes) ?? "" + } + + DispatchQueue.main.async { + self.text.text = string + } + } + } + + @IBAction func loginWithFacebook(_ sender: UIButton) { + account.createOAuth2Session( + provider: "facebook", + success: "https://demo.appwrite.io/auth/oauth2/success", + failure: "https://demo.appwrite.io/auth/oauth2/failure") { result in + var string: String = "" + + switch result { + case .failure(let error): string = error.message + case .success(let response): string = response.description + } + + DispatchQueue.main.async { + self.text.text = string + } + } + + } + + @IBAction func download(_ sender: Any) { + storage.getFileDownload(fileId: "614afaf579352") { result in + switch result { + case .failure(let error): + DispatchQueue.main.async { + self.text.text = error.message + } + case .success(var response): + let data = response.body!.readData(length: response.body!.readableBytes)! + DispatchQueue.main.async { + self.image.image = UIImage(data: data) + } + } + } + } + + @IBAction func upload(_ sender: Any) { + picker?.present() + + } + + @IBAction func subscribe(_ sender: Any) { + _ = realtime.subscribe(channel:"collections.\(COLLECTION_ID).documents") { message in + DispatchQueue.main.async { + self.text.text = String(describing: message) + } + } + } + + @IBAction func createDocument(_ sender: Any) { + database.createDocument( + collectionId: COLLECTION_ID, + data: [ + "name": "Name \(Int.random(in: 0...Int.max))", + "description": "Description \(Int.random(in: 0...Int.max))" + ] + ) { result in + var string: String = "" + + switch result { + case .failure(let error): string = error.message + case .success(var response): + string = response.body!.readString(length: response.body!.readableBytes) ?? "" + } + + DispatchQueue.main.async { + self.text.text = string + } + } + } + +} + +extension ViewController: ImagePickerDelegate { + func didSelect(image: UIImage?) { + var output = "" + + let buffer = ByteBufferAllocator() + .buffer(data: image!.jpegData(compressionQuality: 1)!) + + let file = File(name: "my_image.jpg", buffer: buffer) + + storage.createFile(file: file) { result in + switch result { + case .failure(let error): + output = error.message + case .success(var response): + output = response.body!.readString(length: response.body!.readableBytes) ?? "" + } + + DispatchQueue.main.async { + self.text.text = output + } + } + } +} diff --git a/templates/swift/example-uikit/UIKitExampleTests/Info.plist b/templates/swift/example-uikit/UIKitExampleTests/Info.plist new file mode 100644 index 000000000..64d65ca49 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExampleTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/templates/swift/example-uikit/UIKitExampleTests/UIKitExampleTests.swift b/templates/swift/example-uikit/UIKitExampleTests/UIKitExampleTests.swift new file mode 100644 index 000000000..62302c439 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExampleTests/UIKitExampleTests.swift @@ -0,0 +1,34 @@ +// +// UIKitExampleTests.swift +// UIKitExampleTests +// +// Created by Jake Barnby on 10/08/21. +// +// + +import XCTest +@testable import UIKitExample + +class UIKitExampleTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/templates/swift/example-uikit/UIKitExampleUITests/Info.plist b/templates/swift/example-uikit/UIKitExampleUITests/Info.plist new file mode 100644 index 000000000..64d65ca49 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExampleUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/templates/swift/example-uikit/UIKitExampleUITests/UIKitExampleUITests.swift b/templates/swift/example-uikit/UIKitExampleUITests/UIKitExampleUITests.swift new file mode 100644 index 000000000..3cec369c4 --- /dev/null +++ b/templates/swift/example-uikit/UIKitExampleUITests/UIKitExampleUITests.swift @@ -0,0 +1,43 @@ +// +// UIKitExampleUITests.swift +// UIKitExampleUITests +// +// Created by Jake Barnby on 10/08/21. +// +// + +import XCTest + +class UIKitExampleUITests: 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. + } + + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testLaunchPerformance() throws { + if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } + } +} diff --git a/tests/SDKTest.php b/tests/SDKTest.php index 8c0116e42..a3b978354 100644 --- a/tests/SDKTest.php +++ b/tests/SDKTest.php @@ -96,6 +96,30 @@ class SDKTest extends TestCase 'supportException' => false, ], + 'swift-server' => [ + 'class' => 'Appwrite\SDK\Language\Swift', + 'build' => [ + 'mkdir -p tests/sdks/swift-server/Tests/AppwriteTests', + 'cp tests/languages/swift-server/Tests.swift tests/sdks/swift-server/Tests/AppwriteTests/Tests.swift', + ], + 'envs' => [ + 'swift-5.5' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/swift-server swift:5.5 swift test', + ], + 'supportException' => true, + ], + + 'swift-client' => [ + 'class' => 'Appwrite\SDK\Language\SwiftClient', + 'build' => [ + 'mkdir -p tests/sdks/swift-client/Tests/AppwriteTests', + 'cp tests/languages/swift-client/Tests.swift tests/sdks/swift-client/Tests/AppwriteTests/Tests.swift', + ], + 'envs' => [ + 'swift-5.5' => 'docker run --rm -v $(pwd):/app -w /app/tests/sdks/swift-client swift:5.5 swift test', + ], + 'supportException' => true, + 'supportRealtime' => true, + ], 'dotnet' => [ 'class' => 'Appwrite\SDK\Language\DotNet', @@ -206,7 +230,7 @@ public function testHTTPSuccess() throw new \Exception('Failed to fetch spec from Appwrite server'); } - $whitelist = ['php', 'cli', 'node', 'ruby', 'python', 'deno', 'dotnet', 'dart', 'flutter', 'web', 'android', 'kotlin']; + $whitelist = ['php', 'cli', 'node', 'ruby', 'python', 'deno', 'dotnet', 'dart', 'flutter', 'web', 'android', 'kotlin', 'swift-server', 'swift-client']; foreach ($this->languages as $language => $options) { if (!empty($whitelist) && !in_array($language, $whitelist)) { diff --git a/tests/languages/swift-client/Tests.swift b/tests/languages/swift-client/Tests.swift new file mode 100644 index 000000000..b3e92803c --- /dev/null +++ b/tests/languages/swift-client/Tests.swift @@ -0,0 +1,192 @@ +import XCTest +import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import Appwrite +import AsyncHTTPClient +import NIO + +class Tests: XCTestCase { + + override func setUp() { + super.setUp() + print( "Test Started") + } + + override func tearDown() { + super.tearDown() + } + + func test() throws { + let group = DispatchGroup() + let client = Client() + .setEndpointRealtime("wss://demo.appwrite.io/v1") + .setProject("console") + .addHeader(key: "Origin", value: "http://localhost") + .setSelfSigned() + + let foo = Foo(client) + let bar = Bar(client) + let general = General(client) + let realtime = Realtime(client) + var realtimeResponse = "Realtime failed!" + + let expectation = XCTestExpectation(description: "realtime server") + realtime.subscribe(channels: ["tests"]) { message in + realtimeResponse = message.payload!["response"] as! String + expectation.fulfill() + } + + + // Foo Tests + group.enter() + foo.get(x: "string", y: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( mock.result) + } + group.leave() + } + group.wait() + group.enter() + foo.post(x: "string", y: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( mock.result) + } + group.leave() + } + group.wait() + group.enter() + foo.put(x: "string", y: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( mock.result) + } + group.leave() + } + group.wait() + group.enter() + foo.patch(x: "string", y: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( mock.result) + } + group.leave() + } + group.wait() + group.enter() + foo.delete(x: "string", y: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( mock.result) + } + group.leave() + } + group.wait() + + // Bar Tests + group.enter() + bar.get(xrequired: "string", xdefault: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( mock.result) + } + group.leave() + } + group.wait() + group.enter() + bar.post(xrequired: "string", xdefault: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( mock.result) + } + group.leave() + } + group.wait() + group.enter() + bar.put(xrequired: "string", xdefault: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( mock.result) + } + group.leave() + } + group.wait() + group.enter() + bar.patch(xrequired: "string", xdefault: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( mock.result) + } + group.leave() + } + group.wait() + group.enter() + bar.delete(xrequired: "string", xdefault: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( mock.result) + } + group.leave() + } + group.wait() + + // General Tests + group.enter() + general.redirect() { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( (mock as! [String: Any])["result"] as! String) + } + group.leave() + } + group.wait() + group.enter() + + let url = URL(fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/../../resources/file.png") + let buffer = ByteBuffer(data: try! Data(contentsOf: url)) + let file = File(name: "file.png", buffer: buffer) + general.upload(x: "string", y: 123, z: ["string in array"], file: file) { result in + switch result { + case .failure(let error): print( error.message) + case .success(let mock): print( mock.result) + } + group.leave() + } + group.wait() + + group.enter() + general.error400() { result in + switch result { + case .failure(let error): print( error.message) + case .success(let error): print( error.message) + } + group.leave() + } + group.wait() + group.enter() + general.error500() { result in + switch result { + case .failure(let error): print( error.message) + case .success(let error): print( error.message) + } + group.leave() + } + group.wait() + group.enter() + general.error502() { result in + switch result { + case .failure(let error): print( error.message) + case .success(let error): print( (error as! Error).message) + } + group.leave() + } + group.wait() + + wait(for: [expectation], timeout: 10.0) + print( realtimeResponse) + } + +} diff --git a/tests/languages/swift-server/Tests.swift b/tests/languages/swift-server/Tests.swift new file mode 100644 index 000000000..1a395b1ff --- /dev/null +++ b/tests/languages/swift-server/Tests.swift @@ -0,0 +1,179 @@ +import XCTest +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import Appwrite +import AsyncHTTPClient +import NIO + +class Tests: XCTestCase { + + override func setUp() { + super.setUp() + print("Test Started") + } + + override func tearDown() { + super.tearDown() + } + + func test() throws { + let group = DispatchGroup() + + let client = Client() + .setEndpointRealtime("wss://demo.appwrite.io/v1") + .setProject("console") + .addHeader(key: "Origin", value: "http://localhost") + .setSelfSigned() + + let foo = Foo(client) + let bar = Bar(client) + let general = General(client) + + // Foo Tests + group.enter() + foo.get(x: "string", y: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print(mock.result) + } + group.leave() + } + group.wait() + group.enter() + foo.post(x: "string", y: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print(mock.result) + } + group.leave() + } + group.wait() + group.enter() + foo.put(x: "string", y: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print(mock.result) + } + group.leave() + } + group.wait() + group.enter() + foo.patch(x: "string", y: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print(mock.result) + } + group.leave() + } + group.wait() + group.enter() + foo.delete(x: "string", y: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print(mock.result) + } + group.leave() + } + group.wait() + + // Bar Tests + group.enter() + bar.get(xrequired: "string", xdefault: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print(mock.result) + } + group.leave() + } + group.wait() + group.enter() + bar.post(xrequired: "string", xdefault: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print(mock.result) + } + group.leave() + } + group.wait() + group.enter() + bar.put(xrequired: "string", xdefault: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print(mock.result) + } + group.leave() + } + group.wait() + group.enter() + bar.patch(xrequired: "string", xdefault: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print(mock.result) + } + group.leave() + } + group.wait() + group.enter() + bar.delete(xrequired: "string", xdefault: 123, z: ["string in array"]) { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print(mock.result) + } + group.leave() + } + group.wait() + + // General Tests + group.enter() + general.redirect() { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print((mock as! [String: Any])["result"] as! String) + } + group.leave() + } + group.wait() + group.enter() + + let url = URL(fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/../../resources/file.png") + let buffer = ByteBuffer(data: try! Data(contentsOf: url)) + let file = File(name: "file.png", buffer: buffer) + general.upload(x: "string", y: 123, z: ["string in array"], file: file) { result in + switch result { + case .failure(let error): print(error.message) + case .success(let mock): print(mock.result) + } + group.leave() + } + group.wait() + + group.enter() + general.error400() { result in + switch result { + case .failure(let error): print(error.message) + case .success(let error): print(error.message) + } + group.leave() + } + group.wait() + group.enter() + general.error500() { result in + switch result { + case .failure(let error): print(error.message) + case .success(let error): print(error.message) + } + group.leave() + } + group.wait() + group.enter() + general.error502() { result in + switch result { + case .failure(let error): print(error.message) + case .success(let error): print((error as! Error).message) + } + group.leave() + } + group.wait() + } +} \ No newline at end of file