diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..69644a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# Xcode Project +**/*.xcodeproj/xcuserdata/ +**/*.xcworkspace/xcuserdata/ +**/.swiftpm/xcode/xcuserdata/ +**/*.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +**/*.xcworkspace/xcshareddata/*.xccheckout +**/*.xcworkspace/xcshareddata/*.xcscmblueprint +**/*.playground/**/timeline.xctimeline +.idea/ + +# Build +**/.build/ +**/Build/ +DerivedData/ +*.ipa +*.iml + +# Carthage +Carthage/ + +# CocoaPods +Pods/ + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/sign&cert + +# CSV +*.orig +.svn + +# Other +*~ +.DS_Store +*.swp +*.save +._* +*.bak diff --git a/AppwriteStarterKit.xcodeproj/project.pbxproj b/AppwriteStarterKit.xcodeproj/project.pbxproj new file mode 100644 index 0000000..dfe4dc4 --- /dev/null +++ b/AppwriteStarterKit.xcodeproj/project.pbxproj @@ -0,0 +1,377 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 92ADF0512D09736D00BCCA85 /* Appwrite in Frameworks */ = {isa = PBXBuildFile; productRef = 92ADF0502D09736D00BCCA85 /* Appwrite */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 92B5189D2D13E913001DB0CF /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 92EFE9062CF9A50000D1DE48 /* AppwriteStarterKit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AppwriteStarterKit.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 92EFE9082CF9A50000D1DE48 /* Sources */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Sources; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 92EFE9032CF9A50000D1DE48 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 92ADF0512D09736D00BCCA85 /* Appwrite in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 92EFE8FD2CF9A50000D1DE48 = { + isa = PBXGroup; + children = ( + 92B5189D2D13E913001DB0CF /* README.md */, + 92EFE9082CF9A50000D1DE48 /* Sources */, + 92EFE9072CF9A50000D1DE48 /* Products */, + ); + sourceTree = ""; + }; + 92EFE9072CF9A50000D1DE48 /* Products */ = { + isa = PBXGroup; + children = ( + 92EFE9062CF9A50000D1DE48 /* AppwriteStarterKit.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 92EFE9052CF9A50000D1DE48 /* AppwriteStarterKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 92EFE92A2CF9A50100D1DE48 /* Build configuration list for PBXNativeTarget "AppwriteStarterKit" */; + buildPhases = ( + 92EFE9022CF9A50000D1DE48 /* Sources */, + 92EFE9032CF9A50000D1DE48 /* Frameworks */, + 92EFE9042CF9A50000D1DE48 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 92EFE9082CF9A50000D1DE48 /* Sources */, + ); + name = AppwriteStarterKit; + packageProductDependencies = ( + 92ADF0502D09736D00BCCA85 /* Appwrite */, + ); + productName = AppwriteStarterKit; + productReference = 92EFE9062CF9A50000D1DE48 /* AppwriteStarterKit.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 92EFE8FE2CF9A50000D1DE48 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1610; + LastUpgradeCheck = 1610; + TargetAttributes = { + 92EFE9052CF9A50000D1DE48 = { + CreatedOnToolsVersion = 16.1; + }; + }; + }; + buildConfigurationList = 92EFE9012CF9A50000D1DE48 /* Build configuration list for PBXProject "AppwriteStarterKit" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 92EFE8FD2CF9A50000D1DE48; + minimizedProjectReferenceProxies = 1; + packageReferences = ( + 92ADF04F2D09736D00BCCA85 /* XCRemoteSwiftPackageReference "sdk-for-apple" */, + ); + preferredProjectObjectVersion = 77; + productRefGroup = 92EFE9072CF9A50000D1DE48 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 92EFE9052CF9A50000D1DE48 /* AppwriteStarterKit */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 92EFE9042CF9A50000D1DE48 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 92EFE9022CF9A50000D1DE48 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 92EFE9282CF9A50100D1DE48 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 92EFE9292CF9A50100D1DE48 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.1; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 92EFE92B2CF9A50100D1DE48 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; + DEVELOPMENT_TEAM = UL8M5L2622; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = AppwriteStarterKit; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIStatusBarHidden = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 16; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "io.appwrite.starter-kit"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 92EFE92C2CF9A50100D1DE48 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Sources/Preview Content\""; + DEVELOPMENT_TEAM = UL8M5L2622; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = AppwriteStarterKit; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIStatusBarHidden = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 16; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "io.appwrite.starter-kit"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 92EFE9012CF9A50000D1DE48 /* Build configuration list for PBXProject "AppwriteStarterKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 92EFE9282CF9A50100D1DE48 /* Debug */, + 92EFE9292CF9A50100D1DE48 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 92EFE92A2CF9A50100D1DE48 /* Build configuration list for PBXNativeTarget "AppwriteStarterKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 92EFE92B2CF9A50100D1DE48 /* Debug */, + 92EFE92C2CF9A50100D1DE48 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 92ADF04F2D09736D00BCCA85 /* XCRemoteSwiftPackageReference "sdk-for-apple" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/appwrite/sdk-for-apple"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 7.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 92ADF0502D09736D00BCCA85 /* Appwrite */ = { + isa = XCSwiftPackageProductDependency; + package = 92ADF04F2D09736D00BCCA85 /* XCRemoteSwiftPackageReference "sdk-for-apple" */; + productName = Appwrite; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 92EFE8FE2CF9A50000D1DE48 /* Project object */; +} diff --git a/AppwriteStarterKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/AppwriteStarterKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/AppwriteStarterKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/AppwriteStarterKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AppwriteStarterKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..03e8744 --- /dev/null +++ b/AppwriteStarterKit.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,132 @@ +{ + "originHash" : "b32cd2b3068b5e1b853d48245be465d6df4c2cf35c74ebfaf7af0be3d364c985", + "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "2119f0d9cc1b334e25447fe43d3693c0e60e6234", + "version" : "1.24.0" + } + }, + { + "identity" : "sdk-for-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/appwrite/sdk-for-apple", + "state" : { + "revision" : "4f54f6255d0981887f0284aef7d6d85bd61016c6", + "version" : "7.0.0" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "ef18d829e8b92d731ad27bb81583edd2094d1ce3", + "version" : "1.3.1" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "96a2f8a0fa41e9e09af4585e2724c4e825410b91", + "version" : "1.6.2" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "dca6594f65308c761a9c409e09fbf35f48d50d34", + "version" : "2.77.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "2e9746cfc57554f70b650b021b6ae4738abef3e6", + "version" : "1.24.1" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "eaa71bb6ae082eee5a07407b1ad0cbd8f48f9dca", + "version" : "1.34.1" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "c7e95421334b1068490b5d41314a50e70bab23d1", + "version" : "2.29.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "bbd5e63cf949b7db0c9edaf7a21e141c52afe214", + "version" : "1.23.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", + "version" : "1.4.0" + } + } + ], + "version" : 3 +} diff --git a/AppwriteStarterKit.xcodeproj/xcshareddata/xcschemes/AppwriteStarterKit.xcscheme b/AppwriteStarterKit.xcodeproj/xcshareddata/xcschemes/AppwriteStarterKit.xcscheme new file mode 100644 index 0000000..7f2fcbc --- /dev/null +++ b/AppwriteStarterKit.xcodeproj/xcshareddata/xcschemes/AppwriteStarterKit.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 427fe91..b5f6564 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,51 @@ -# starter-for-ios -Appwrite's starter kit for iOS  +# iOS Starter Kit with Appwrite + +Kickstart your iOS development with this ready-to-use starter project integrated with [Appwrite](https://appwrite.io). + +This guide will help you quickly set up, customize, and build your iOS app. + +--- + +## 🚀 Getting Started + +### Clone the Project +Clone this repository to your local machine using Git or directly from `Xcode`: + +```bash +git clone https://github.com/appwrite/starter-for-ios +``` + +Alternatively, open the repository URL in `Xcode` to clone it directly. + +--- + +## 🛠️ Development Guide + +1. **Configure Appwrite** +Navigate to `Sources/Config.plist` and update the values to match your Appwrite project credentials. + +2. **Customize as Needed** +Modify the starter kit to suit your app's requirements. Adjust UI, features, or backend integrations as per your needs. + +3. **Run the App** +Select a target device (simulator or connected physical iOS device) in `Xcode`, and click **Run** to start the app. + +--- + +## 📦 Building for Production + +To create a production build of your app: + +1. Open **Products** > **Schemes** in the menu bar. +2. Choose **Edit Scheme** or **New Scheme** to define your build settings. + - For a new scheme, fill in the required information. +3. Go to **Run** > **Build Configuration** and select **Release**. +4. Build and deploy your app in release mode. + +--- + +## 💡 Additional Notes + +- This starter project is designed to streamline your iOS development with Appwrite. +- Refer to the [Appwrite Documentation](https://appwrite.io/docs) for detailed integration guidance. + diff --git a/Sources/AppwriteApp.swift b/Sources/AppwriteApp.swift new file mode 100644 index 0000000..ace17df --- /dev/null +++ b/Sources/AppwriteApp.swift @@ -0,0 +1,16 @@ +import SwiftUI + +/// The main entry point for the application, initializing the Appwrite SDK +/// and setting up the main view with the environment object. +@main +struct AppwriteApp: App { + let appwrite = AppwriteSDK() + + var body: some Scene { + WindowGroup { + ContentView() + .preferredColorScheme(.light) /// Set light mode for the app + .environmentObject(appwrite) /// Provide the Appwrite SDK to the environment + } + } +} diff --git a/Sources/AppwriteSplashIcon.png b/Sources/AppwriteSplashIcon.png new file mode 100644 index 0000000..0de243d Binary files /dev/null and b/Sources/AppwriteSplashIcon.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/1024.png b/Sources/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 0000000..64990d2 Binary files /dev/null and b/Sources/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/114.png b/Sources/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000..d244dd3 Binary files /dev/null and b/Sources/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/120.png b/Sources/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 0000000..5aa98c8 Binary files /dev/null and b/Sources/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/180.png b/Sources/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 0000000..3983388 Binary files /dev/null and b/Sources/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/29.png b/Sources/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 0000000..72f9d6a Binary files /dev/null and b/Sources/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/40.png b/Sources/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 0000000..a76bae2 Binary files /dev/null and b/Sources/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/57.png b/Sources/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 0000000..5026e70 Binary files /dev/null and b/Sources/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/58.png b/Sources/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 0000000..ade177b Binary files /dev/null and b/Sources/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/60.png b/Sources/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 0000000..d0c5705 Binary files /dev/null and b/Sources/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/80.png b/Sources/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 0000000..86f3d69 Binary files /dev/null and b/Sources/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/87.png b/Sources/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 0000000..a55a6b8 Binary files /dev/null and b/Sources/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json b/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..73d3b7f --- /dev/null +++ b/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]} \ No newline at end of file diff --git a/Sources/Assets.xcassets/AppwriteIcon.imageset/Appwrite.png b/Sources/Assets.xcassets/AppwriteIcon.imageset/Appwrite.png new file mode 100644 index 0000000..592acb2 Binary files /dev/null and b/Sources/Assets.xcassets/AppwriteIcon.imageset/Appwrite.png differ diff --git a/Sources/Assets.xcassets/AppwriteIcon.imageset/Contents.json b/Sources/Assets.xcassets/AppwriteIcon.imageset/Contents.json new file mode 100644 index 0000000..2b09447 --- /dev/null +++ b/Sources/Assets.xcassets/AppwriteIcon.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Appwrite.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Appwrite.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Appwrite.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/Contents.json b/Sources/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Sources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Assets.xcassets/LaunchBackground.colorset/Contents.json b/Sources/Assets.xcassets/LaunchBackground.colorset/Contents.json new file mode 100644 index 0000000..22c4bb0 --- /dev/null +++ b/Sources/Assets.xcassets/LaunchBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/Config.plist b/Sources/Config.plist new file mode 100644 index 0000000..4d4ff25 --- /dev/null +++ b/Sources/Config.plist @@ -0,0 +1,12 @@ + + + + + APPWRITE_PROJECT_ID + my-project-id + APPWRITE_PROJECT_NAME + My project name + APPWRITE_PUBLIC_ENDPOINT + https://cloud.appwrite.io/v1 + + diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift new file mode 100644 index 0000000..5f93796 --- /dev/null +++ b/Sources/ContentView.swift @@ -0,0 +1,53 @@ +import SwiftUI + +/// Main content view for the application, responsible for rendering the UI components +/// and managing the state for connection to the Appwrite server, logs and the connection status. +struct ContentView: View { + /// An array of log entries to store connection logs. + @State private var logs: [Log] = [] + + /// The current status of the connection (idle, loading, success, error, etc.). + @State private var status: Status = .idle + + @State private var isSheetOpen: Bool = false + + var body: some View { + ZStack { + EmptyView().addCheckeredBackground() + + ScrollView { + VStack { + /// Top platform view displaying platform and appwrite logo + TopPlatformView( + status: status + ) + + /// Ping appwrite for a connection check + ConnectionStatusView( + logs: $logs, + status: $status + ) + + /// A list of info. cards + GettingStartedCards() + + /// Spacer to add some space for scrolling + Spacer(minLength: (16 * 3)) + } + /// smoothly moves the getting started cards + .animation(.default, value: status) + } + .padding([.bottom], 16) + .scrollIndicators(.hidden) + + /// Bottom sheet view to show logs with a collapsible behavior. + VStack { + CollapsibleBottomSheet(title: "Logs", logs: logs) + } + } + } +} + +#Preview { + ContentView().environmentObject(AppwriteSDK()) +} diff --git a/Sources/LaunchScreen.storyboard b/Sources/LaunchScreen.storyboard new file mode 100644 index 0000000..9250943 --- /dev/null +++ b/Sources/LaunchScreen.storyboard @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/Preview Content/Preview Assets.xcassets/Contents.json b/Sources/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Sources/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/components/modifier/CheckeredModifier.swift b/Sources/components/modifier/CheckeredModifier.swift new file mode 100644 index 0000000..d1d6b14 --- /dev/null +++ b/Sources/components/modifier/CheckeredModifier.swift @@ -0,0 +1,85 @@ +import SwiftUI + +/// A custom view modifier that adds a checkered background pattern with a gradient effect +/// and a radial gradient overlay. The checkered pattern consists of gray vertical and horizontal lines +/// drawn over the view's background. The modifier also includes a mask and a radial gradient +/// to create a layered visual effect. +struct CheckeredModifier: ViewModifier { + + func body(content: Content) -> some View { + ZStack { + Color(hex: "#FAFAFB") + + GeometryReader { geometry in + ZStack { + Canvas { context, size in + let lineThickness: CGFloat = 0.75 + let gridSize = min(geometry.size.width * 0.1, 64) + + // Draw vertical lines + for x in stride(from: 0, through: size.width, by: gridSize) { + let path = Path(CGRect(x: x, y: 0, width: lineThickness, height: size.height)) + context.fill(path, with: .color(Color.gray.opacity(0.3))) + } + + // Draw horizontal lines + for y in stride(from: 0, through: size.height, by: gridSize) { + let path = Path(CGRect(x: 0, y: y, width: size.width, height: lineThickness)) + context.fill(path, with: .color(Color.gray.opacity(0.3))) + } + } + .frame(width: geometry.size.width, height: geometry.size.height) + .mask( + LinearGradient( + gradient: Gradient(colors: [ + Color.black.opacity(0), + Color.black.opacity(0.8), + Color.black.opacity(0.6), + Color.black.opacity(0.4), + Color.black.opacity(0.2), + Color.black.opacity(0.1) + ]), + startPoint: .top, + endPoint: .bottom + ) + ) + + // Radial Gradient + RadialGradient( + gradient: Gradient(stops: [ + .init(color: Color.black.opacity(0), location: 0.0), + .init(color: Color.black.opacity(0.8), location: 0.2), + .init(color: Color.black.opacity(0.4), location: 0.5), + .init(color: Color.clear, location: 1.0) + ]), + center: .center, + startRadius: 0, + endRadius: max(geometry.size.width, geometry.size.height) * 2 + ) + .blendMode(.destinationOut) + + LinearGradient( + gradient: Gradient(stops: [ + .init(color: Color.black.opacity(0), location: 0.0), + .init(color: Color.black.opacity(0.3), location: 0.7), + .init(color: Color.black.opacity(0.6), location: 1.0) + ]), + startPoint: .top, + endPoint: .bottom + ) + .blendMode(.destinationOut) + } + .compositingGroup() + } + }.ignoresSafeArea() + + content + } +} + +#Preview { + VStack { + + } + .addCheckeredBackground() +} diff --git a/Sources/components/ui/CollapsibleBottomSheet.swift b/Sources/components/ui/CollapsibleBottomSheet.swift new file mode 100644 index 0000000..a7c8c85 --- /dev/null +++ b/Sources/components/ui/CollapsibleBottomSheet.swift @@ -0,0 +1,333 @@ +import SwiftUI + +/// A view that displays a collapsible bottom sheet showing logs. It includes a header with a +/// title and a count of logs, and the content of the bottom sheet can be expanded or collapsed +/// based on user interaction. +struct CollapsibleBottomSheet: View { + let title: String + let logs: [Log] + + @State private var isExpanded: Bool = false + + var body: some View { + Spacer() + + VStack(spacing: 0) { + // Header Section + VStack { + HStack { + // Title and log count + HStack(spacing: 8) { + Text(title) + .font(.subheadline) + .fontWeight(.semibold) + + Text("\(logs.count)") + .font(.subheadline) + .fontWeight(.semibold) + .frame(minWidth: 20, minHeight: 20) + .padding(.vertical, 2) + .padding(.horizontal, logs.count > 99 ? 5 : logs.count > 9 ? 3 : 2) + .background(Color.black.opacity(0.1)) + .cornerRadius(6) + } + .foregroundColor(Color(hex: "#56565C")) + + Spacer() + + // Chevron icon for expanding/collapsing + Image(systemName: "chevron.down") + .foregroundColor(Color(hex: "#97979B")) + .rotationEffect(.degrees(isExpanded ? 180 : 0)) + } + .padding() + .contentShape(Rectangle()) + .onTapGesture { + withAnimation { + isExpanded.toggle() + } + } + } + .background(Color.white) + .overlay(Divider().background(Color(hex: "#EDEDF0")), alignment: .top) + + // Collapsible Content Section + if isExpanded { + LogsBottomSheet(logs: logs) + .transition(.move(edge: .bottom)) + .background(Color.white) + } + } + /// push down + .padding(.bottom, -16) + .fixedSize(horizontal: false, vertical: true) + } +} + +/// A view displaying logs in a table format, including a project section and a list of logs. +/// If there are no logs, a placeholder message is shown. +struct LogsBottomSheet: View { + let logs: [Log] + + private let columns: [(name: String, width: CGFloat)] = [ + ("Date", 170), + ("Status", 90), + ("Method", 100), + ("Path", 125), + ("Response", 225) + ] + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + + // Project Section + ProjectSection() + + // Logs Table + if logs.isEmpty { + VStack { + HStack(spacing: 0) { + Text("Logs") + .font(.subheadline) + .padding(.vertical, 12) + .background(Color(hex: "#F9F9FA")) + .lineLimit(1) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal) + .foregroundColor(Color(hex: "#97979B")) + .background(Color(hex: "#FAFAFB")) + .overlay( + VStack { + Divider().background(Color(hex: "#EDEDF0")) + Spacer() + Divider().background(Color(hex: "#EDEDF0")) + } + ) + + Text("There are no logs to show") + .font(.subheadline.monospaced()) + .foregroundColor(Color(hex: "#56565C")) + .frame(maxWidth: .infinity, alignment: .leading) + .padding() + } + } else { + ScrollView(.horizontal) { + VStack(spacing: 0) { + // Table Headers + LogsTableHeader(columns: columns) + + // Table Rows + ForEach(logs) { log in + LogsTableRow(log: log, columns: columns) + } + } + } + } + }.animation(.default, value: logs.count) + } + .frame(maxHeight: UIScreen.main.bounds.height * 0.485) + } +} + +/// A view to display project information like endpoint, project ID, name, and version. +struct ProjectSection: View { + @EnvironmentObject var appwrite: AppwriteSDK + private var appwriteProjectData: (endpoint: String, projectId: String, projectName: String, version: String) { + appwrite.getProjectInfo() + } + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + // Project Title + Text("Project") + .font(.subheadline) + .padding(.horizontal) + .padding(.vertical, 12) + .foregroundColor(Color(hex: "#97979B")) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color(hex: "#FAFAFB")) + .overlay( + VStack { + Divider().background(Color(hex: "#EDEDF0")) + Spacer() + Divider().background(Color(hex: "#EDEDF0")) + } + ) + + // Project Details in a Grid + Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 16) { + GridRow { + ProjectRow(title: "Endpoint", value: appwriteProjectData.endpoint) + + + ProjectRow(title: "Project ID", value: appwriteProjectData.projectId) + } + GridRow { + ProjectRow(title: "Project name", value: appwriteProjectData.projectName) + + ProjectRow(title: "Version", value: appwriteProjectData.version) + } + } + .padding(.vertical, 8) + .padding(.horizontal) + .frame(maxWidth: .infinity, alignment: .leading) + } + } +} + +/// A reusable component to display individual project details like endpoint, project ID, etc. +struct ProjectRow: View { + let title: String + let value: String + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(title).font(.caption).foregroundColor(Color(hex: "#97979B")) + Text(value) + .font(.callout) + .foregroundColor(Color(hex: "#56565C")) + .lineLimit(1) + .truncationMode(.middle) + } + .padding([.top, .bottom], 4) + } +} + +/// A component that displays a log row in the table, with dynamic content based on the column name. +struct LogsTableRow: View { + let log: Log + let columns: [(name: String, width: CGFloat)] + + var body: some View { + HStack(spacing: 0) { + ForEach(columns, id: \.name) { column in + Group { + switch column.name { + case "Date": + Text(log.date) + case "Status": + StatusTag(status: log.status) + case "Method": + Text(log.method) + case "Path": + Text(log.path) + case "Response": + Text(log.response) + .padding(.vertical, 2) + .padding(.horizontal, 5) + .background(Color.gray.opacity(0.25)) + .cornerRadius(6) + default: + EmptyView() + } + } + .font(.subheadline.monospaced()) + .foregroundColor(Color(hex: "#56565C")) + .frame(width: column.width, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } + } + .padding(.horizontal) + .padding(.vertical, 10) + .background(Color.white) + .overlay(Divider(), alignment: .bottom) + } +} + +/// A view to display the header of the logs table, with column names as headers. +struct LogsTableHeader: View { + let columns: [(name: String, width: CGFloat)] + + var body: some View { + HStack(spacing: 0) { + ForEach(columns, id: \.name) { column in + Text(column.name) + .font(.subheadline) + .frame(width: column.width, alignment: .leading) + .background(Color(hex: "#F9F9FA")) + .lineLimit(1) + .truncationMode(.tail) + } + } + .padding(.horizontal) + .padding(.vertical, 12) + .foregroundColor(Color(hex: "#97979B")) + .background(Color(hex: "#FAFAFB")) + .overlay( + VStack { + Divider().background(Color(hex: "#EDEDF0")) + Spacer() + Divider().background(Color(hex: "#EDEDF0")) + } + ) + } +} + +/// A view to display a status tag with conditional styling based on the status value. +struct StatusTag: View { + let status: String + + var body: some View { + Text(status) + .font(.footnote) + .fontWeight(.medium) + .padding(.vertical, 2) + .padding(.horizontal, 5) + .background(Color(hex: statusColor.background).cornerRadius(6).opacity(0.24)) + .foregroundColor(Color(hex: statusColor.text)) + .fixedSize(horizontal: true, vertical: true) + } + + private var statusColor: (background: String, text: String) { + if let statusCode = Int(status), statusCode >= 200 && statusCode < 400 { + return ("#10B981", "#0A714F") + } else { + return ("#FF453A", "#B31212") + } + } +} + +/// A sample log model for displaying log entries. +struct Log: Identifiable { + let id = UUID() + let date: String + let status: String + let method: String + let path: String + let response: String +} + +@available(iOS 17.0, *) +#Preview { + @Previewable @State var showEmpty = false + + ZStack { + VStack { + // Button to toggle between empty and full logs state + Button("Logs state: \(showEmpty ? "Empty" : "Full")") { + showEmpty.toggle() + } + } + + VStack { + Spacer() + + CollapsibleBottomSheet( + title: "Logs", + logs: showEmpty ? [] : + Array(repeating: + Log( + date: "Dec 10, 02:51", + status: "200", + method: "GET", + path: "/v1/ping", + response: "Success"), + count: 5 + ) + ) + .environmentObject(AppwriteSDK()) + } + } +} diff --git a/Sources/components/ui/ConnectionLine.swift b/Sources/components/ui/ConnectionLine.swift new file mode 100644 index 0000000..45ea7c9 --- /dev/null +++ b/Sources/components/ui/ConnectionLine.swift @@ -0,0 +1,81 @@ +import SwiftUI + +/// A view that animates a connection line with a checkmark in the middle. The left and right +/// lines expand and contract based on the `show` state, with a tick appearing after a delay. +struct ConnectionLine: View { + let show: Bool + + @State private var showTick: Bool = false + @State private var expandLines: Bool = false + + var body: some View { + HStack { + // Left Line + LinearGradient( + gradient: Gradient(colors: [ + Color(hex: "#FE9567").opacity(0.15), + Color(hex: "#F02E65"), + ]), + startPoint: .leading, + endPoint: .trailing + ) + .frame(width: expandLines ? nil : 0, height: 1) + .animation(.easeInOut(duration: show ? 1.0 : 0).delay(showTick ? 0.2 : 0), value: expandLines) + + // Tick + ZStack { + // TODO: get the checkmark and circle sizes checked. + Circle() + .strokeBorder(Color(hex: "#F02E65").opacity(0.32), lineWidth: 1.8) + .background(Circle().fill(Color(hex: "#F02E65").opacity(0.08))) + .frame(width: 30, height: 30) + + Image(systemName: "checkmark") + .foregroundColor(Color(hex: "#FD366E")) + .font(.system(size: 15, weight: .bold)) + } + .opacity(showTick ? 1 : 0) + .animation(.easeIn(duration: show ? 0.5 : 0), value: showTick) + + // Right Line + LinearGradient( + gradient: Gradient(colors: [ + Color(hex: "#F02E65"), + Color(hex: "#FE9567").opacity(0.15), + ]), + startPoint: .leading, + endPoint: .trailing + ) + .frame(width: expandLines ? nil : 0, height: 1) + .animation(.easeInOut(duration: show ? 1.0 : 0).delay(showTick ? 0.2 : 0), value: expandLines) + } + .frame(height: 40) + .onChange(of: show) { newValue in + if newValue { + showTick = true + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + expandLines = true + } + } else { + expandLines = false + showTick = false + } + } + } +} + +@available(iOS 17.0, *) +#Preview { + @Previewable @State var showConnection = false + + ZStack { + VStack { + Button("Toggle Connection Line") { + showConnection.toggle() + } + .padding() + + ConnectionLine(show: showConnection).padding() + } + } +} diff --git a/Sources/components/ui/ConnectionStatusView.swift b/Sources/components/ui/ConnectionStatusView.swift new file mode 100644 index 0000000..8a44546 --- /dev/null +++ b/Sources/components/ui/ConnectionStatusView.swift @@ -0,0 +1,90 @@ +import SwiftUI + +/// A view that displays the current connection status and allows the user to send a ping +/// to verify the connection. It shows different messages based on the connection status. +struct ConnectionStatusView: View { + + @Binding var logs: [Log] + @Binding var status: Status + @EnvironmentObject var appwrite: AppwriteSDK + + var body: some View { + VStack(spacing: 8) { + // Main Status Message + Group { + if status == .loading { + HStack(alignment: .center, spacing: 16) { + ProgressView().scaleEffect(1) + Text("Waiting for connection...") + .font(.title3) + } + } else if status == .success { + Text("Congratulations!").font(.title2) + } else { + Text("Check connection").font(.title2) + } + } + .foregroundColor(Color(hex: "#2D2D31")) + + // Substatus Message + Group { + if status == .success { + Text("You connected your app successfully.") + } else if status == .idle || status == .error { + Text("Send a ping to verify the connection") + } + } + .font(.callout) + .foregroundColor(Color(hex: "#56565C")) + + // Ping Button + Text("Send a ping") + .font(.callout) + .fontWeight(.medium) + .padding(12) + .foregroundColor(.white) + .background(Color(hex: "#FD366E")) + .cornerRadius(12) + .padding(.top, 24) + .contentShape(Rectangle()) + .onTapGesture { + UIImpactFeedbackGenerator(style: .light).impactOccurred() + sendPing() + } + .opacity(status == .loading ? 0 : 1) + } + .padding(16) + .animation(.default, value: status) + .multilineTextAlignment(.center) + } + + func sendPing() { + status = .loading + Task { + let result = await appwrite.ping() + let statusCode = Int(result.status) ?? 0 + + // Delay to smooth out the transition + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + logs.append(result) + status = statusCode >= 200 && statusCode < 400 ? .success : .error + } + } + } +} + +enum Status: String { + case idle, loading, success, error +} + +@available(iOS 17.0, *) +#Preview { + @Previewable @State var logs: [Log] = [] + @Previewable @State var status: Status = .idle + let appwrite = AppwriteSDK() + + ConnectionStatusView( + logs: $logs, + status: $status + ).environmentObject(appwrite) +} diff --git a/Sources/components/ui/GettingStartedCards.swift b/Sources/components/ui/GettingStartedCards.swift new file mode 100644 index 0000000..57badd4 --- /dev/null +++ b/Sources/components/ui/GettingStartedCards.swift @@ -0,0 +1,117 @@ +import SwiftUI + +/// A view that contains a list of informational cards displayed vertically. +struct GettingStartedCards: View { + var body: some View { + VStack(spacing: 16) { + // First Card: Edit your app + GeneralInfoCard( + title: "Edit your app", + link: nil, + subtitle: { + codeViewContent + } + ) + + // Second Card: Head to Appwrite Cloud + GeneralInfoCard( + title: "Head to Appwrite Cloud", + link: "https://cloud.appwrite.io", + subtitle: { + Text("Start managing your project from the Appwrite console") + } + ) + + // Third Card: Explore docs + GeneralInfoCard( + title: "Explore docs", + link: "https://appwrite.io/docs", + subtitle: { + Text("Discover the full power of Appwrite by diving into our documentation") + } + ) + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } + + private var codeViewContent: some View { + let baseFont = Font.body + var attributedString = AttributedString("Edit ") + attributedString.font = baseFont + + var codeSnippet = AttributedString(" ContentView.swift ") + codeSnippet.font = .subheadline.monospaced() + codeSnippet.backgroundColor = Color(hex: "#EDEDF0") + codeSnippet.inlinePresentationIntent = .code + + var continuation = AttributedString(" to get started with building your app") + continuation.font = baseFont + + return Text(attributedString + codeSnippet + continuation) + } + +} + +/// A reusable card component that displays a title and a subtitle with optional link functionality. +/// If a link is provided, the card becomes clickable and opens the destination URL. +struct GeneralInfoCard: View { + let title: String + let link: String? + @ViewBuilder let subtitle: Subtitle + + var body: some View { + Group { + if let link = link, let url = URL(string: link) { + Link(destination: url) { + cardContent + .padding() + .background(Color.white) + .cornerRadius(12) + .frame(maxWidth: .infinity, alignment: .leading) + } + .buttonStyle(PlainButtonStyle()) + } else { + cardContent + .padding() + .background(Color.white) + .cornerRadius(12) + .frame(maxWidth: .infinity, alignment: .leading) + } + } + .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color(hex: "#EDEDF0"), lineWidth: 1)) + } + + private var cardContent: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(title) + .font(.system(size: 20)) + .foregroundColor(Color(hex: "#2D2D31")) + + if link != nil { + Spacer() + Image(systemName: "arrow.right") + .foregroundColor(Color(hex: "#D8D8DB")) + } + } + + subtitle + .font(.subheadline) + .foregroundColor(Color(hex: "#56565C")) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +#Preview { + ZStack { + Color.clear.addCheckeredBackground() + + VStack { + Spacer() + GettingStartedCards() + } + } +} diff --git a/Sources/components/ui/TopPlatformView.swift b/Sources/components/ui/TopPlatformView.swift new file mode 100644 index 0000000..613d3ab --- /dev/null +++ b/Sources/components/ui/TopPlatformView.swift @@ -0,0 +1,114 @@ +import SwiftUI + +/// A view that displays two platform icons with a connection line in between, +/// representing the connection status between the platforms. +struct TopPlatformView: View { + let status: Status + + var body: some View { + HStack(spacing: -2.5) { + PlatformIcon(isAppwriteIcon: false) { + Image(systemName: "apple.logo") + .resizable() + .aspectRatio(contentMode: .fit) + } + + ConnectionLine(show: status == .success) + .frame(maxWidth: .infinity) + + PlatformIcon(isAppwriteIcon: true) { + Image("AppwriteIcon") + .resizable() + .aspectRatio(contentMode: .fit) + } + } + .padding(8) + .padding(.horizontal, 40) + } +} + +/// A reusable component that displays a rounded platform icon with customizable content. +struct PlatformIcon: View { + + let isAppwriteIcon: Bool + @ViewBuilder let content: () -> Content + + var body: some View { + ZStack { + // Card Content + ZStack { + // Outer Card + RoundedRectangle(cornerRadius: 24) + .fill(Color.white.opacity(0.32)) + .frame(width: 100, height: 100) + .overlay( + RoundedRectangle(cornerRadius: 24) + .inset(by: 0.5) + .stroke(Color(hex: "#19191C").opacity(0.04), lineWidth: 1) + ) + + + + // Inner Card + RoundedRectangle(cornerRadius: 16) + .fill(Color.white) + .frame(width: 86.04652, height: 86.04652) + .overlay( + RoundedRectangle(cornerRadius: 16) + .inset(by: 0.5) + .stroke(Color(hex: "#FAFAFB"), lineWidth: 1) + ) + .shadow(color: .black.opacity(0.02), radius: 4, x: 0, y: 6) + .shadow(color: .black.opacity(0.02), radius: 6, x: 0, y: 2) + + + // Content + content() + .frame( + width: isAppwriteIcon ? 37.86047 : 41.86047, + height: isAppwriteIcon ? 37.86047 : 41.86047, + alignment: .center + ) + } + } + .frame(width: 105, height: 105) + } +} + +@available(iOS 17.0, *) +#Preview { + @Previewable @State var status: Status = .idle + + ZStack { + EmptyView().addCheckeredBackground() + + ScrollView { + VStack { + /// Top platform view displaying platform and appwrite logo + TopPlatformView( + status: status + ) + + /// Ping appwrite for a connection check + ConnectionStatusView( + logs: .constant([]), + status: $status + ) + + /// A list of info. cards + GettingStartedCards() + + /// Spacer to add some space for scrolling + Spacer(minLength: (16 * 3)) + } + } + .padding([.bottom], 16) + .scrollIndicators(.hidden) + + /// Bottom sheet view to show logs with a collapsible behavior. + VStack { + Spacer() + CollapsibleBottomSheet(title: "Logs", logs: []) + } + }.environmentObject(AppwriteSDK()) +} diff --git a/Sources/extensions/Color+Hex.swift b/Sources/extensions/Color+Hex.swift new file mode 100644 index 0000000..074a65e --- /dev/null +++ b/Sources/extensions/Color+Hex.swift @@ -0,0 +1,23 @@ +import Foundation +import SwiftUI + +/// A custom initializer for `Color` that accepts a hex string to create a color. +extension Color { + init(hex: String) { + // Remove the leading "#" if present + let hexString = hex.hasPrefix("#") ? String(hex.dropFirst()) : hex + + // Ensure the hex string is exactly 6 characters (valid RGB hex code) + guard hexString.count == 6, let rgb = UInt64(hexString, radix: 16) else { + self.init(red: 0, green: 0, blue: 0) + return + } + + // Extract red, green, and blue components + let r = Double((rgb >> 16) & 0xFF) / 255.0 + let g = Double((rgb >> 8) & 0xFF) / 255.0 + let b = Double(rgb & 0xFF) / 255.0 + + self.init(red: r, green: g, blue: b) + } +} diff --git a/Sources/extensions/View+Background.swift b/Sources/extensions/View+Background.swift new file mode 100644 index 0000000..a48b6c2 --- /dev/null +++ b/Sources/extensions/View+Background.swift @@ -0,0 +1,8 @@ +import SwiftUI + +/// A view modifier extension to easily apply a checkered background to any view. +extension View { + func addCheckeredBackground() -> some View { + self.modifier(CheckeredModifier()) + } +} diff --git a/Sources/server/Appwrite.swift b/Sources/server/Appwrite.swift new file mode 100644 index 0000000..1bc0530 --- /dev/null +++ b/Sources/server/Appwrite.swift @@ -0,0 +1,117 @@ +import Appwrite +import Foundation + +/// A class that provides convenient access to Appwrite API methods for interacting with the project, account, and databases. +class AppwriteSDK: ObservableObject { + + private let client: Client + private let account: Account + private let databases: Databases + + /// Change these in Config.plist + private let APPWRITE_PROJECT_ID: String + private let APPWRITE_PROJECT_NAME: String + private let APPWRITE_PUBLIC_ENDPOINT: String + + init() { + let config = AppwriteSDK.loadConfig() + + self.APPWRITE_PROJECT_ID = config.projectId + self.APPWRITE_PROJECT_NAME = config.projectName + self.APPWRITE_PUBLIC_ENDPOINT = config.endpoint + + client = Client() + .setProject(APPWRITE_PROJECT_ID) + .setEndpoint(APPWRITE_PUBLIC_ENDPOINT) + + account = Account(client) + databases = Databases(client) + } + + /// Returns project-related information such as endpoint, project ID, name, and version. + func getProjectInfo() -> (endpoint: String, projectId: String, projectName: String, version: String) { + return ( + client.endPoint, + APPWRITE_PROJECT_ID, + APPWRITE_PROJECT_NAME, + client.headers["x-appwrite-response-format"] ?? "1.6.0" + ) + } + + /// Performs a ping request to the Appwrite API and returns the response as a `Log`. + /// - Returns: A `Log` object representing the ping request's result. + func ping() async -> Log { + do { + return try await executeRequest() + } catch { + return Log( + date: getCurrentDate(), + status: "Error", + method: "GET", + path: "/ping", + response: "Request failed: \(error.localizedDescription)" + ) + } + } + + // TODO: Remove this once the SDK supports ping. + /// Executes a manual request to the /ping endpoint of the Appwrite API and returns the result as a `Log`. + /// - Throws: An error if the request fails. + /// - Returns: A `Log` object with the response data. + private func executeRequest() async throws -> Log { + let url = URL(string: client.endPoint + "/ping")! + var request = URLRequest(url: url) + request.httpMethod = "GET" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + // Add custom headers from client + client.headers.forEach { key, value in + request.setValue(value, forHTTPHeaderField: key) + } + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse else { + throw NSError(domain: "InvalidResponse", code: 0, userInfo: nil) + } + + let status = String(httpResponse.statusCode) + let responseString: String + + if httpResponse.statusCode == 200, let stringResponse = String(data: data, encoding: .utf8) { + responseString = stringResponse + } else { + responseString = "Request failed with status code \(status)" + } + + return Log( + date: getCurrentDate(), + status: status, + method: "GET", + path: "/ping", + response: responseString + ) + } + + /// Returns the current date formatted as "MMMM dd, HH:mm". + /// - Returns: A string representing the current date. + private func getCurrentDate() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "MMM dd, HH:mm" + return formatter.string(from: Date()) + } + + /// Loads configuration values from the Config.plist file. + /// - Returns: A tuple containing the project ID, project name, and endpoint URL as strings. + private static func loadConfig() -> (projectId: String, projectName: String, endpoint: String) { + guard let configPath = Bundle.main.path(forResource: "Config", ofType: "plist"), + let config = NSDictionary(contentsOfFile: configPath) as? [String: Any], + let projectId = config["APPWRITE_PROJECT_ID"] as? String, + let projectName = config["APPWRITE_PROJECT_NAME"] as? String, + let endpoint = config["APPWRITE_PUBLIC_ENDPOINT"] as? String else { + fatalError("Missing or invalid Config.plist or required keys") + } + + return (projectId, projectName, endpoint) + } +}