diff --git a/src/routes/docs/tutorials/apple/step-1/+page.markdoc b/src/routes/docs/tutorials/apple/step-1/+page.markdoc index fdc8e745d8..05d48ae5e3 100644 --- a/src/routes/docs/tutorials/apple/step-1/+page.markdoc +++ b/src/routes/docs/tutorials/apple/step-1/+page.markdoc @@ -1,18 +1,34 @@ --- layout: tutorial -title: Coming soon +title: Build an habit tracker with SwiftUI description: Learn to build an Apple app with no backend code using an Appwrite backend. framework: Apple category: Mobile and native step: 1 -draft: true +back: /docs --- -Improve the docs, add this guide. +**Habit tracker**: an app to keep track of your habit. +In this tutorial, you will build Habit tracker with Appwrite and SwiftUI. -We still don't have this guide in place, but we do have some great news. -The Appwrite docs, just like Appwrite, is completely open sourced. -This means, anyone can help improve them and add new guides and tutorials. +{% only_dark %} +![Habit Tracker Screenshot](/images/docs/tutorials/dark/apple-habit-tracker.png) +{% /only_dark %} +{% only_light %} +![Habit Tracker Screenshot](/images/docs/tutorials/apple-habit-tracker.png) +{% /only_light %} -If you see this page, **we're actively looking for contributions to this page**. -Follow our contribution guidelines, open a PR to [our Website repo](https://github.com/appwrite/website), and collaborate with our core team to improve this page. \ No newline at end of file +# Concepts {% #concepts %} + +This tutorial will introduce the following concepts: + +1. Setting up your first project +2. Authentication +3. Databases and collections +4. Queries and pagination + + +# Prerequisites {% #prerequisites %} + +1. Basic knowledge of Swift and SwiftUI. +2. Have [Xcode](https://developer.apple.com/download/all/?q=Xcode) installed on your computer. \ No newline at end of file diff --git a/src/routes/docs/tutorials/apple/step-2/+page.markdoc b/src/routes/docs/tutorials/apple/step-2/+page.markdoc new file mode 100644 index 0000000000..2d735e9763 --- /dev/null +++ b/src/routes/docs/tutorials/apple/step-2/+page.markdoc @@ -0,0 +1,23 @@ +--- +layout: tutorial +title: Create app +description: Create and app with Appwrite Cloud and Xcode. +step: 2 +--- + +# Setup new project {% #new-project %} + +Open Xcode and select **Create new project**, then select app as template for iOS and click **Next**. Fill out all of the required information, such as Product Name, Organization identifier, Interface (SwiftUI), and click **Next**. Select the folder where you want to set up the project and click **Create**. + +![Create project screen](/images/docs/tutorials/xcode-new-project-setup.png) + +# Add the Appwrite SDK {% #appwrite-sdk %} +To add the Appwrite SDK for Apple as a dependency, open the **File** menu and click **Add Package Dependencies**. + +In the **Package URL** search box, enter `https://github.com/appwrite/sdk-for-apple`. + +Once the SDK is found, select **Up to Next Major Version** as your **Dependency Rule** and click **Add Package**. + +When dependency resolution is complete, click **Add Package** again to add the SDK package to your target. + +![Create project screen](/images/docs/tutorials/xcode-add-appwrite-sdk.png) \ No newline at end of file diff --git a/src/routes/docs/tutorials/apple/step-3/+page.markdoc b/src/routes/docs/tutorials/apple/step-3/+page.markdoc new file mode 100644 index 0000000000..148e39573e --- /dev/null +++ b/src/routes/docs/tutorials/apple/step-3/+page.markdoc @@ -0,0 +1,75 @@ +--- +layout: tutorial +title: Set up Appwrite +description: Import and configure a project with Appwrite Cloud and Xcode. +step: 3 +--- + +# Create project {% #create-project %} + +Head to the [Appwrite Console](https://cloud.appwrite.io/console). + +{% only_dark %} +![Create project screen](/images/docs/quick-starts/dark/create-project.png) +{% /only_dark %} +{% only_light %} +![Create project screen](/images/docs/quick-starts/create-project.png) +{% /only_light %} + +If this is your first time using Appwrite, create an account and create your first project. + +Then, under **Add a platform**, add an **Apple app**. Choose any of **iOS**, **macOS**, **watchOS** or **tvOS** as your Apple platform. If you are creating a multi-platform app, you can add more platforms later. + +Add your app's **product name** and **bundle identifier**, your bundle identifier is the one entered when creating an Xcode project. For existing projects, you should use the **bundle identifier** from your project files **Identity** section. + +{% only_dark %} +![Add a platform](/images/docs/quick-starts/dark/add-platform.png) +{% /only_dark %} +{% only_light %} +![Add a platform](/images/docs/quick-starts/add-platform.png) +{% /only_light %} + +# Setup Database {% #setup-database %} + +In Appwrite, data is stored as a collection of documents. Create a collection in the [Appwrite Console](https://cloud.appwrite.io/) to store our habits. + +# Create database {% #create-database %} + +Head to your [Appwrite Console](https://cloud.appwrite.io/console/) and create a database and name it `Habit-SwiftUI`. +Optionally, add a custom database ID. + +# Create collection {% #create-collection %} +Create a collection and name it `habits`. Optionally, add a custom collection ID. + +Navigate to **Attributes** and create attributes by clicking **Create attribute** and add the following attributes. + +| Field | Type | Required | Size | Min | Max | Default Value | +|----------------|----------|----------|------|-----|-----|---------------| +| userId | String | Yes | 250 | | | | +| title | String | Yes | 250 | | | | +| description | String | No | | | | | +| icon | String | No | 200 | | | calendar | +| goals | Integer | Yes | | 1 | 10 | 1 | +| goalCompleted | Integer | Yes | | 0 | 10 | 0 | +| startDate | DateTime | No | | | | | +| endDate | DateTime | No | | | | | + +Attributes define the structure of your collection's documents. Enter **Attribute key** and **Size**. For example, `title` and `100`. + +Navigate to **Settings** > **Permissions** and add a new role **All Users**. +Check the **CREATE**, **UPDATE**, **DELETE** and **READ** permissions, so anyone can create and read documents. + +Create a swift file name `Database` in the `Shared/Constant` folder. We will use this file to store the database id and collection id from Appwrite as enum. + +```swift +import Foundation + +enum Database: String { + case habit = "[DATABASE_ID]" +} + + +enum DatabaseCollections : String { + case habits = "[COLLECTION_ID]" +} +``` \ No newline at end of file diff --git a/src/routes/docs/tutorials/apple/step-4/+page.markdoc b/src/routes/docs/tutorials/apple/step-4/+page.markdoc new file mode 100644 index 0000000000..9a9f8e2678 --- /dev/null +++ b/src/routes/docs/tutorials/apple/step-4/+page.markdoc @@ -0,0 +1,157 @@ +--- +layout: tutorial +title: Manage service and view models +description: Manage Appwrite wervice using Appwrite Apple SDK and SwiftUI application View Model. +step: 4 +--- + +# Appwrite service {% #appwrite-service %} + +Create a new file AppwriteService.swift inside `Shared/Services` folder and add the following code to it, replacing [YOUR_PROJECT_ID] with your project ID. The purpose of this file is to initialize Appwrite SDK and create necessary methods needed for both authentication and read/write from documents. + +```swift +import Foundation +import Appwrite +import JSONCodable + +class AppwriteService { + var client: Client + var account: Account + var database: Databases + + public init() { + self.client = Client() + .setEndpoint("https://cloud.appwrite.io/v1") + .setProject("[YOUR_PROJECT_ID]") + self.account = Account(client) + self.database = Databases(client) + } + + + public func getDocs(_ db: Database, _ collection: DatabaseCollections, queries: [String]? = nil) async throws -> DocumentList { + try await database.listDocuments( + databaseId: db.rawValue, + collectionId: collection.rawValue, + queries: queries, + nestedType: T.self + ) + } + + public func insertDoc(_ db: Database, _ collection: DatabaseCollections, data: Any) async throws { + _ = try await database.createDocument( + databaseId: db.rawValue, + collectionId: collection.rawValue, + documentId: ID.unique(), + data: data + ) + } + + + public func updateDoc(_ db: Database, _ collection: DatabaseCollections, _ id: String, data: Any) async throws { + _ = try await database.updateDocument( + databaseId: db.rawValue, + collectionId: collection.rawValue, + documentId: id, + data: data + ) + } + + + public func removeDoc(_ db: Database, _ collection: DatabaseCollections, _ id: String, data: Any) async throws { + _ = try await database.deleteDocument( + databaseId: db.rawValue, + collectionId: collection.rawValue, + documentId: id + ) + } + +} +``` + +# Snackbar service {% #snackbar-service %} + +In order to manage error toast, we need a view model to handle and dispatch error messsgae to our UI. Create a new file SnackbarService.swift inside `Shared/Services` folder and add the following code to it. + +```swift +import SwiftUI + +struct SnackBarState: Identifiable, Equatable { + static func == (lhs: SnackBarState, rhs: SnackBarState) -> Bool { + lhs.id == rhs.id + } + + let id = UUID() + let hasError: Bool + let error: Error +} + +class SnackBarService: ObservableObject { + @Published private (set) var snackBarState: SnackBarState? + + @MainActor + func displayError(_ error: Error) { + snackBarState = nil + snackBarState = SnackBarState(hasError: true, error: error) + DispatchQueue.main.asyncAfter(deadline: .now() + 4.0, execute: { + withAnimation(.easeOut(duration: 0.3)) { + self.snackBarState = nil + } + }) + } +} +``` + +Add the following code to SnackbarView.swift inside `Shared/Views` folder. + +```swift +import SwiftUI + +struct SnackbarView: View { + @State private var isAnimating: Bool = false + @State var text: String + @State var isError: Bool = true + + var body: some View { + HStack { + Text(text) + .foregroundColor(.white) + .lineLimit(1...2) + .multilineTextAlignment(.leading) + .padding() + } + .background(isError ? .red : Color.accentColor) + .cornerRadius(10) + .padding(.top, 15) + .opacity(isAnimating ? 1 : 0) + .offset(y: isAnimating ? -20 : 20) + .onAppear { + withAnimation(.easeOut(duration: 0.3)) { + isAnimating.toggle() + } + } + } +} +``` + +# Habit model {% #habit-model %} + +Add the following code to HabitModel.swift inside `Shared/Models` folder. + +```swift +import Appwrite +import Foundation + + + +struct HabitModel: Codable, Identifiable { + let id: String + let userId: String + let title: String + let description: String? + let goals: Int? + let goalCompleted: Int? + let icon: String + let startDate: String? + let endDate: String? +} +``` \ No newline at end of file diff --git a/src/routes/docs/tutorials/apple/step-5/+page.markdoc b/src/routes/docs/tutorials/apple/step-5/+page.markdoc new file mode 100644 index 0000000000..26e4d92700 --- /dev/null +++ b/src/routes/docs/tutorials/apple/step-5/+page.markdoc @@ -0,0 +1,402 @@ +--- +layout: tutorial +title: Manage navigation +description: Add navigation to your SwiftUI app with Appwrite authentication. +step: 5 +--- + +# Router {% #router %} + +Routing is the process of navigating from one view to another, and SwiftUI makes use of the NavigationStack to allow us to control and manage how we push and pop views on the navigation stack. In order to define all our routes, specify the view for each route, and implement push and pop methods, we need to create a routing object. This code should be added to the `Router.swift` file located in the root folder. + +```swift +import SwiftUI + + +enum Tabs { + case home, progress, setting +} + +enum Route { + case appwrite + case home + case auth + case add_habit +} + +extension Route: View { + var body: some View { + switch self { + case .appwrite: + ContentView() + case .home: + HomeView() + case .auth: + AuthScreen() + case .add_habit: + AddHabitScreen() + } + + } +} + + +extension Route: Hashable { + static func == (lhs: Route, rhs: Route) -> Bool { + return lhs.compareString == rhs.compareString + } + + var compareString: String { + switch self { + case .appwrite: + return "appwrite" + case .home: + return "home" + case .auth: + return "auth" + case .add_habit: + return "addHabit" + } + } +} + + +final class Router: ObservableObject { + @Published var routes = [Route]() + @Published var selectedTab: Tabs = .home + + func push(_ screen: Route) { + routes.append(screen) + } + + func pushReplacement(_ screen: Route) { + if routes.isEmpty { + routes.append(screen) + } else { + routes[routes.count - 1] = screen + } + } + + func pop() { + routes.removeLast() + } + + func popUntil(predicate: (Route) -> Bool) { + if let last = routes.popLast() { + guard predicate(last) else { + popUntil(predicate: predicate) + return + } + } + } + + func reset() { + routes = [] + } +} +``` + +# ContentView {% #contentview %} + +Update the `ContentView.swift` to reflect some changes related to our navigation + +```swift +import SwiftUI + +struct ContentView: View { + + @EnvironmentObject private var router: Router + @EnvironmentObject private var userViewModel: UserViewModel + @EnvironmentObject private var snackBarService: SnackBarService + @Environment(\.colorScheme) private var theme + + + var body: some View { + NavigationStack(path: $router.routes) { + VStack { + + Image(theme == .dark ? "appwrite_light" : "appwrite_dark") + .frame(width: 132, height: 24) + + } + .task { + + let isAuthenticated = await userViewModel.getCurrentSession() + + if !isAuthenticated { + router.pushReplacement(.auth) + } else { + router.pushReplacement(.home) + } + } + .navigationDestination(for: Route.self, destination: { $0 }) + + } + .overlay(alignment: .top) { + if (snackBarService.snackBarState?.hasError == true) { + SnackbarView(text: snackBarService.snackBarState?.error.localizedDescription ?? "An error occured") + } + } + + } +} +``` + +> Note: You can get the Appwrite logo on [Appwrite website](https://appwrite.io/assets/) and add it to your project assets + +# Inject services and view models {% #inject-services-and-view-models %} + +One of the important aspects of injecting services in SwiftUI with `@EnvironmentObject` is data sharing. This approach makes the service available and accessible in all your view components. Let's inject the Appwrite Service and Snackbar Service into our ViewModel, and then inject it into our app using `@EnvironmentObject`. Update the entry file with the following code. + +```swift +import SwiftUI + +@main +struct appwrite_hacktoberfestApp: App { + + private var appwriteService = AppwriteService() + private var snackbarService = SnackBarService() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(Router()) + .environmentObject(UserViewModel(appwriteService, snackbarService)) + .environmentObject(HabitViewModel(appwriteService, snackbarService)) + .environmentObject(snackbarService) + } + } +} +``` + +Note: Change ``appwrite_hacktoberfestApp`` to your project name + + +## User view model {% #user-view-models %} + +Add the following code to UserViewModel.swift inside `Home/ViewModels` folder. + +```swift +import Foundation +import Appwrite + +struct UserState { + var userId: String? = nil + var loading: Bool = false + var isLogout: Bool = false + +} + +final class UserViewModel: ObservableObject { + + private var appwriteService: AppwriteService + private var snackbarService: SnackBarService + @Published var userState: UserState = UserState() + + init(_ appwriteService: AppwriteService, _ snackbarService: SnackBarService) { + self.appwriteService = appwriteService + self.snackbarService = snackbarService + } + + @MainActor + func getCurrentSession() async -> Bool { + userState.loading = true + defer { userState.loading = false } + + do { + + let res = try await appwriteService.currentSession() + + print(res) + + userState.userId = res.id + userState.isLogout = false + + return true + + } catch { + + print(error) + userState.isLogout = true + + return false + } + + } + + @MainActor + func login(_ email: String, _ password: String) async { + + do { + let session = try await appwriteService.onLogin(email, password) + + print(session) + } catch { + + snackbarService.displayError(error) + + } + + } + + @MainActor + func register(_ email: String, _ password: String) async { + + do { + let session = try await appwriteService.onRegister(email, password) + + print(session) + } catch { + + snackbarService.displayError(error) + + } + + } + + @MainActor + func logout() async { + + do { + _ = try await appwriteService.onLogout() + + } catch { + + snackbarService.displayError(error) + + } + + } + + @MainActor + func deleteAccount() async { + + do { + _ = try await appwriteService.onAccountDelete() + + } catch { + + snackbarService.displayError(error) + + } + + } +} +``` + +# Habit view model {% #habit-view-model %} + +`HabitViewModel` class serves as the view model for handling Habit-related data and business logic in our app including fetch records, create new record, update record etc. Add the following code to `HabitViewModel.swift` inside `Home/ViewModels` folder. + +```swift +import Appwrite +import Foundation + + +struct HabitState { + var habits: [HabitModel] = [] + var gettingHabit: Bool = false +} + +struct SelectedHabitState { + var habit: HabitModel? = nil + var isSelected: Bool = false +} + + +final class HabitViewModel: ObservableObject { + + private var appwriteService: AppwriteService + private var snackbarService: SnackBarService + @Published var habitState: HabitState = HabitState() + @Published var selectedHabitState: SelectedHabitState = SelectedHabitState() + + init(_ appwriteService: AppwriteService, _ snackbarService: SnackBarService) { + self.appwriteService = appwriteService + self.snackbarService = snackbarService + } + + + @MainActor + func fetchHabits() async { + + do { + + habitState.gettingHabit = true + + defer { habitState.gettingHabit = false } + + let data: DocumentList = try await appwriteService.getDocs(.habit, .habits) + + habitState.habits = data.documents.map{ doc in + + HabitModel(id: doc.id, userId: doc.data.userId, title: doc.data.title, description: doc.data.description, goals: doc.data.goals, goalCompleted: doc.data.goalCompleted, icon: doc.data.icon, startDate: doc.data.startDate, endDate: doc.data.endDate) + + } + + } catch { + + snackbarService.displayError(error) + + } + + } + + + + @MainActor + func create(_ title: String, _ description: String, _ goals: Int, _ icon: String, _ startDate: String, _ endDate: String, _ userId: String) async { + + do { + + try await appwriteService.insertDoc( + .habit, + .habits, + data: [ + "title": title, + "description": description, + "goals": goals, + "goalCompleted": 0, + "icon": icon, + "startDate": startDate, + "endDate": endDate, + "userId": userId + ] + ) + + } catch { + + print(error) + + snackbarService.displayError(error) + + } + + } + + + @MainActor + func update(_ id: String, _ goalCompleted: Int) async { + + do { + + try await appwriteService.updateDoc( + .habit, + .habits, + id, + data: [ + "goalCompleted": goalCompleted + ] + ) + + } catch { + + print(error) + + snackbarService.displayError(error) + + } + + } +} +``` \ No newline at end of file diff --git a/src/routes/docs/tutorials/apple/step-6/+page.markdoc b/src/routes/docs/tutorials/apple/step-6/+page.markdoc new file mode 100644 index 0000000000..5a41fa4b5b --- /dev/null +++ b/src/routes/docs/tutorials/apple/step-6/+page.markdoc @@ -0,0 +1,147 @@ +--- +layout: tutorial +title: Add authentication +description: Add authentication to your SwiftUI application. +step: 6 +--- + +# Authentication {% #authentication %} + +In order to allow creating OAuth sessions, the following URL scheme must be added to your **Info.plist** file. + +```xml +CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + io.appwrite + CFBundleURLSchemes + + appwrite-callback-[PROJECT_ID] + + + +``` + +In `AppwriteService.swift`, let's update the object to include the auth methods by adding the following snippets. + +```swift + public func currentSession() async throws -> User<[String: AnyCodable]> { + try await account.get() + } + + public func onRegister( + _ email: String, + _ password: String + ) async throws -> User<[String: AnyCodable]> { + try await account.create( + userId: ID.unique(), + email: email, + password: password + ) + } + + public func onLogin( + _ email: String, + _ password: String + ) async throws -> Session { + try await account.createEmailSession( + email: email, + password: password + ) + } + + public func onLogout() async throws { + _ = try await account.deleteSession( + sessionId: "current" + ) + } + + + public func onAccountDelete() async throws { + _ = try await account.deleteSession( + sessionId: "current" + ) + } +``` + +Add the following code to `AuthView.swift` inside the `Auth/View` folder. This UI is designed to handle both user login and registration. + +```swift +import SwiftUI + +struct AuthScreen: View { + + @State var email: String = "" + @State var password: String = "" + @State var isRegister: Bool = false + @FocusState private var focusedTextField: FormTextField? + @EnvironmentObject private var userViewModel: UserViewModel + @EnvironmentObject private var router: Router + + enum FormTextField { + case email, password + } + + + var body: some View { + NavigationView { + + + VStack { + + Form { + Section { + + TextField("Email",text: $email) + .focused($focusedTextField, equals: .email) + .onSubmit { focusedTextField = .password } + .submitLabel(.next) + + SecureField("Password", text: $password) + .focused($focusedTextField, equals: .password) + .onSubmit { focusedTextField = nil } + .submitLabel(.continue) + + } + + Button( action: { Task { + if isRegister { + + await userViewModel.register(email, password) + + isRegister = false + + } else { + + await userViewModel.login(email, password) + + router.pushReplacement(.home) + + } + + }}, + label: { + Text(isRegister ? "Register" : "Login") + }) + + } + + Button(isRegister ? "Already have account, Login" : "Don't have account, Register") { + isRegister = !isRegister + } + } + + .navigationTitle(isRegister ? "Create account" : "Welcome") + + + + } + .navigationBarBackButtonHidden(true) + + } +} + +``` \ No newline at end of file diff --git a/src/routes/docs/tutorials/apple/step-7/+page.markdoc b/src/routes/docs/tutorials/apple/step-7/+page.markdoc new file mode 100644 index 0000000000..0fa3e6329b --- /dev/null +++ b/src/routes/docs/tutorials/apple/step-7/+page.markdoc @@ -0,0 +1,379 @@ +--- +layout: tutorial +title: Habit UI +description: Add, update and query Database in SwiftUI +step: 7 +--- + +# HomeView UI {% #homeview-ui %} + +Using `TabView` user will be able to switch between multiple tab from home screen to progress to setting/preference. And with ``blur`` modifier we able to blur the home view and show modal of medium size for user to update the habit goal completed. Also performing async task inside ``task`` modifier in order to fetch habits record + +```swift +import SwiftUI + +struct HomeView: View { + + @EnvironmentObject private var router: Router + @EnvironmentObject private var userViewModel: UserViewModel + @EnvironmentObject private var habitViewModel: HabitViewModel + + var currentDate: Date { + return Date() + } + + var formattedDate: String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "MMMM d, yyyy" + return dateFormatter.string(from: currentDate) + } + + + var body: some View { + TabView(selection: $router.selectedTab) { + + HomeScreen() + .onTapGesture { router.selectedTab = .home } + .tabItem { + Label("Home", systemImage: "house") + } + .tag(Tabs.home) + ProgressScreen() + .onTapGesture { router.selectedTab = .progress } + .tabItem { + Label("Progress", systemImage: "chart.bar.xaxis") + } + .tag(Tabs.progress) + SettingScreen() + .onTapGesture { router.selectedTab = .setting } + .tabItem { + Label("Setting", systemImage: "gearshape") + } + .tag(Tabs.setting) + + + } + .toolbar { + switch router.selectedTab { + case .home: + ToolbarItem(placement: .topBarLeading) { + VStack(alignment: .leading){ + Text("Today") + .font(.title3) + .fontWeight(.bold) + Text("\(formattedDate)") + .font(.caption) + .fontWeight(.medium) + } + + } + ToolbarItem(placement: .topBarTrailing) { + Button { + router.push(.add_habit) + } label: { + Image(systemName: "plus") + } + } + case .progress: + ToolbarItem(placement: .topBarLeading) { + Text("Progress") + .font(.title3) + .fontWeight(.bold) + } + case .setting: + + ToolbarItem(placement: .topBarLeading) { + Text("Setting") + .font(.title3) + .fontWeight(.bold) + } + + } + } + .task { + await habitViewModel.fetchHabits() + } + .navigationBarBackButtonHidden(true) + .blur(radius: habitViewModel.selectedHabitState.isSelected ? 20 : 0) + + if habitViewModel.selectedHabitState.isSelected { + UpdateHabitView() + } + } +} +``` + +Add the following code to HomeScreen.swift inside **Home/View** folder. Here we will be using newly introduce `ContentUnavailableView` if the habits is empty else render ``HabitCellView`` inside the loop. + +```swift +import SwiftUI + +struct HomeScreen: View { + + @EnvironmentObject private var habitViewModel: HabitViewModel + @EnvironmentObject private var router: Router + @State var selectedDay: Int? = nil + + var currentMonth: String { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "MMM" + + let currentDate = Date() + return dateFormatter.string(from: currentDate) + } + + + var numberOfDaysInTheMonth: Int { + + let calendar = Calendar.current + let currentDate = Date() + + // Get the range of days for the current month + if let range = calendar.range(of: .day, in: .month, for: currentDate) { + return range.count + } + + // Default to 0 if the range cannot be determined + return 0 + + } + + var body: some View { + ScrollView(showsIndicators: false) { + Spacer(minLength: 15) + + if numberOfDaysInTheMonth > 0 { + + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(1.. 0 && goalCompleted == 0 { + return 0 + } + + return (screenWidth / CGFloat(totalGoal)) * CGFloat(goalCompleted) + } + + var body: some View { + ZStack(alignment: .leading) { + + Theme.pink + .frame(width: width) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + HStack(spacing: 10) { + HStack { + + Image(systemName: habit.icon) + .renderingMode(.original) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(Theme.blue) + .padding(.all, 3) + .frame(width: 30, height: 30) + .background(Theme.blueLight) + .clipShape(RoundedRectangle(cornerRadius: 99)) + + VStack(alignment: .leading, spacing: 4) { + Text(habit.title) + .font(.system(size: 16)) + Text("\(habit.goalCompleted ?? 0)/\(habit.goals ?? 0)") + .font(.system(size: 12)) + } + } + Spacer() + if isCompleted == false { + Button { + habitViewModel.selectedHabitState = SelectedHabitState(habit: habit, isSelected: true) + } label: { + Image(systemName: "plus") + .foregroundStyle(theme == .dark ? .white : .black) + } + + } + } + .padding(EdgeInsets(top: 6, leading: 20, bottom: 6, trailing: 20)) + } + .background(theme == .dark ? .white.opacity(0.2) : .white) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(color: Color.black.opacity(0.1), radius: 10, x: 4, y: 4) + + } +} +``` + +Do the samething for `DateFilterView` + +```swift +import SwiftUI + +struct DateFilterView: View { + var month: String + var day: Int + @Binding var selectedDay: Int? + + + var body: some View { + Button { + selectedDay = day + } label: { + VStack(spacing: 4) { + Text(month) + .font(.system(size: 14)) + Text("\(day)") + .font(.system(size: 12)) + + } + } + .frame(width: 50, height: 50) + .background(selectedDay == day ? Theme.blue : Theme.blueLight) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .foregroundColor(selectedDay == day ? .white : Theme.blue) + } +} +``` + +Lastly, for UpdateHabitView + +```swift +import SwiftUI + +struct UpdateHabitView: View { + + @EnvironmentObject private var habitViewModel: HabitViewModel + @State private var goalCompleted: Int = 1 + + var body: some View { + VStack { + + Image(systemName: habitViewModel.selectedHabitState.habit?.icon ?? "calendar") + .renderingMode(.original) + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundColor(Theme.blue) + .padding(.all, 10) + .frame(width: 100, height: 100) + .background(Theme.blueLight) + .clipShape(RoundedRectangle(cornerRadius: 99)) + + + Text("Goal Completed") + .font(.caption) + .padding(.bottom, 20) + + Button { + Task { + + if habitViewModel.selectedHabitState.habit != nil && goalCompleted < habitViewModel.selectedHabitState.habit!.goals! { + + let data: Int = goalCompleted + (habitViewModel.selectedHabitState.habit!.goalCompleted ?? 0) + + await habitViewModel.update(habitViewModel.selectedHabitState.habit!.id!, data) + + // Update record + await habitViewModel.fetchHabits() + + habitViewModel.selectedHabitState = SelectedHabitState() + + } + + } + } label: { + Text("Update Habit") + } + } + .frame(width: 300, height: 300) + .background(Color(.systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .shadow(radius: 20) + .overlay(alignment: .topTrailing) { + Button { + habitViewModel.selectedHabitState = SelectedHabitState() + } label: { + ZStack { + Circle() + .frame(width: 30, height: 30) + .foregroundStyle(.white) + .opacity(0.6) + Image(systemName: "xmark") + .imageScale(.small) + .frame(width: 44, height: 44) + .foregroundStyle(.black) + } + } + } + } +} +``` + diff --git a/src/routes/docs/tutorials/apple/step-8/+page.markdoc b/src/routes/docs/tutorials/apple/step-8/+page.markdoc new file mode 100644 index 0000000000..74f1d5925e --- /dev/null +++ b/src/routes/docs/tutorials/apple/step-8/+page.markdoc @@ -0,0 +1,99 @@ +--- +layout: tutorial +title: Add Habit +description: Handle create habit and store in Appwrite Database. +step: 9 +--- + +# Add Habit Screen {% #habit-ui %} + +Here will explore a simple "Add Habit" screen that lets users input information to create a new habit. This screen covers various aspects like the habit title, description, goals, duration, and habit icon. Trigger the create method in HabitViewModel then pop the navigation back to HomeScreen. + +```swift +import SwiftUI + +struct AddHabitScreen: View { + + @EnvironmentObject private var router: Router + @EnvironmentObject private var habitViewModel: HabitViewModel + @EnvironmentObject private var userViewModel: UserViewModel + @State private var title: String = "" + @State private var description: String = "" + @State private var habitIcon: String = "figure.walk" + + @State private var startDate: Date = .now + @State private var endDate: Date = .now + + @State private var goals: Int? = nil + + @State private var taskRepeat: String? = nil + + let icons: [String] = [ + "figure.walk", + "book.pages" + // Add as more icon as you want + ] + + let gridItems = [ + GridItem(.flexible(minimum: 50, maximum: 100), spacing: 16), + GridItem(.flexible(minimum: 50, maximum: 100), spacing: 16), + GridItem(.flexible(minimum: 50, maximum: 100), spacing: 16), + GridItem(.flexible(minimum: 50, maximum: 100), spacing: 16), + ] + + + var body: some View { + Form { + Section("Title") { + TextField("Habit Title", text: $title) + TextField("Description", text: $description) + } + + Section("Task") { + + TextField("Goals", value: $goals, format: .number) + .keyboardType(.numberPad) + } + + Section("Period") { + DatePicker("Start", selection: $startDate, displayedComponents: .date) + DatePicker("End", selection: $endDate, displayedComponents: .date) + } + + Section("Icons") { + + LazyVGrid(columns: gridItems, spacing: 16) { + ForEach(icons, id: \.self) { icon in + Image(systemName: icon) + .renderingMode(.original) + .resizable() + .foregroundColor(habitIcon == icon ? .white : Theme.pink) + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + .padding(.all, 10) + .background((habitIcon == icon ? Theme.pink : .clear)) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .onTapGesture { + habitIcon = icon + } + } + } + + } + + Button { + Task { + + await habitViewModel.create(title, description, goals ?? 0, habitIcon, startDate.formatted(), endDate.formatted(), userViewModel.userState.userId!) + + router.pop() + } + } label: { + Text("Create Habit") + } + } + .navigationTitle("New habit") + + } +} +``` \ No newline at end of file diff --git a/src/routes/docs/tutorials/apple/step-9/+page.markdoc b/src/routes/docs/tutorials/apple/step-9/+page.markdoc new file mode 100644 index 0000000000..97ff38eed2 --- /dev/null +++ b/src/routes/docs/tutorials/apple/step-9/+page.markdoc @@ -0,0 +1,22 @@ +--- +layout: tutorial +title: Next Steps +description: Test your app in Xcode +step: 10 +--- + +# Test your project {% #test-project %} +At the top level menu click on **Product** then **Scheme** and click on **Choose scheme**, you will notice that the active scheme is selected if not click on it to mark it selected then next to it select any iOS Simulator you wish to run the app on then click on play button. + +# Delete Appwrite Project in Console {% #delete-appwrite-project-in-console %} + +Head to your [Appwrite Console](https://cloud.appwrite.io/console/) and select the project you want to delete then head over to `Settings` + +{% only_dark %} +![Delete Appwrite Project Screenshot](/images/docs/tutorials/dark/delete-appwrite-project.png) +{% /only_dark %} +{% only_light %} +![Delete Appwrite Project Screenshot](/images/docs/tutorials/delete-appwrite-project.png) +{% /only_light %} + +Click on `Delete`, then follow the instruction provided in the dialog to confirm deleting your project on Appwrite Console. \ No newline at end of file diff --git a/static/images/docs/tutorials/apple-habit-tracker.png b/static/images/docs/tutorials/apple-habit-tracker.png new file mode 100644 index 0000000000..c2a1b2b093 Binary files /dev/null and b/static/images/docs/tutorials/apple-habit-tracker.png differ diff --git a/static/images/docs/tutorials/dark/apple-habit-tracker.png b/static/images/docs/tutorials/dark/apple-habit-tracker.png new file mode 100644 index 0000000000..79df9b55dd Binary files /dev/null and b/static/images/docs/tutorials/dark/apple-habit-tracker.png differ diff --git a/static/images/docs/tutorials/dark/delete-appwrite-project.png b/static/images/docs/tutorials/dark/delete-appwrite-project.png new file mode 100644 index 0000000000..66b7455f07 Binary files /dev/null and b/static/images/docs/tutorials/dark/delete-appwrite-project.png differ diff --git a/static/images/docs/tutorials/dark/xcode-add-appwrite-sdk.png b/static/images/docs/tutorials/dark/xcode-add-appwrite-sdk.png new file mode 100644 index 0000000000..93213ab09f Binary files /dev/null and b/static/images/docs/tutorials/dark/xcode-add-appwrite-sdk.png differ diff --git a/static/images/docs/tutorials/dark/xcode-new-project-setup.png b/static/images/docs/tutorials/dark/xcode-new-project-setup.png new file mode 100644 index 0000000000..33e814d393 Binary files /dev/null and b/static/images/docs/tutorials/dark/xcode-new-project-setup.png differ diff --git a/static/images/docs/tutorials/delete-appwrite-project.png b/static/images/docs/tutorials/delete-appwrite-project.png new file mode 100644 index 0000000000..59b5a8447a Binary files /dev/null and b/static/images/docs/tutorials/delete-appwrite-project.png differ diff --git a/static/images/docs/tutorials/xcode-add-appwrite-sdk.png b/static/images/docs/tutorials/xcode-add-appwrite-sdk.png new file mode 100644 index 0000000000..bcf19069b8 Binary files /dev/null and b/static/images/docs/tutorials/xcode-add-appwrite-sdk.png differ diff --git a/static/images/docs/tutorials/xcode-new-project-setup.png b/static/images/docs/tutorials/xcode-new-project-setup.png new file mode 100644 index 0000000000..6465309f0e Binary files /dev/null and b/static/images/docs/tutorials/xcode-new-project-setup.png differ