├── .DS_Store ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── swift.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ConfigTemplate.xcconfig ├── LICENSE ├── README.md ├── Steps.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── brittanyrima.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcshareddata │ └── xcschemes │ ├── Steps.xcscheme │ ├── StepsTests.xcscheme │ └── StepsWidgetExtension.xcscheme ├── Steps ├── .DS_Store ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── StepTracker.png │ ├── Contents.json │ ├── avatar 1.imageset │ │ ├── Contents.json │ │ └── man copy 2.png │ ├── avatar 10.imageset │ │ ├── Contents.json │ │ └── woman.png │ ├── avatar 2.imageset │ │ ├── Contents.json │ │ └── man copy.png │ ├── avatar 3.imageset │ │ ├── Contents.json │ │ └── man.png │ ├── avatar 4.imageset │ │ ├── Contents.json │ │ └── old-man.png │ ├── avatar 5.imageset │ │ ├── Contents.json │ │ └── woman-2 copy 2.png │ ├── avatar 6.imageset │ │ ├── Contents.json │ │ └── woman-2 copy 3.png │ ├── avatar 7.imageset │ │ ├── Contents.json │ │ └── woman-2 copy.png │ ├── avatar 8.imageset │ │ ├── Contents.json │ │ └── woman-2.png │ ├── avatar 9.imageset │ │ ├── Contents.json │ │ └── woman-3.png │ └── background.imageset │ │ ├── Contents.json │ │ └── background.jpeg ├── CoreData │ ├── PersistenceController.swift │ └── Steps.xcdatamodeld │ │ └── Steps.xcdatamodel │ │ └── contents ├── Info.plist ├── Model │ ├── Award.swift │ ├── Calorie.swift │ ├── Contributor.swift │ ├── Distance.swift │ └── Step.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Screens │ ├── AwardDetailView.swift │ ├── AwardLockedView.swift │ ├── AwardView.swift │ ├── EditStepsGoalView.swift │ ├── GoalView.swift │ ├── HomeView.swift │ ├── MainView.swift │ ├── ProfileView.swift │ ├── SettingsView.swift │ ├── StepsCountView.swift │ └── StepsDetailView.swift ├── Steps.entitlements ├── StepsApp.swift ├── StepsConfig.xcconfig ├── StepsConfigTemplate.xcconfig ├── StepsWidgetConfig.xcconfig ├── StepsWidgetConfigTemplate.xcconfig ├── Utilities │ ├── Constants.swift │ ├── Extensions │ │ ├── Date+Ext.swift │ │ ├── FileManager+Ext.swift │ │ ├── Localizable.xcstrings │ │ └── UIImage+Ext.swift │ └── ImageStorage.swift ├── ViewModel │ ├── AddGoalViewModel.swift │ ├── ContributorsViewModel.swift │ ├── GoalViewModel.swift │ ├── ProfileViewModel.swift │ ├── SettingsViewModel.swift │ └── StepsViewModel.swift └── Views │ ├── AddGoalView.swift │ ├── AwardBadgeView.swift │ ├── BackgroundView.swift │ ├── BadgeImageView.swift │ ├── CircleProgressBar.swift │ ├── CircleView.swift │ ├── ContributorsClient.swift │ ├── ContributorsView.swift │ ├── CurrentStepsCardView.swift │ ├── FactView.swift │ ├── GoalListRowView.swift │ ├── MountainView.swift │ ├── NewWeekStepsView.swift │ ├── ProfileButtonView.swift │ ├── ProfileItemButtonView.swift │ ├── StepsDetailCardView.swift │ ├── StepsGoalCardView.swift │ └── WeekStepsView.swift ├── StepsTests ├── AddGoalViewTests.swift ├── ContributorsTests.swift ├── SettingsViewTests.swift └── StepsTests.swift ├── StepsWidget ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ └── StepTracker.png │ ├── Contents.json │ └── WidgetBackground.colorset │ │ └── Contents.json ├── Info.plist ├── StepsGraphWidget.swift ├── StepsWidget.swift └── StepsWidgetBundle.swift └── StepsWidgetExtension.entitlements /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj merge=union 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: brittanyarima # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: BuildAndTest 2 | 3 | on: [ pull_request ] 4 | 5 | jobs: 6 | build-and-test: 7 | 8 | runs-on: macos-13 9 | 10 | timeout-minutes: 40 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Run a multi-line script 16 | run: | 17 | sudo xcode-select -s /Applications/Xcode_15.0.1.app 18 | xcodebuild test -scheme Steps CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -destination "platform=iOS Simulator,name=iPad (10th generation)" 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | SECRETS.swift 2 | StepsConfig.xcconfig 3 | *.xcuserstate 4 | 5 | *.pbxproj 6 | Config.xcconfig 7 | Steps.xcodeproj/project.pbxproj 8 | *.xcuserstate 9 | Steps/StepsConfig.xcconfig 10 | *.xcuserstate 11 | 12 | ### Xcode Patch ### 13 | *.xcodeproj/* 14 | !*.xcodeproj/project.pbxproj 15 | !*.xcodeproj/xcshareddata/ 16 | !*.xcodeproj/project.xcworkspace/ 17 | !*.xcworkspace/contents.xcworkspacedata 18 | /*.gcno 19 | **/xcshareddata/WorkspaceSettings.xcsettings 20 | 21 | 22 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 23 | 24 | ## User settings 25 | xcuserdata/ 26 | 27 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 28 | *.xcscmblueprint 29 | *.xccheckout 30 | 31 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 32 | build/ 33 | DerivedData/ 34 | *.moved-aside 35 | *.pbxuser 36 | !default.pbxuser 37 | *.mode1v3 38 | !default.mode1v3 39 | *.mode2v3 40 | !default.mode2v3 41 | *.perspectivev3 42 | !default.perspectivev3 43 | 44 | ## Obj-C/Swift specific 45 | *.hmap 46 | 47 | ## App packaging 48 | *.ipa 49 | *.dSYM.zip 50 | *.dSYM 51 | 52 | ## Playgrounds 53 | timeline.xctimeline 54 | playground.xcworkspace 55 | 56 | # Swift Package Manager 57 | # 58 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 59 | Packages/ 60 | Package.pins 61 | Package.resolved 62 | # *.xcodeproj 63 | # 64 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 65 | # hence it is not needed unless you have added a package configuration file to your project 66 | # .swiftpm 67 | 68 | .build/ 69 | 70 | # CocoaPods 71 | # 72 | # We recommend against adding the Pods directory to your .gitignore. However 73 | # you should judge for yourself, the pros and cons are mentioned at: 74 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 75 | # 76 | # Pods/ 77 | # 78 | # Add this line if you want to avoid checking in source code from the Xcode workspace 79 | # *.xcworkspace 80 | 81 | # Carthage 82 | # 83 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 84 | # Carthage/Checkouts 85 | 86 | Carthage/Build/ 87 | 88 | # fastlane 89 | # 90 | # It is recommended to not store the screenshots in the git repo. 91 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 92 | # For more information about the recommended setup visit: 93 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 94 | 95 | fastlane/report.xml 96 | fastlane/Preview.html 97 | fastlane/screenshots/**/*.png 98 | fastlane/test_output 99 | 100 | # Code Injection 101 | # 102 | # After new code Injection tools there's a generated folder /iOSInjectionProject 103 | # https://github.com/johnno1962/injectionforxcode 104 | 105 | iOSInjectionProject/ 106 | 107 | .DS_Store 108 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct - Steps 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behaviour that contributes to a positive environment for our 15 | community include: 16 | 17 | * Demonstrating empathy and kindness toward other people 18 | * Being respectful of differing opinions, viewpoints, and experiences 19 | * Giving and gracefully accepting constructive feedback 20 | * Accepting responsibility and apologising to those affected by our mistakes, 21 | and learning from the experience 22 | * Focusing on what is best not just for us as individuals, but for the 23 | overall community 24 | 25 | Examples of unacceptable behaviour include: 26 | 27 | * The use of sexualised language or imagery, and sexual attention or advances 28 | * Trolling, insulting or derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or email 31 | address, without their explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying and enforcing our standards of 38 | acceptable behaviour and will take appropriate and fair corrective action in 39 | response to any instances of unacceptable behaviour. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or reject 42 | comments, commits, code, wiki edits, issues, and other contributions that are 43 | not aligned to this Code of Conduct, or to ban 44 | temporarily or permanently any contributor for other behaviours that they deem 45 | inappropriate, threatening, offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies within all community spaces, and also applies when 50 | an individual is officially representing the community in public spaces. 51 | Examples of representing our community include using an official e-mail address, 52 | posting via an official social media account, or acting as an appointed 53 | representative at an online or offline event. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behaviour may be 58 | reported to the community leaders responsible for enforcement at . 59 | All complaints will be reviewed and investigated promptly and fairly. 60 | 61 | All community leaders are obligated to respect the privacy and security of the 62 | reporter of any incident. 63 | 64 | ## Attribution 65 | 66 | This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org/), version 67 | [1.4](https://www.contributor-covenant.org/version/1/4/code-of-conduct/code_of_conduct.md) and 68 | [2.0](https://www.contributor-covenant.org/version/2/0/code_of_conduct/code_of_conduct.md), 69 | and was generated by [contributing-gen](https://github.com/bttger/contributing-gen). 70 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contribution Guidelines 3 | 4 | First off, thanks for taking the time to contribute! ❤️ 5 | 6 | All types of contributions are encouraged and valued. Beginners welcome! I look forward to your contributions. 🎉 7 | 8 | > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: 9 | > - Star the project 10 | > - Tweet about it 11 | > - Refer this project in your project's readme 12 | > - Mention the project at local meetups and tell your friends/colleagues 13 | 14 | ## Code of Conduct 15 | 16 | This project and everyone participating in it is governed by the 17 | [Steps Code of Conduct](https://github.com/brittanyarima/Steps/blob/main/CODE_OF_CONDUCT.md). 18 | By participating, you are expected to uphold this code. 19 | 20 | ## I Have a Question 21 | This repo is great for beginners! So if you have a question and need clarification, I recommend the following: 22 | 23 | - Open an [Issue](https://github.com/brittanyarima/Steps/issues/new). 24 | - Provide as much context as you can about what you're running into. 25 | 26 | ## I Want To Contribute 27 | 28 | > ### Legal Notice 29 | > When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project licence. 30 | 31 | ### Suggesting Enhancements 32 | 33 | This section guides you through submitting an enhancement suggestion for Steps, **including completely new features and minor improvements to existing functionality**. 34 | 35 | 36 | 37 | #### Before Submitting an Enhancement 38 | 39 | - Make sure that you are using the latest version. 40 | - Perform a [search](https://github.com/brittanyarima/Steps/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue. 41 | - Find out whether your idea fits with the scope and aims of the project. 42 | 43 | 44 | #### How Do I Submit a Good Enhancement Suggestion? 45 | 46 | Enhancement suggestions are tracked as [GitHub issues](https://github.com/brittanyarima/Steps/issues). 47 | 48 | - Use a **clear and descriptive title** for the issue to identify the suggestion. 49 | - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. 50 | - **Describe the current behaviour** and **explain which behaviour you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. 51 | - You may want to **include screenshots** which can help you demonstrate the steps or point out the part which the suggestion is related to. 52 | - **Explain why this enhancement would be useful** to most Steps users. You may also want to point out the other projects that solved it better and which could serve as inspiration. 53 | 54 | 55 | 56 | ### Your First Code Contribution 57 | 58 | - Download Xcode 15.0 or later, and macOS 14.0 or later. 59 | - Fork the repo to your profile 60 | - Clone to your computer 61 | - Setup the upstream remote 62 | 63 | 64 | ```sh 65 | git remote add upstream (https://github.com/brittanyarima/Steps.git 66 | ``` 67 | 68 | **BEFORE** starting on an issue, comment on the issue you want to work on. 69 | * This prevents two people from working on the same issue. The maintainer will assign you that issue, and you can get started on it. 70 | 71 | * Checkout a new branch (from the `dev` branch) to work on an issue: 72 | 73 | ```sh 74 | git checkout -b issueNumber-feature-name 75 | ``` 76 | * When your feature/fix is complete open a pull request, PR, from your feature branch to the `dev` branch 77 | * No commits should be made to the `main` branch directly. The `main` branch shall only consist of the deployed code 78 | 79 | **New to Git?** 80 | * Here's a great resource [Getting Started](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/overview/getting-started-with-github-desktop) 81 | 82 | 83 | ## Attribution 84 | - This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)! 85 | -------------------------------------------------------------------------------- /ConfigTemplate.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // ConfigTemplate.xcconfig 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 10/2/23. 6 | // 7 | 8 | // Configuration settings file format documentation can be found at: 9 | // https://help.apple.com/xcode/#/dev745c5c974 10 | 11 | BUNDLE_ID = YOUR_BUNDLE_ID_PLACEHOLDER 12 | TEAM_ID = YOUR_TEAM_ID_PLACEHOLDER 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Brittany Rima 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 🏃Steps 3 | 4 | This app is open source and ready for you to contribute! Built fully with SwiftUI and open to contributers of any experience level (beginners welcome🙂)! 5 | 6 | # Getting Started 7 | * Read the [Code of Conduct](https://github.com/brittanyarima/Steps/blob/main/CODE_OF_CONDUCT.md) 8 | * Read the [CONTRIBUTING.md](https://github.com/brittanyarima/Steps/blob/main/CONTRIBUTING.md) guidelines 9 | * Download Xcode 15 or later, and macOS 14.0 or later 10 | * Browse the open [issues](https://github.com/brittanyarima/Steps/issues) and **comment** which you would like to work on 11 | * Fork this repo 12 | * Clone the repo to your machine 13 | * In the same folder that contains the `StepsConfigTemplate.xcconfig`, run this command, in Terminal, to create a new Xcode configuration file (which properly sets up the signing information) 14 | 15 | ```sh 16 | cp StepsConfigTemplate.xcconfig StepsConfig.xcconfig 17 | ``` 18 | 19 | * In the StepsConfig.xcconfig file, fill in your `DEVELOPMENT_TEAM` & `BUNDLE_ID` 20 | * Example: `DEVELOPMENT_TEAM = IdNumber` & `BUNDLE_ID = com.name.steps` 21 | * You can find your Team ID by logging into the Apple Developer Portal 22 | * This is only needed when running on a real device for iOS, this works with both free or paid Apple Developer accounts. 23 | * 24 | * You will need to do this AGAIN to set up the Widget extension 25 | 26 | ```sh 27 | cp StepsWidgetConfigTemplate.xcconfig StepsWidgetConfig.xcconfig 28 | ``` 29 | * ❗️Make sure you at .widget to this `PRODUCT_BUNDLE_IDENTIFIER` ie. com.name.steps.widget 30 | * Build the project 31 | 32 | * Checkout a new branch (from the `dev` branch) to work on an issue 33 | Checkout any issue labeled `hacktoberfest` to start contributing. 34 | 35 | * Start contributing! 36 | * Submit PR to merge with `dev` branch 37 | * If you've never contributed to open-source before there are a ton of great tutorials out there to help get you started 38 | * Issues labeled `good-first-issue` are great for beginners. 39 | 40 | 41 |

42 | 43 | 44 | 45 | 46 | 47 | 48 |

49 | 50 | **Tech Used** 51 | - 🎨 SwiftUI 52 | - ❤️‍🩹 HealthKit 53 | - 📊 Swift Charts 54 | - 🔔 Local Notifications 55 | - 🗂️ MVVM 56 | - 💾 App Storage 57 | 58 | 59 | # Contributors 60 | 61 | 62 | 63 | 64 | Made with [contrib.rocks](https://contrib.rocks). 65 | 66 | 67 | # License 68 | This project is licensed under [MIT License](https://github.com/brittanyarima/Steps/blob/main/LICENSE). 69 | -------------------------------------------------------------------------------- /Steps.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Steps.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Steps.xcodeproj/project.xcworkspace/xcuserdata/brittanyrima.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps.xcodeproj/project.xcworkspace/xcuserdata/brittanyrima.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /Steps.xcodeproj/xcshareddata/xcschemes/Steps.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /Steps.xcodeproj/xcshareddata/xcschemes/StepsTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 16 | 19 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 46 | 47 | 49 | 50 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Steps.xcodeproj/xcshareddata/xcschemes/StepsWidgetExtension.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 6 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 45 | 46 | 49 | 55 | 56 | 57 | 58 | 59 | 71 | 74 | 80 | 81 | 82 | 83 | 89 | 90 | 91 | 92 | 96 | 97 | 101 | 102 | 106 | 107 | 108 | 109 | 117 | 119 | 125 | 126 | 127 | 128 | 130 | 131 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /Steps/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/.DS_Store -------------------------------------------------------------------------------- /Steps/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "tvos", 6 | "reference" : "systemIndigoColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "StepTracker.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/AppIcon.appiconset/StepTracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/AppIcon.appiconset/StepTracker.png -------------------------------------------------------------------------------- /Steps/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "man copy 2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 1.imageset/man copy 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/avatar 1.imageset/man copy 2.png -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 10.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "woman.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 10.imageset/woman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/avatar 10.imageset/woman.png -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "man copy.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 2.imageset/man copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/avatar 2.imageset/man copy.png -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "man.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 3.imageset/man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/avatar 3.imageset/man.png -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "old-man.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 4.imageset/old-man.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/avatar 4.imageset/old-man.png -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "woman-2 copy 2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 5.imageset/woman-2 copy 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/avatar 5.imageset/woman-2 copy 2.png -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "woman-2 copy 3.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 6.imageset/woman-2 copy 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/avatar 6.imageset/woman-2 copy 3.png -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 7.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "woman-2 copy.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 7.imageset/woman-2 copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/avatar 7.imageset/woman-2 copy.png -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 8.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "woman-2.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 8.imageset/woman-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/avatar 8.imageset/woman-2.png -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 9.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "woman-3.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/avatar 9.imageset/woman-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/avatar 9.imageset/woman-3.png -------------------------------------------------------------------------------- /Steps/Assets.xcassets/background.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "background.jpeg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Steps/Assets.xcassets/background.imageset/background.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/Steps/Assets.xcassets/background.imageset/background.jpeg -------------------------------------------------------------------------------- /Steps/CoreData/PersistenceController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersistenceController.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 1/7/23. 6 | // 7 | 8 | import Foundation 9 | import CoreData 10 | 11 | struct PersistenceController { 12 | static let shared = PersistenceController() 13 | let container: NSPersistentContainer 14 | 15 | static var preview: PersistenceController = { 16 | let controller = PersistenceController(inMemory: true) 17 | 18 | for _ in 0..<10 { 19 | let goal = Goal(context: controller.container.viewContext) 20 | goal.id = UUID() 21 | goal.name = "Run a marathon" 22 | goal.date = Date() 23 | goal.isComplete = Bool.random() 24 | } 25 | return controller 26 | }() 27 | 28 | init(inMemory: Bool = false) { 29 | container = NSPersistentContainer(name: "Steps") 30 | 31 | if inMemory { 32 | container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null") 33 | } 34 | 35 | container.loadPersistentStores { description, error in 36 | if let error = error { 37 | fatalError("Error: \(error.localizedDescription)") 38 | } 39 | } 40 | } 41 | 42 | func save() { 43 | let context = container.viewContext 44 | 45 | if context.hasChanges { 46 | do { 47 | try context.save() 48 | } catch { 49 | print("❗️Error saving to core data. \(error.localizedDescription)") 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Steps/CoreData/Steps.xcdatamodeld/Steps.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Steps/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Steps/Model/Award.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Award.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/16/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum Award: String, CaseIterable { 11 | case firstStep 12 | case goal 13 | case doubleTrouble 14 | case threes 15 | case perfectweek 16 | case messi 17 | case motivated 18 | case firstGoal 19 | case dreamerGoal 20 | case goGetter 21 | 22 | 23 | var name: String { 24 | switch self { 25 | case .firstStep: 26 | return Constants.firstStepsName 27 | case .goal: 28 | return Constants.goalName 29 | case .doubleTrouble: 30 | return Constants.doubleTroubleName 31 | case .threes: 32 | return Constants.threesName 33 | case .perfectweek: 34 | return Constants.perfectWeekName 35 | case .messi: 36 | return Constants.messiName 37 | case .motivated: 38 | return Constants.motivatedName 39 | case .firstGoal: 40 | return Constants.firstGoalName 41 | case .dreamerGoal: 42 | return Constants.dreamerGoalName 43 | case .goGetter: 44 | return Constants.goGetterName 45 | } 46 | } 47 | 48 | var description: String { 49 | switch self { 50 | case .firstStep: 51 | return Constants.firstStepsDescription 52 | case .goal: 53 | return Constants.goalDescription 54 | case .doubleTrouble: 55 | return Constants.doubleTroubleDescription 56 | case .threes: 57 | return Constants.threesDescription 58 | case .perfectweek: 59 | return Constants.perfectWeekDescription 60 | case .messi: 61 | return Constants.soccerFieldDescription 62 | case .motivated: 63 | return Constants.motivatedDescription 64 | case .firstGoal: 65 | return Constants.firstGoalDescription 66 | case .dreamerGoal: 67 | return Constants.dreamerGoalDescription 68 | case .goGetter: 69 | return Constants.goGetterDescription 70 | } 71 | } 72 | 73 | var image: String { 74 | switch self { 75 | case .firstStep: 76 | return "figure.walk" 77 | case .goal: 78 | return "soccerball" 79 | case .doubleTrouble: 80 | return "figure.walk.motion" 81 | case .threes: 82 | return "figure.dance" 83 | case .perfectweek: 84 | return "medal" 85 | case .messi: 86 | return "sportscourt" 87 | case .motivated: 88 | return "checklist.unchecked" 89 | case .firstGoal: 90 | return "checkmark.seal" 91 | case .dreamerGoal: 92 | return "list.bullet.rectangle" 93 | case .goGetter: 94 | return "text.badge.checkmark.rtl" 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Steps/Model/Calorie.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Calorie.swift 3 | // Steps 4 | // 5 | // Created by Mohd Wasif Raza on 13/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | struct Calorie: Identifiable { 12 | let id = UUID() 13 | let value: Int 14 | let date: Date 15 | } 16 | -------------------------------------------------------------------------------- /Steps/Model/Contributor.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // Contributor.swift 4 | // Steps 5 | // 6 | // Created by Drag0ndust on 05.10.23. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Contributor: Codable, Hashable, Identifiable { 12 | let login: String 13 | let id: Int 14 | let nodeID: String 15 | let avatarURL: String 16 | let gravatarID: String 17 | let url, htmlURL, followersURL: String 18 | let followingURL, gistsURL, starredURL: String 19 | let subscriptionsURL, organizationsURL, reposURL: String 20 | let eventsURL: String 21 | let receivedEventsURL: String 22 | let type: String 23 | let siteAdmin: Bool 24 | let contributions: Int 25 | 26 | enum CodingKeys: String, CodingKey { 27 | case login, id 28 | case nodeID = "node_id" 29 | case avatarURL = "avatar_url" 30 | case gravatarID = "gravatar_id" 31 | case url 32 | case htmlURL = "html_url" 33 | case followersURL = "followers_url" 34 | case followingURL = "following_url" 35 | case gistsURL = "gists_url" 36 | case starredURL = "starred_url" 37 | case subscriptionsURL = "subscriptions_url" 38 | case organizationsURL = "organizations_url" 39 | case reposURL = "repos_url" 40 | case eventsURL = "events_url" 41 | case receivedEventsURL = "received_events_url" 42 | case type 43 | case siteAdmin = "site_admin" 44 | case contributions 45 | } 46 | } 47 | 48 | extension Contributor { 49 | // TODO: Improve Contributor mock 50 | static let mock: Self = .init( 51 | login: "login", 52 | id: 1, 53 | nodeID: "nodeID", 54 | avatarURL: "avatarURL", 55 | gravatarID: "gravatarID", 56 | url: "url", 57 | htmlURL: "htmlURL", 58 | followersURL: "followersURL", 59 | followingURL: "followingURL", 60 | gistsURL: "gistsURL", 61 | starredURL: "starredURL", 62 | subscriptionsURL: "subscriptionsURL", 63 | organizationsURL: "organizationsURL", 64 | reposURL: "reposURL", 65 | eventsURL: "eventsURL", 66 | receivedEventsURL: "receivedEventsURL", 67 | type: "type", 68 | siteAdmin: false, 69 | contributions: 1 70 | ) 71 | } 72 | 73 | extension [Contributor] { 74 | static let mock: Self = [.mock] 75 | } 76 | -------------------------------------------------------------------------------- /Steps/Model/Distance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Distance.swift 3 | // Steps 4 | // 5 | // Created by Mohd Wasif Raza on 13/10/23. 6 | // 7 | 8 | import Foundation 9 | 10 | 11 | struct Distance: Identifiable { 12 | let id = UUID() 13 | let value: Int 14 | let date: Date 15 | } 16 | -------------------------------------------------------------------------------- /Steps/Model/Step.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Step.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/15/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Step: Identifiable { 11 | let id = UUID() 12 | let count: Int 13 | let date: Date 14 | } 15 | 16 | extension [Step] { 17 | static let mock: Self = [ 18 | .init(count: 1_234, date: Date(timeIntervalSinceNow: -3600 * 4)), 19 | .init(count: 2_345, date: Date(timeIntervalSinceNow: -3600 * 3)), 20 | .init(count: 3_567, date: Date(timeIntervalSinceNow: -3600 * 2)), 21 | .init(count: 4_124, date: Date(timeIntervalSinceNow: -3600)), 22 | .init(count: 6_789, date: Date()), 23 | ] 24 | 25 | static let mock1Element: Self = Array(Self.mock[0...0]) 26 | static let mock2Element: Self = Array(Self.mock[0...1]) 27 | static let mock3Element: Self = Array(Self.mock[0...2]) 28 | static let mock4Element: Self = Array(Self.mock[0...3]) 29 | static let mock5Element: Self = Array(Self.mock[0...4]) 30 | } 31 | -------------------------------------------------------------------------------- /Steps/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Steps/Screens/AwardDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AwardDetailView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/21/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AwardDetailView: View { 11 | let award: Award 12 | @ObservedObject var viewModel: StepsViewModel 13 | @State private var rotationAngle: Angle = .degrees(0) 14 | 15 | var body: some View { 16 | VStack { 17 | BadgeImageView(award: award) 18 | .foregroundColor(.indigo) 19 | .rotation3DEffect(rotationAngle, axis: (x: 0, y: 360, z: 0)) 20 | .gesture( 21 | DragGesture() 22 | .onChanged { value in 23 | self.rotationAngle = Angle(degrees: value.translation.width) 24 | } 25 | .onEnded { _ in 26 | withAnimation(.easeOut) { 27 | self.rotationAngle = .degrees(0) 28 | } 29 | } 30 | ) 31 | .animation(.interactiveSpring(), value: rotationAngle) 32 | 33 | Text(award.name) 34 | .font(.title2) 35 | .bold() 36 | .foregroundColor(.indigo) 37 | 38 | Text(award.description) 39 | .foregroundColor(.secondary) 40 | .padding() 41 | .padding(.horizontal, 55) 42 | .multilineTextAlignment(.center) 43 | } 44 | } 45 | } 46 | 47 | struct AwardDetailView_Previews: PreviewProvider { 48 | static var previews: some View { 49 | AwardDetailView(award: .goal, viewModel: StepsViewModel()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Steps/Screens/AwardLockedView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AwardLockedView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/21/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AwardLockedView: View { 11 | var body: some View { 12 | VStack { 13 | Image(systemName: "lock.fill") 14 | .resizable() 15 | .scaledToFit() 16 | .frame(width: 100, height: 100) 17 | .foregroundColor(.indigo) 18 | 19 | Text(Constants.locked) 20 | .font(.title2) 21 | .bold() 22 | .foregroundColor(.indigo) 23 | 24 | Text(Constants.haveNotUnlockedAwardDesc) 25 | .foregroundColor(.secondary) 26 | .padding() 27 | .padding(.horizontal, 55) 28 | .multilineTextAlignment(.center) 29 | } 30 | } 31 | } 32 | 33 | struct AwardLockedView_Previews: PreviewProvider { 34 | static var previews: some View { 35 | AwardLockedView() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Steps/Screens/AwardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BadgeView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/13/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AwardView: View { 11 | @ObservedObject var viewModel: StepsViewModel 12 | 13 | var columns: [GridItem] { 14 | [GridItem(.adaptive(minimum: 150, maximum: 150))] 15 | } 16 | 17 | var body: some View { 18 | NavigationStack { 19 | ScrollView { 20 | Text(Constants.unlockAwardsDesc) 21 | .font(.caption) 22 | .foregroundColor(.secondary) 23 | .padding() 24 | 25 | LazyVGrid(columns: columns) { 26 | ForEach(Award.allCases, id: \.name) { award in 27 | AwardBadgeView(award: award, viewModel: viewModel) 28 | } 29 | } 30 | } 31 | .navigationTitle("🏆 \(Constants.weeklyAwards)") 32 | } 33 | } 34 | } 35 | 36 | struct BadgeView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | AwardView(viewModel: StepsViewModel()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Steps/Screens/EditStepsGoalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditStepsGoalView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/17/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EditStepsGoalView: View { 11 | @Binding var goal: Int 12 | @Environment(\.dismiss) var dismiss 13 | 14 | var body: some View { 15 | VStack(spacing: 20) { 16 | Text(Constants.newDailyStepsGoal) 17 | .font(.title3) 18 | .bold() 19 | Spacer() 20 | HStack(spacing: 20) { 21 | Text("\(goal)") 22 | .font(.system(size: 34)) 23 | .bold() 24 | .foregroundColor(.indigo) 25 | 26 | Stepper(Constants.steps, value: $goal, in: 1000...30000, step: 1000) 27 | .labelsHidden() 28 | } 29 | 30 | Button(Constants.done) { dismiss() } 31 | .tint(.mint) 32 | .buttonStyle(.bordered) 33 | 34 | Spacer() 35 | } 36 | .padding() 37 | } 38 | } 39 | 40 | struct EditStepsGoalView_Previews: PreviewProvider { 41 | static var previews: some View { 42 | EditStepsGoalView(goal: .constant(10000)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Steps/Screens/GoalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GoalView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 1/7/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GoalView: View { 11 | @Environment(\.managedObjectContext) var context 12 | @ObservedObject var vm: GoalViewModel 13 | 14 | // @State private var isShowingSheet = false 15 | // @State private var selectedTab = Constants.incomplete 16 | // @State private var isShowingPaywall = true 17 | 18 | 19 | init( 20 | viewModel: GoalViewModel 21 | ) { 22 | self.vm = viewModel 23 | } 24 | 25 | @FetchRequest( 26 | entity: Goal.entity(), 27 | sortDescriptors: [NSSortDescriptor(keyPath: \Goal.date, ascending: false)], 28 | predicate: NSPredicate(format: "isComplete == false")) var incompleteGoals: FetchedResults 29 | 30 | @FetchRequest( 31 | entity: Goal.entity(), 32 | sortDescriptors: [NSSortDescriptor(keyPath: \Goal.date, ascending: false)], 33 | predicate: NSPredicate(format: "isComplete == true")) var completeGoals: FetchedResults 34 | 35 | var body: some View { 36 | NavigationStack { 37 | VStack { 38 | GoalPickerView(selectedTab: $vm.selectedTab) 39 | 40 | if vm.selectedTab == Constants.incomplete && incompleteGoals.isEmpty { 41 | Text("🥳 \(Constants.addSomeMoreGoals)") 42 | .fontWeight(.semibold) 43 | .foregroundColor(.secondary) 44 | } 45 | 46 | List { 47 | if vm.selectedTab == Constants.incomplete { 48 | ForEach(incompleteGoals) { goal in 49 | GoalListRowView(goal: goal) 50 | } 51 | .onDelete(perform: removeIncompleteGoal) 52 | } else { 53 | ForEach(completeGoals) { goal in 54 | GoalListRowView(goal: goal) 55 | } 56 | .onDelete(perform: removeCompleteGoal) 57 | } 58 | } 59 | .scrollContentBackground(.hidden) 60 | .listRowSeparator(.hidden) 61 | .toolbar { EditButton() } 62 | 63 | AddGoalButton(isShowingSheet: $vm.isShowingSheet) 64 | } 65 | .navigationTitle("✅ \(Constants.myGoals)") 66 | .sheet(isPresented: $vm.isShowingSheet) { 67 | AddGoalView() 68 | .presentationDetents([.height(300)]) 69 | } 70 | } 71 | } 72 | 73 | func removeIncompleteGoal(at offsets: IndexSet) { 74 | for index in offsets { 75 | let goal = incompleteGoals[index] 76 | context.delete(goal) 77 | } 78 | 79 | do { 80 | try context.save() 81 | } catch { 82 | print(Constants.coreDataError) 83 | } 84 | } 85 | 86 | func removeCompleteGoal(at offsets: IndexSet) { 87 | for index in offsets { 88 | let goal = completeGoals[index] 89 | context.delete(goal) 90 | } 91 | 92 | do { 93 | try context.save() 94 | } catch { 95 | print(Constants.coreDataError) 96 | } 97 | } 98 | } 99 | 100 | struct GoalView_Previews: PreviewProvider { 101 | static var previews: some View { 102 | GoalView(viewModel: .init()) 103 | } 104 | } 105 | 106 | //MARK: - SUPPORTING VIEWS 107 | 108 | fileprivate struct GoalPickerView: View { 109 | @Binding var selectedTab: String 110 | let tabOptions = [Constants.incomplete, Constants.complete] 111 | 112 | var body: some View { 113 | Picker(Constants.goalsTitle, selection: $selectedTab) { 114 | ForEach(tabOptions, id: \.self) { tab in 115 | Text(tab) 116 | } 117 | } 118 | .pickerStyle(.segmented) 119 | .padding() 120 | } 121 | } 122 | 123 | fileprivate struct AddGoalButton: View { 124 | @Binding var isShowingSheet: Bool 125 | 126 | var body: some View { 127 | Button { 128 | isShowingSheet.toggle() 129 | } label: { 130 | Label(Constants.addGoal, systemImage: "plus") 131 | .fontWeight(.semibold) 132 | } 133 | .buttonStyle(.bordered) 134 | .tint(.indigo) 135 | .padding(.bottom, 30) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Steps/Screens/HomeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/6/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HomeView: View { 11 | @ObservedObject var viewModel: StepsViewModel 12 | 13 | var body: some View { 14 | NavigationStack { 15 | ZStack { 16 | MountainView(viewModel: viewModel) 17 | .edgesIgnoringSafeArea(.all) 18 | 19 | NavigationLink { 20 | StepsDetailView(viewModel: viewModel) 21 | } label: { 22 | VStack { 23 | Spacer() 24 | HStack { 25 | CurrentStepsCardView(steps: viewModel.currentSteps) 26 | Spacer() 27 | } 28 | .padding() 29 | Spacer() 30 | } 31 | .padding(.bottom, 100) 32 | } 33 | } 34 | .onAppear { 35 | viewModel.requestAuthorization { isSuccess in 36 | if isSuccess == true { 37 | viewModel.calculateSteps { statsCollection in 38 | if let statsCollection = statsCollection { 39 | viewModel.updateUIFromStats(statsCollection) 40 | } 41 | } 42 | viewModel.calculateLastWeeksSteps { statsCollection in 43 | if let statsCollection = statsCollection { 44 | viewModel.updateWeekUIFromStats(statsCollection) 45 | } 46 | } 47 | viewModel.calculateMonthSteps { statsCollection in 48 | if let statsCollection = statsCollection { 49 | viewModel.updateMonthUIFromStats(statsCollection) 50 | } 51 | } 52 | viewModel.calculateCalories { statsCollection in 53 | if let statsCollection = statsCollection { 54 | viewModel.updateCalorieUIFromStats(statsCollection) 55 | } 56 | } 57 | viewModel.calculateDistance { statsCollection in 58 | if let statsCollection = statsCollection { 59 | viewModel.updateDistanceUIFromStats(statsCollection) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | struct HomeView_Previews: PreviewProvider { 70 | static var previews: some View { 71 | HomeView(viewModel: StepsViewModel()) 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /Steps/Screens/MainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/13/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MainView: View { 11 | @StateObject var stepsViewModel = StepsViewModel() 12 | 13 | var body: some View { 14 | TabView { 15 | HomeView(viewModel: stepsViewModel) 16 | .tabItem { 17 | Label(Constants.homeTab, systemImage: "house") 18 | } 19 | 20 | GoalView(viewModel: .init()) 21 | .tabItem { 22 | Label(Constants.goalsTab, systemImage: "checklist") 23 | } 24 | 25 | AwardView(viewModel: stepsViewModel) 26 | .tabItem { 27 | Label(Constants.awardsTab, systemImage: "trophy") 28 | } 29 | 30 | SettingsView(stepsViewModel: stepsViewModel) 31 | .tabItem { 32 | Label(Constants.settingsTab, systemImage: "slider.vertical.3") 33 | } 34 | } 35 | .tint(.indigo) 36 | } 37 | } 38 | 39 | struct MainView_Previews: PreviewProvider { 40 | static var previews: some View { 41 | MainView() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Steps/Screens/ProfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileView.swift 3 | // Steps 4 | // 5 | // Created by Yashraj jadhav on 16/10/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProfileView: View { 11 | 12 | @StateObject var viewModel = ProfileViewModel() 13 | 14 | var body: some View { 15 | 16 | VStack { 17 | HStack(spacing: 26) { 18 | Image(viewModel.profileImage ?? "avatar 1") 19 | .resizable() 20 | .frame(width: 120, height: 120) 21 | .clipShape(Circle()) 22 | .overlay( 23 | Circle() 24 | .stroke(Color.indigo, lineWidth: 2) 25 | ) 26 | .shadow(radius: 4) 27 | .onTapGesture { 28 | withAnimation { 29 | viewModel.presentEditImage() 30 | } 31 | } 32 | 33 | VStack(alignment: .leading, spacing: 4) { 34 | Text("Hello") 35 | .font(.title) 36 | .foregroundColor(Color.gray) 37 | .minimumScaleFactor(0.5) 38 | 39 | Text(viewModel.profileName ?? "Name") 40 | .font(.title) 41 | .fontWeight(.bold) 42 | .foregroundColor(.accentColor) 43 | } 44 | } 45 | 46 | if viewModel.isEditingName { 47 | 48 | TextField("Name...", text: $viewModel.currentName) 49 | .padding() 50 | .background( 51 | RoundedRectangle(cornerRadius: 10) 52 | .stroke() 53 | ) 54 | HStack { 55 | ProfileButtonView(title: "Cancel", backgroundColor: Color.gray.opacity(0.2)) { 56 | withAnimation { 57 | viewModel.dismissEdit() 58 | } 59 | } 60 | .foregroundColor(Color.red) 61 | 62 | ProfileButtonView(title: "Done", backgroundColor: Color.primary) { 63 | if !viewModel.currentName.isEmpty { 64 | withAnimation { 65 | viewModel.setNewName() 66 | } 67 | } 68 | } 69 | .foregroundColor(Color(uiColor: UIColor.systemBackground)) 70 | } 71 | } 72 | 73 | if viewModel.isEditingImage { 74 | ScrollView(.horizontal) { 75 | HStack { 76 | ForEach(viewModel.images, id: \.self) { image in 77 | Button { 78 | withAnimation { 79 | viewModel.didSelectNewImage(name: image) 80 | } 81 | } label: { 82 | VStack { 83 | Image(image) 84 | .resizable() 85 | .scaledToFit() 86 | .frame(width: 100, height: 100) 87 | .padding() 88 | 89 | if viewModel.isSelectedImage == image { 90 | Circle() 91 | .frame(width: 16, height: 16) 92 | .foregroundColor(Color.primary) 93 | } 94 | } 95 | .padding() 96 | } 97 | } 98 | } 99 | } 100 | .background( 101 | RoundedRectangle(cornerRadius: 10) 102 | .fill(Color.gray.opacity(0.15))) 103 | 104 | ProfileButtonView(title: "Done", backgroundColor: Color.primary) { 105 | withAnimation { 106 | viewModel.setNewImage() 107 | } 108 | } 109 | .foregroundColor(Color(uiColor: UIColor.systemBackground)) 110 | .padding(.bottom) 111 | } 112 | 113 | VStack { 114 | ProfileItemButtonView(title: "Edit Name", image: "square.and.pencil") { 115 | withAnimation { 116 | viewModel.presentEditName() 117 | } 118 | } 119 | 120 | ProfileItemButtonView(title: "Edit Image", image: "square.and.pencil") { 121 | withAnimation { 122 | viewModel.presentEditImage() 123 | } 124 | } 125 | } 126 | .background( 127 | RoundedRectangle(cornerRadius: 10) 128 | .fill(Color.gray.opacity(0.15)) 129 | ) 130 | 131 | VStack { 132 | Link(destination: URL(string: Constants.privacyURL)!) { 133 | HStack { 134 | Image(systemName: "doc") 135 | 136 | Text("Privacy Policy") 137 | } 138 | .foregroundColor(Color.accentColor) 139 | .padding() 140 | .frame(maxWidth: .infinity, alignment: .leading) 141 | } 142 | 143 | Link(destination: URL(string: Constants.termsURL)!) { 144 | HStack { 145 | Image(systemName: "doc") 146 | 147 | Text("Terms of Service") 148 | } 149 | .foregroundColor(Color.accentColor) 150 | .padding() 151 | .frame(maxWidth: .infinity, alignment: .leading) 152 | } 153 | } 154 | .background( 155 | RoundedRectangle(cornerRadius: 10) 156 | .fill(Color.gray.opacity(0.15)) 157 | ) 158 | } 159 | .padding() 160 | .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) 161 | 162 | .alert("Oops!", isPresented: $viewModel.showAlert) { 163 | Text("Ok") 164 | } message: { 165 | Text("We were unable to open your mail application. Please make sure you have one installed.") 166 | } 167 | } 168 | } 169 | #Preview { 170 | ProfileView() 171 | } 172 | -------------------------------------------------------------------------------- /Steps/Screens/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/13/22. 6 | // 7 | 8 | import SwiftUI 9 | import UserNotifications 10 | import PhotosUI 11 | 12 | struct SettingsView: View { 13 | @StateObject var viewModel = SettingsViewModel() 14 | @ObservedObject var stepsViewModel: StepsViewModel 15 | 16 | var body: some View { 17 | NavigationStack { 18 | VStack(spacing: 30) { 19 | StepsGoalCardView(steps: stepsViewModel.goal, showingEditView: $viewModel.showingEditView) 20 | .padding() 21 | 22 | Form { 23 | Section { 24 | Toggle(Constants.notifications, isOn: $viewModel.notificationsOn) 25 | } header: { 26 | Label(Constants.notificationSettings, systemImage: "bell") 27 | } 28 | 29 | Section { 30 | if stepsViewModel.backgroundImage != nil { 31 | Button("Reset background image") { 32 | stepsViewModel.backgroundImageSelection = nil 33 | } 34 | .buttonStyle(.borderless) 35 | } else { 36 | PhotosPicker(selection: $stepsViewModel.backgroundImageSelection, 37 | matching: .images, 38 | photoLibrary: .shared()) { 39 | Text("Set background image") 40 | } 41 | .buttonStyle(.borderless) 42 | } 43 | } header: { 44 | Label("Home Screen", systemImage: "iphone.gen3") 45 | } 46 | 47 | Link(Constants.termsOfUse, destination: URL(string: Constants.termsURL)!) 48 | Link(Constants.privacyPolicy, destination: URL(string: Constants.privacyURL)!) 49 | 50 | Button { 51 | viewModel.showContributors = true 52 | } label: { 53 | Text("Contributors") 54 | } 55 | } 56 | .scrollContentBackground(.hidden) 57 | } 58 | .padding() 59 | .padding(.vertical, 20) 60 | .sheet(isPresented: $viewModel.showingEditView, content: { 61 | EditStepsGoalView(goal: $stepsViewModel.goal) 62 | .presentationDetents([.height(250)]) 63 | }) 64 | .sheet(isPresented: $viewModel.showContributors, content: { 65 | NavigationStack { 66 | ContributorsView() 67 | } 68 | }) 69 | .task(id: viewModel.notificationsOn) { 70 | await viewModel.requestNotificationAuth() 71 | } 72 | .navigationTitle("⚙️ \(Constants.settingsTitle)") 73 | .tint(.indigo) 74 | } 75 | .alert(isPresented: $stepsViewModel.showBackgroundImageAlert) { 76 | Alert(title: Text("Failed to set background image."), 77 | message: Text("Please choose another image.")) 78 | } 79 | } 80 | } 81 | 82 | struct SettingsView_Previews: PreviewProvider { 83 | static var previews: some View { 84 | SettingsView(stepsViewModel: StepsViewModel()) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Steps/Screens/StepsCountView.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// StepsCountView.swift 3 | //// Steps 4 | //// 5 | //// Created by Mohd Wasif Raza on 04/10/23. 6 | //// 7 | // 8 | //import SwiftUI 9 | // 10 | //struct StepsCountView: View { 11 | // @ObservedObject var viewModel: StepsViewModel 12 | // @State private var selectedTab = Constants.week 13 | // 14 | // var body: some View { 15 | // VStack(spacing: 20) { 16 | // GoalPickerView(selectedTab: $selectedTab) 17 | // 18 | // if selectedTab == Constants.week { 19 | // NewWeekStepsView(viewModel: viewModel) 20 | // } 21 | // else { 22 | // MonthStepsView(viewModel: viewModel) 23 | // } 24 | // Spacer() 25 | // 26 | // } 27 | // .padding(.top, 30) 28 | // } 29 | //} 30 | // 31 | //struct StepsCountView_Previews: PreviewProvider { 32 | // static var previews: some View { 33 | // StepsCountView(viewModel: StepsViewModel()) 34 | // } 35 | //} 36 | // 37 | ////MARK: - SUPPORTING VIEWS 38 | // 39 | //fileprivate struct GoalPickerView: View { 40 | // @Binding var selectedTab: String 41 | // let tabOptions = [Constants.week, Constants.month] 42 | // 43 | // var body: some View { 44 | // Picker(Constants.goalsTitle, selection: $selectedTab) { 45 | // ForEach(tabOptions, id: \.self) { tab in 46 | // Text(tab) 47 | // } 48 | // } 49 | // .pickerStyle(.segmented) 50 | // .padding() 51 | // } 52 | //} 53 | -------------------------------------------------------------------------------- /Steps/Screens/StepsDetailView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StepsDetailView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/14/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StepsDetailView: View { 11 | @ObservedObject var viewModel: StepsViewModel 12 | 13 | var body: some View { 14 | ScrollView{ 15 | VStack(spacing: 20) { 16 | FactView(viewModel: viewModel) 17 | CircleProgressBar(value: viewModel.currentSteps, maxValue: viewModel.goal) 18 | .padding() 19 | LazyVGrid(columns: Array(repeating: GridItem(spacing: 20), count: 2), content: { 20 | StepsDetailCardView(title: Constants.caloriesBurned, image: "flame.fill", value: "\(viewModel.currentCalories) kcal") 21 | StepsDetailCardView(title: Constants.distance, image: "figure.walk", value: "\(viewModel.currentDistance) meters") 22 | }).padding(.horizontal) 23 | 24 | NewWeekStepsView(viewModel: viewModel) 25 | Spacer() 26 | } 27 | .padding(.top, 30) 28 | .padding(.horizontal) 29 | } 30 | } 31 | } 32 | 33 | struct StepsDetailView_Previews: PreviewProvider { 34 | static var previews: some View { 35 | StepsDetailView(viewModel: StepsViewModel()) 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /Steps/Steps.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.developer.healthkit 6 | 7 | com.apple.developer.healthkit.access 8 | 9 | health-records 10 | 11 | com.apple.developer.healthkit.background-delivery 12 | 13 | com.apple.security.application-groups 14 | 15 | group.com.BrittanyRima.Steps 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Steps/StepsApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StepsApp.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/6/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct StepsApp: App { 12 | @Environment(\.scenePhase) var scenePhase 13 | let persistenceController = PersistenceController.shared 14 | 15 | var body: some Scene { 16 | WindowGroup { 17 | MainView() 18 | .environment(\.managedObjectContext, persistenceController.container.viewContext) 19 | } 20 | .onChange(of: scenePhase) { _ in 21 | persistenceController.save() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Steps/StepsConfig.xcconfig: -------------------------------------------------------------------------------- 1 | 2 | DEVELOPMENT_TEAM = AGU49S8C2X 3 | PRODUCT_BUNDLE_IDENTIFIER = com.BrittanyRima.Steps 4 | -------------------------------------------------------------------------------- /Steps/StepsConfigTemplate.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // StepsConfigTemplate.xcconfig 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 10/2/23. 6 | // 7 | 8 | // Configuration settings file format documentation can be found at: 9 | // https://help.apple.com/xcode/#/dev745c5c974 10 | 11 | DEVELOPMENT_TEAM = ID_HERE 12 | PRODUCT_BUNDLE_IDENTIFIER = com.BundleID.here 13 | -------------------------------------------------------------------------------- /Steps/StepsWidgetConfig.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // StepsWidgetConfigTemplate.xcconfig 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 10/5/23. 6 | // 7 | 8 | // Configuration settings file format documentation can be found at: 9 | // https://help.apple.com/xcode/#/dev745c5c974 10 | 11 | DEVELOPMENT_TEAM = AGU49S8C2X 12 | PRODUCT_BUNDLE_IDENTIFIER = com.BrittanyRima.Steps.Widget 13 | -------------------------------------------------------------------------------- /Steps/StepsWidgetConfigTemplate.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // StepsWidgetConfigTemplate.xcconfig 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 10/5/23. 6 | // 7 | 8 | // Configuration settings file format documentation can be found at: 9 | // https://help.apple.com/xcode/#/dev745c5c974 10 | 11 | DEVELOPMENT_TEAM = ID_HERE 12 | PRODUCT_BUNDLE_IDENTIFIER = com.BundleID.here.Widget 13 | -------------------------------------------------------------------------------- /Steps/Utilities/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/23/22. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Constants { 11 | // MARK: URLs 12 | static let termsURL = "https://www.apple.com/legal/internet-services/itunes/dev/stdeula/" 13 | static let privacyURL = "https://sites.google.com/view/steptrackerapp/home" 14 | 15 | // MARK: Identifiers 16 | static let appGroupID = "group.com.BrittanyRima.Steps" 17 | 18 | // MARK: Messages 19 | static let goalKey = "goal" 20 | static let notificationKey = "notifications" 21 | 22 | static let stepCountKey = "stepCount" 23 | static let backgroundImageKey = "backgroundImage" 24 | static let steps = NSLocalizedString("Steps", comment: "") 25 | static let weeklySteps = NSLocalizedString("Weekly Steps", comment: "Title for weekly steps") 26 | static let monthlySteps = "Monthly Steps" 27 | static let goal = "Goal" 28 | static let day = "Day" 29 | static let dailyGoal = NSLocalizedString("Daily Goal", comment: "daily goal label in the graph view of steps.") 30 | static let soccerball = "soccerball" 31 | static let done = NSLocalizedString("Done", comment: "Button title to edit steps goal") 32 | static let firstStepsName = "First Steps" 33 | static let firstStepsDescription = "You took more than 100 steps this week, you're on your way to your goal. Keep it up!" 34 | static let goalName = "Gooooaaaaal" 35 | static let goalDescription = "You reached your step goal at least once this week!" 36 | static let doubleTroubleName = "Double Trouble" 37 | static let doubleTroubleDescription = "You doubled your steps goal this week! You are incredible!" 38 | static let threesName = "Threes" 39 | static let threesDescription = "You tripled your steps goal this week! Wow!" 40 | static let perfectWeekName = "Perfect Week" 41 | static let perfectWeekDescription = "You reached your steps goal every day the past 7 days. You are incredible!" 42 | static let messiName = "Don't Messi With You" 43 | static let soccerFieldDescription = "You walked 100 soccer fields in a single day this week! " 44 | static let motivatedName = "Motivated" 45 | static let motivatedDescription = "You created your first goal!" 46 | static let firstGoalName = "First Goal!" 47 | static let firstGoalDescription = "You completed your first goal!" 48 | static let dreamerGoalName = "Dreamer" 49 | static let dreamerGoalDescription = "You created 5 goals!" 50 | static let goGetterName = "Go Getter" 51 | static let goGetterDescription = "You completed 5 goals!" 52 | static let dailyStepsGoal = "Daily Steps Goal" 53 | static let enterSteps = NSLocalizedString("%i steps", comment: "") 54 | static let currentSteps = NSLocalizedString("Current Steps", comment: "Title for current steps") 55 | static let addNewGoal = NSLocalizedString("Add New Goal", comment: "🖋️ Add New goal title") 56 | static let goalIdeas = NSLocalizedString("Goal ideas", comment: "💡Goal ideas label when creating new goal") 57 | static let walkWithFriend = NSLocalizedString("Walk a 5K, Walk with a friend, Increase steps goal", comment: "label message when creating a new goal.") 58 | static let newGoalField = NSLocalizedString("New goal...", comment: "New goal... label for text field in add new goal view.") 59 | static let save = NSLocalizedString("Save", comment: "Save label inside add new goal view.") 60 | static let unknownName = "Unknown Name" 61 | static let unlockAwardsDesc = NSLocalizedString("Can you can unlock all of these awards this week?", comment: "label below the Weekly award title inside weekly awards view") 62 | static let weeklyAwards = NSLocalizedString("Weekly Awards", comment: "Weekly awards title") 63 | static let notifications = NSLocalizedString("Notifications", comment: "Notifications label") 64 | static let notificationSettings = NSLocalizedString("Notification settings", comment: "Notifications settings label") 65 | 66 | /// The string to be passed into UNNotificationRequest 67 | static let notificationsIdentifier = "daily-notification" 68 | static let termsOfUse = NSLocalizedString("Terms of Use", comment: "Terms of use label") 69 | static let privacyPolicy = NSLocalizedString("Privacy Policy", comment: "privacy policy label") 70 | static let settingsTitle = NSLocalizedString("Settings", comment: "Settings title screen") 71 | static let newDailyStepsGoal = "Set a New Daily Steps Goal" 72 | static let haveNotUnlockedAwardDesc = NSLocalizedString("You haven't unlocked this award yet this week. Keep getting those steps in and completing goals to unlock it. You can do it!", comment: "message of weekly award blocked.") 73 | static let locked = NSLocalizedString("Locked", comment: "label title for locked") 74 | static let myGoals = NSLocalizedString(" My Goals", comment: "✅ My Goals view title ") 75 | static let addGoal = NSLocalizedString("Add Goal", comment: "Add goal label to add new a goal") 76 | static let goalsTitle = "Goals" 77 | static let addSomeMoreGoals = NSLocalizedString("Time to add some more goals!", comment: "🥳 Time to add some more goals label inside Goals view.") 78 | static let week = NSLocalizedString("Week", comment: "label title for week") 79 | static let month = NSLocalizedString("Month", comment: "label title for Month") 80 | static let incomplete = NSLocalizedString("Incomplete", comment: "Incomplete label to see incompleted goals") 81 | static let complete = NSLocalizedString("Complete", comment: "complete label to see completed goals") 82 | static let coreDataError = "❗️ Error saving delete goal to core data" 83 | static let goodMorningTitle = "Good Morning!" 84 | static let reachStepGoalDescription = "Try to reach your steps goal today. We believe in you!" 85 | static let walkedOneSoccerFieldToday = "You've walked about 1 soccer field today so far. Keep it up!" 86 | static let walkedFullSoccerFieldToday = NSLocalizedString("Keep walking. You've almost walked a full soccer field today so far!", comment: "Comment in the header of Steps view") 87 | static let walkedCustomSoccerFieldToday = "You've walked about %i soccer fields today so far. Keep it up!" 88 | static let stepsWidget = "StepsWidget" 89 | static let stepsWidgetName = NSLocalizedString("Current Steps", comment: "current steps label in the steps widget") 90 | static let stepsWidgetDescription = "View your current steps count and progress." 91 | static let stepsGraphWidget = "StepsGraphWidget" 92 | static let stepsGraphWidgetName = "Steps Graph" 93 | static let stepsGraphWidgetDescription = "View your steps progress throughout the day." 94 | static let caloriesBurned = "Calories Burned" 95 | static let distance = "Distance Walked" 96 | 97 | // MARK: Tabs 98 | static let homeTab = NSLocalizedString("Home", comment: "Home tab title") 99 | static let goalsTab = NSLocalizedString("Goals", comment: "Goals tab title") 100 | static let awardsTab = NSLocalizedString("Awards", comment: "Awards tab title") 101 | static let stepsTab = NSLocalizedString("Steps", comment: "Steps tab title") 102 | static let settingsTab = NSLocalizedString("Settings", comment: "Settings tab title") 103 | } 104 | 105 | extension UserDefaults { 106 | static let appGroup: UserDefaults? = UserDefaults(suiteName: Constants.appGroupID) 107 | } 108 | -------------------------------------------------------------------------------- /Steps/Utilities/Extensions/Date+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+Ext.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/14/22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | static func sundayAt12AM() -> Date { 12 | return Calendar(identifier: .iso8601).date(from: Calendar(identifier: .iso8601).dateComponents([.yearForWeekOfYear], from: Date()))! 13 | } 14 | 15 | static func from(year: Int, month: Int, day: Int) -> Date { 16 | let components = DateComponents(year: year, month: month, day: day) 17 | return Calendar.current.date(from: components)! 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Steps/Utilities/Extensions/FileManager+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManager+Ext.swift 3 | // Steps 4 | // 5 | // Created by Raphael on 05.10.23. 6 | // 7 | 8 | import Foundation 9 | 10 | extension FileManager { 11 | func documentsDirectory() -> URL { 12 | let path = urls(for: .documentDirectory, in: .userDomainMask) 13 | return path.first! 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Steps/Utilities/Extensions/Localizable.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | " Goal ideas" : { 5 | "comment" : "Title for goal ideas", 6 | "extractionState" : "manual", 7 | "localizations" : { 8 | "bn" : { 9 | "stringUnit" : { 10 | "state" : "translated", 11 | "value" : "💡গোল আইডিয়া" 12 | } 13 | }, 14 | "es-419" : { 15 | "stringUnit" : { 16 | "state" : "translated", 17 | "value" : "💡 Ideas para metas" 18 | } 19 | }, 20 | "fr" : { 21 | "stringUnit" : { 22 | "state" : "translated", 23 | "value" : "💡Idées d'objectifs" 24 | } 25 | }, 26 | "ko" : { 27 | "stringUnit" : { 28 | "state" : "translated", 29 | "value" : "목표 아이디어" 30 | } 31 | } 32 | } 33 | }, 34 | " My Goals" : { 35 | "comment" : "✅ My Goals view title ", 36 | "localizations" : { 37 | "bn" : { 38 | "stringUnit" : { 39 | "state" : "translated", 40 | "value" : "আমার গোল" 41 | } 42 | }, 43 | "es-419" : { 44 | "stringUnit" : { 45 | "state" : "translated", 46 | "value" : "Mis Metas" 47 | } 48 | }, 49 | "fr" : { 50 | "stringUnit" : { 51 | "state" : "translated", 52 | "value" : "Mes objectifs" 53 | } 54 | }, 55 | "ko" : { 56 | "stringUnit" : { 57 | "state" : "translated", 58 | "value" : "나의 목표" 59 | } 60 | } 61 | } 62 | }, 63 | "%i steps" : { 64 | "comment" : "# and steps title inside the widget. EX: 2000 (steps)", 65 | "extractionState" : "manual", 66 | "localizations" : { 67 | "bn" : { 68 | "stringUnit" : { 69 | "state" : "translated", 70 | "value" : "%i স্টেপস" 71 | } 72 | }, 73 | "es-419" : { 74 | "stringUnit" : { 75 | "state" : "translated", 76 | "value" : "%i pasos" 77 | } 78 | }, 79 | "fr" : { 80 | "stringUnit" : { 81 | "state" : "translated", 82 | "value" : "%i étapes" 83 | } 84 | }, 85 | "ko" : { 86 | "stringUnit" : { 87 | "state" : "needs_review", 88 | "value" : "%i보" 89 | } 90 | } 91 | } 92 | }, 93 | "%lld" : { 94 | "localizations" : { 95 | "bn" : { 96 | "stringUnit" : { 97 | "state" : "translated", 98 | "value" : "%lld" 99 | } 100 | }, 101 | "es-419" : { 102 | "stringUnit" : { 103 | "state" : "translated", 104 | "value" : "%lld" 105 | } 106 | }, 107 | "fr" : { 108 | "stringUnit" : { 109 | "state" : "translated", 110 | "value" : "%lld" 111 | } 112 | }, 113 | "ko" : { 114 | "stringUnit" : { 115 | "state" : "translated", 116 | "value" : "%lld" 117 | } 118 | } 119 | } 120 | }, 121 | "%lld steps" : { 122 | "comment" : "Title for number of steps\nNumber of steps", 123 | "extractionState" : "stale", 124 | "localizations" : { 125 | "bn" : { 126 | "stringUnit" : { 127 | "state" : "translated", 128 | "value" : "%lld স্টেপস" 129 | } 130 | }, 131 | "es-419" : { 132 | "stringUnit" : { 133 | "state" : "translated", 134 | "value" : "Numero de pasos" 135 | } 136 | }, 137 | "fr" : { 138 | "stringUnit" : { 139 | "state" : "translated", 140 | "value" : "%lld étapes" 141 | } 142 | }, 143 | "ko" : { 144 | "stringUnit" : { 145 | "state" : "translated", 146 | "value" : "%lld보" 147 | } 148 | } 149 | } 150 | }, 151 | "⚙️ %@" : { 152 | "localizations" : { 153 | "ko" : { 154 | "stringUnit" : { 155 | "state" : "translated", 156 | "value" : "⚙️ %@" 157 | } 158 | } 159 | } 160 | }, 161 | "✅ %@" : { 162 | "localizations" : { 163 | "ko" : { 164 | "stringUnit" : { 165 | "state" : "translated", 166 | "value" : "✅ %@" 167 | } 168 | } 169 | } 170 | }, 171 | "🏆 %@" : { 172 | "localizations" : { 173 | "ko" : { 174 | "stringUnit" : { 175 | "state" : "translated", 176 | "value" : "🏆 %@" 177 | } 178 | } 179 | } 180 | }, 181 | "💡 %@" : { 182 | "localizations" : { 183 | "ko" : { 184 | "stringUnit" : { 185 | "state" : "translated", 186 | "value" : "💡 %@" 187 | } 188 | } 189 | } 190 | }, 191 | "🖋️ %@" : { 192 | "localizations" : { 193 | "ko" : { 194 | "stringUnit" : { 195 | "state" : "translated", 196 | "value" : "🖋️ %@" 197 | } 198 | } 199 | } 200 | }, 201 | "🥳 %@" : { 202 | "localizations" : { 203 | "ko" : { 204 | "stringUnit" : { 205 | "state" : "translated", 206 | "value" : "🥳 %@" 207 | } 208 | } 209 | } 210 | }, 211 | "Add Goal" : { 212 | "comment" : "Add goal label to add new a goal", 213 | "localizations" : { 214 | "bn" : { 215 | "stringUnit" : { 216 | "state" : "translated", 217 | "value" : "গোল অ্যাড করুন" 218 | } 219 | }, 220 | "es-419" : { 221 | "stringUnit" : { 222 | "state" : "translated", 223 | "value" : "Agregar Nueva meta" 224 | } 225 | }, 226 | "fr" : { 227 | "stringUnit" : { 228 | "state" : "translated", 229 | "value" : "Ajouter un objectif" 230 | } 231 | }, 232 | "ko" : { 233 | "stringUnit" : { 234 | "state" : "translated", 235 | "value" : "목표 추가" 236 | } 237 | } 238 | } 239 | }, 240 | "Add New Goal" : { 241 | "comment" : "🖋️ Add New goal title", 242 | "localizations" : { 243 | "bn" : { 244 | "stringUnit" : { 245 | "state" : "translated", 246 | "value" : "নতুন অ্যাড যোগ করুন" 247 | } 248 | }, 249 | "es-419" : { 250 | "stringUnit" : { 251 | "state" : "translated", 252 | "value" : "Añade nueva meta\n" 253 | } 254 | }, 255 | "fr" : { 256 | "stringUnit" : { 257 | "state" : "translated", 258 | "value" : "Ajouter un nouvel objectif" 259 | } 260 | }, 261 | "ko" : { 262 | "stringUnit" : { 263 | "state" : "translated", 264 | "value" : "새로운 목표 추가" 265 | } 266 | } 267 | } 268 | }, 269 | "Awards" : { 270 | "comment" : "Awards tab title", 271 | "localizations" : { 272 | "bn" : { 273 | "stringUnit" : { 274 | "state" : "translated", 275 | "value" : "অ্যাওয়ার্ডস" 276 | } 277 | }, 278 | "es-419" : { 279 | "stringUnit" : { 280 | "state" : "translated", 281 | "value" : "Premios" 282 | } 283 | }, 284 | "fr" : { 285 | "stringUnit" : { 286 | "state" : "translated", 287 | "value" : "Prix" 288 | } 289 | }, 290 | "ko" : { 291 | "stringUnit" : { 292 | "state" : "translated", 293 | "value" : "수상" 294 | } 295 | } 296 | } 297 | }, 298 | "Can you can unlock all of these awards this week?" : { 299 | "comment" : "label below the Weekly award title inside weekly awards view", 300 | "localizations" : { 301 | "bn" : { 302 | "stringUnit" : { 303 | "state" : "translated", 304 | "value" : "আপনি কি এই সপ্তাহে এই সমস্ত অ্যাওয়ার্ডস আনলক করতে পারেন?" 305 | } 306 | }, 307 | "es-419" : { 308 | "stringUnit" : { 309 | "state" : "translated", 310 | "value" : "Podrás desbloquear todas estas recompensas esta semana? " 311 | } 312 | }, 313 | "fr" : { 314 | "stringUnit" : { 315 | "state" : "translated", 316 | "value" : "Pouvez-vous débloquer toutes ces récompenses cette semaine ?" 317 | } 318 | }, 319 | "ko" : { 320 | "stringUnit" : { 321 | "state" : "translated", 322 | "value" : "이번 주에 이 모든 상을 해제할 수 있을까요?" 323 | } 324 | } 325 | } 326 | }, 327 | "Complete" : { 328 | "comment" : "complete label to see completed goals", 329 | "localizations" : { 330 | "bn" : { 331 | "stringUnit" : { 332 | "state" : "translated", 333 | "value" : "কমপ্লিট" 334 | } 335 | }, 336 | "es-419" : { 337 | "stringUnit" : { 338 | "state" : "translated", 339 | "value" : "Completado" 340 | } 341 | }, 342 | "fr" : { 343 | "stringUnit" : { 344 | "state" : "translated", 345 | "value" : "Complet" 346 | } 347 | }, 348 | "ko" : { 349 | "stringUnit" : { 350 | "state" : "translated", 351 | "value" : "완료" 352 | } 353 | } 354 | } 355 | }, 356 | "Contributors" : { 357 | "localizations" : { 358 | "bn" : { 359 | "stringUnit" : { 360 | "state" : "translated", 361 | "value" : "কন্ট্রিবিউটর" 362 | } 363 | }, 364 | "es-419" : { 365 | "stringUnit" : { 366 | "state" : "translated", 367 | "value" : "Colaboradores" 368 | } 369 | }, 370 | "fr" : { 371 | "stringUnit" : { 372 | "state" : "translated", 373 | "value" : "Contributeurs" 374 | } 375 | }, 376 | "ko" : { 377 | "stringUnit" : { 378 | "state" : "translated", 379 | "value" : "기여자들" 380 | } 381 | } 382 | } 383 | }, 384 | "Current Steps" : { 385 | "comment" : "Title for current steps", 386 | "extractionState" : "manual", 387 | "localizations" : { 388 | "bn" : { 389 | "stringUnit" : { 390 | "state" : "translated", 391 | "value" : "বর্তমান স্টেপস" 392 | } 393 | }, 394 | "es-419" : { 395 | "stringUnit" : { 396 | "state" : "translated", 397 | "value" : "Pasos actuales" 398 | } 399 | }, 400 | "fr" : { 401 | "stringUnit" : { 402 | "state" : "translated", 403 | "value" : "Étapes actuelles" 404 | } 405 | }, 406 | "ko" : { 407 | "stringUnit" : { 408 | "state" : "translated", 409 | "value" : "현재 걸음 수" 410 | } 411 | } 412 | } 413 | }, 414 | "Daily Goal" : { 415 | "comment" : "daily goal label in the graph view of steps.", 416 | "localizations" : { 417 | "bn" : { 418 | "stringUnit" : { 419 | "state" : "translated", 420 | "value" : "ডেইলি গোল" 421 | } 422 | }, 423 | "es-419" : { 424 | "stringUnit" : { 425 | "state" : "translated", 426 | "value" : "Metas Diarias " 427 | } 428 | }, 429 | "fr" : { 430 | "stringUnit" : { 431 | "state" : "translated", 432 | "value" : "Objectif quotidien" 433 | } 434 | }, 435 | "ko" : { 436 | "stringUnit" : { 437 | "state" : "translated", 438 | "value" : "일일 목표" 439 | } 440 | } 441 | } 442 | }, 443 | "Daily Steps Goal" : { 444 | "comment" : "Title for daily steps goal", 445 | "extractionState" : "stale", 446 | "localizations" : { 447 | "bn" : { 448 | "stringUnit" : { 449 | "state" : "translated", 450 | "value" : "ডেইলি স্টেপ গোল" 451 | } 452 | }, 453 | "es-419" : { 454 | "stringUnit" : { 455 | "state" : "translated", 456 | "value" : "Metas de Pasos Diarios " 457 | } 458 | }, 459 | "fr" : { 460 | "stringUnit" : { 461 | "state" : "translated", 462 | "value" : "Objectif de pas quotidiens" 463 | } 464 | }, 465 | "ko" : { 466 | "stringUnit" : { 467 | "state" : "translated", 468 | "value" : "일보 목표" 469 | } 470 | } 471 | } 472 | }, 473 | "Day" : { 474 | "extractionState" : "stale", 475 | "localizations" : { 476 | "bn" : { 477 | "stringUnit" : { 478 | "state" : "translated", 479 | "value" : "ডে" 480 | } 481 | }, 482 | "es-419" : { 483 | "stringUnit" : { 484 | "state" : "translated", 485 | "value" : "Día" 486 | } 487 | }, 488 | "fr" : { 489 | "stringUnit" : { 490 | "state" : "translated", 491 | "value" : "Jour" 492 | } 493 | }, 494 | "ko" : { 495 | "stringUnit" : { 496 | "state" : "translated", 497 | "value" : "일" 498 | } 499 | } 500 | } 501 | }, 502 | "Done" : { 503 | "comment" : "Button title to edit steps goal", 504 | "localizations" : { 505 | "bn" : { 506 | "stringUnit" : { 507 | "state" : "translated", 508 | "value" : "সম্পন্ন" 509 | } 510 | }, 511 | "es-419" : { 512 | "stringUnit" : { 513 | "state" : "translated", 514 | "value" : "Hecho" 515 | } 516 | }, 517 | "fr" : { 518 | "stringUnit" : { 519 | "state" : "translated", 520 | "value" : "Fait" 521 | } 522 | }, 523 | "ko" : { 524 | "stringUnit" : { 525 | "state" : "translated", 526 | "value" : "완료" 527 | } 528 | } 529 | } 530 | }, 531 | "Failed to set background image." : { 532 | "localizations" : { 533 | "bn" : { 534 | "stringUnit" : { 535 | "state" : "translated", 536 | "value" : "ব্যাকগ্রাউন্ড ইমেজ সেট করতে ব্যর্থ হয়েছে।" 537 | } 538 | }, 539 | "es-419" : { 540 | "stringUnit" : { 541 | "state" : "translated", 542 | "value" : "Fallo al intentar establecer nueva imagen de fondo de pantalla. " 543 | } 544 | }, 545 | "fr" : { 546 | "stringUnit" : { 547 | "state" : "translated", 548 | "value" : "Échec de la définition de l'image d'arrière-plan." 549 | } 550 | }, 551 | "ko" : { 552 | "stringUnit" : { 553 | "state" : "translated", 554 | "value" : "배경 이미지 설정에 실패했습니다." 555 | } 556 | } 557 | } 558 | }, 559 | "Goal" : { 560 | "extractionState" : "stale", 561 | "localizations" : { 562 | "bn" : { 563 | "stringUnit" : { 564 | "state" : "translated", 565 | "value" : "গোল" 566 | } 567 | }, 568 | "es-419" : { 569 | "stringUnit" : { 570 | "state" : "translated", 571 | "value" : "Meta" 572 | } 573 | }, 574 | "fr" : { 575 | "stringUnit" : { 576 | "state" : "translated", 577 | "value" : "But" 578 | } 579 | }, 580 | "ko" : { 581 | "stringUnit" : { 582 | "state" : "translated", 583 | "value" : "목표" 584 | } 585 | } 586 | } 587 | }, 588 | "Goal ideas" : { 589 | "comment" : "💡Goal ideas label when creating new goal", 590 | "localizations" : { 591 | "bn" : { 592 | "stringUnit" : { 593 | "state" : "translated", 594 | "value" : "গোল আইডিয়া" 595 | } 596 | }, 597 | "es-419" : { 598 | "stringUnit" : { 599 | "state" : "translated", 600 | "value" : "Ideas para metas" 601 | } 602 | }, 603 | "fr" : { 604 | "stringUnit" : { 605 | "state" : "translated", 606 | "value" : "Idées d'objectifs" 607 | } 608 | }, 609 | "ko" : { 610 | "stringUnit" : { 611 | "state" : "translated", 612 | "value" : "목표 아이디어" 613 | } 614 | } 615 | } 616 | }, 617 | "Goal: %lld" : { 618 | 619 | }, 620 | "Goals" : { 621 | "comment" : "Goals tab title", 622 | "localizations" : { 623 | "bn" : { 624 | "stringUnit" : { 625 | "state" : "translated", 626 | "value" : "গোল" 627 | } 628 | }, 629 | "es-419" : { 630 | "stringUnit" : { 631 | "state" : "translated", 632 | "value" : "Metas" 633 | } 634 | }, 635 | "fr" : { 636 | "stringUnit" : { 637 | "state" : "translated", 638 | "value" : "Buts" 639 | } 640 | }, 641 | "ko" : { 642 | "stringUnit" : { 643 | "state" : "translated", 644 | "value" : "목표들" 645 | } 646 | } 647 | } 648 | }, 649 | "Hello" : { 650 | 651 | }, 652 | "Home" : { 653 | "comment" : "Home tab title", 654 | "localizations" : { 655 | "bn" : { 656 | "stringUnit" : { 657 | "state" : "translated", 658 | "value" : "হোম" 659 | } 660 | }, 661 | "es-419" : { 662 | "stringUnit" : { 663 | "state" : "translated", 664 | "value" : "Inicio" 665 | } 666 | }, 667 | "fr" : { 668 | "stringUnit" : { 669 | "state" : "translated", 670 | "value" : "Domicile" 671 | } 672 | }, 673 | "ko" : { 674 | "stringUnit" : { 675 | "state" : "translated", 676 | "value" : "홈" 677 | } 678 | } 679 | } 680 | }, 681 | "Home Screen" : { 682 | "localizations" : { 683 | "bn" : { 684 | "stringUnit" : { 685 | "state" : "translated", 686 | "value" : "হোম স্ক্রিন" 687 | } 688 | }, 689 | "es-419" : { 690 | "stringUnit" : { 691 | "state" : "translated", 692 | "value" : "Página de inicio" 693 | } 694 | }, 695 | "fr" : { 696 | "stringUnit" : { 697 | "state" : "translated", 698 | "value" : "Écran d’accueil" 699 | } 700 | }, 701 | "ko" : { 702 | "stringUnit" : { 703 | "state" : "translated", 704 | "value" : "홈 화면" 705 | } 706 | } 707 | } 708 | }, 709 | "Incomplete" : { 710 | "comment" : "Incomplete label to see incompleted goals", 711 | "localizations" : { 712 | "bn" : { 713 | "stringUnit" : { 714 | "state" : "translated", 715 | "value" : "অসম্পূর্ণ" 716 | } 717 | }, 718 | "es-419" : { 719 | "stringUnit" : { 720 | "state" : "translated", 721 | "value" : "Inconmpleto" 722 | } 723 | }, 724 | "fr" : { 725 | "stringUnit" : { 726 | "state" : "translated", 727 | "value" : "Incomplet" 728 | } 729 | }, 730 | "ko" : { 731 | "stringUnit" : { 732 | "state" : "translated", 733 | "value" : "미완료" 734 | } 735 | } 736 | } 737 | }, 738 | "Keep walking. You've almost walked a full soccer field today so far!" : { 739 | "comment" : "Comment in the header of Steps View", 740 | "extractionState" : "manual", 741 | "localizations" : { 742 | "bn" : { 743 | "stringUnit" : { 744 | "state" : "translated", 745 | "value" : "হাটতে থাকুন। আপনি আজ এ পর্যন্ত প্রায় পুরো ফুটবল মাঠে হেঁটেছেন!" 746 | } 747 | }, 748 | "es-419" : { 749 | "stringUnit" : { 750 | "state" : "translated", 751 | "value" : "Sigue caminando. Casi has caminado una cancha de fútbol entera!" 752 | } 753 | }, 754 | "fr" : { 755 | "stringUnit" : { 756 | "state" : "translated", 757 | "value" : "Continuer à marcher. Vous avez presque parcouru un terrain de football complet aujourd’hui jusqu’à présent !" 758 | } 759 | }, 760 | "ko" : { 761 | "stringUnit" : { 762 | "state" : "translated", 763 | "value" : "계속 걷으세요. 오늘 당신은 지금까지 축구장 하나를 거의 다 걸었어요!" 764 | } 765 | } 766 | } 767 | }, 768 | "Locked" : { 769 | "comment" : "label title for locked", 770 | "localizations" : { 771 | "bn" : { 772 | "stringUnit" : { 773 | "state" : "translated", 774 | "value" : "লক্‌ড" 775 | } 776 | }, 777 | "es-419" : { 778 | "stringUnit" : { 779 | "state" : "translated", 780 | "value" : "Bloqueado" 781 | } 782 | }, 783 | "fr" : { 784 | "stringUnit" : { 785 | "state" : "translated", 786 | "value" : "Verrouillé" 787 | } 788 | }, 789 | "ko" : { 790 | "stringUnit" : { 791 | "state" : "translated", 792 | "value" : "잠김" 793 | } 794 | } 795 | } 796 | }, 797 | "Month" : { 798 | "comment" : "Label title for Month", 799 | "extractionState" : "manual", 800 | "localizations" : { 801 | "bn" : { 802 | "stringUnit" : { 803 | "state" : "translated", 804 | "value" : "মাস" 805 | } 806 | }, 807 | "es-419" : { 808 | "stringUnit" : { 809 | "state" : "translated", 810 | "value" : "Mes" 811 | } 812 | }, 813 | "fr" : { 814 | "stringUnit" : { 815 | "state" : "translated", 816 | "value" : "Mois" 817 | } 818 | }, 819 | "ko" : { 820 | "stringUnit" : { 821 | "state" : "translated", 822 | "value" : "월" 823 | } 824 | } 825 | } 826 | }, 827 | "Name..." : { 828 | 829 | }, 830 | "New goal..." : { 831 | "comment" : "New goal... label for text field in add new goal view.", 832 | "localizations" : { 833 | "bn" : { 834 | "stringUnit" : { 835 | "state" : "translated", 836 | "value" : "নতুন গোল..." 837 | } 838 | }, 839 | "es-419" : { 840 | "stringUnit" : { 841 | "state" : "translated", 842 | "value" : "Nueva meta..." 843 | } 844 | }, 845 | "fr" : { 846 | "stringUnit" : { 847 | "state" : "translated", 848 | "value" : "Nouvel objectif..." 849 | } 850 | }, 851 | "ko" : { 852 | "stringUnit" : { 853 | "state" : "translated", 854 | "value" : "새로운 목표..." 855 | } 856 | } 857 | } 858 | }, 859 | "Notification settings" : { 860 | "comment" : "Notifications settings label", 861 | "localizations" : { 862 | "bn" : { 863 | "stringUnit" : { 864 | "state" : "translated", 865 | "value" : "নোটিফিকেশন সেটিংস" 866 | } 867 | }, 868 | "es-419" : { 869 | "stringUnit" : { 870 | "state" : "translated", 871 | "value" : "Ajuste de notificaciones" 872 | } 873 | }, 874 | "fr" : { 875 | "stringUnit" : { 876 | "state" : "translated", 877 | "value" : "Paramètres de notification" 878 | } 879 | }, 880 | "ko" : { 881 | "stringUnit" : { 882 | "state" : "translated", 883 | "value" : "알림 설정" 884 | } 885 | } 886 | } 887 | }, 888 | "Notifications" : { 889 | "comment" : "Notifications label", 890 | "localizations" : { 891 | "bn" : { 892 | "stringUnit" : { 893 | "state" : "translated", 894 | "value" : "নটিফিকেশন্স" 895 | } 896 | }, 897 | "es-419" : { 898 | "stringUnit" : { 899 | "state" : "translated", 900 | "value" : "Noticicaciones" 901 | } 902 | }, 903 | "fr" : { 904 | "stringUnit" : { 905 | "state" : "translated", 906 | "value" : "Notifications" 907 | } 908 | }, 909 | "ko" : { 910 | "stringUnit" : { 911 | "state" : "translated", 912 | "value" : "알림" 913 | } 914 | } 915 | } 916 | }, 917 | "Ok" : { 918 | 919 | }, 920 | "Oops!" : { 921 | 922 | }, 923 | "Please choose another image." : { 924 | "localizations" : { 925 | "bn" : { 926 | "stringUnit" : { 927 | "state" : "translated", 928 | "value" : "অনুগ্রহ করে অন্য একটি ছবি বেছে নিন।" 929 | } 930 | }, 931 | "es-419" : { 932 | "stringUnit" : { 933 | "state" : "translated", 934 | "value" : "Por favor elige otra imagen. " 935 | } 936 | }, 937 | "fr" : { 938 | "stringUnit" : { 939 | "state" : "translated", 940 | "value" : "Veuillez choisir une autre image." 941 | } 942 | }, 943 | "ko" : { 944 | "stringUnit" : { 945 | "state" : "translated", 946 | "value" : "다른 이미지를 선택해주세요." 947 | } 948 | } 949 | } 950 | }, 951 | "Privacy Policy" : { 952 | "comment" : "privacy policy label", 953 | "localizations" : { 954 | "bn" : { 955 | "stringUnit" : { 956 | "state" : "translated", 957 | "value" : "গোপনীয়তা নীতি" 958 | } 959 | }, 960 | "es-419" : { 961 | "stringUnit" : { 962 | "state" : "translated", 963 | "value" : "Política de Privacidad" 964 | } 965 | }, 966 | "fr" : { 967 | "stringUnit" : { 968 | "state" : "translated", 969 | "value" : "Politique de confidentialité" 970 | } 971 | }, 972 | "ko" : { 973 | "stringUnit" : { 974 | "state" : "translated", 975 | "value" : "개인 정보 정책" 976 | } 977 | } 978 | } 979 | }, 980 | "Reset background image" : { 981 | "localizations" : { 982 | "bn" : { 983 | "stringUnit" : { 984 | "state" : "translated", 985 | "value" : "ব্যাকগ্রাউন্ড ইমেজ রিসেট করুন" 986 | } 987 | }, 988 | "es-419" : { 989 | "stringUnit" : { 990 | "state" : "translated", 991 | "value" : "Revertir cambios en el fondo de pantalla de la pagina de inicio" 992 | } 993 | }, 994 | "fr" : { 995 | "stringUnit" : { 996 | "state" : "translated", 997 | "value" : "Réinitialiser l’image d’arrière-plan" 998 | } 999 | }, 1000 | "ko" : { 1001 | "stringUnit" : { 1002 | "state" : "translated", 1003 | "value" : "배경 이미지 재설정" 1004 | } 1005 | } 1006 | } 1007 | }, 1008 | "Save" : { 1009 | "comment" : "Save label inside add new goal view.", 1010 | "localizations" : { 1011 | "bn" : { 1012 | "stringUnit" : { 1013 | "state" : "translated", 1014 | "value" : "সেভ" 1015 | } 1016 | }, 1017 | "es-419" : { 1018 | "stringUnit" : { 1019 | "state" : "translated", 1020 | "value" : "Guardar" 1021 | } 1022 | }, 1023 | "fr" : { 1024 | "stringUnit" : { 1025 | "state" : "translated", 1026 | "value" : "Sauvegarder" 1027 | } 1028 | }, 1029 | "ko" : { 1030 | "stringUnit" : { 1031 | "state" : "translated", 1032 | "value" : "저장" 1033 | } 1034 | } 1035 | } 1036 | }, 1037 | "Set a New Daily Steps Goal" : { 1038 | "comment" : "Title for set a new daily steps goal", 1039 | "extractionState" : "stale", 1040 | "localizations" : { 1041 | "bn" : { 1042 | "stringUnit" : { 1043 | "state" : "translated", 1044 | "value" : "একটি নতুন ডেইলি স্টেপ গোল সেট করুন" 1045 | } 1046 | }, 1047 | "es-419" : { 1048 | "stringUnit" : { 1049 | "state" : "translated", 1050 | "value" : "Establecer una Nueva Meta Diaria de Pasos" 1051 | } 1052 | }, 1053 | "fr" : { 1054 | "stringUnit" : { 1055 | "state" : "translated", 1056 | "value" : "Définir un nouvel objectif de pas quotidiens" 1057 | } 1058 | }, 1059 | "ko" : { 1060 | "stringUnit" : { 1061 | "state" : "translated", 1062 | "value" : "새로운 일일 걸음 수 목표 설정" 1063 | } 1064 | } 1065 | } 1066 | }, 1067 | "Set background image" : { 1068 | "localizations" : { 1069 | "bn" : { 1070 | "stringUnit" : { 1071 | "state" : "translated", 1072 | "value" : "ব্যাকগ্রাউন্ড ইমেজ সেট করুন" 1073 | } 1074 | }, 1075 | "es-419" : { 1076 | "stringUnit" : { 1077 | "state" : "translated", 1078 | "value" : "Establecer fondo de pantalla" 1079 | } 1080 | }, 1081 | "fr" : { 1082 | "stringUnit" : { 1083 | "state" : "translated", 1084 | "value" : "Définir l’image d’arrière-plan" 1085 | } 1086 | }, 1087 | "ko" : { 1088 | "stringUnit" : { 1089 | "state" : "translated", 1090 | "value" : "배경 이미지 설정" 1091 | } 1092 | } 1093 | } 1094 | }, 1095 | "Settings" : { 1096 | "comment" : "Settings tab title\n Settings title screen", 1097 | "localizations" : { 1098 | "bn" : { 1099 | "stringUnit" : { 1100 | "state" : "translated", 1101 | "value" : "সেটিংস" 1102 | } 1103 | }, 1104 | "es-419" : { 1105 | "stringUnit" : { 1106 | "state" : "translated", 1107 | "value" : "Ajustes" 1108 | } 1109 | }, 1110 | "fr" : { 1111 | "stringUnit" : { 1112 | "state" : "translated", 1113 | "value" : "Paramètres" 1114 | } 1115 | }, 1116 | "ko" : { 1117 | "stringUnit" : { 1118 | "state" : "translated", 1119 | "value" : "설정" 1120 | } 1121 | } 1122 | } 1123 | }, 1124 | "Steps" : { 1125 | "comment" : "Steps tab title", 1126 | "localizations" : { 1127 | "bn" : { 1128 | "stringUnit" : { 1129 | "state" : "translated", 1130 | "value" : "স্টেপস" 1131 | } 1132 | }, 1133 | "es-419" : { 1134 | "stringUnit" : { 1135 | "state" : "translated", 1136 | "value" : "Pasos" 1137 | } 1138 | }, 1139 | "fr" : { 1140 | "stringUnit" : { 1141 | "state" : "translated", 1142 | "value" : "Escalier" 1143 | } 1144 | }, 1145 | "ko" : { 1146 | "stringUnit" : { 1147 | "state" : "translated", 1148 | "value" : "걸음 수" 1149 | } 1150 | } 1151 | } 1152 | }, 1153 | "Terms of Service" : { 1154 | 1155 | }, 1156 | "Terms of Use" : { 1157 | "comment" : "Terms of use label", 1158 | "localizations" : { 1159 | "bn" : { 1160 | "stringUnit" : { 1161 | "state" : "translated", 1162 | "value" : "ব্যবহারের শর্তাবলী" 1163 | } 1164 | }, 1165 | "es-419" : { 1166 | "stringUnit" : { 1167 | "state" : "translated", 1168 | "value" : "Términos de Uso" 1169 | } 1170 | }, 1171 | "fr" : { 1172 | "stringUnit" : { 1173 | "state" : "translated", 1174 | "value" : "Conditions d’utilisation" 1175 | } 1176 | }, 1177 | "ko" : { 1178 | "stringUnit" : { 1179 | "state" : "translated", 1180 | "value" : "이용 약관" 1181 | } 1182 | } 1183 | } 1184 | }, 1185 | "Time to add some more goals!" : { 1186 | "comment" : "🥳 Time to add some more goals label inside Goals view.", 1187 | "localizations" : { 1188 | "bn" : { 1189 | "stringUnit" : { 1190 | "state" : "translated", 1191 | "value" : "আরও কিছু গোল অ্যাড করার সময়!" 1192 | } 1193 | }, 1194 | "es-419" : { 1195 | "stringUnit" : { 1196 | "state" : "translated", 1197 | "value" : "Tiempo de añadir nuevas metas!\n" 1198 | } 1199 | }, 1200 | "fr" : { 1201 | "stringUnit" : { 1202 | "state" : "translated", 1203 | "value" : "Il est temps d’ajouter d’autres buts !" 1204 | } 1205 | }, 1206 | "ko" : { 1207 | "stringUnit" : { 1208 | "state" : "translated", 1209 | "value" : "더 많은 목표를 추가할 시간이에요!" 1210 | } 1211 | } 1212 | } 1213 | }, 1214 | "Walk a 5K, Walk with a friend, Increase steps goal" : { 1215 | "comment" : "label message when creating a new goal.", 1216 | "localizations" : { 1217 | "bn" : { 1218 | "stringUnit" : { 1219 | "state" : "translated", 1220 | "value" : "৫ কিমি হাঁটুন, বন্ধুর সাথে হাঁটুন, স্টেপ গোল বাড়ান" 1221 | } 1222 | }, 1223 | "es-419" : { 1224 | "stringUnit" : { 1225 | "state" : "translated", 1226 | "value" : "Caminar un 5k, Aumentar mis pasos..." 1227 | } 1228 | }, 1229 | "fr" : { 1230 | "stringUnit" : { 1231 | "state" : "translated", 1232 | "value" : "Marcher un 5 km, marcher avec un ami, augmenter l’objectif de pas" 1233 | } 1234 | }, 1235 | "ko" : { 1236 | "stringUnit" : { 1237 | "state" : "translated", 1238 | "value" : "5K 걷기, 친구와 함께 걷기, 걸음 수 목표 증가" 1239 | } 1240 | } 1241 | } 1242 | }, 1243 | "We were unable to open your mail application. Please make sure you have one installed." : { 1244 | 1245 | }, 1246 | "Week" : { 1247 | "comment" : "label title for week", 1248 | "extractionState" : "manual", 1249 | "localizations" : { 1250 | "bn" : { 1251 | "stringUnit" : { 1252 | "state" : "translated", 1253 | "value" : "সপ্তাহ" 1254 | } 1255 | }, 1256 | "es-419" : { 1257 | "stringUnit" : { 1258 | "state" : "translated", 1259 | "value" : "Semana" 1260 | } 1261 | }, 1262 | "fr" : { 1263 | "stringUnit" : { 1264 | "state" : "translated", 1265 | "value" : "Semaine" 1266 | } 1267 | }, 1268 | "ko" : { 1269 | "stringUnit" : { 1270 | "state" : "translated", 1271 | "value" : "주" 1272 | } 1273 | } 1274 | } 1275 | }, 1276 | "Weekly Awards" : { 1277 | "comment" : "Awards navigation title", 1278 | "extractionState" : "manual", 1279 | "localizations" : { 1280 | "bn" : { 1281 | "stringUnit" : { 1282 | "state" : "translated", 1283 | "value" : "সাপ্তাহিক অ্যাওয়ার্ডস" 1284 | } 1285 | }, 1286 | "es-419" : { 1287 | "stringUnit" : { 1288 | "state" : "translated", 1289 | "value" : "Premios Semanales" 1290 | } 1291 | }, 1292 | "fr" : { 1293 | "stringUnit" : { 1294 | "state" : "translated", 1295 | "value" : "Récompenses hebdomadaires" 1296 | } 1297 | }, 1298 | "ko" : { 1299 | "stringUnit" : { 1300 | "state" : "translated", 1301 | "value" : "주간 수상" 1302 | } 1303 | } 1304 | } 1305 | }, 1306 | "Weekly Steps" : { 1307 | "comment" : "Title for weekly steps", 1308 | "localizations" : { 1309 | "bn" : { 1310 | "stringUnit" : { 1311 | "state" : "translated", 1312 | "value" : "সাপ্তাহিক স্টেপস" 1313 | } 1314 | }, 1315 | "es-419" : { 1316 | "stringUnit" : { 1317 | "state" : "translated", 1318 | "value" : "Pasos Semanales" 1319 | } 1320 | }, 1321 | "fr" : { 1322 | "stringUnit" : { 1323 | "state" : "translated", 1324 | "value" : "Étapes hebdomadaires" 1325 | } 1326 | }, 1327 | "ko" : { 1328 | "stringUnit" : { 1329 | "state" : "translated", 1330 | "value" : "주간 걸음 수" 1331 | } 1332 | } 1333 | } 1334 | }, 1335 | "You haven't unlocked this award yet this week. Keep getting those steps in and completing goals to unlock it. You can do it!" : { 1336 | "comment" : "message of weekly award blocked.", 1337 | "localizations" : { 1338 | "bn" : { 1339 | "stringUnit" : { 1340 | "state" : "translated", 1341 | "value" : "আপনি এই সপ্তাহে এখনও এই অ্যাওয়ার্ড আনলক করেননি। স্টেপস দিয়ে গোল কমপ্লিট করে অ্যাওয়ার্ড আনলক করে নিন৷ আপনি এটা করতে পারবেন!" 1342 | } 1343 | }, 1344 | "es-419" : { 1345 | "stringUnit" : { 1346 | "state" : "translated", 1347 | "value" : "Aún no has desbloqueado este premio esta semana. Continúe realizando esos pasos y completando objetivos para desbloquearlo. ¡Puedes hacerlo!" 1348 | } 1349 | }, 1350 | "fr" : { 1351 | "stringUnit" : { 1352 | "state" : "translated", 1353 | "value" : "Vous n’avez pas encore débloqué ce prix cette semaine. Continuez à suivre ces étapes et à remplir des objectifs pour le débloquer. Tu peux le faire!" 1354 | } 1355 | }, 1356 | "ko" : { 1357 | "stringUnit" : { 1358 | "state" : "translated", 1359 | "value" : "이번 주에 아직 이 상을 해제하지 않았어요. 계속해서 걸음 수를 늘리고 목표를 완료하여 해제하세요. 할 수 있어요!" 1360 | } 1361 | } 1362 | } 1363 | } 1364 | }, 1365 | "version" : "1.0" 1366 | } -------------------------------------------------------------------------------- /Steps/Utilities/Extensions/UIImage+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Ext.swift 3 | // Steps 4 | // 5 | // Created by Raphael on 05.10.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension UIImage { 11 | func png(isOpaque: Bool = true) -> Data? { flattened(isOpaque: isOpaque).pngData() } 12 | func flattened(isOpaque: Bool = true) -> UIImage { 13 | if imageOrientation == .up { return self } 14 | let format = imageRendererFormat 15 | format.opaque = isOpaque 16 | return UIGraphicsImageRenderer(size: size, format: format).image { _ in draw(at: .zero) } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Steps/Utilities/ImageStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageStorage.swift 3 | // Steps 4 | // 5 | // Created by Raphael on 05.10.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | func saveImage(image: UIImage, key: String) { 11 | let path = FileManager.default.documentsDirectory() 12 | .appendingPathComponent(key) 13 | .appendingPathExtension("png") 14 | if let pngData = image.png() { 15 | try? pngData.write(to: path) 16 | } 17 | } 18 | 19 | func loadImage(key: String) -> UIImage? { 20 | let path = FileManager.default.documentsDirectory() 21 | .appendingPathComponent(key) 22 | .appendingPathExtension("png") 23 | return UIImage(contentsOfFile: path.path()) 24 | } 25 | 26 | func deleteImage(key: String) { 27 | let path = FileManager.default.documentsDirectory() 28 | .appendingPathComponent(key) 29 | .appendingPathExtension("png") 30 | try? FileManager.default.removeItem(at: path) 31 | } 32 | -------------------------------------------------------------------------------- /Steps/ViewModel/AddGoalViewModel.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // AddGoalViewModel.swift 4 | // Steps 5 | // 6 | // Created by Daniel Lyons on 10/6/23. 7 | // 8 | 9 | import SwiftUI 10 | import Combine 11 | import CoreData 12 | 13 | final class AddGoalViewModel: ObservableObject { 14 | let context: NSManagedObjectContext 15 | @Published var name = "" 16 | @Published var isComplete = false 17 | @Published var date = Date() 18 | 19 | init( 20 | context: NSManagedObjectContext = PersistenceController.shared.container.viewContext, 21 | name: String = "", 22 | isComplete: Bool = false, 23 | date: Date = Date() 24 | ) { 25 | self.context = context 26 | self.name = name 27 | self.isComplete = isComplete 28 | self.date = date 29 | } 30 | 31 | func addGoal() { 32 | let newGoal = Goal(context: self.context) 33 | newGoal.name = self.name 34 | newGoal.isComplete = self.isComplete 35 | newGoal.date = self.date 36 | newGoal.id = UUID() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Steps/ViewModel/ContributorsViewModel.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // ContributorsViewModel.swift 4 | // Steps 5 | // 6 | // Created by Drag0ndust on 05.10.23. 7 | // 8 | 9 | import Foundation 10 | import Dependencies 11 | 12 | class ContributorsViewModel: ObservableObject { 13 | @Published var contributors: [Contributor] = [] 14 | 15 | 16 | @Dependency(\.contributors) var contributorsClient 17 | 18 | func fetchContributors() async { 19 | self.contributors = await self.contributorsClient.fetchContributors() 20 | 21 | } 22 | 23 | 24 | // func fetchContributors() async { 25 | // guard let url = URL(string: "https://api.github.com/repos/brittanyarima/Steps/contributors") else { return } 26 | // 27 | // do { 28 | // let (data, _) = try await URLSession.shared.data(from: url) 29 | // let contributors = try JSONDecoder().decode([Contributor].self, from: data) 30 | // 31 | // await MainActor.run { 32 | // self.contributors = contributors 33 | // } 34 | // } catch { 35 | // print("❗️ Error laoding contributors from Github API: \(error.localizedDescription)") 36 | // } 37 | // } 38 | } 39 | -------------------------------------------------------------------------------- /Steps/ViewModel/GoalViewModel.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // GoalViewModel.swift 4 | // Steps 5 | // 6 | // Created by Daniel Lyons on 10/6/23. 7 | // 8 | 9 | import Combine 10 | import CoreData 11 | 12 | final class GoalViewModel: ObservableObject { 13 | let context: NSManagedObjectContext 14 | @Published var selectedTab = Constants.incomplete 15 | @Published var isShowingSheet = false 16 | @Published var isShowingPaywall = true 17 | 18 | init( 19 | context: NSManagedObjectContext = PersistenceController.shared.container.viewContext, 20 | selectedTab: String = Constants.incomplete, 21 | isShowingSheet: Bool = false, 22 | isShowingPaywall: Bool = true 23 | ) { 24 | self.context = context 25 | self.isShowingSheet = isShowingSheet 26 | self.selectedTab = selectedTab 27 | self.isShowingPaywall = isShowingPaywall 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Steps/ViewModel/ProfileViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewModel.swift 3 | // Steps 4 | // 5 | // Created by Yashraj jadhav on 16/10/23. 6 | // 7 | 8 | import SwiftUI 9 | class ProfileViewModel : ObservableObject { 10 | 11 | @Published var showAlert = false 12 | @Published var isEditingName = false 13 | @Published var currentName = "" 14 | @Published var isEditingImage = false 15 | @Published var profileName : String? = UserDefaults.standard.string(forKey: "profileName") 16 | @Published var isSelectedImage : String? = UserDefaults.standard.string(forKey: "profileImage") 17 | @Published var profileImage : String? = UserDefaults.standard.string(forKey: "profileImage") 18 | 19 | var images = ["avatar 1", "avatar 2", "avatar 3", "avatar 4", "avatar 5", "avatar 6", "avatar 7", "avatar 8", "avatar 9", "avatar 10"] 20 | 21 | func presentEditName() { 22 | isEditingName = true 23 | isEditingImage = false 24 | } 25 | 26 | func presentEditImage() { 27 | isEditingName = false 28 | isEditingImage = true 29 | } 30 | 31 | func dismissEdit() { 32 | isEditingName = false 33 | isEditingImage = false 34 | } 35 | 36 | func setNewName() { 37 | profileName = currentName 38 | UserDefaults.standard.set(currentName, forKey: "profileName") 39 | self.dismissEdit() 40 | } 41 | 42 | func didSelectNewImage(name: String){ 43 | isSelectedImage = name 44 | } 45 | 46 | func setNewImage() { 47 | profileImage = isSelectedImage 48 | UserDefaults.standard.set(isSelectedImage, forKey: "profileImage") 49 | self.dismissEdit() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Steps/ViewModel/SettingsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewModel.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/20/22. 6 | // 7 | 8 | import SwiftUI 9 | import UserNotifications 10 | import DependenciesAdditions 11 | 12 | class SettingsViewModel: ObservableObject { 13 | @Published var showingEditView = false 14 | @Published var showContributors: Bool = false 15 | 16 | @AppStorage(Constants.notificationKey) var notificationsOn = false 17 | @Dependency(\.userNotificationCenter) var userNotificationCenter 18 | @Dependency(\.logger) var logger 19 | 20 | func requestNotificationAuth() async { 21 | do { 22 | let isAuthorized = try await userNotificationCenter.requestAuthorization( 23 | options: [.alert, .badge, .sound, .provisional] 24 | ) 25 | if isAuthorized { 26 | await self.scheduleDailyNotification() 27 | } else { 28 | self.userNotificationCenter.removeAllPendingNotificationRequests() 29 | } 30 | } catch { 31 | logger.error("Error requesting authorization from UserNotificationCenter: \(error)") 32 | } 33 | } 34 | 35 | func scheduleDailyNotification() async { 36 | let content = UNMutableNotificationContent() 37 | content.title = Constants.goodMorningTitle 38 | content.body = Constants.reachStepGoalDescription 39 | 40 | var dateComponents = DateComponents() 41 | dateComponents.hour = 9 42 | dateComponents.minute = 0 43 | 44 | let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true) 45 | let request = UNNotificationRequest(identifier: Constants.notificationsIdentifier, content: content, trigger: trigger) 46 | 47 | do { 48 | try await self.userNotificationCenter.add(request) 49 | } catch { 50 | logger.error("Error scheduling notifications: \(error.localizedDescription)") 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Steps/ViewModel/StepsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StepsViewModel.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/14/22. 6 | // 7 | 8 | import SwiftUI 9 | import HealthKit 10 | 11 | import DependenciesAdditions 12 | import Dependencies 13 | import _AppStorageDependency 14 | import PhotosUI 15 | import CoreTransferable 16 | 17 | 18 | class StepsViewModel: ObservableObject { 19 | var healthStore: HKHealthStore? 20 | var query: HKStatisticsCollectionQuery? 21 | @AppStorage(Constants.stepCountKey, store: UserDefaults(suiteName: Constants.appGroupID)) var stepCount: Int = 0 22 | 23 | // 👇🏼 Still experimental 24 | // @Dependency.AppStorage( 25 | // Constants.stepCountKey, 26 | // store: .init(suitename: Constants.appGroupID) 27 | // ) var stepCount: Int = 0 28 | 29 | 30 | 31 | @Dependency(\.userDefaults) var userDefaults 32 | 33 | @Published var weekSteps: [Step] = [] // Data for the week chart 34 | @Published var monthSteps: [Step] = [] // Data for the month chart 35 | @Published var calories: [Calorie] = [] 36 | @Published var totalDistance: [Distance] = [] 37 | 38 | @Published var steps: [Step] = [] 39 | @AppStorage(Constants.goalKey, store: .appGroup) var goal: Int = 10_000 40 | 41 | init() { 42 | if HKHealthStore.isHealthDataAvailable() { 43 | healthStore = HKHealthStore() 44 | } 45 | 46 | self.backgroundImage = loadImage(key: Constants.backgroundImageKey) 47 | } 48 | 49 | var currentSteps: Int { 50 | steps.last?.count ?? 0 51 | } 52 | 53 | var currentCalories: Int { 54 | calories.last?.value ?? 0 55 | } 56 | 57 | var currentDistance: Int { 58 | totalDistance.last?.value ?? 0 59 | } 60 | 61 | var soccerFieldsWalkedString: String { 62 | let numOfFields = currentSteps / 144 // For every 144 Steps you've walked about 1 soccer field. 63 | 64 | if numOfFields > 1 { 65 | return String(format: Constants.walkedCustomSoccerFieldToday, numOfFields) 66 | } else if numOfFields == 0 { 67 | return Constants.walkedFullSoccerFieldToday 68 | } else { 69 | return Constants.walkedOneSoccerFieldToday 70 | } 71 | } 72 | 73 | var checkPointOneReached: Bool { 74 | Double(currentSteps) >= (Double(goal) * 0.25) 75 | } 76 | 77 | var checkPointTwoReached: Bool { 78 | Double(currentSteps) >= (Double(goal) * 0.5) 79 | } 80 | 81 | var checkPointThreeReached: Bool { 82 | Double(currentSteps) >= (Double(goal) * 0.75) 83 | } 84 | 85 | var checkPointFourReached: Bool { 86 | currentSteps >= goal 87 | } 88 | 89 | func calculateSteps(completion: @escaping (HKStatisticsCollection?) -> Void) { 90 | let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)! 91 | let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date()) 92 | let anchorDate = Date.sundayAt12AM() 93 | let daily = DateComponents(day: 1) 94 | let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate) 95 | 96 | query = HKStatisticsCollectionQuery(quantityType: stepType, 97 | quantitySamplePredicate: predicate, 98 | options: .cumulativeSum, 99 | anchorDate: anchorDate, 100 | intervalComponents: daily) 101 | query!.initialResultsHandler = { query, statsCollection, error in 102 | completion(statsCollection) 103 | } 104 | 105 | if let healthStore = healthStore, let query = self.query { 106 | healthStore.execute(query) 107 | } 108 | } 109 | 110 | func calculateLastWeeksSteps(completion: @escaping (HKStatisticsCollection?) -> Void) { 111 | let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)! 112 | let startDate = Calendar.current.date(byAdding: .weekOfYear, value: -6, to: Date()) 113 | let anchorDate = Date.sundayAt12AM() 114 | let week = DateComponents(day: 1) 115 | 116 | let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate) 117 | 118 | query = HKStatisticsCollectionQuery(quantityType: stepType, 119 | quantitySamplePredicate: predicate, 120 | options: .cumulativeSum, 121 | anchorDate: anchorDate, 122 | intervalComponents: week) 123 | query!.initialResultsHandler = { query, statsCollection, error in 124 | completion(statsCollection) 125 | } 126 | 127 | if let healthStore = healthStore, let query = self.query { 128 | healthStore.execute(query) 129 | } 130 | } 131 | 132 | func calculateMonthSteps(completion: @escaping (HKStatisticsCollection?) -> Void) { 133 | let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)! 134 | let startDate = Calendar.current.date(byAdding: .day, value: -30, to: Date()) 135 | let anchorDate = Date.sundayAt12AM() 136 | let month = DateComponents(day: 1) 137 | 138 | let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate) 139 | 140 | query = HKStatisticsCollectionQuery(quantityType: stepType, 141 | quantitySamplePredicate: predicate, 142 | options: .cumulativeSum, 143 | anchorDate: anchorDate, 144 | intervalComponents: month) 145 | query!.initialResultsHandler = { query, statsCollection, error in 146 | completion(statsCollection) 147 | } 148 | 149 | if let healthStore = healthStore, let query = self.query { 150 | healthStore.execute(query) 151 | } 152 | } 153 | 154 | func calculateCalories(completion: @escaping (HKStatisticsCollection?) -> Void) { 155 | let calories = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.activeEnergyBurned)! 156 | let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date()) 157 | let anchorDate = Date.sundayAt12AM() 158 | let daily = DateComponents(day: 1) 159 | let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate) 160 | 161 | query = HKStatisticsCollectionQuery(quantityType: calories, 162 | quantitySamplePredicate: predicate, 163 | options: .cumulativeSum, 164 | anchorDate: anchorDate, 165 | intervalComponents: daily) 166 | query!.initialResultsHandler = { query, statsCollection, error in 167 | completion(statsCollection) 168 | } 169 | if let healthStore = healthStore, let query = self.query { 170 | healthStore.execute(query) 171 | } 172 | } 173 | 174 | func calculateDistance(completion: @escaping (HKStatisticsCollection?) -> Void) { 175 | 176 | let distance = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.distanceWalkingRunning)! 177 | let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date()) 178 | let anchorDate = Date.sundayAt12AM() 179 | let daily = DateComponents(day: 1) 180 | let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate) 181 | 182 | query = HKStatisticsCollectionQuery(quantityType: distance, 183 | quantitySamplePredicate: predicate, 184 | options: .cumulativeSum, 185 | anchorDate: anchorDate, 186 | intervalComponents: daily) 187 | 188 | query!.initialResultsHandler = { query, statsCollection, error in 189 | completion(statsCollection) 190 | } 191 | 192 | if let healthStore = healthStore, let query = self.query { 193 | healthStore.execute(query) 194 | } 195 | } 196 | 197 | func updateUIFromStats(_ statsCollection: HKStatisticsCollection) { 198 | let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())! 199 | let endDate = Date() 200 | 201 | // Clear the array before enumerating 202 | DispatchQueue.main.async { 203 | self.steps.removeAll() 204 | } 205 | 206 | statsCollection.enumerateStatistics(from: startDate, to: endDate) { stats, stop in 207 | let count = stats.sumQuantity()?.doubleValue(for: .count()) 208 | let step = Step(count: Int(count ?? 0), date: stats.startDate) 209 | 210 | DispatchQueue.main.async { 211 | self.steps.append(step) 212 | self.stepCount = self.steps.last?.count ?? 0 213 | } 214 | } 215 | } 216 | 217 | func updateWeekUIFromStats(_ statsCollection: HKStatisticsCollection) { 218 | let startDate = Calendar.current.date(byAdding: .day, value: -6, to: Date())! 219 | let endDate = Date() 220 | statsCollection.enumerateStatistics(from: startDate, to: endDate) { stats, stop in 221 | let count = stats.sumQuantity()?.doubleValue(for: .count()) 222 | let step = Step(count: Int(count ?? 0), date: stats.startDate) 223 | 224 | DispatchQueue.main.async { 225 | self.weekSteps.append(step) 226 | self.stepCount = self.weekSteps.last?.count ?? 0 227 | } 228 | } 229 | } 230 | 231 | func updateMonthUIFromStats(_ statsCollection: HKStatisticsCollection) { 232 | let startDate = Calendar.current.date(byAdding: .day, value: -30, to: Date())! 233 | let endDate = Date() 234 | statsCollection.enumerateStatistics(from: startDate, to: endDate) { stats, stop in 235 | let count = stats.sumQuantity()?.doubleValue(for: .count()) 236 | let step = Step(count: Int(count ?? 0), date: stats.endDate) 237 | 238 | DispatchQueue.main.async { 239 | self.monthSteps.append(step) 240 | self.stepCount = self.monthSteps.last?.count ?? 0 241 | } 242 | } 243 | } 244 | 245 | func updateCalorieUIFromStats(_ statsCollection: HKStatisticsCollection) { 246 | let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())! 247 | let endDate = Date() 248 | 249 | statsCollection.enumerateStatistics(from: startDate, to: endDate) { stats, stop in 250 | let caloriesBurned = stats.sumQuantity()?.doubleValue(for: .largeCalorie()) 251 | let calorie = Calorie(value: Int(caloriesBurned ?? 0), date: stats.startDate) 252 | 253 | DispatchQueue.main.async { 254 | self.calories.append(calorie) 255 | } 256 | } 257 | } 258 | 259 | func updateDistanceUIFromStats(_ statsCollection: HKStatisticsCollection) { 260 | let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())! 261 | let endDate = Date() 262 | 263 | statsCollection.enumerateStatistics(from: startDate, to: endDate) { stats, stop in 264 | let distanceWalked = stats.sumQuantity()?.doubleValue(for: .meter()) 265 | let distance = Distance(value: Int(distanceWalked ?? 0), date: stats.startDate) 266 | 267 | DispatchQueue.main.async { 268 | self.totalDistance.append(distance) 269 | } 270 | } 271 | } 272 | 273 | func requestAuthorization(completion: @escaping (Bool) -> Void) { 274 | let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)! 275 | let calorieType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.activeEnergyBurned)! 276 | let distanceType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.distanceWalkingRunning)! 277 | guard let healthStore = self.healthStore else { return completion(false) } 278 | 279 | healthStore.requestAuthorization(toShare: [], read: [stepType, calorieType, distanceType]) { success, error in 280 | completion(success) 281 | } 282 | } 283 | 284 | // MARK: - Background Image 285 | 286 | @Published var showBackgroundImageAlert = false 287 | 288 | enum BackgroundImageState { 289 | case empty 290 | case loading(Progress) 291 | case success(UIImage) 292 | case failure(Error) 293 | } 294 | 295 | @Published var backgroundImage: UIImage? = nil 296 | 297 | @Published private(set) var backgroundImageState: BackgroundImageState = .empty { 298 | didSet { 299 | switch backgroundImageState { 300 | case .success(let image): 301 | backgroundImage = image 302 | saveImage(image: image, key: Constants.backgroundImageKey) 303 | case .loading: 304 | return 305 | case .empty: 306 | backgroundImage = nil 307 | deleteImage(key: Constants.backgroundImageKey) 308 | case .failure: 309 | self.showBackgroundImageAlert = true 310 | } 311 | } 312 | } 313 | 314 | @Published var backgroundImageSelection: PhotosPickerItem? = nil { 315 | didSet { 316 | if let backgroundImageSelection { 317 | let progress = loadTransferable(from: backgroundImageSelection) 318 | backgroundImageState = .loading(progress) 319 | } else { 320 | backgroundImageState = .empty 321 | } 322 | } 323 | } 324 | 325 | enum TransferError: Error { 326 | case importFailed 327 | } 328 | 329 | struct BackgroundImage: Transferable { 330 | let image: UIImage 331 | 332 | static var transferRepresentation: some TransferRepresentation { 333 | DataRepresentation(importedContentType: .image) { data in 334 | guard let uiImage = UIImage(data: data) else { 335 | throw TransferError.importFailed 336 | } 337 | return BackgroundImage(image: uiImage) 338 | } 339 | } 340 | } 341 | 342 | private func loadTransferable(from backgroundImageSelection: PhotosPickerItem) -> Progress { 343 | return backgroundImageSelection.loadTransferable(type: BackgroundImage.self) { result in 344 | DispatchQueue.main.async { 345 | guard backgroundImageSelection == self.backgroundImageSelection else { 346 | print("Failed to get the selected item.") 347 | return 348 | } 349 | switch result { 350 | case .success(let backgroundImage?): 351 | self.backgroundImageState = .success(backgroundImage.image) 352 | case .success(nil): 353 | self.backgroundImageState = .empty 354 | case .failure(let error): 355 | self.backgroundImageState = .failure(error) 356 | } 357 | } 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /Steps/Views/AddGoalView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddGoalView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 1/7/23. 6 | // 7 | 8 | import SwiftUI 9 | import Dependencies 10 | 11 | struct AddGoalView: View { 12 | @Environment(\.managedObjectContext) var context 13 | @Environment(\.dismiss) var dismiss 14 | @ObservedObject var vm: AddGoalViewModel 15 | 16 | init(viewModel: AddGoalViewModel = .init()) { 17 | self.vm = viewModel 18 | } 19 | 20 | var body: some View { 21 | VStack() { 22 | VStack(spacing: 7) { 23 | Text("🖋️ \(Constants.addNewGoal)") 24 | .font(.title) 25 | .bold() 26 | .padding(.bottom) 27 | 28 | Text("💡 \(Constants.goalIdeas)") 29 | .bold() 30 | .foregroundColor(.secondary) 31 | .font(.caption) 32 | VStack(alignment: .leading, spacing: 5) { 33 | Text(Constants.walkWithFriend) 34 | } 35 | .foregroundColor(.secondary) 36 | .font(.caption) 37 | } 38 | .padding() 39 | 40 | Spacer() 41 | 42 | TextField(Constants.newGoalField, text: $vm.name) 43 | .padding(.horizontal) 44 | .frame(height: 55) 45 | .background(Color(.tertiarySystemFill)) 46 | .cornerRadius(10) 47 | .foregroundColor(.indigo) 48 | 49 | Button { 50 | vm.addGoal() 51 | dismiss() 52 | } label: { 53 | Text(Constants.save) 54 | .fontWeight(.semibold) 55 | } 56 | .buttonStyle(.bordered) 57 | .tint(.indigo) 58 | .padding() 59 | } 60 | .padding(14) 61 | } 62 | } 63 | 64 | struct AddGoalView_Previews: PreviewProvider { 65 | static var previews: some View { 66 | AddGoalView() 67 | .environment(\.managedObjectContext, PersistenceController.shared.container.viewContext) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Steps/Views/AwardBadgeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AwardBadgeView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/16/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AwardBadgeView: View { 11 | let award: Award 12 | @ObservedObject var viewModel: StepsViewModel 13 | @FetchRequest( 14 | entity: Goal.entity(), 15 | sortDescriptors: []) var goals: FetchedResults 16 | 17 | var isAwardUnlocked: Bool { 18 | switch award.name { 19 | case Constants.firstStepsName: 20 | return viewModel.steps.contains { $0.count > 100 } 21 | case Constants.goalName: 22 | return viewModel.steps.contains { $0.count >= viewModel.goal } 23 | case Constants.doubleTroubleName: 24 | return viewModel.steps.contains { $0.count >= (viewModel.goal * 2)} 25 | case Constants.threesName: 26 | return viewModel.steps.contains { $0.count >= (viewModel.goal * 3)} 27 | case Constants.perfectWeekName: 28 | if viewModel.steps.count == 0 { return false } 29 | return viewModel.steps.allSatisfy { $0.count > viewModel.goal } 30 | case Constants.messiName: 31 | return viewModel.steps.contains { $0.count >= 14400 } // about 100 soccer fields 32 | case Constants.motivatedName: 33 | return goals.count > 1 34 | case Constants.firstGoalName: 35 | return goals.count > 1 && goals.last?.isComplete == true 36 | case Constants.dreamerGoalName: 37 | return goals.count > 4 38 | case Constants.goGetterName: 39 | var completed = 0 40 | for goal in goals { 41 | if goal.isComplete { 42 | completed += 1 43 | } 44 | } 45 | return completed > 4 46 | default: 47 | return false 48 | } 49 | } 50 | 51 | var body: some View { 52 | VStack { 53 | NavigationLink { 54 | if isAwardUnlocked { 55 | AwardDetailView(award: award, viewModel: viewModel) 56 | } else { AwardLockedView() } 57 | } label: { 58 | VStack { 59 | BadgeImageView(award: award) 60 | .foregroundColor(isAwardUnlocked ? .indigo : .indigo.opacity(0.2)) 61 | } 62 | .padding() 63 | } 64 | } 65 | } 66 | } 67 | 68 | struct AwardBadgeView_Previews: PreviewProvider { 69 | static var previews: some View { 70 | AwardBadgeView(award: .perfectweek, viewModel: StepsViewModel()) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Steps/Views/BackgroundView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundView.swift 3 | // Steps 4 | // 5 | // Created by Raphael on 04.10.23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BackgroundView: View { 11 | @ObservedObject var stepsModel: StepsViewModel 12 | 13 | var body: some View { 14 | GeometryReader { geo in 15 | if let backgroundImage = stepsModel.backgroundImage { 16 | Image(uiImage: backgroundImage) 17 | .resizable() 18 | .aspectRatio(contentMode: .fill) 19 | .frame(width: geo.size.width, height: geo.size.height) 20 | .clipped() 21 | } else { 22 | Image("background") 23 | .resizable() 24 | .aspectRatio(contentMode: .fill) 25 | .frame(width: geo.size.width, height: geo.size.height) 26 | .clipped() 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Steps/Views/BadgeImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BadgeImageView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/21/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BadgeImageView: View { 11 | let award: Award 12 | 13 | var body: some View { 14 | Image(systemName: award.image) 15 | .resizable() 16 | .scaledToFit() 17 | .padding() 18 | .frame(width: 100, height: 100) 19 | .overlay { 20 | Circle() 21 | .stroke(style: StrokeStyle(lineWidth: 3)) 22 | } 23 | } 24 | } 25 | 26 | struct BadgeImageView_Previews: PreviewProvider { 27 | static var previews: some View { 28 | BadgeImageView(award: .firstStep) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Steps/Views/CircleProgressBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleProgressBar.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/9/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CircleProgressBar: View { 11 | let value: Int 12 | let maxValue: Int 13 | @State private var drawingStroke = false 14 | 15 | var body: some View { 16 | ZStack { 17 | Circle() 18 | .stroke(.indigo.opacity(0.1), lineWidth: 10) 19 | 20 | Circle() 21 | .trim(from: 0, to: drawingStroke ? CGFloat(self.value) / CGFloat(self.maxValue) : 0) 22 | .stroke(.indigo, style: StrokeStyle(lineWidth: 10, lineCap: .round)) 23 | .rotationEffect(Angle(degrees: -90.0)) 24 | .animation(.easeOut(duration: 3), value: drawingStroke) 25 | .onAppear { 26 | drawingStroke = true 27 | } 28 | 29 | VStack { 30 | Text("\(value)") 31 | .font(.system(size: 40)) 32 | 33 | 34 | Text("Steps") 35 | .foregroundColor(.secondary) 36 | } 37 | } 38 | .frame(width: 200, height: 200) 39 | .padding() 40 | } 41 | } 42 | 43 | struct CircleProgressBar_Previews: PreviewProvider { 44 | static var previews: some View { 45 | CircleProgressBar(value: 3022, maxValue: 10000) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Steps/Views/CircleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/28/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CircleView: View { 11 | let opacity: CGFloat 12 | 13 | var body: some View { 14 | ZStack { 15 | Circle() 16 | .stroke(style: StrokeStyle(lineWidth: 5)) 17 | .frame(width: 20) 18 | .foregroundColor(.indigo) 19 | 20 | Circle() 21 | .frame(width: 11) 22 | .opacity(opacity) 23 | .foregroundColor(.indigo) 24 | } 25 | } 26 | } 27 | 28 | struct CircleView_Previews: PreviewProvider { 29 | static var previews: some View { 30 | CircleView(opacity: 0.2) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Steps/Views/ContributorsClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContributorsClient.swift 3 | // Steps 4 | // 5 | // Created by Daniel Lyons on 10/6/23. 6 | // 7 | 8 | import Foundation 9 | import Dependencies 10 | 11 | struct ContributorsClient { 12 | 13 | /// A closure that asynchronously fetches a Contributors 14 | var fetchContributors: () async -> [Contributor] 15 | } 16 | 17 | // MARK: Live Dependency Key 18 | extension ContributorsClient: DependencyKey { 19 | 20 | /// A live value of `ContributorsClient` for 21 | static let liveValue = ContributorsClient( 22 | fetchContributors: { 23 | guard let url = URL(string: "https://api.github.com/repos/brittanyarima/Steps/contributors") else { return [] } 24 | 25 | do { 26 | let (data, _) = try await URLSession.shared.data(from: url) 27 | let contributors = try JSONDecoder().decode([Contributor].self, from: data) 28 | 29 | return contributors 30 | } catch { 31 | print("❗️ Error laoding contributors from Github API: \(error.localizedDescription)") 32 | return [] 33 | } 34 | } 35 | ) 36 | } 37 | 38 | 39 | 40 | // MARK: Test Dependency Key 41 | extension ContributorsClient: TestDependencyKey { 42 | /// A preview instance of `ContributorsClient` which by default will be used in Xcode Previews 43 | static let previewValue = ContributorsClient.liveValue 44 | // It should be safe to use the live value in previews for now since the client can only fetch, not write. 45 | 46 | /// A test instance of `ContributorsClient` which by default will be used in XCTests 47 | static let testValue: Self = .init( 48 | fetchContributors: { 49 | return [Contributor].mock 50 | } 51 | ) 52 | } 53 | 54 | // MARK: Dependency Values 55 | extension DependencyValues { 56 | var contributors: ContributorsClient { 57 | get { self[ContributorsClient.self] } 58 | set { self[ContributorsClient.self] = newValue } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Steps/Views/ContributorsView.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // ContributorsView.swift 4 | // Steps 5 | // 6 | // Created by Drag0ndust on 05.10.23. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct ContributorsView: View { 12 | @Environment(\.openURL) private var openURL 13 | @Environment(\.dismiss) private var dismiss 14 | 15 | @StateObject var viewModel = ContributorsViewModel() 16 | 17 | let columns = [ 18 | GridItem(.adaptive(minimum: 100)) 19 | ] 20 | 21 | var body: some View { 22 | Group { 23 | if !viewModel.contributors.isEmpty { 24 | ScrollView { 25 | LazyVGrid(columns: columns) { 26 | ForEach(viewModel.contributors) { element in 27 | VStack { 28 | AsyncImage(url: URL(string: element.avatarURL)) { image in 29 | image 30 | .resizable() 31 | .scaledToFit() 32 | } placeholder: { 33 | ProgressView() 34 | } 35 | .clipShape(RoundedRectangle(cornerRadius: 20)) 36 | 37 | Text(element.login) 38 | .font(.caption) 39 | } 40 | .onTapGesture { 41 | openURL(URL(string: element.htmlURL)!) 42 | } 43 | } 44 | } 45 | .padding() 46 | } 47 | } else { 48 | ProgressView() 49 | } 50 | } 51 | .navigationTitle("Contributors") 52 | .toolbar { 53 | ToolbarItem(placement: .topBarTrailing) { 54 | Image(systemName: "xmark") 55 | .onTapGesture { 56 | dismiss() 57 | } 58 | } 59 | } 60 | .task { 61 | await viewModel.fetchContributors() 62 | } 63 | } 64 | } 65 | 66 | #Preview { 67 | NavigationStack { 68 | ContributorsView() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Steps/Views/CurrentStepsCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CurrentStepsCardView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/28/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CurrentStepsCardView: View { 11 | let steps: Int 12 | 13 | var body: some View { 14 | ZStack { 15 | Rectangle() 16 | .fill(.white) 17 | .cornerRadius(12) 18 | .shadow(radius: 5) 19 | 20 | VStack(alignment: .leading, spacing: 10) { 21 | HStack { 22 | Text(Constants.currentSteps) 23 | Spacer() 24 | } 25 | 26 | HStack { 27 | Text(String(format: Constants.enterSteps, steps)) 28 | .font(.system(size: 32)) 29 | .bold() 30 | 31 | Spacer() 32 | 33 | Image(systemName: "chevron.right") 34 | .font(.title2) 35 | } 36 | .foregroundColor(.indigo) 37 | 38 | Spacer() 39 | } 40 | .padding() 41 | } 42 | .frame(width: 300, height: 80) 43 | } 44 | } 45 | 46 | struct CurrentStepsCardView_Previews: PreviewProvider { 47 | static var previews: some View { 48 | CurrentStepsCardView(steps: 2000) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Steps/Views/FactView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FactView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/16/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct FactView: View { 11 | @ObservedObject var viewModel: StepsViewModel 12 | 13 | var body: some View { 14 | HStack { 15 | Image(systemName: Constants.soccerball) 16 | .font(.title) 17 | .padding(.horizontal, 2) 18 | .foregroundColor(.secondary) 19 | 20 | Text(viewModel.soccerFieldsWalkedString) 21 | .font(.footnote) 22 | .fontWeight(.light) 23 | .fixedSize(horizontal: false, vertical: true) 24 | 25 | Spacer() 26 | } 27 | .frame(width: 300) 28 | .padding() 29 | .background(Color(.tertiarySystemFill)) 30 | .cornerRadius(12) 31 | } 32 | } 33 | 34 | struct FactView_Previews: PreviewProvider { 35 | static var previews: some View { 36 | FactView(viewModel: StepsViewModel()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Steps/Views/GoalListRowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GoalListRowView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 1/7/23. 6 | // 7 | 8 | import CoreData 9 | import SwiftUI 10 | 11 | struct GoalListRowView: View { 12 | @ObservedObject var goal: Goal 13 | 14 | var body: some View { 15 | HStack { 16 | Text(goal.name ?? Constants.unknownName) 17 | Spacer() 18 | if goal.isComplete { 19 | Image(systemName: "checkmark.circle.fill") 20 | .foregroundColor(.indigo) 21 | .rotationEffect(.degrees(0)) 22 | .opacity(1) 23 | .scaleEffect(1) 24 | .animation( 25 | Animation.easeInOut(duration: 0.6) 26 | .repeatCount(1, autoreverses: false), 27 | value: goal.isComplete 28 | ) 29 | .onTapGesture { 30 | toggleGoal() 31 | } 32 | } else { 33 | Image(systemName: "circle") 34 | .foregroundColor(.indigo) 35 | .onTapGesture { 36 | toggleGoal() 37 | } 38 | } 39 | } 40 | .font(.title3) 41 | .padding() 42 | .background(.indigo.opacity(0.2)) 43 | .foregroundColor(.indigo) 44 | .fontWeight(.medium) 45 | .cornerRadius(12) 46 | .multilineTextAlignment(.leading) 47 | } 48 | 49 | private func toggleGoal() { 50 | withAnimation { 51 | goal.isComplete.toggle() 52 | if goal.hasChanges { 53 | PersistenceController.shared.save() 54 | } 55 | } 56 | } 57 | } 58 | 59 | struct GoalListRowView_Previews: PreviewProvider { 60 | static let moc = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) 61 | static var previews: some View { 62 | let goal = Goal(context: moc) 63 | GoalListRowView(goal: goal) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Steps/Views/MountainView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MountainView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/23/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MountainView: View { 11 | @ObservedObject var viewModel: StepsViewModel 12 | 13 | var body: some View { 14 | GeometryReader { geo in 15 | ZStack { 16 | 17 | VStack(spacing: 200) { 18 | CircleView(opacity: viewModel.checkPointFourReached ? 1 : 0.2) 19 | CircleView(opacity: viewModel.checkPointThreeReached ? 1 : 0.2) 20 | CircleView(opacity: viewModel.checkPointTwoReached ? 1 : 0.2) 21 | CircleView(opacity: viewModel.checkPointOneReached ? 1: 0.2) 22 | } 23 | .offset(x: geo.size.width / 2 - 40) 24 | .padding(.bottom, 100) 25 | .padding(.top, 50) 26 | 27 | BackgroundView(stepsModel: viewModel) 28 | .frame(width: geo.size.width, height: geo.size.height) 29 | .edgesIgnoringSafeArea(.all) 30 | .opacity(0.5) 31 | } 32 | } 33 | } 34 | } 35 | 36 | struct MountainView_Previews: PreviewProvider { 37 | static var previews: some View { 38 | MountainView(viewModel: StepsViewModel()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Steps/Views/NewWeekStepsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewWeekStepsView.swift 3 | // Steps 4 | // 5 | // Created by Mohd Wasif Raza on 04/10/23. 6 | // 7 | 8 | import SwiftUI 9 | import Charts 10 | 11 | struct NewWeekStepsView: View { 12 | @ObservedObject var viewModel: StepsViewModel 13 | 14 | var body: some View { 15 | VStack { 16 | HStack { 17 | Text(Constants.weeklySteps) 18 | Spacer() 19 | } 20 | .padding(.horizontal) 21 | 22 | Chart { 23 | RuleMark(y: .value(Constants.goal, viewModel.goal)) 24 | .foregroundStyle(.mint) 25 | .lineStyle(StrokeStyle(lineWidth: 1, dash: [5])) 26 | .annotation(position: .automatic, content: { 27 | Text("Goal: \(viewModel.goal)") 28 | .font(.caption) 29 | .foregroundColor(.secondary) 30 | .padding() 31 | }) 32 | 33 | ForEach(viewModel.steps) { step in 34 | BarMark( 35 | x: .value(Constants.day, step.date, unit: .weekday), 36 | y: .value(Constants.steps, step.count) 37 | ) 38 | .foregroundStyle(.indigo) 39 | .annotation(content: { 40 | Text("\(step.count)") 41 | .foregroundColor(.secondary) 42 | .font(.caption) 43 | }) 44 | } 45 | } 46 | .frame(height: 200) 47 | .padding() 48 | .chartXAxis { 49 | AxisMarks(values: viewModel.steps.map { $0.date}) { date in 50 | AxisValueLabel(format: 51 | .dateTime.month(.twoDigits).day()) 52 | .font(.caption2) 53 | } 54 | } 55 | .chartYAxis(.hidden) 56 | 57 | HStack { 58 | Image(systemName: "line.diagonal") 59 | .rotationEffect(Angle(degrees: 45)) 60 | .foregroundColor(.mint) 61 | 62 | Text(Constants.dailyGoal) 63 | .foregroundColor(.secondary) 64 | } 65 | .font(.caption2) 66 | .padding(.leading) 67 | } 68 | .frame(maxWidth: 500) 69 | } 70 | } 71 | 72 | 73 | struct NewWeekStepsView_Previews: PreviewProvider { 74 | static var previews: some View { 75 | NewWeekStepsView(viewModel: StepsViewModel()) 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /Steps/Views/ProfileButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileButtonView.swift 3 | // Steps 4 | // 5 | // Created by Yashraj jadhav on 16/10/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProfileButtonView: View { 11 | @State var title : String 12 | @State var backgroundColor : Color 13 | var action : (()->Void) 14 | 15 | var body: some View { 16 | Button { 17 | action() 18 | } label: { 19 | Text(title) 20 | .padding() 21 | .frame(maxWidth: 200) 22 | .background( 23 | RoundedRectangle(cornerRadius: 10) 24 | .fill(backgroundColor)) 25 | } 26 | } 27 | } 28 | 29 | #Preview { 30 | ProfileButtonView(title: "", backgroundColor: .red) {} 31 | } 32 | -------------------------------------------------------------------------------- /Steps/Views/ProfileItemButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileItemButtonView.swift 3 | // Steps 4 | // 5 | // Created by Yashraj jadhav on 16/10/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProfileItemButtonView: View { 11 | @State var title : String 12 | @State var image : String 13 | var action : (()->Void) 14 | 15 | var body: some View { 16 | Button{ 17 | action() 18 | } label: { 19 | HStack{ 20 | Image(systemName: image) 21 | Text(title) 22 | } 23 | .foregroundColor(.accentColor) 24 | } 25 | .padding() 26 | .frame(maxWidth: .infinity , alignment: .leading ) 27 | } 28 | } 29 | #Preview { 30 | ProfileItemButtonView(title: "Edit Image", image: "square.and.pencil"){} 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Steps/Views/StepsDetailCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardView.swift 3 | // Steps 4 | // 5 | // Created by Mohd Wasif Raza on 13/10/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | 11 | 12 | struct StepsDetailCardView: View { 13 | @State var title: String 14 | @State var image: String 15 | @State var value: String 16 | var body: some View { 17 | ZStack{ 18 | Color(uiColor: .tertiarySystemFill) 19 | .cornerRadius(15) 20 | VStack { 21 | HStack(alignment: .top) { 22 | Text(title) 23 | .font(.system(size: 16)) 24 | 25 | Spacer() 26 | Image(systemName: image) 27 | .foregroundColor(.indigo) 28 | }.padding() 29 | Text(value) 30 | .font(.system(size: 24)) 31 | } 32 | .padding() 33 | } 34 | } 35 | } 36 | 37 | #Preview { 38 | StepsDetailCardView(title: "Calories Burned", image: "flame.fill", value: "530 KCAL") 39 | } 40 | -------------------------------------------------------------------------------- /Steps/Views/StepsGoalCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StepsGoalCardView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/17/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct StepsGoalCardView: View { 11 | let steps: Int 12 | @Binding var showingEditView: Bool 13 | 14 | var body: some View { 15 | ZStack { 16 | Rectangle() 17 | .fill(.white) 18 | .cornerRadius(12) 19 | .shadow(radius: 5) 20 | 21 | VStack(alignment: .leading, spacing: 10) { 22 | HStack { 23 | Text(Constants.dailyStepsGoal) 24 | Spacer() 25 | 26 | Button { 27 | showingEditView = true 28 | } label: { 29 | Image(systemName: "pencil.circle") 30 | } 31 | } 32 | .foregroundColor(.indigo) 33 | 34 | Text(String(format: Constants.enterSteps, steps)) 35 | .font(.system(size: 32)) 36 | .bold() 37 | .foregroundColor(.indigo) 38 | 39 | Spacer() 40 | } 41 | .padding() 42 | } 43 | .frame(width: 300, height: 80) 44 | } 45 | } 46 | 47 | struct StepsGoalCardView_Previews: PreviewProvider { 48 | static var previews: some View { 49 | StepsGoalCardView(steps: 10000, showingEditView: .constant(false)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Steps/Views/WeekStepsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeekStepsView.swift 3 | // Steps 4 | // 5 | // Created by Brittany Rima on 12/14/22. 6 | // 7 | 8 | import SwiftUI 9 | import Charts 10 | 11 | struct WeekStepsView: View { 12 | @ObservedObject var viewModel: StepsViewModel 13 | 14 | var body: some View { 15 | VStack(alignment: .leading) { 16 | Text(Constants.weeklySteps) 17 | 18 | Chart { 19 | RuleMark(y: .value(Constants.goal, viewModel.goal)) 20 | .foregroundStyle(.mint) 21 | .lineStyle(StrokeStyle(lineWidth: 1, dash: [5])) 22 | 23 | ForEach(viewModel.weekSteps) { step in 24 | BarMark( 25 | x: .value(Constants.day, step.date, unit: .weekday), 26 | y: .value(Constants.steps, step.count) 27 | ) 28 | .foregroundStyle(.indigo) 29 | } 30 | } 31 | .frame(height: 150) 32 | .padding() 33 | .chartXAxis { 34 | AxisMarks(values: viewModel.weekSteps.map { $0.date}) { date in 35 | AxisValueLabel(format: .dateTime.weekday(.narrow)) 36 | .offset(x: 5) 37 | } 38 | } 39 | 40 | HStack { 41 | Image(systemName: "line.diagonal") 42 | .rotationEffect(Angle(degrees: 45)) 43 | .foregroundColor(.mint) 44 | 45 | Text(Constants.dailyGoal) 46 | .foregroundColor(.secondary) 47 | } 48 | .font(.caption2) 49 | .padding(.leading) 50 | } 51 | .padding() 52 | } 53 | } 54 | 55 | struct WeekStepsView_Previews: PreviewProvider { 56 | static var previews: some View { 57 | WeekStepsView(viewModel: StepsViewModel()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /StepsTests/AddGoalViewTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddTaskViewTests.swift 3 | // StepsTests 4 | // 5 | // Created by Daniel Lyons on 10/6/23. 6 | // 7 | 8 | import XCTest 9 | import Dependencies 10 | @testable import Steps 11 | 12 | @MainActor 13 | final class AddGoalViewTests: XCTestCase { 14 | func test_Demo(){ 15 | let arr = [1,2,3] 16 | XCTAssert(arr.count == 3) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /StepsTests/ContributorsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContributorsTests.swift 3 | // StepsTests 4 | // 5 | // Created by Daniel Lyons on 10/6/23. 6 | // 7 | 8 | import XCTest 9 | import Dependencies 10 | @testable import Steps 11 | 12 | @MainActor 13 | final class ContributorsTests: XCTestCase { 14 | func testFetchContributors() async { 15 | let vm = ContributorsViewModel() 16 | 17 | await vm.fetchContributors() 18 | XCTAssertEqual(vm.contributors, [Contributor].mock) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /StepsTests/SettingsViewTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import Dependencies 4 | import DependenciesAdditions 5 | import UserNotifications 6 | @testable import Steps 7 | 8 | 9 | @MainActor 10 | final class SettingsViewTests: XCTestCase { 11 | func testScheduleDailyNotification() async throws { 12 | let vm = SettingsViewModel() 13 | let requests = ActorIsolated<[UNNotificationRequest]>([]) 14 | 15 | await withDependencies { 16 | let add = { @Sendable userNotificationRequest in 17 | await requests.setValue([userNotificationRequest]) 18 | } 19 | $0.userNotificationCenter.$add = add 20 | $0.userNotificationCenter.$pendingNotificationRequests = { @Sendable in 21 | return await requests.value 22 | } 23 | 24 | } operation: { 25 | await vm.scheduleDailyNotification() 26 | @Dependency(\.userNotificationCenter) var userNotificationCenter 27 | let pendingNotifications = await userNotificationCenter.pendingNotificationRequests() 28 | let addedNotification = pendingNotifications[0] 29 | 30 | 31 | XCTAssertEqual(addedNotification.identifier, Constants.notificationsIdentifier) 32 | let content = addedNotification.content 33 | XCTAssertEqual(content.title, Constants.goodMorningTitle) 34 | XCTAssertEqual(content.body, Constants.reachStepGoalDescription) 35 | 36 | let trigger = UNCalendarNotificationTrigger( 37 | dateMatching: DateComponents(hour: 9, minute: 0), 38 | repeats: true 39 | ) 40 | XCTAssertEqual(addedNotification.trigger, trigger) 41 | } 42 | 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /StepsTests/StepsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Dependencies 3 | import DependenciesAdditions 4 | @testable import Steps 5 | 6 | @MainActor 7 | final class StepsTests: XCTestCase { 8 | 9 | func testStepCheckpointsReached() { 10 | let vm = StepsViewModel() 11 | vm.steps = .mock 12 | vm.goal = 10_000 13 | 14 | XCTAssertEqual(6_789, vm.currentSteps) 15 | XCTAssertEqual(true, vm.checkPointOneReached) // 25% 16 | XCTAssertEqual(true, vm.checkPointTwoReached) // 50% 17 | XCTAssertEqual(false, vm.checkPointThreeReached) // 75% 18 | XCTAssertEqual(false, vm.checkPointFourReached) // 100% 19 | } 20 | 21 | func testSoccerFieldsWalkedString() { 22 | let vm = StepsViewModel() 23 | 24 | // For every 144 Steps you've walked about 1 soccer field. 25 | vm.steps = [Step(count: 0, date: .now)] 26 | XCTAssertEqual(vm.soccerFieldsWalkedString, Constants.walkedFullSoccerFieldToday) 27 | vm.steps = [Step(count: 145, date: .now)] 28 | XCTAssertEqual(vm.soccerFieldsWalkedString, Constants.walkedOneSoccerFieldToday) 29 | vm.steps = [Step(count:289, date: .now)] 30 | XCTAssertEqual(vm.soccerFieldsWalkedString, String(format: Constants.walkedCustomSoccerFieldToday, 2)) 31 | } 32 | 33 | // TODO: Test @AppStorage 34 | func testUserDefaults() async { 35 | let newGoal = 1_234 36 | 37 | let vm = StepsViewModel() 38 | vm.goal = newGoal 39 | 40 | let resultGoal = UserDefaults.appGroup?.value(forKey: Constants.goalKey) as? Int 41 | XCTAssertEqual(newGoal, resultGoal) 42 | 43 | 44 | // Not sure yet how to inject UserDefaults dependency into @AppStorage. 45 | // Even though both @Dependency(\.userDefaults) and @AppStorage write to UserDefaults, @AppStorage is unaware of @Dependency 46 | // vm.stepCount = 2_345 47 | // XCTAssertEqual(2_345, userDefaults.integer(forKey: Constants.stepCountKey)) 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /StepsWidget/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /StepsWidget/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "StepTracker.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /StepsWidget/Assets.xcassets/AppIcon.appiconset/StepTracker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brittanyarima/Steps/c043f9ec74a545c9a881e525fe4290ef3dab4064/StepsWidget/Assets.xcassets/AppIcon.appiconset/StepTracker.png -------------------------------------------------------------------------------- /StepsWidget/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /StepsWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /StepsWidget/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSExtension 6 | 7 | NSExtensionPointIdentifier 8 | com.apple.widgetkit-extension 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /StepsWidget/StepsGraphWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StepsGraphWidget.swift 3 | // Steps 4 | // 5 | // Created by Daniel Lyons on 10/3/23. 6 | // 7 | 8 | import Foundation 9 | import WidgetKit 10 | import SwiftUI 11 | import HealthKit 12 | import Charts 13 | 14 | struct StepsGraphProvider: TimelineProvider { 15 | @AppStorage("stepCount", store: UserDefaults.appGroup) var stepCount: Int = 0 16 | @AppStorage("goal", store: UserDefaults.appGroup) var goal: Int = 10_000 17 | var healthStore: HKHealthStore? 18 | var query: HKStatisticsCollectionQuery? 19 | 20 | init() { 21 | if HKHealthStore.isHealthDataAvailable() { 22 | self.healthStore = HKHealthStore() 23 | } 24 | } 25 | 26 | func placeholder(in context: Context) -> StepsGraphEntry { 27 | // TODO: get the steps count 28 | StepsGraphEntry(stepsRecords: [Step(count: 4_364, date: Date())], goal: goal) 29 | 30 | } 31 | 32 | func getSnapshot(in context: Context, completion: @escaping (StepsGraphEntry) -> ()) { 33 | // TODO: get the steps count 34 | 35 | let entry = StepsGraphEntry(stepsRecords: [Step(count: 5_364, date: Date())], goal: goal) 36 | completion(entry) 37 | } 38 | 39 | func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { 40 | // TODO: get the steps count 41 | 42 | let entry = StepsGraphEntry(stepsRecords: [Step(count: 6_364, date: Date())], goal: goal) 43 | let currentDate = Date() 44 | let futureDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)! 45 | let timeline = Timeline(entries: [entry], policy: .after(futureDate)) 46 | completion(timeline) 47 | } 48 | 49 | // mutating func calculateSteps(completion: @escaping (HKStatisticsCollection?) -> Void) { 50 | // let stepType = HKQuantityType.quantityType(forIdentifier: HKQuantityTypeIdentifier.stepCount)! 51 | // let startDate = Calendar.current.date(byAdding: .day, value: -1, to: Date()) 52 | // let anchorDate = Date.sundayAt12AM() 53 | // let daily = DateComponents(day: 1) 54 | // let predicate = HKQuery.predicateForSamples(withStart: startDate, end: Date(), options: .strictStartDate) 55 | // 56 | // query = HKStatisticsCollectionQuery(quantityType: stepType, 57 | // quantitySamplePredicate: predicate, 58 | // options: .cumulativeSum, 59 | // anchorDate: anchorDate, 60 | // intervalComponents: daily) 61 | // query!.initialResultsHandler = { query, statsCollection, error in 62 | // completion(statsCollection) 63 | // } 64 | // 65 | // if let healthStore = healthStore, let query = self.query { 66 | // healthStore.execute(query) 67 | // } 68 | // } 69 | 70 | // mutating func updateUIFromStats(_ statsCollection: HKStatisticsCollection) { 71 | // let startDate = Calendar.current.date(byAdding: .day, value: -7, to: Date())! 72 | // let endDate = Date() 73 | // 74 | // statsCollection.enumerateStatistics(from: startDate, to: endDate) { stats, stop in 75 | // let count = stats.sumQuantity()?.doubleValue(for: .count()) 76 | // let step = Step(count: Int(count ?? 0), date: stats.startDate) 77 | // 78 | // DispatchQueue.main.async { 79 | // self.steps.append(step) 80 | // self.stepCount = self.steps.last?.count ?? 0 81 | // } 82 | // } 83 | // } 84 | } 85 | 86 | struct StepsGraphEntry: TimelineEntry { 87 | let date = Date() 88 | let stepsRecords: [Step] 89 | let goal: Int 90 | 91 | var progress: Double { 92 | guard let latestSteps = stepsRecords.last 93 | else { return 0.0 } 94 | 95 | return Double(latestSteps.count / goal) 96 | } 97 | 98 | var stepsCount: Int { 99 | return stepsRecords.last?.count ?? 0 100 | } 101 | } 102 | 103 | struct StepsGraphWidgetEntryView: View { 104 | var entry: StepsGraphProvider.Entry 105 | @Environment(\.widgetFamily) var widgetFamily 106 | 107 | @ViewBuilder var systemMediumWidgetView: some View { 108 | HStack { 109 | VStack { 110 | ProgressView(value: Double(entry.stepsCount), total: Double(entry.goal)) { 111 | Text("🏃") 112 | }.progressViewStyle(.circular) 113 | .foregroundStyle(.indigo.gradient) 114 | .padding([.top, .trailing, .bottom]) 115 | Text("\(entry.stepsCount) / \(entry.goal)") 116 | .contentTransition(.numericText()) 117 | .font(.caption) 118 | } 119 | 120 | Chart(entry.stepsRecords) { record in 121 | Plot { 122 | LineMark(x: .value("Date", record.date), y: .value("Steps", record.count)) 123 | AreaMark(x: .value("Date", record.date), y: .value("Steps", record.count)) 124 | .opacity(0.5) 125 | 126 | RuleMark(y: .value("Steps", entry.goal)) 127 | .foregroundStyle(.green.gradient) 128 | } 129 | .interpolationMethod(.cardinal(tension: 0.8)) 130 | .foregroundStyle(.indigo.gradient) 131 | 132 | } 133 | .chartYAxis { 134 | AxisMarks { value in 135 | AxisValueLabel(String( 136 | "\(value.as(Double.self)?.formatted(.number.notation(.compactName)) ?? "")" 137 | )) 138 | } 139 | } 140 | } 141 | } 142 | 143 | @ViewBuilder var systemLargeWidgetView: some View { 144 | VStack { 145 | Text("🏃 \(entry.stepsCount) / \(entry.goal.formatted(.number.notation(.compactName))) steps") 146 | .contentTransition(.numericText()) 147 | 148 | Chart(entry.stepsRecords) { record in 149 | Plot { 150 | LineMark(x: .value("Date", record.date), y: .value("Steps", record.count)) 151 | AreaMark(x: .value("Date", record.date), y: .value("Steps", record.count)) 152 | .opacity(0.5) 153 | RuleMark(y: .value("Steps", entry.goal)) 154 | .foregroundStyle(.green.gradient) 155 | } 156 | .interpolationMethod(.cardinal(tension: 0.5)) 157 | .foregroundStyle(.indigo.gradient) 158 | } 159 | .chartYAxis { 160 | AxisMarks { value in 161 | AxisValueLabel(String( 162 | "\(value.as(Double.self)?.formatted(.number.notation(.compactName)) ?? "")" 163 | )) 164 | } 165 | } 166 | } 167 | } 168 | 169 | @ViewBuilder var accessoryRectangularWidgetView: some View { 170 | HStack { 171 | ProgressView(value: Double(entry.stepsCount), total: Double(entry.goal)) { 172 | Text("🏃") 173 | }.progressViewStyle(.circular) 174 | 175 | Chart(entry.stepsRecords) { record in 176 | Plot { 177 | LineMark(x: .value("Date", record.date), y: .value("Steps", record.count)) 178 | AreaMark(x: .value("Date", record.date), y: .value("Steps", record.count)) 179 | .opacity(0.5) 180 | RuleMark(y: .value("Steps", entry.goal)) 181 | .foregroundStyle(.green.gradient) 182 | } 183 | .interpolationMethod(.cardinal(tension: 0.5)) 184 | .foregroundStyle(.indigo.gradient) 185 | } 186 | .chartYAxis {} 187 | .chartXAxis {} 188 | } 189 | } 190 | 191 | var body: some View { 192 | Group { 193 | switch widgetFamily { 194 | case .accessoryRectangular: 195 | self.accessoryRectangularWidgetView 196 | case .systemMedium: 197 | self.systemMediumWidgetView 198 | case .systemLarge: 199 | self.systemLargeWidgetView 200 | default: 201 | let _ = print("Unexpected widget family \(widgetFamily.description).") 202 | self.systemMediumWidgetView 203 | } 204 | }.containerBackground() 205 | } 206 | } 207 | 208 | struct StepsGraphWidget: Widget { 209 | let kind: String = Constants.stepsGraphWidget 210 | 211 | var body: some WidgetConfiguration { 212 | StaticConfiguration(kind: kind, provider: StepsGraphProvider()) { entry in 213 | StepsGraphWidgetEntryView(entry: entry) 214 | } 215 | .configurationDisplayName(Constants.stepsGraphWidgetName) 216 | .description(Constants.stepsGraphWidgetDescription) 217 | .supportedFamilies([.systemMedium, .systemLarge, .accessoryRectangular]) 218 | } 219 | } 220 | 221 | struct StepsGraphWidgetMedium_Previews: PreviewProvider { 222 | static var previews: some View { 223 | StepsGraphWidgetEntryView(entry: StepsGraphEntry(stepsRecords: .mock, goal: 10_000)) 224 | .previewContext(WidgetPreviewContext(family: .systemMedium)) 225 | } 226 | } 227 | 228 | struct StepsGraphWidgetLarge_Previews: PreviewProvider { 229 | static var previews: some View { 230 | StepsGraphWidgetEntryView(entry: StepsGraphEntry(stepsRecords: .mock, goal: 10_000)) 231 | .previewContext(WidgetPreviewContext(family: .systemLarge)) 232 | } 233 | } 234 | 235 | struct StepsGraphWidgetAccessoryRectangular_Previews: PreviewProvider { 236 | static var previews: some View { 237 | StepsGraphWidgetEntryView(entry: StepsGraphEntry(stepsRecords: .mock, goal: 10_000)) 238 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) 239 | } 240 | } 241 | 242 | 243 | // Workaround: To use `#Preview`, first set deploy target to 17.0 or later 244 | // Useful for testing animation transitions 245 | // Be patient: Mock data currently takes a long time to load 246 | 247 | //#Preview("Medium", as: .systemMedium) { 248 | // StepsGraphWidget() 249 | //} timeline: { 250 | // StepsGraphEntry(stepsRecords: .mock1Element, goal: 10_000) 251 | // StepsGraphEntry(stepsRecords: .mock2Element, goal: 10_000) 252 | // StepsGraphEntry(stepsRecords: .mock3Element, goal: 10_000) 253 | // StepsGraphEntry(stepsRecords: .mock4Element, goal: 10_000) 254 | // StepsGraphEntry(stepsRecords: .mock5Element, goal: 10_000) 255 | //} 256 | // 257 | //#Preview("Large", as: .systemLarge) { 258 | // StepsGraphWidget() 259 | //} timeline: { 260 | // StepsGraphEntry(stepsRecords: .mock1Element, goal: 10_000) 261 | // StepsGraphEntry(stepsRecords: .mock2Element, goal: 10_000) 262 | // StepsGraphEntry(stepsRecords: .mock3Element, goal: 10_000) 263 | // StepsGraphEntry(stepsRecords: .mock4Element, goal: 10_000) 264 | // StepsGraphEntry(stepsRecords: .mock5Element, goal: 10_000) 265 | //} 266 | -------------------------------------------------------------------------------- /StepsWidget/StepsWidget.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StepsWidget.swift 3 | // StepsWidget 4 | // 5 | // Created by Brittany Rima on 12/13/22. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | import HealthKit 11 | 12 | struct StepsProvider: TimelineProvider { 13 | @AppStorage("stepCount", store: UserDefaults.appGroup) var stepCount: Int = 0 14 | @AppStorage("goal", store: UserDefaults.appGroup) var goal: Int = 10_000 15 | 16 | func placeholder(in context: Context) -> StepEntry { 17 | StepEntry(steps: 5_678, goal: goal) 18 | 19 | } 20 | 21 | func getSnapshot(in context: Context, completion: @escaping (StepEntry) -> ()) { 22 | let entry = StepEntry(steps: stepCount, goal: goal) 23 | completion(entry) 24 | } 25 | 26 | func getTimeline(in context: Context, completion: @escaping (Timeline) -> ()) { 27 | let entry = StepEntry(steps: stepCount, goal: goal) 28 | let currentDate = Date() 29 | let futureDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)! 30 | let timeline = Timeline(entries: [entry], policy: .after(futureDate)) 31 | completion(timeline) 32 | } 33 | } 34 | 35 | struct StepEntry: TimelineEntry { 36 | let date = Date() 37 | let steps: Int 38 | let goal: Int 39 | 40 | var progress: Double { 41 | return Double(steps) / Double(goal) 42 | } 43 | } 44 | 45 | struct StepsWidgetEntryView : View { 46 | var entry: StepsProvider.Entry 47 | @Environment(\.widgetFamily) var widgetFamily 48 | 49 | @ViewBuilder var accessoryWidgetView: some View { 50 | VStack(alignment: .trailing) { 51 | Text("🏃 \(entry.steps)") 52 | .font(.body) 53 | .bold() 54 | } 55 | .foregroundColor(.indigo) 56 | } 57 | 58 | @ViewBuilder var systemSmallWidgetView: some View { 59 | VStack(alignment: .center) { 60 | ProgressView(value: entry.progress) { 61 | Text("🏃") 62 | } 63 | .progressViewStyle(.circular) 64 | .foregroundStyle(.indigo) 65 | .font(.title2) 66 | 67 | Text("\(entry.steps) / \(entry.goal.formatted(.number.notation(.compactName)))") 68 | .font(.title2) 69 | .bold() 70 | .foregroundColor(.indigo) 71 | .contentTransition(.numericText()) 72 | } 73 | 74 | } 75 | 76 | 77 | 78 | var body: some View { 79 | Group { 80 | switch widgetFamily { 81 | case .accessoryRectangular: 82 | self.accessoryWidgetView 83 | case .systemSmall: 84 | self.systemSmallWidgetView 85 | default: 86 | let _ = print("Widget family \(widgetFamily.description).") 87 | self.accessoryWidgetView 88 | } 89 | }.containerBackground() 90 | } 91 | } 92 | 93 | extension View { 94 | /// Calls [`.containerBackground(.thinMaterial, for: .widget)`](https://developer.apple.com/documentation/swiftui/view/containerbackground(_:for:)) if on iOS 17 95 | /// 96 | /// This is a workaround. Xcode 15 previews don't seem to work without this. 97 | @ViewBuilder 98 | func containerBackground() -> some View { 99 | // `containerBackground(_:for:)` is not available pre iOS 17 100 | if #available(iOSApplicationExtension 17.0, *) { 101 | self.containerBackground(.thinMaterial, for: .widget) 102 | } else { 103 | self 104 | } 105 | } 106 | } 107 | 108 | 109 | struct StepsWidget: Widget { 110 | let kind: String = Constants.stepsWidget 111 | 112 | var body: some WidgetConfiguration { 113 | StaticConfiguration(kind: kind, provider: StepsProvider()) { entry in 114 | StepsWidgetEntryView(entry: entry) 115 | 116 | } 117 | .configurationDisplayName(Constants.stepsWidgetName) 118 | .description(Constants.stepsWidgetDescription) 119 | .supportedFamilies([.accessoryRectangular, .systemSmall]) 120 | } 121 | } 122 | 123 | struct StepsWidget_Previews: PreviewProvider { 124 | static var previews: some View { 125 | StepsWidgetEntryView(entry: StepEntry(steps: 7200, goal: 10_000)) 126 | .previewContext(WidgetPreviewContext(family: .accessoryRectangular)) 127 | } 128 | } 129 | 130 | struct StepsWidgetSmall_Previews: PreviewProvider { 131 | static var previews: some View { 132 | StepsWidgetEntryView(entry: StepEntry(steps: 7_201, goal: 10_000)) 133 | .previewContext(WidgetPreviewContext(family: .systemSmall)) 134 | } 135 | } 136 | 137 | 138 | // Workaround: To use `#Preview`, first set deploy target to 17.0 or later 139 | // Useful for testing animation transitions 140 | //#Preview("Small", as: .systemSmall) { 141 | // StepsWidget() 142 | //} timeline: { 143 | // StepEntry(steps: 1200, goal: 10_000) 144 | // StepEntry(steps: 5633, goal: 10_000) 145 | // StepEntry(steps: 6622, goal: 10_000) 146 | // StepEntry(steps: 7297, goal: 10_000) 147 | //} 148 | -------------------------------------------------------------------------------- /StepsWidget/StepsWidgetBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StepsWidgetBundle.swift 3 | // StepsWidget 4 | // 5 | // Created by Brittany Rima on 12/13/22. 6 | // 7 | 8 | import WidgetKit 9 | import SwiftUI 10 | 11 | @main 12 | struct StepsWidgetBundle: WidgetBundle { 13 | var body: some Widget { 14 | StepsWidget() 15 | // StepsGraphWidget() TODO: Connect to data 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /StepsWidgetExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | 8 | group.com.BrittanyRima.Steps 9 | 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------