├── Tasky ├── avatar.png ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── 100.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 128.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 16.png │ │ ├── 167.png │ │ ├── 172.png │ │ ├── 180.png │ │ ├── 196.png │ │ ├── 20.png │ │ ├── 216.png │ │ ├── 256.png │ │ ├── 29.png │ │ ├── 32.png │ │ ├── 40.png │ │ ├── 48.png │ │ ├── 50.png │ │ ├── 512.png │ │ ├── 55.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 64.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── 88.png │ │ ├── 1024.png │ │ └── Contents.json │ ├── avatar.imageset │ │ ├── avatar.png │ │ └── Contents.json │ ├── placeholder.imageset │ │ ├── placeholder.jpg │ │ └── Contents.json │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── Black.colorset │ │ └── Contents.json │ ├── Blue.colorset │ │ └── Contents.json │ ├── Gray.colorset │ │ └── Contents.json │ ├── Pink.colorset │ │ └── Contents.json │ ├── Red.colorset │ │ └── Contents.json │ ├── Teal.colorset │ │ └── Contents.json │ ├── Green.colorset │ │ └── Contents.json │ ├── Indigo.colorset │ │ └── Contents.json │ ├── Purple.colorset │ │ └── Contents.json │ └── Yellow.colorset │ │ └── Contents.json ├── Font Awesome 5 Pro-Light-300.otf ├── Font Awesome 5 Pro-Solid-900.otf ├── Font Awesome 5 Pro-Regular-400.otf ├── Font Awesome 5 Brands-Regular-400.otf ├── Font Awesome 5 Pro-Light-300.otf.otf ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Tasky.xcdatamodeld │ ├── .xccurrentversion │ └── Tasky.xcdatamodel │ │ └── contents ├── Models │ ├── TaskyUser.swift │ ├── Project.swift │ └── Task.swift ├── Tasky.entitlements ├── TaskyApp.swift ├── Views │ ├── Components │ │ ├── Indicator.swift │ │ ├── ProgressBar.swift │ │ ├── Chip.swift │ │ ├── ImagePicker.swift │ │ └── Avatar.swift │ ├── EmailSignInForm.swift │ ├── EditNameView.swift │ ├── Sheets │ │ ├── UpdateNameSheet.swift │ │ ├── NewProjectSheet.swift │ │ ├── NewTagSheet.swift │ │ ├── NewTaskSheet.swift │ │ ├── UpdateTaskSheet.swift │ │ ├── ProfileSheet.swift │ │ ├── PeopleSheet.swift │ │ └── TaskDetailSheet.swift │ ├── UpdateProjectForm.swift │ ├── PhoneLoginView.swift │ ├── ProjectView.swift │ ├── ProjectListView.swift │ ├── LoginView.swift │ ├── TaskView.swift │ └── TaskListView.swift ├── AppDelegate.swift ├── ContentView.swift ├── ViewModels │ ├── ProjectListViewModel.swift │ └── ProjectViewModel.swift ├── Info.plist ├── Persistence.swift ├── Services │ ├── AvatarService.swift │ ├── UserService.swift │ └── AuthService.swift └── Repositories │ └── ProjectRepository.swift ├── TaskyTests ├── Info.plist └── TaskyTests.swift ├── TaskyUITests ├── Info.plist └── TaskyUITests.swift ├── README.md ├── .gitignore └── Tasky.xcodeproj └── project.pbxproj /Tasky/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/avatar.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tasky/Font Awesome 5 Pro-Light-300.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Font Awesome 5 Pro-Light-300.otf -------------------------------------------------------------------------------- /Tasky/Font Awesome 5 Pro-Solid-900.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Font Awesome 5 Pro-Solid-900.otf -------------------------------------------------------------------------------- /Tasky/Font Awesome 5 Pro-Regular-400.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Font Awesome 5 Pro-Regular-400.otf -------------------------------------------------------------------------------- /Tasky/Font Awesome 5 Brands-Regular-400.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Font Awesome 5 Brands-Regular-400.otf -------------------------------------------------------------------------------- /Tasky/Font Awesome 5 Pro-Light-300.otf.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Font Awesome 5 Pro-Light-300.otf.otf -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/avatar.imageset/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/avatar.imageset/avatar.png -------------------------------------------------------------------------------- /Tasky/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/placeholder.imageset/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Livinglist/Tasky/HEAD/Tasky/Assets.xcassets/placeholder.imageset/placeholder.jpg -------------------------------------------------------------------------------- /Tasky/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 | -------------------------------------------------------------------------------- /Tasky/Tasky.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Tasky.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tasky/Models/TaskyUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/3/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TaskyUser: Identifiable, Codable, Equatable { 11 | var id: String 12 | var firstName: String 13 | var lastName: String 14 | 15 | var fullName:String{ 16 | firstName + " " + lastName 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tasky/Tasky.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.applesignin 8 | 9 | Default 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/avatar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "avatar.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 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/placeholder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "placeholder.jpg", 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 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/Black.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "darkTextColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "ios", 19 | "reference" : "darkTextColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/Blue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "systemBlueColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "ios", 19 | "reference" : "systemBlueColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/Gray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "systemGrayColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "ios", 19 | "reference" : "systemGrayColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/Pink.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "systemPinkColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "ios", 19 | "reference" : "systemPinkColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/Red.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "systemRedColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "ios", 19 | "reference" : "systemRedColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/Teal.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "systemTealColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "ios", 19 | "reference" : "systemTealColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/Green.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "systemGreenColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "ios", 19 | "reference" : "systemGreenColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/Indigo.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "systemIndigoColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "ios", 19 | "reference" : "systemIndigoColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/Purple.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "systemPurpleColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "ios", 19 | "reference" : "systemPurpleColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/Yellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "systemYellowColor" 7 | }, 8 | "idiom" : "universal" 9 | }, 10 | { 11 | "appearances" : [ 12 | { 13 | "appearance" : "luminosity", 14 | "value" : "dark" 15 | } 16 | ], 17 | "color" : { 18 | "platform" : "ios", 19 | "reference" : "systemYellowColor" 20 | }, 21 | "idiom" : "universal" 22 | } 23 | ], 24 | "info" : { 25 | "author" : "xcode", 26 | "version" : 1 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tasky/Tasky.xcdatamodeld/Tasky.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tasky/TaskyApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskyApp.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSVGCoder 10 | 11 | @main 12 | struct TaskyApp: App { 13 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 14 | 15 | let persistenceController = PersistenceController.shared 16 | 17 | init() { 18 | SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared) 19 | } 20 | 21 | var body: some Scene { 22 | WindowGroup { 23 | ContentView() 24 | .environment(\.managedObjectContext, persistenceController.container.viewContext) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /TaskyTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TaskyUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tasky/Views/Components/Indicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Indicator.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/10/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | fileprivate enum Constants { 11 | static let radius: CGFloat = 16 12 | static let indicatorHeight: CGFloat = 6 13 | static let indicatorWidth: CGFloat = 60 14 | static let snapRatio: CGFloat = 0.25 15 | static let minHeightRatio: CGFloat = 0.3 16 | } 17 | 18 | struct Indicator: View { 19 | var body: some View { 20 | RoundedRectangle(cornerRadius: Constants.radius) 21 | .fill(Color.secondary) 22 | .frame( 23 | width: Constants.indicatorWidth, 24 | height: Constants.indicatorHeight 25 | ) 26 | 27 | } 28 | } 29 | 30 | struct Indicator_Previews: PreviewProvider { 31 | static var previews: some View { 32 | Indicator() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tasky/Views/Components/ProgressBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProgressBar.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/5/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProgressBar: View { 11 | @Binding var value: Float 12 | var color: Color = Color(.orange) 13 | 14 | var body: some View { 15 | GeometryReader { geometry in 16 | ZStack(alignment: .leading) { 17 | Rectangle().frame(width: geometry.size.width , height: geometry.size.height) 18 | .opacity(0.3) 19 | .foregroundColor(Color(UIColor.systemTeal)) 20 | 21 | Rectangle().frame(width: min(CGFloat(self.value)*geometry.size.width, geometry.size.width), height: geometry.size.height) 22 | .foregroundColor(color) 23 | .animation(.linear) 24 | }.cornerRadius(45.0) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tasky/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import Foundation 9 | import Firebase 10 | import UIKit 11 | 12 | class AppDelegate: NSObject, UIApplicationDelegate { 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 14 | // Configure FirebaseApp 15 | FirebaseApp.configure() 16 | 17 | return true 18 | } 19 | 20 | func application(_ application: UIApplication , didReceiveRemoteNotification notification: [AnyHashable : Any], 21 | fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) 22 | { 23 | if Auth.auth().canHandleNotification(notification) 24 | { 25 | completionHandler(UIBackgroundFetchResult.noData); 26 | return 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /TaskyTests/TaskyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskyTests.swift 3 | // TaskyTests 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import XCTest 9 | @testable import Tasky 10 | 11 | class TaskyTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Tasky/Models/Project.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import Foundation 9 | import FirebaseFirestoreSwift 10 | 11 | struct Project: Identifiable, Codable, Equatable { 12 | @DocumentID var id: String? 13 | var name: String 14 | var tasks: [Task] 15 | var managerId: String? 16 | var collaboratorIds: [String]? 17 | var timestamp: Double 18 | var tags: [String: String]? 19 | 20 | static func == (lhs: Self, rhs: Self) -> Bool { 21 | lhs.id == rhs.id 22 | } 23 | } 24 | 25 | #if DEBUG 26 | let testProject = Project(id: UUID().uuidString, name: "Huge Project", tasks: [], managerId: nil, collaboratorIds: [], timestamp: Date().timeIntervalSince1970) 27 | 28 | let testData = (1...10).map { i in 29 | Project(name: "Project #\(i)", tasks: [Task(id: "\(i)", title: "Task \(i)", content: "content of a task", taskStatus: .awaiting, timestamp: NSDate().timeIntervalSince1970)], managerId: "test", timestamp: NSDate().timeIntervalSince1970) 30 | } 31 | #endif 32 | -------------------------------------------------------------------------------- /Tasky/Views/Components/Chip.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Chip.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/19/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SmallChip: View { 11 | let color: Color 12 | let label: String 13 | let onPressed: () -> () 14 | 15 | var body: some View { 16 | Button(action: onPressed, label: { 17 | Text(label).font(.system(size: 10)).foregroundColor(.white).padding(.horizontal, 6).padding(.vertical, 3).background(color).cornerRadius(12.0) 18 | }) 19 | } 20 | } 21 | 22 | struct Chip: View { 23 | let color: Color 24 | let label: String 25 | let onPressed: () -> () 26 | 27 | var body: some View { 28 | Button(action: onPressed, label: { 29 | Text(label).font(.system(size: 14)).foregroundColor(.white).padding(.horizontal, 6).padding(.vertical, 3).background(color).cornerRadius(12.0) 30 | }) 31 | } 32 | } 33 | 34 | struct Chip_Previews: PreviewProvider { 35 | static var previews: some View { 36 | Chip(color: Color.blue, label: "Bug", onPressed: {}) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tasky/Models/Task.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Task.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/3/21. 6 | // 7 | 8 | import Foundation 9 | import FirebaseFirestoreSwift 10 | 11 | enum TaskStatus: Int, Codable, CustomStringConvertible{ 12 | case awaiting 13 | case inProgress 14 | case completed 15 | case aborted 16 | 17 | var description : String { 18 | switch self { 19 | case .awaiting: return "Awaiting" 20 | case .inProgress: return "In Progress" 21 | case .completed: return "Completed" 22 | case .aborted: return "Aborted" 23 | } 24 | } 25 | } 26 | 27 | struct Task: Identifiable, Codable, Equatable{ 28 | var id: String 29 | var title: String 30 | var content: String 31 | var taskStatus: TaskStatus 32 | var timestamp: Double 33 | var dueTimestamp: Double? 34 | var creatorId: String? 35 | var assigneesId: [String]? 36 | var tags: [String: String]? 37 | 38 | static func == (lhs: Self, rhs: Self) -> Bool { 39 | lhs.id == rhs.id 40 | } 41 | } 42 | 43 | let testTask = Task(id: "", title: "", content: "", taskStatus: .awaiting, timestamp: 0, dueTimestamp: nil, creatorId: nil, assigneesId: []) 44 | -------------------------------------------------------------------------------- /Tasky/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import SwiftUI 9 | import CoreData 10 | import Firebase 11 | 12 | struct ContentView: View { 13 | @ObservedObject var authService = AuthService() 14 | @State var userExists:Bool = false 15 | @State var user: User? 16 | 17 | var body: some View { 18 | ZStack{ 19 | if self.user == nil { 20 | LoginView().transition(.slide) 21 | } else { 22 | if self.userExists { 23 | ProjectListView(authService: authService).transition(.slide) 24 | } else { 25 | EditNameView(authService: authService).transition(.slide) 26 | } 27 | } 28 | }.onReceive(authService.$userExists){ userExists in 29 | withAnimation{ 30 | self.userExists = userExists 31 | } 32 | }.onReceive(authService.$user){ user in 33 | withAnimation{ 34 | self.user = user 35 | } 36 | } 37 | } 38 | } 39 | 40 | struct ContentView_Previews: PreviewProvider { 41 | static var previews: some View { 42 | LoginView() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tasky/Views/EmailSignInForm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewProjectSheet.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EmailSignInForm: View { 11 | @State var question: String = "" 12 | @State var answer: String = "" 13 | @Environment(\.presentationMode) var presentationMode 14 | 15 | var body: some View { 16 | VStack(alignment: .center, spacing: 30) { 17 | VStack(alignment: .leading, spacing: 10) { 18 | Text("Email") 19 | .foregroundColor(.gray) 20 | TextField("Enter the email", text: $question) 21 | .textFieldStyle(RoundedBorderTextFieldStyle()) 22 | } 23 | VStack(alignment: .leading, spacing: 10) { 24 | Text("Password") 25 | .foregroundColor(.gray) 26 | TextField("Enter the password", text: $answer) 27 | .textFieldStyle(RoundedBorderTextFieldStyle()) 28 | } 29 | 30 | Button(action: addProject) { 31 | Text("Sign In") 32 | .foregroundColor(.blue) 33 | } 34 | Spacer() 35 | } 36 | .padding(EdgeInsets(top: 80, leading: 40, bottom: 0, trailing: 40)) 37 | } 38 | 39 | private func addProject() { 40 | 41 | presentationMode.wrappedValue.dismiss() 42 | } 43 | } 44 | 45 | struct EmailSignInForm_Previews: PreviewProvider { 46 | static var previews: some View { 47 | EmailSignInForm() 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /Tasky/Views/EditNameView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailSheet.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/10/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct EditNameView: View { 11 | @ObservedObject var authService: AuthService 12 | @State var firstName:String = "" 13 | @State var lastName:String = "" 14 | 15 | var body: some View { 16 | Form{ 17 | Section{ 18 | TextField("First Name", text: $firstName) 19 | TextField("Last Name", text: $lastName) 20 | } 21 | Button(action: saveChnages) { 22 | Text("Save") 23 | .foregroundColor(.blue) 24 | } 25 | }.onAppear(perform: { 26 | guard let givenName = UserDefaults.standard.string(forKey: "firstName") else { return } 27 | let familyName = UserDefaults.standard.string(forKey: "lastName") ?? "I" 28 | 29 | firstName = givenName 30 | lastName = familyName 31 | saveChnages() 32 | }) 33 | } 34 | 35 | func saveChnages(){ 36 | guard !firstName.isEmpty && !lastName.isEmpty else { 37 | return 38 | } 39 | 40 | authService.updateUserName(firstName: firstName, lastName: lastName) 41 | } 42 | } 43 | 44 | struct UserDetailSheet_Previews: PreviewProvider { 45 | static var previews: some View { 46 | EmptyView() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tasky/Views/Components/ImagePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePicker.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/15/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct ImagePicker: UIViewControllerRepresentable { 12 | 13 | class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { 14 | let parent: ImagePicker 15 | 16 | init(_ parent: ImagePicker) { 17 | self.parent = parent 18 | } 19 | 20 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { 21 | if let uiImage = info[.originalImage] as? UIImage { 22 | parent.image = uiImage 23 | } 24 | 25 | parent.presentationMode.wrappedValue.dismiss() 26 | } 27 | } 28 | 29 | @Environment(\.presentationMode) var presentationMode 30 | @Binding var image: UIImage? 31 | 32 | func makeCoordinator() -> Coordinator { 33 | Coordinator(self) 34 | } 35 | 36 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController { 37 | let picker = UIImagePickerController() 38 | picker.delegate = context.coordinator 39 | return picker 40 | } 41 | 42 | func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext) { 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tasky/Views/Sheets/UpdateNameSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateNameSheet.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/15/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UpdateNameSheet: View { 11 | @Environment(\.presentationMode) var presentationMode 12 | @ObservedObject var authService: AuthService 13 | @ObservedObject var userService: UserService 14 | @State var firstName:String = "" 15 | @State var lastName:String = "" 16 | 17 | var body: some View { 18 | Form{ 19 | Section{ 20 | TextField("First Name", text: $firstName) 21 | TextField("Last Name", text: $lastName) 22 | } 23 | Button(action: saveChnages) { 24 | Text("Save") 25 | .foregroundColor(.blue) 26 | } 27 | }.onAppear(perform: { 28 | firstName = userService.user?.firstName ?? "" 29 | lastName = userService.user?.lastName ?? "" 30 | }) 31 | } 32 | 33 | func saveChnages(){ 34 | guard !firstName.isEmpty && !lastName.isEmpty else { 35 | return 36 | } 37 | 38 | UserService.cache.removeValue(forKey: authService.user!.uid) 39 | 40 | authService.updateUserName(firstName: firstName, lastName: lastName) 41 | 42 | presentationMode.wrappedValue.dismiss() 43 | } 44 | } 45 | 46 | struct UpdateNameSheet_Previews: PreviewProvider { 47 | static var previews: some View { 48 | EmptyView() 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /TaskyUITests/TaskyUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskyUITests.swift 3 | // TaskyUITests 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import XCTest 9 | 10 | class TaskyUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tasky/Views/Components/Avatar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Avatar.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/16/21. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | 11 | struct Avatar: View, Equatable { 12 | static func == (lhs: Avatar, rhs: Avatar) -> Bool { 13 | return lhs.userId == rhs.userId && lhs.avatarService.avatarUrl?.absoluteURL == rhs.avatarService.avatarUrl?.absoluteURL 14 | } 15 | 16 | @ObservedObject var avatarService: AvatarService = AvatarService() 17 | let userId:String 18 | 19 | init(userId: String) { 20 | print("init Avatar") 21 | self.userId = userId 22 | avatarService.fetchAvatar(userId: userId) 23 | } 24 | 25 | var body: some View { 26 | WebImage(url: avatarService.avatarUrl) 27 | // Supports options and context, like `.delayPlaceholder` to show placeholder only when error 28 | .onSuccess { image, data, cacheType in 29 | } 30 | .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size 31 | .placeholder(Image("placeholder")) // Placeholder Image 32 | .transition(.fade(duration: 0.5)) // Fade Transition with duration 33 | .scaledToFill() 34 | .background(Color.white) 35 | .clipShape(Circle()) 36 | .frame(width: 40, height: 40, alignment: .center) 37 | } 38 | } 39 | 40 | struct Avatar_Previews: PreviewProvider { 41 | static var previews: some View { 42 | EmptyView() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tasky/ViewModels/ProjectListViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectListViewModel.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import Combine 11 | 12 | 13 | class ProjectListViewModel: ObservableObject { 14 | 15 | @Published var projectViewModels: [ProjectViewModel] = [] 16 | 17 | private var cancellables: Set = [] 18 | 19 | @Published var projectRepository:ProjectRepository 20 | 21 | init(authService: AuthService) { 22 | self.projectRepository = ProjectRepository(authService: authService) 23 | 24 | projectRepository.$projects.map { projects in 25 | return projects.map(ProjectViewModel.init).sorted { (lfs, rhs) -> Bool in 26 | let res = lfs.project.name.compare(rhs.project.name, options: NSString.CompareOptions.caseInsensitive, range: nil, locale: nil) 27 | if res == .orderedAscending { 28 | return true 29 | } else { 30 | return false 31 | } 32 | } 33 | } 34 | .assign(to: \.projectViewModels, on: self) 35 | .store(in: &cancellables) 36 | } 37 | 38 | func add(_ project: Project) { 39 | ProjectRepository.add(project) 40 | } 41 | 42 | func delete(project: Project){ 43 | projectRepository.remove(project) 44 | } 45 | 46 | func remove(id: String){ 47 | projectViewModels.removeAll { model in 48 | if model.project.id == id { 49 | return true 50 | } 51 | return false 52 | } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Tasky/Views/Sheets/NewProjectSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewProjectSheet.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NewProjectSheet: View { 11 | @State var projectName: String = "" 12 | 13 | @Environment(\.presentationMode) var presentationMode 14 | @ObservedObject var projectListViewModel: ProjectListViewModel 15 | 16 | var body: some View { 17 | VStack(alignment: .center) { 18 | Indicator().padding() 19 | // Text("Name") 20 | // .foregroundColor(.gray) 21 | Color.clear.frame(height: 36) 22 | 23 | TextField("Enter the project name", text: $projectName) 24 | .textFieldStyle(RoundedBorderTextFieldStyle()) 25 | 26 | Color.clear.frame(height: 36) 27 | 28 | Button(action: addProject) { 29 | Text("Add New Project") 30 | .foregroundColor(.blue) 31 | } 32 | Spacer() 33 | } 34 | .padding(EdgeInsets(top: 0, leading: 40, bottom: 0, trailing: 40)) 35 | } 36 | 37 | private func addProject() { 38 | guard !projectName.isEmpty else { return } 39 | 40 | let project = Project(name: projectName, tasks: [], managerId: AuthService.currentUser?.uid, timestamp: Date().timeIntervalSince1970) 41 | 42 | projectListViewModel.add(project) 43 | 44 | presentationMode.wrappedValue.dismiss() 45 | } 46 | } 47 | 48 | struct NewProjectForm_Previews: PreviewProvider { 49 | static var previews: some View { 50 | EmptyView() 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /Tasky/Views/UpdateProjectForm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewProjectSheet.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UpdateProjectForm: View { 11 | @State var projectName: String = "" 12 | 13 | @Environment(\.presentationMode) var presentationMode 14 | @ObservedObject var projectViewModel: ProjectViewModel 15 | 16 | init(projectViewModel: ProjectViewModel) { 17 | self.projectViewModel = projectViewModel 18 | } 19 | 20 | var body: some View { 21 | VStack(alignment: .center, spacing: 30) { 22 | VStack(alignment: .leading, spacing: 10) { 23 | Text("Name") 24 | .foregroundColor(.gray) 25 | TextField("Enter the project name", text: $projectName) 26 | .textFieldStyle(RoundedBorderTextFieldStyle()) 27 | } 28 | 29 | Button(action: updateProject) { 30 | Text("Save Changes") 31 | .foregroundColor(.blue) 32 | } 33 | Spacer() 34 | } 35 | .padding(EdgeInsets(top: 80, leading: 40, bottom: 0, trailing: 40)) 36 | .onAppear(perform: { 37 | self.projectName = projectViewModel.project.name 38 | }) 39 | } 40 | 41 | private func updateProject() { 42 | guard !projectName.isEmpty else { 43 | return 44 | } 45 | 46 | projectViewModel.update(withNewName: projectName.trimmingCharacters(in: .whitespaces)) 47 | 48 | presentationMode.wrappedValue.dismiss() 49 | } 50 | } 51 | 52 | struct UpdateProjectForm_Previews: PreviewProvider { 53 | static var previews: some View { 54 | 55 | //UpdateProjectForm(projectViewModel: ProjectViewModel(project: testProject)) 56 | Text("") 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /Tasky/Views/Sheets/NewTagSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewTagSheet.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/19/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NewTagSheet: View { 11 | @Environment(\.presentationMode) var presentationMode 12 | @ObservedObject var projectViewModel: ProjectViewModel 13 | @State var label: String = "" 14 | @State var colorString: String = "Blue" 15 | 16 | var body: some View { 17 | NavigationView{ 18 | Form{ 19 | TextField("Tag label", text: $label) 20 | Picker(selection: $colorString, label: Text("Tag color"), content: { 21 | Color("Black").frame(width: 24, height: 24).tag("Black") 22 | Color("Blue").frame(width: 24, height: 24).tag("Blue") 23 | Color("Gray").frame(width: 24, height: 24).tag("Gray") 24 | Color("Green").frame(width: 24, height: 24).tag("Green") 25 | Color("Indigo").frame(width: 24, height: 24).tag("Indigo") 26 | Color("Pink").frame(width: 24, height: 24).tag("Pink") 27 | Color("Purple").frame(width: 24, height: 24).tag("Purple") 28 | Color("Red").frame(width: 24, height: 24).tag("Red") 29 | Color("Teal").frame(width: 24, height: 24).tag("Teal") 30 | Color("Yellow").frame(width: 24, height: 24).tag("Yellow") 31 | }) 32 | Button(action: addTag, label: { 33 | Text("Add tag") 34 | }) 35 | }.padding(EdgeInsets(top: 24.0, leading: 0.0, bottom: 0.0, trailing: 0.0)).navigationBarTitle("").navigationBarHidden(true) 36 | }.navigationViewStyle(StackNavigationViewStyle()) 37 | } 38 | 39 | func addTag(){ 40 | guard !label.isEmpty else { return } 41 | 42 | projectViewModel.addTag(label: label, colorString: colorString) 43 | 44 | presentationMode.wrappedValue.dismiss() 45 | } 46 | } 47 | 48 | struct NewTagSheet_Previews: PreviewProvider { 49 | static var previews: some View { 50 | EmptyView() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Tasky/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleURLTypes 20 | 21 | 22 | CFBundleTypeRole 23 | Editor 24 | CFBundleURLSchemes 25 | 26 | com.googleusercontent.apps.8232031565-d9n6vuqb0rdjbr6jq0mj90mjm347pgfv 27 | 28 | 29 | 30 | CFBundleVersion 31 | $(CURRENT_PROJECT_VERSION) 32 | LSRequiresIPhoneOS 33 | 34 | UIAppFonts 35 | 36 | Font Awesome 5 Pro-Light-300.otf 37 | Font Awesome 5 Pro-Regular-400.otf 38 | Font Awesome 5 Pro-Solid-900.otf 39 | Font Awesome 5 Brands-Regular-400.otf 40 | 41 | UIApplicationSceneManifest 42 | 43 | UIApplicationSupportsMultipleScenes 44 | 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | UILaunchScreen 49 | 50 | UIRequiredDeviceCapabilities 51 | 52 | armv7 53 | 54 | UISupportedInterfaceOrientations 55 | 56 | UIInterfaceOrientationPortrait 57 | 58 | UISupportedInterfaceOrientations~ipad 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationPortraitUpsideDown 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /Tasky/Views/Sheets/NewTaskSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewProjectSheet.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct NewTaskSheet: View { 11 | @State var title: String = "" 12 | @State var content: String = "" 13 | @State var selectedDate: Date = Date().advanced(by: 86400) //86400 seconds == 1 day 14 | @State var enableDueDate: Bool = false 15 | @Environment(\.presentationMode) var presentationMode 16 | @ObservedObject var projectViewModel: ProjectViewModel 17 | 18 | var body: some View { 19 | NavigationView{ 20 | Form{ 21 | Section{ 22 | TextField("Title", text: $title) 23 | TextEditor(text: $content).frame(height: 120) 24 | } 25 | 26 | Section{ 27 | Toggle(isOn: $enableDueDate, label: { 28 | Text("With Due Date") 29 | }) 30 | if enableDueDate { 31 | DatePicker("", selection: $selectedDate) 32 | } 33 | } 34 | 35 | Button(action: addTask) { 36 | Text("Add New Task") 37 | .foregroundColor(.blue) 38 | } 39 | }.padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 0.0)).navigationBarTitle("").navigationBarHidden(true) 40 | }.navigationViewStyle(StackNavigationViewStyle()) 41 | } 42 | 43 | private func addTask() { 44 | if self.title.isEmpty { 45 | return 46 | } 47 | 48 | let task = Task(id: UUID().uuidString, title: title, content: content, taskStatus: .awaiting, timestamp: NSDate().timeIntervalSince1970, dueTimestamp: enableDueDate ? selectedDate.timeIntervalSince1970: nil, creatorId: AuthService.currentUser?.uid) 49 | 50 | projectViewModel.addTask(task: task) 51 | 52 | presentationMode.wrappedValue.dismiss() 53 | } 54 | } 55 | 56 | struct NewTaskForm_Previews: PreviewProvider { 57 | static var previews: some View { 58 | EmptyView() 59 | // NewTaskSheet(projectViewModel: ProjectViewModel(project: Project(id: "", name: "", tasks: [], managerId: "",timestamp: Date().timeIntervalSince1970), projectRepo: ProjectRepository)) 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /Tasky/Persistence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Persistence.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import CoreData 9 | 10 | struct PersistenceController { 11 | static let shared = PersistenceController() 12 | 13 | static var preview: PersistenceController = { 14 | let result = PersistenceController(inMemory: true) 15 | let viewContext = result.container.viewContext 16 | for _ in 0..<10 { 17 | let newItem = Item(context: viewContext) 18 | newItem.timestamp = Date() 19 | } 20 | do { 21 | try viewContext.save() 22 | } catch { 23 | // Replace this implementation with code to handle the error appropriately. 24 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 25 | let nsError = error as NSError 26 | fatalError("Unresolved error \(nsError), \(nsError.userInfo)") 27 | } 28 | return result 29 | }() 30 | 31 | let container: NSPersistentCloudKitContainer 32 | 33 | init(inMemory: Bool = false) { 34 | container = NSPersistentCloudKitContainer(name: "Tasky") 35 | if inMemory { 36 | container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") 37 | } 38 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 39 | if let error = error as NSError? { 40 | // Replace this implementation with code to handle the error appropriately. 41 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 42 | 43 | /* 44 | Typical reasons for an error here include: 45 | * The parent directory does not exist, cannot be created, or disallows writing. 46 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 47 | * The device is out of space. 48 | * The store could not be migrated to the current model version. 49 | Check the error message to determine what the actual problem was. 50 | */ 51 | fatalError("Unresolved error \(error), \(error.userInfo)") 52 | } 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tasky/Views/PhoneLoginView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhoneLoginView.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/3/21. 6 | // 7 | 8 | import SwiftUI 9 | import iPhoneNumberField 10 | import FirebaseAuth 11 | 12 | struct PhoneLoginView: View { 13 | @State var phoneNumber = "" 14 | @State var verificationCode = "" 15 | @State var reqestSent: Bool = false 16 | 17 | var body: some View { 18 | if reqestSent { 19 | Form{ 20 | TextField("6-Digit Code", text: $verificationCode).keyboardType(.numberPad).onReceive( verificationCode.publisher.collect()) { 21 | self.verificationCode = String($0.prefix(6)) 22 | } 23 | 24 | Button(action: { 25 | guard !verificationCode.isEmpty else { return } 26 | 27 | let verificationID = UserDefaults.standard.string(forKey: "authVerificationID") 28 | let credential = PhoneAuthProvider.provider().credential( 29 | withVerificationID: verificationID!, 30 | verificationCode: verificationCode) 31 | AuthService.signIn(withCredential: credential) 32 | }, label: { 33 | Text("Sign in") 34 | }) 35 | } 36 | }else { 37 | iPhoneNumberField(text: $phoneNumber) 38 | .maximumDigits(10) 39 | .prefixHidden(false) 40 | .flagHidden(true) 41 | .flagSelectable(false) 42 | .font(UIFont(size: 30, weight: .bold, design: .rounded)) 43 | .padding() 44 | Button(action: { 45 | print("the phone number is \(phoneNumber)") 46 | phoneNumber = "+1" + phoneNumber.trimmingCharacters(in: ["(", ")","-"]) 47 | print("the phone number is \(phoneNumber)") 48 | reqestSent.toggle() 49 | PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate: nil) { (verificationID, error) in 50 | if let error = error { 51 | print(error.localizedDescription) 52 | return 53 | } 54 | // Sign in using the verificationID and the code sent to the user 55 | // ... 56 | UserDefaults.standard.set(verificationID, forKey: "authVerificationID") 57 | } 58 | 59 | }, label: { 60 | Text("Continue") 61 | }) 62 | } 63 | } 64 | } 65 | 66 | struct PhoneLoginView_Previews: PreviewProvider { 67 | static var previews: some View { 68 | PhoneLoginView() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tasky/Views/Sheets/UpdateTaskSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdateTaskSheet.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/15/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct UpdateTaskSheet: View { 11 | @State var title: String = "" 12 | @State var content: String = "" 13 | @State var selectedDate: Date = Date().advanced(by: 86400) //86400 seconds == 1 day 14 | @State var enableDueDate: Bool = false 15 | @State var task:Task = testTask 16 | @Environment(\.presentationMode) var presentationMode 17 | @ObservedObject var projectViewModel: ProjectViewModel 18 | 19 | init(projectViewModel: ProjectViewModel) { 20 | self.projectViewModel = projectViewModel 21 | } 22 | 23 | var body: some View { 24 | NavigationView{ 25 | Form{ 26 | Section{ 27 | TextField("Title", text: $title) 28 | TextEditor(text: $content).frame(height: 120) 29 | } 30 | 31 | Section{ 32 | Toggle(isOn: $enableDueDate, label: { 33 | Text("With Due Date") 34 | }) 35 | if enableDueDate { 36 | DatePicker("", selection: $selectedDate) 37 | } 38 | } 39 | 40 | Button(action: updateTask) { 41 | Text("Save changes") 42 | .foregroundColor(.blue) 43 | } 44 | }.padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 0.0)).navigationBarTitle("").navigationBarHidden(true).onAppear(perform: { 45 | self.task = projectViewModel.selectedTask 46 | self.title = task.title 47 | self.content = task.content 48 | if task.dueTimestamp == nil { 49 | enableDueDate = false 50 | } else { 51 | enableDueDate = true 52 | selectedDate = Date(timeIntervalSince1970: task.dueTimestamp!) 53 | } 54 | }) 55 | }.navigationViewStyle(StackNavigationViewStyle()) 56 | } 57 | 58 | private func updateTask() { 59 | if self.title.isEmpty { 60 | return 61 | } 62 | 63 | let updatedTask = Task(id: self.task.id, title: title, content: content, taskStatus: self.task.taskStatus, timestamp: self.task.timestamp, dueTimestamp: enableDueDate ? selectedDate.timeIntervalSince1970: nil, creatorId: self.task.creatorId, tags: self.task.tags) 64 | 65 | projectViewModel.updateTask(task: updatedTask) 66 | 67 | presentationMode.wrappedValue.dismiss() 68 | } 69 | } 70 | 71 | struct UpdateTaskSheet_Previews: PreviewProvider { 72 | static var previews: some View { 73 | EmptyView() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tasky ![48](https://user-images.githubusercontent.com/7277662/108041817-eaaeea80-6ff3-11eb-9d05-8da57896cd6b.png) 2 | 3 | 4 | ![iOS](https://img.shields.io/badge/iOS-14%20-blue) 5 | [![App Store](https://img.shields.io/itunes/v/1552534120?label=App%20Store)](https://apps.apple.com/us/app/tasky-task-made-easy/id1552534120) 6 | [![App Store](https://img.shields.io/badge/Price-Free-orange)](https://img.shields.io/badge/Price-Free-orange) 7 | [![Visits Badge](https://badges.pufler.dev/visits/livinglist/Tasky)](https://badges.pufler.dev) 8 | [![GitHub](https://img.shields.io/github/stars/livinglist/Tasky?style=social)](https://img.shields.io/github/stars/livinglist/Tasky?style=social) 9 | 10 | 11 |

12 | Screen Shot 2020-03-03 at 1 22 57 PM 13 | Screen Shot 2020-03-03 at 1 22 57 PM 14 | Screen Shot 2020-03-03 at 1 22 57 PM 15 | Screen Shot 2020-03-03 at 1 22 57 PM 16 | Screen Shot 2020-08-20 at 6 16 26 PM 17 | Screen Shot 2020-08-20 at 6 16 26 PM 18 | Screen Shot 2020-08-20 at 6 16 43 PM 19 | Screen Shot 2020-08-20 at 6 21 33 PM 20 | Screen Shot 2020-03-03 at 1 22 57 PM 21 | Screen Shot 2020-08-20 at 6 21 48 PM 22 |

23 | 24 | 25 | Tasky is a task management app made with SwiftUI that allows user to manage their personal project using Kanban method. 26 | 27 | What you can do now in Tasky: 28 | - Sign in with Apple or phone number to sync user-generated data. 29 | - Create projects. 30 | - Create tasks with title, content and a due data. 31 | - Change status of tasks. 32 | - Search for other users using username. 33 | - Add other users as collaborators in projects. 34 | - Add tags to task. 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Firebase ### 2 | .idea 3 | **/node_modules/* 4 | **/.firebaserc 5 | 6 | ### Firebase Patch ### 7 | .runtimeconfig.json 8 | .firebase/ 9 | GoogleService-Info.plist 10 | 11 | ### Swift ### 12 | # Xcode 13 | # 14 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 15 | 16 | ## User settings 17 | xcuserdata/ 18 | 19 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 20 | *.xcscmblueprint 21 | *.xccheckout 22 | 23 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 24 | build/ 25 | DerivedData/ 26 | *.moved-aside 27 | *.pbxuser 28 | !default.pbxuser 29 | *.mode1v3 30 | !default.mode1v3 31 | *.mode2v3 32 | !default.mode2v3 33 | *.perspectivev3 34 | !default.perspectivev3 35 | 36 | ## Obj-C/Swift specific 37 | *.hmap 38 | 39 | ## App packaging 40 | *.ipa 41 | *.dSYM.zip 42 | *.dSYM 43 | 44 | ## Playgrounds 45 | timeline.xctimeline 46 | playground.xcworkspace 47 | 48 | # Swift Package Manager 49 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 50 | # Packages/ 51 | # Package.pins 52 | # Package.resolved 53 | # *.xcodeproj 54 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 55 | # hence it is not needed unless you have added a package configuration file to your project 56 | # .swiftpm 57 | 58 | .build/ 59 | 60 | # CocoaPods 61 | # We recommend against adding the Pods directory to your .gitignore. However 62 | # you should judge for yourself, the pros and cons are mentioned at: 63 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 64 | Pods/ 65 | # Add this line if you want to avoid checking in source code from the Xcode workspace 66 | # *.xcworkspace 67 | 68 | # Carthage 69 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 70 | # Carthage/Checkouts 71 | 72 | Carthage/Build/ 73 | 74 | # Accio dependency management 75 | Dependencies/ 76 | .accio/ 77 | 78 | # fastlane 79 | # It is recommended to not store the screenshots in the git repo. 80 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 81 | # For more information about the recommended setup visit: 82 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 83 | 84 | fastlane/report.xml 85 | fastlane/Preview.html 86 | fastlane/screenshots/**/*.png 87 | fastlane/test_output 88 | 89 | # Code Injection 90 | # After new code Injection tools there's a generated folder /iOSInjectionProject 91 | # https://github.com/johnno1962/injectionforxcode 92 | 93 | iOSInjectionProject/ 94 | 95 | ### Xcode ### 96 | # Xcode 97 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 98 | 99 | 100 | 101 | 102 | ## Gcc Patch 103 | /*.gcno 104 | 105 | ### Xcode Patch ### 106 | *.xcodeproj/* 107 | !*.xcodeproj/project.pbxproj 108 | !*.xcodeproj/xcshareddata/ 109 | !*.xcworkspace/contents.xcworkspacedata 110 | **/xcshareddata/WorkspaceSettings.xcsettings 111 | 112 | # End of https://www.toptal.com/developers/gitignore/api/swift,xcode,firebase -------------------------------------------------------------------------------- /Tasky/Views/ProjectView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectView.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProjectView: View{ 11 | @ObservedObject var projectViewModel: ProjectViewModel 12 | @ObservedObject var projectListViewModel: ProjectListViewModel 13 | @State var showContent: Bool = false 14 | @State var viewState = CGSize.zero 15 | @State var showAlert = false 16 | @State var progressValue:Float 17 | 18 | init(projectViewModel: ProjectViewModel, projectListViewModel: ProjectListViewModel) { 19 | self.projectViewModel = projectViewModel 20 | self.projectListViewModel = projectListViewModel 21 | let completedCount = Float(projectViewModel.project.tasks.filter { task -> Bool in 22 | if task.taskStatus == .completed { 23 | return true 24 | } 25 | return false 26 | }.count) 27 | let total = Float(projectViewModel.project.tasks.count) 28 | let val = completedCount/total 29 | self._progressValue = State(initialValue: val) 30 | } 31 | 32 | var body: some View { 33 | NavigationLink(destination: TaskListView(projectListViewModel: projectListViewModel,projectViewModel: projectViewModel, onDelete: { project in 34 | print("first layer of ondelete") 35 | projectListViewModel.delete(project: project) 36 | })){ 37 | GeometryReader { geometry in 38 | VStack(alignment: .leading) { 39 | HStack{ 40 | Text("\(projectViewModel.project.name)").font(.title).foregroundColor(.black).lineLimit(1) 41 | Spacer() 42 | }.padding(.leading, 12).padding(.top, 8) 43 | HStack{ 44 | Text("\(projectViewModel.project.tasks.count) tasks").font(.body).foregroundColor(.black).opacity(0.8) 45 | Spacer() 46 | Image(systemName: "chevron.right").foregroundColor(Color(.systemGray4)).imageScale(.small).padding(.trailing, 12) 47 | }.padding(.leading, 12) 48 | Spacer() 49 | ScrollView(.horizontal){ 50 | HStack{ 51 | Avatar(userId: projectViewModel.project.managerId!).equatable() 52 | ForEach(projectViewModel.project.collaboratorIds ?? [], id: \.self){ id in 53 | Avatar(userId: id).equatable().padding(.leading, 0) 54 | } 55 | }.padding([.leading, .bottom], 12) 56 | } 57 | //ProgressBar(value: $progressValue, color: Color(.)).frame(height: 24).padding() 58 | } 59 | .frame(width: geometry.size.width, height: 120) 60 | .background(Color.orange) 61 | .cornerRadius(8) 62 | .shadow(color: Color(.orange).opacity(0.3), radius: 3, x: 2, y: 2) 63 | } 64 | }.navigationViewStyle(StackNavigationViewStyle()) 65 | } 66 | } 67 | 68 | struct ProjectView_Previews: PreviewProvider { 69 | static var previews: some View { 70 | //let project = testData[0] 71 | // return ProjectView(projectViewModel: ProjectViewModel(project: project)) 72 | return Text("") 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /Tasky/Services/AvatarService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AvatarService.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/15/21. 6 | // 7 | 8 | import Foundation 9 | import Firebase 10 | import FirebaseStorage 11 | 12 | class AvatarService: ObservableObject { 13 | @Published var avatarUrl: URL? 14 | 15 | func fetchAvatar(userId: String){ 16 | if AvatarService.cache.keys.contains(userId) { 17 | self.avatarUrl = AvatarService.cache[userId]! 18 | return 19 | } 20 | 21 | let storage = Storage.storage() 22 | let storageRef = storage.reference() 23 | 24 | // Child references can also take paths delimited by '/' 25 | // spaceRef now points to "images/space.jpg" 26 | // imagesRef still points to "images" 27 | let spaceRef = storageRef.child("images/\(userId).jpg") 28 | 29 | 30 | spaceRef.downloadURL { url , err in 31 | guard let downloadURL = url else { 32 | print(err!.localizedDescription) 33 | UserDefaults.standard.setValue(true, forKey: "AvatarUpdated") 34 | AvatarService.cache[userId] = URL(string: "https://avatars.dicebear.com/4.5/api/jdenticon/\(userId).svg")! 35 | self.avatarUrl = AvatarService.cache[userId] 36 | return 37 | } 38 | 39 | UserDefaults.standard.setValue(true, forKey: "AvatarUpdated") 40 | AvatarService.cache[userId] = downloadURL.absoluteURL 41 | self.avatarUrl = downloadURL.absoluteURL 42 | } 43 | } 44 | 45 | func uploadAvatar(data: Data, userId: String){ 46 | let storage = Storage.storage() 47 | let storageRef = storage.reference() 48 | 49 | // Child references can also take paths delimited by '/' 50 | // spaceRef now points to "images/space.jpg" 51 | // imagesRef still points to "images" 52 | let spaceRef = storageRef.child("images/\(userId).jpg") 53 | 54 | spaceRef.putData(data, metadata: nil) { (metadata, err) in 55 | spaceRef.downloadURL { url, err in 56 | guard let downloadURL = url else { 57 | print(err!.localizedDescription) 58 | AvatarService.cache[userId] = URL(string: "https://avatars.dicebear.com/4.5/api/jdenticon/\(userId).svg")! 59 | self.avatarUrl = AvatarService.cache[userId] 60 | return 61 | } 62 | 63 | AvatarService.cache[userId] = downloadURL.absoluteURL 64 | self.avatarUrl = downloadURL.absoluteURL 65 | } 66 | } 67 | } 68 | 69 | func deleteAvatar(userId: String){ 70 | let storage = Storage.storage() 71 | let storageRef = storage.reference() 72 | 73 | // Child references can also take paths delimited by '/' 74 | // spaceRef now points to "images/space.jpg" 75 | // imagesRef still points to "images" 76 | let spaceRef = storageRef.child("images/\(userId).jpg") 77 | 78 | spaceRef.delete { err in 79 | if let err = err { 80 | print("\(err.localizedDescription)") 81 | return 82 | } 83 | 84 | AvatarService.cache[userId] = URL(string: "https://avatars.dicebear.com/4.5/api/jdenticon/\(userId).svg")! 85 | self.avatarUrl = AvatarService.cache[userId] 86 | } 87 | } 88 | } 89 | 90 | extension AvatarService{ 91 | static var cache: [String: URL] = [:] 92 | } 93 | -------------------------------------------------------------------------------- /Tasky/Views/ProjectListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectListView.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | import FASwiftUI 11 | 12 | struct ProjectListView: View { 13 | @ObservedObject var authService: AuthService 14 | @ObservedObject var userService: UserService = UserService() 15 | @ObservedObject var projectListViewModel:ProjectListViewModel 16 | @State var showForm = false 17 | @State var showAlert = false 18 | @State var showProfileSheet = false 19 | @State var fullName = "" 20 | 21 | init(authService: AuthService) { 22 | self.authService = authService 23 | self.projectListViewModel = ProjectListViewModel(authService: authService) 24 | guard let uid = authService.user?.uid else { 25 | return 26 | } 27 | self.userService.fetchUserBy(id: uid) 28 | } 29 | 30 | var leadingItem: some View { 31 | HStack{ 32 | Menu(content: { 33 | Button(action: { showProfileSheet = true }) { 34 | Text("Profile") 35 | Image(systemName: "person.crop.square") 36 | } 37 | Divider() 38 | Button(action: { showAlert = true }) { 39 | Text("Sign Out") 40 | Image(systemName: "figure.walk") 41 | } 42 | }, label: { 43 | if authService.user != nil { 44 | Avatar(userId: authService.user!.uid).equatable() 45 | } 46 | }).frame(width: 40, height: 40) 47 | Text("\(fullName)") 48 | .font(.body) 49 | .foregroundColor(Color(.systemGray)) 50 | }.sheet(isPresented: $showProfileSheet){ 51 | ProfileSheet(authService: self.authService, userService: self.userService) 52 | } 53 | 54 | } 55 | 56 | var body: some View { 57 | NavigationView { 58 | VStack{ 59 | GeometryReader { geometry in 60 | ScrollView(.vertical) { 61 | VStack(spacing: 24) { 62 | ForEach(projectListViewModel.projectViewModels) { projectViewModel in 63 | ProjectView(projectViewModel: projectViewModel, projectListViewModel: projectListViewModel) 64 | .padding([.leading, .trailing]).padding(.bottom, 12).transition(.slide).animation(.easeIn) 65 | } 66 | }.frame(width: geometry.size.width, height: 124.0*CGFloat(projectListViewModel.projectViewModels.count)) 67 | } 68 | } 69 | } 70 | .sheet(isPresented: $showForm) { 71 | NewProjectSheet(projectListViewModel: projectListViewModel) 72 | } 73 | .navigationBarTitle("My Projects") 74 | .navigationBarItems(leading: leadingItem, 75 | trailing: 76 | Button(action: { showForm.toggle() }) { 77 | FAText(iconName: "plus", size: 26) 78 | }) 79 | }.navigationBarBackButtonHidden(true) 80 | .navigationViewStyle(StackNavigationViewStyle()) 81 | .alert(isPresented: $showAlert) { 82 | Alert(title: Text("Sign out?"), message: Text(""), primaryButton: .default(Text("Cancel")), secondaryButton: .destructive(Text("Yes"), action: { 83 | showAlert = false 84 | do { 85 | try AuthService.signOut() 86 | 87 | } catch { 88 | 89 | } 90 | })) 91 | }.onReceive(userService.$user, perform: { user in 92 | fullName = ((user?.firstName ?? "") + " " + (user?.lastName ?? "")).trimmingCharacters(in: .whitespaces) 93 | }) 94 | } 95 | } 96 | 97 | struct ProjectListView_Previews: PreviewProvider { 98 | static var previews: some View { 99 | EmptyView() 100 | } 101 | } 102 | 103 | -------------------------------------------------------------------------------- /Tasky/Views/Sheets/ProfileSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileSheet.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/10/21. 6 | // 7 | 8 | import SwiftUI 9 | import SDWebImageSwiftUI 10 | 11 | fileprivate enum ActiveSheet: Identifiable { 12 | case updateNameSheet, imagePickerSheet 13 | 14 | var id: Int { 15 | hashValue 16 | } 17 | } 18 | 19 | struct ProfileSheet: View { 20 | @ObservedObject var authService: AuthService 21 | @ObservedObject var userService: UserService 22 | @ObservedObject var avatarService: AvatarService = AvatarService() 23 | @State fileprivate var activeSheet: ActiveSheet? 24 | @State var image: Image? 25 | @State private var inputImage: UIImage? 26 | var imageSelected:Bool { 27 | self.inputImage != nil 28 | } 29 | 30 | init(authService: AuthService, userService: UserService) { 31 | self.authService = authService 32 | self.userService = userService 33 | self.avatarService.fetchAvatar(userId: authService.user?.uid ?? "") 34 | } 35 | 36 | var body: some View { 37 | VStack{ 38 | Indicator().padding() 39 | Menu(content: { 40 | Button(action: { activeSheet = .imagePickerSheet }) { 41 | Text("Edit") 42 | Image(systemName: "pencil.circle") 43 | } 44 | Divider() 45 | Button(action: { avatarService.deleteAvatar(userId: authService.user?.uid ?? "") }) { 46 | Text("Delete") 47 | Image(systemName: "trash") 48 | } 49 | }, label: { 50 | WebImage(url: avatarService.avatarUrl!) 51 | // Supports options and context, like `.delayPlaceholder` to show placeholder only when error 52 | .onSuccess { image, data, cacheType in 53 | } 54 | .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size 55 | .placeholder(Image("placeholder")) // Placeholder Image 56 | .transition(.fade(duration: 0.5)) // Fade Transition with duration 57 | .scaledToFill() 58 | .background(Color.white) 59 | .clipShape(Circle()) 60 | .frame(width: 160, height: 160, alignment: .center) 61 | }) 62 | Color.clear.frame(height: 24) 63 | ZStack(alignment: .topTrailing){ 64 | HStack{ 65 | Text("\(self.userService.user?.firstName ?? "")") 66 | .font(.title) 67 | Text("\(self.userService.user?.lastName ?? "")") 68 | .font(.title) 69 | } 70 | Button(action: { 71 | activeSheet = .updateNameSheet 72 | }) { 73 | Image(systemName: "pencil") 74 | .frame(width: 24, height: 24) 75 | .foregroundColor(Color.black) 76 | .background(Color.white) 77 | .clipShape(Circle()) 78 | .offset(x: 20, y: -12) 79 | .shadow(color: Color(.black).opacity(0.6), radius: 2, x: 1, y: 1) 80 | } 81 | } 82 | Spacer() 83 | }.sheet(item: $activeSheet, onDismiss: { 84 | if imageSelected { 85 | loadImage() 86 | } 87 | }){ item in 88 | switch item { 89 | case .imagePickerSheet: 90 | ImagePicker(image: self.$inputImage) 91 | case .updateNameSheet: 92 | UpdateNameSheet(authService: authService, userService: userService) 93 | } 94 | } 95 | } 96 | 97 | func loadImage() { 98 | print("loading image") 99 | guard let inputImage = inputImage else { return } 100 | guard let uid = authService.user?.uid else { return } 101 | let compressedData = inputImage.jpegData(compressionQuality: 0.5)! 102 | avatarService.uploadAvatar(data: compressedData, userId: uid) 103 | //image = Image(uiImage: inputImage) 104 | } 105 | } 106 | 107 | struct ProfileSheet_Previews: PreviewProvider { 108 | static var previews: some View { 109 | //ProfileView() 110 | EmptyView() 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Tasky/ViewModels/ProjectViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectViewModel.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | 12 | class ProjectViewModel: ObservableObject, Identifiable { 13 | @Published var project: Project 14 | 15 | var selectedTask: Task 16 | 17 | private var cancellables: Set = [] 18 | 19 | var id = "" 20 | 21 | init(project: Project) { 22 | self.project = project 23 | self.selectedTask = testTask 24 | 25 | $project 26 | .compactMap { $0.id } 27 | .assign(to: \.id, on: self) 28 | .store(in: &cancellables) 29 | } 30 | 31 | // MARK: - Operations on tasks 32 | func addTask(task: Task){ 33 | self.project.tasks.append(task) 34 | ProjectRepository.add(task: task, to: self.project) 35 | } 36 | 37 | func updateTask(task: Task){ 38 | let index = project.tasks.firstIndex(where: { t -> Bool in 39 | if task.id == t.id { 40 | return true 41 | } 42 | return false 43 | })! 44 | 45 | project.tasks.remove(at: index) 46 | 47 | let updatedTask = Task(id: task.id, title: task.title, content: task.content, taskStatus: task.taskStatus, timestamp: task.timestamp, dueTimestamp: task.dueTimestamp, creatorId: task.creatorId, assigneesId: task.assigneesId, tags: task.tags) 48 | project.tasks.insert(updatedTask, at: index) 49 | ProjectRepository.update(project, task: updatedTask) 50 | } 51 | 52 | func updateTaskStatus(withId id: String, to taskStatus: TaskStatus){ 53 | let index = project.tasks.firstIndex(where: { task -> Bool in 54 | if task.id == id { 55 | return true 56 | } 57 | return false 58 | })! 59 | 60 | let task = project.tasks.remove(at: index) 61 | 62 | let updatedTask = Task(id: task.id, title: task.title, content: task.content, taskStatus: taskStatus, timestamp: task.timestamp, dueTimestamp: task.dueTimestamp, creatorId: task.creatorId, assigneesId: task.assigneesId, tags: task.tags) 63 | project.tasks.insert(updatedTask, at: index) 64 | ProjectRepository.update(project, task: updatedTask) 65 | } 66 | 67 | func remove(task: Task){ 68 | guard let index = self.project.tasks.firstIndex(where: {$0.id == task.id}) else { 69 | return 70 | } 71 | self.project.tasks.remove(at: index) 72 | ProjectRepository.remove(task: task, from: self.project) 73 | } 74 | 75 | // MARK: - Operations on tags 76 | func addTag(label: String, colorString: String){ 77 | ProjectRepository.addTag(label: label, colorString: colorString, to: self.project) 78 | } 79 | 80 | func addTag(toTaskWithId id: String, label: String, colorString: String){ 81 | let index = project.tasks.firstIndex(where: { task -> Bool in 82 | if task.id == id { 83 | return true 84 | } 85 | return false 86 | })! 87 | 88 | let task = self.project.tasks.remove(at: index) 89 | 90 | ProjectRepository.addTag(to: task, in: self.project, label: label, colorString: colorString) 91 | } 92 | 93 | //Remove tag from the entire project. 94 | func removeTag(label: String){ 95 | ProjectRepository.removeTag(label: label, from: self.project) 96 | } 97 | 98 | //Remove tag from the task. 99 | func removeTag(label: String, from task: Task){ 100 | ProjectRepository.removeTag(label: label, from: task, in: self.project) 101 | } 102 | 103 | // MARK: - Operations on collaborators 104 | func addCollaborator(userId: String){ 105 | guard let projectId = self.project.id else { return } 106 | ProjectRepository.addCollaborator(userId: userId, to: projectId) 107 | } 108 | 109 | func removeCollaborator(userId: String){ 110 | guard let projectId = self.project.id else { return } 111 | ProjectRepository.removeCollaborator(userId: userId, from: projectId) 112 | } 113 | 114 | 115 | func update(withNewName newName: String) { 116 | self.project.name = newName 117 | ProjectRepository.update(self.project, withName: newName) 118 | } 119 | 120 | func selected(task: Task){ 121 | self.selectedTask = task 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Tasky/Views/Sheets/PeopleSheet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PeopleSheet.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/16/21. 6 | // 7 | 8 | import SwiftUI 9 | import SPAlert 10 | 11 | 12 | struct PeopleListView: View, Equatable{ 13 | static func == (lhs: PeopleListView, rhs: PeopleListView) -> Bool { 14 | lhs.users == rhs.users 15 | } 16 | 17 | let users: [TaskyUser] 18 | let onPressed: (TaskyUser)->() 19 | 20 | var body: some View { 21 | List{ 22 | ForEach(users) { user in 23 | HStack{ 24 | Avatar(userId: user.id) 25 | Text("\(user.firstName) \(user.lastName)") 26 | Spacer() 27 | Button(action: { 28 | onPressed(user) 29 | }, label: { 30 | Image(systemName: "plus.circle").font(.system(size: 24)).foregroundColor(.blue) 31 | }) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | struct PeopleSheet: View { 39 | @Environment(\.presentationMode) var presentationMode 40 | @ObservedObject var projectViewModel: ProjectViewModel 41 | @ObservedObject var userService: UserService = UserService() 42 | @State var showAlert: Bool = false 43 | @State var selectedUser: TaskyUser = TaskyUser(id: "", firstName: "", lastName: "") 44 | @State var text: String = "" 45 | @State private var isEditing = false 46 | 47 | var body: some View { 48 | VStack{ 49 | HStack { 50 | TextField("Search ...", text: $text).onChange(of: text, perform: { val in 51 | userService.searchUserBy(name: text) 52 | }) 53 | .padding(7) 54 | .padding(.horizontal, 25) 55 | .background(Color(.systemGray6)) 56 | .cornerRadius(8) 57 | .overlay( 58 | HStack { 59 | Image(systemName: "magnifyingglass") 60 | .foregroundColor(.gray) 61 | .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) 62 | .padding(.leading, 8) 63 | if isEditing { 64 | Button(action: { 65 | self.text = "" 66 | 67 | }) { 68 | Image(systemName: "multiply.circle.fill") 69 | .foregroundColor(.gray) 70 | .padding(.trailing, 8) 71 | } 72 | } 73 | } 74 | ) 75 | .padding(.horizontal, 10) 76 | .onTapGesture { 77 | self.isEditing = true 78 | } 79 | 80 | if isEditing { 81 | Button(action: { 82 | self.isEditing = false 83 | self.text = "" 84 | 85 | // Dismiss the keyboard 86 | UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) 87 | }) { 88 | Text("Cancel") 89 | } 90 | .padding(.trailing, 10) 91 | .transition(.move(edge: .trailing)) 92 | .animation(.default) 93 | } 94 | }.padding() 95 | 96 | PeopleListView(users: userService.resultUsers, onPressed: { user in 97 | selectedUser = user 98 | showAlert.toggle() 99 | }).equatable() 100 | 101 | }.alert(isPresented: $showAlert, content: { 102 | Alert(title: Text("Add \(selectedUser.fullName) as collaborator?"), message: Text(""), primaryButton: .default(Text("Cancel")), secondaryButton: .default(Text("Yes"), action: { 103 | self.projectViewModel.addCollaborator(userId: selectedUser.id) 104 | SPAlert.present(title: "Added to Project", preset: .done, haptic: .success) 105 | presentationMode.wrappedValue.dismiss() 106 | })) 107 | }) 108 | } 109 | } 110 | 111 | struct PeopleSheet_Previews: PreviewProvider { 112 | static var previews: some View { 113 | //PeopleSheet() 114 | EmptyView() 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tasky/Services/UserService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirestoreService.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/9/21. 6 | 7 | 8 | import Foundation 9 | import Firebase 10 | 11 | class UserService: ObservableObject { 12 | @Published var userName: String? 13 | @Published var user: TaskyUser? 14 | @Published var resultUsers: [TaskyUser] = [] 15 | 16 | func fetchUserBy(id: String){ 17 | if let user = UserService.cache[id] { 18 | self.user = user 19 | return 20 | } 21 | 22 | Firestore.firestore().collection("users").document(id).getDocument { (docSnapshot, err) in 23 | if let err = err { 24 | print("Error fetching user \(id), error: \(err.localizedDescription)") 25 | return 26 | } 27 | 28 | self.user = try? docSnapshot?.data(as: TaskyUser.self) 29 | 30 | guard let user = self.user else { return } 31 | 32 | self.userName = user.firstName + " " + user.lastName 33 | UserService.cache[id] = user 34 | } 35 | } 36 | 37 | func fetchUsersBy(ids: [String]){ 38 | self.resultUsers.removeAll() 39 | 40 | for id in ids { 41 | if let user = UserService.cache[id] { 42 | self.resultUsers.append(user) 43 | continue 44 | } 45 | 46 | Firestore.firestore().collection("users").document(id).getDocument { (docSnapshot, err) in 47 | if let err = err { 48 | print("Error fetching user \(id), error: \(err.localizedDescription)") 49 | return 50 | } 51 | 52 | let u = try? docSnapshot?.data(as: TaskyUser.self) 53 | 54 | guard let user = u else { return } 55 | 56 | print("appending \(user)") 57 | self.resultUsers.append(user) 58 | 59 | UserService.cache[id] = user 60 | } 61 | } 62 | } 63 | 64 | func searchUserBy(name: String){ 65 | if name.isEmpty { return } 66 | 67 | resultUsers.removeAll() 68 | 69 | let splitStr = name.split(separator: " ", maxSplits: 2, omittingEmptySubsequences: false) 70 | let firstName = splitStr[0] 71 | let lastName = splitStr.count > 1 ? splitStr[1] : "" 72 | 73 | print("Searching for \(firstName), the name is \(name) \(splitStr)") 74 | 75 | var firstNameTokens:[String] = [] 76 | 77 | for i in 0..), id: \.key){ key, value in 40 | Chip(color: Color(value), label: key) { 41 | self.selectedTag = key 42 | self.activeActionSheet = .removeTagSheet 43 | } 44 | } 45 | } 46 | Button(action: { 47 | if projectViewModel.project.tags != nil { 48 | self.activeActionSheet = .addTagSheet 49 | } 50 | }, label: { 51 | Image(systemName: "plus.circle").font(.system(size: 24)).foregroundColor(.blue) 52 | }) 53 | Spacer() 54 | }.padding(.leading, 12).padding(.bottom, 0) 55 | HStack{ 56 | if self.task.taskStatus == .completed { 57 | Text("\(task.title)").font(.headline).strikethrough() 58 | }else{ 59 | Text("\(task.title)").font(.headline) 60 | } 61 | Spacer() 62 | }.padding(.leading, 12).padding(.top, 8).padding(.bottom, 12) 63 | HStack{ 64 | Text("\(task.content)").font(.subheadline).opacity(0.8) 65 | Spacer() 66 | }.padding(.leading, 12) 67 | Spacer() 68 | HStack{ 69 | Spacer() 70 | Text("created on \(dateString) by \(creatorName ?? "")").font(.footnote).opacity(0.5).padding(.trailing, 12).padding(.bottom, 8) 71 | }.padding(.leading, 12) 72 | }.actionSheet(item: $activeActionSheet) { item in 73 | switch item { 74 | case .addTagSheet: 75 | return addTagSheet 76 | case .removeTagSheet: 77 | return removeTagSheet 78 | } 79 | }.alert(isPresented: $showRemoveTagAlert, content: { 80 | Alert(title: Text("Remove \(selectedTag!)?"), message: Text(""), primaryButton: .default(Text("Cancel")), secondaryButton: .default(Text("Yes"), action: { 81 | projectViewModel.removeTag(label: selectedTag!, from: task) 82 | })) 83 | 84 | }) 85 | } 86 | 87 | var addTagSheet: ActionSheet{ 88 | let tags = projectViewModel.project.tags! 89 | var buttons:[ActionSheet.Button] = [] 90 | 91 | for (key, value) in tags { 92 | buttons.append( 93 | .default(Text(key), action: { 94 | projectViewModel.addTag(toTaskWithId: self.task.id, label: key, colorString: value) 95 | })) 96 | } 97 | 98 | buttons.append(.cancel()) 99 | 100 | return ActionSheet(title: Text("Add a tag"), buttons: buttons) 101 | } 102 | 103 | var removeTagSheet: ActionSheet{ 104 | var buttons:[ActionSheet.Button] = [] 105 | 106 | buttons.append(.default(Text("Remove"), action: { 107 | self.showRemoveTagAlert.toggle() 108 | })) 109 | buttons.append(.cancel()) 110 | 111 | return ActionSheet(title: Text("\(selectedTag!)"), buttons: buttons) 112 | } 113 | 114 | } 115 | 116 | struct TaskDetailSheet_Previews: PreviewProvider { 117 | static var previews: some View { 118 | EmptyView() 119 | // TaskDetailSheet(task: Task(id: "", title: "Task", content: "details", taskStatus: .awaiting, timestamp: Date().timeIntervalSince1970, dueTimestamp: nil, creatorId: nil, assigneesId: nil)) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Tasky/Services/AuthService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationService.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import Foundation 9 | import Firebase 10 | 11 | class AuthService: ObservableObject { 12 | @Published var user: User? 13 | @Published var userExists: Bool = false 14 | 15 | private var authenticationStateHandler: AuthStateDidChangeListenerHandle? 16 | 17 | var signedIn :Bool { 18 | return !(Auth.auth().currentUser == nil) 19 | } 20 | 21 | init() { 22 | addListeners() 23 | self.user = Auth.auth().currentUser 24 | if self.user != nil { 25 | self.userExists = true 26 | } 27 | } 28 | 29 | //Check whether or not the user is first time user, if not we will ask user to enter their first and last name. 30 | func checkUserExists(userId: String, user: User?){ 31 | let docRef = Firestore.firestore().collection("users").document(userId) 32 | docRef.getDocument { (doc, error) in 33 | if doc?.exists ?? false { 34 | self.userExists = true 35 | self.user = user 36 | print("Document data: \(doc!.data()!)") 37 | } else { 38 | self.userExists = false 39 | self.user = user 40 | print("Document does not exist") 41 | } 42 | } 43 | } 44 | 45 | func updateUserName(firstName: String, lastName: String){ 46 | guard let userId = Auth.auth().currentUser?.uid else { 47 | return 48 | } 49 | Firestore.firestore().collection("users").document(userId).setData(["firstName": firstName, "lastName":lastName, "id":userId]) 50 | self.userExists = true 51 | } 52 | 53 | private func addListeners() { 54 | if let handle = authenticationStateHandler { 55 | Auth.auth().removeStateDidChangeListener(handle) 56 | } 57 | 58 | authenticationStateHandler = Auth.auth() 59 | .addStateDidChangeListener { _, user in 60 | guard let uid = user?.uid else { 61 | self.user = nil 62 | self.userExists = false 63 | return 64 | } 65 | self.checkUserExists(userId: uid, user: user) 66 | } 67 | } 68 | } 69 | 70 | 71 | extension AuthService{ 72 | static var currentUser:User? { Auth.auth().currentUser } 73 | 74 | static func signIn(withEmail email: String, password: String) { 75 | Auth.auth().signIn(withEmail: email, password: password) { (authResult : AuthDataResult?, error : Error?) in 76 | 77 | } 78 | } 79 | 80 | static func signIn(verificationID: String, verificationCode:String) { 81 | let credential = PhoneAuthProvider.provider().credential( 82 | withVerificationID: verificationID, 83 | verificationCode: verificationCode) 84 | 85 | Auth.auth().signIn(with: credential) { (authResult, error) in 86 | if let error = error { 87 | let authError = error as NSError 88 | if (authError.code == AuthErrorCode.secondFactorRequired.rawValue) { 89 | // The user is a multi-factor user. Second factor challenge is required. 90 | let resolver = authError.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver 91 | var displayNameString = "" 92 | for tmpFactorInfo in (resolver.hints) { 93 | displayNameString += tmpFactorInfo.displayName ?? "" 94 | displayNameString += " " 95 | } 96 | 97 | } else { 98 | return 99 | } 100 | return 101 | } 102 | } 103 | 104 | } 105 | 106 | static func signIn(withCredential credential: AuthCredential){ 107 | Auth.auth().signIn(with: credential) { (authResult, error) in 108 | if (error != nil) { 109 | // Error. If error.code == .MissingOrInvalidNonce, make sure 110 | // you're sending the SHA256-hashed nonce as a hex string with 111 | // your request to Apple. 112 | print(error?.localizedDescription as Any) 113 | return 114 | } 115 | print("signed in \(authResult?.user.displayName)") 116 | } 117 | } 118 | 119 | static func signOut() throws { 120 | try Auth.auth().signOut() 121 | } 122 | 123 | static func changeName(to name: String){ 124 | guard let changeReq = Auth.auth().currentUser?.createProfileChangeRequest() else { return } 125 | changeReq.displayName = name 126 | changeReq.commitChanges { err in 127 | guard err == nil else { 128 | print("Failed to commit changes") 129 | return 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Tasky/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"idiom":"watch","filename":"172.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"86x86","expected-size":"172","role":"quickLook"},{"idiom":"watch","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"40x40","expected-size":"80","role":"appLauncher"},{"idiom":"watch","filename":"88.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"40mm","scale":"2x","size":"44x44","expected-size":"88","role":"appLauncher"},{"idiom":"watch","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"50x50","expected-size":"100","role":"appLauncher"},{"idiom":"watch","filename":"196.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"98x98","expected-size":"196","role":"quickLook"},{"idiom":"watch","filename":"216.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"108x108","expected-size":"216","role":"quickLook"},{"idiom":"watch","filename":"48.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"24x24","expected-size":"48","role":"notificationCenter"},{"idiom":"watch","filename":"55.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"27.5x27.5","expected-size":"55","role":"notificationCenter"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"3x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"2x"},{"size":"1024x1024","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch-marketing","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} -------------------------------------------------------------------------------- /Tasky/Views/LoginView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import SwiftUI 9 | import CryptoKit 10 | import FirebaseAuth 11 | import FASwiftUI 12 | import AuthenticationServices 13 | 14 | struct LoginView: View { 15 | @Environment(\.colorScheme) var colorScheme 16 | @State var currentNonce:String? 17 | @State var showPhoneLoginSheet: Bool = false 18 | 19 | //Hashing function using CryptoKit 20 | func sha256(_ input: String) -> String { 21 | let inputData = Data(input.utf8) 22 | let hashedData = SHA256.hash(data: inputData) 23 | let hashString = hashedData.compactMap { 24 | return String(format: "%02x", $0) 25 | }.joined() 26 | 27 | return hashString 28 | } 29 | 30 | private func randomNonceString(length: Int = 32) -> String { 31 | precondition(length > 0) 32 | let charset: Array = 33 | Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._") 34 | var result = "" 35 | var remainingLength = length 36 | 37 | while remainingLength > 0 { 38 | let randoms: [UInt8] = (0 ..< 16).map { _ in 39 | var random: UInt8 = 0 40 | let errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random) 41 | if errorCode != errSecSuccess { 42 | fatalError("Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)") 43 | } 44 | return random 45 | } 46 | 47 | randoms.forEach { random in 48 | if remainingLength == 0 { 49 | return 50 | } 51 | 52 | if random < charset.count { 53 | result.append(charset[Int(random)]) 54 | remainingLength -= 1 55 | } 56 | } 57 | } 58 | 59 | return result 60 | } 61 | 62 | var body: some View { 63 | NavigationView{ 64 | VStack{ 65 | Image(uiImage: getAppIcon()).resizable().scaledToFit().frame(width: 120, height: 120).cornerRadius(26).shadow(radius: /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/) 66 | Color.clear.frame(height: 36) 67 | Button(action: { showPhoneLoginSheet.toggle() }){ 68 | HStack{ 69 | FAText(iconName: "phone", size: 14).foregroundColor(.white) 70 | Text("Sign in with Phone").foregroundColor(.white) 71 | }.frame(width: 280, height: 45, alignment: .center).background(Color(.orange)).overlay( 72 | RoundedRectangle(cornerRadius: 0) 73 | .stroke(Color.blue, lineWidth: 0)) 74 | }.cornerRadius(6.0).padding(.bottom, 6) 75 | // Button(action: /*@START_MENU_TOKEN@*/{}/*@END_MENU_TOKEN@*/){ 76 | // HStack{ 77 | // FAText(iconName: "google", size: 14).foregroundColor(.white) 78 | // Text("Sign in with Gmail").foregroundColor(.white) 79 | // }.frame(width: 280, height: 45, alignment: .center).background(Color(.systemBlue)).overlay( 80 | // RoundedRectangle(cornerRadius: 0) 81 | // .stroke(Color.blue, lineWidth: 0)) 82 | // }.cornerRadius(6.0).disabled(true) 83 | SignInWithAppleButton( 84 | //Request 85 | onRequest: { request in 86 | let nonce = randomNonceString() 87 | currentNonce = nonce 88 | request.requestedScopes = [.fullName, .email] 89 | request.nonce = sha256(nonce) 90 | }, 91 | 92 | //Completion 93 | onCompletion: { result in 94 | switch result { 95 | case .success(let authResults): 96 | 97 | switch authResults.credential { 98 | case let appleIDCredential as ASAuthorizationAppleIDCredential: 99 | 100 | guard let nonce = currentNonce else { 101 | fatalError("Invalid state: A login callback was received, but no login request was sent.") 102 | } 103 | guard let appleIDToken = appleIDCredential.identityToken else { 104 | fatalError("Invalid state: A login callback was received, but no login request was sent.") 105 | } 106 | guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else { 107 | print("Unable to serialize token string from data: \(appleIDToken.debugDescription)") 108 | return 109 | } 110 | 111 | let firstName = appleIDCredential.fullName?.givenName 112 | let lastName = appleIDCredential.fullName?.familyName 113 | 114 | UserDefaults.standard.setValue(firstName, forKey: "firstName") 115 | UserDefaults.standard.setValue(lastName, forKey: "lastName") 116 | 117 | let credential = OAuthProvider.credential(withProviderID: "apple.com",idToken: idTokenString,rawNonce: nonce) 118 | 119 | AuthService.signIn(withCredential: credential) 120 | 121 | print("\(String(describing: Auth.auth().currentUser?.uid))") 122 | default: 123 | break 124 | 125 | } 126 | default: 127 | break 128 | } 129 | 130 | } 131 | ).frame(width: 280, height: 45, alignment: .center).signInWithAppleButtonStyle(colorScheme == .dark ? .white : .black) 132 | 133 | } 134 | }.sheet(isPresented: $showPhoneLoginSheet){ 135 | PhoneLoginView() 136 | } 137 | } 138 | 139 | 140 | func getAppIcon() -> UIImage { 141 | var appIcon: UIImage! { 142 | guard let iconsDictionary = Bundle.main.infoDictionary?["CFBundleIcons"] as? [String:Any], 143 | let primaryIconsDictionary = iconsDictionary["CFBundlePrimaryIcon"] as? [String:Any], 144 | let iconFiles = primaryIconsDictionary["CFBundleIconFiles"] as? [String], 145 | let lastIcon = iconFiles.last else { return nil } 146 | return UIImage(named: lastIcon) 147 | } 148 | return appIcon 149 | } 150 | 151 | struct LoginView_Previews: PreviewProvider { 152 | static var previews: some View { 153 | LoginView() 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Tasky/Views/TaskView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskView.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/4/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct TaskView: View { 11 | @ObservedObject var projectViewModel:ProjectViewModel 12 | @ObservedObject var userService: UserService = UserService() 13 | @State var showDetailSheet: Bool = false 14 | var task: Task 15 | var onEditPressed: () -> () 16 | var onRemovePressed: () -> () 17 | var onStatusChanged: (TaskStatus) ->() 18 | var onChipPressed: (String)->() 19 | 20 | init(task: Task, projectViewModel: ProjectViewModel,onEditPressed: @escaping () -> (),onRemovePressed: @escaping () -> (), onStatusChanged: @escaping (TaskStatus) ->(), onChipPressed: @escaping (String) ->()) { 21 | self.projectViewModel = projectViewModel 22 | self.onEditPressed = onEditPressed 23 | self.onRemovePressed = onRemovePressed 24 | self.onStatusChanged = onStatusChanged 25 | self.onChipPressed = onChipPressed 26 | self.task = task 27 | if task.creatorId != nil { 28 | self.userService.fetchUserBy(id: task.creatorId ?? "") 29 | } 30 | } 31 | 32 | var body: some View { 33 | let dateFormatter = DateFormatter() 34 | dateFormatter.dateFormat = "MMM dd, yyyy" 35 | let dateFromTimestamp = Date(timeIntervalSince1970: TimeInterval(TimeInterval(self.task.timestamp))) 36 | let dateString = dateFormatter.string(from: dateFromTimestamp) 37 | 38 | let dueDateFormatter = DateFormatter() 39 | dueDateFormatter.dateFormat = "MMM dd, yyyy" 40 | let dueDateFromTimestamp = self.task.dueTimestamp == nil ? nil : Date(timeIntervalSince1970: TimeInterval(TimeInterval(self.task.dueTimestamp!))) 41 | let dueDateString = dueDateFromTimestamp == nil ? nil : dateFormatter.string(from: dueDateFromTimestamp!) 42 | 43 | var color = Color.orange 44 | 45 | if task.taskStatus == .aborted { 46 | color = Color.gray 47 | } 48 | 49 | 50 | return GeometryReader { geometry in 51 | VStack(alignment: .leading, spacing: 0) { 52 | if dueDateString != nil { 53 | Text("due on \(dueDateString!)").font(.footnote).foregroundColor(.yellow).opacity(1.0).padding(.leading, 12).padding(.top, 8) 54 | } 55 | HStack{ 56 | if self.task.taskStatus == .completed { 57 | Text("\(task.title)").font(.headline).strikethrough().lineLimit(1) 58 | }else{ 59 | Text("\(task.title)").font(.headline).lineLimit(1) 60 | } 61 | Spacer() 62 | }.padding(.leading, 12).padding(.top, dueDateString == nil ? 8 : 0) 63 | HStack{ 64 | Text("\(self.task.content)").font(.subheadline).lineLimit(2).foregroundColor(.black).opacity(0.8).padding(.vertical, 0) 65 | Spacer() 66 | }.padding(.leading, 12).padding(.vertical, 0) 67 | Spacer() 68 | HStack{ 69 | if task.tags != nil { 70 | ForEach(task.tags!.sorted(by: >), id: \.key){ key, value in 71 | SmallChip(color: Color(value), label: key) { 72 | onChipPressed(key) 73 | } 74 | } 75 | } 76 | }.padding(.leading, 12).padding(.vertical, 0) 77 | HStack{ 78 | Spacer() 79 | Text("created on \(dateString) by \(self.userService.user?.firstName ?? "") \(self.userService.user?.lastName ?? "")").font(.footnote).foregroundColor(.black).opacity(0.5).padding(.trailing, 12).padding(.bottom, 8) 80 | }.padding(.leading, 12).padding(.top, 0) 81 | } 82 | .background(color) 83 | .cornerRadius(8) 84 | .frame(width: geometry.size.width, height: 120) 85 | .contentShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) 86 | .shadow(color: color.opacity(0.3), radius: 3, x: 2, y: 2) 87 | .contextMenu { 88 | if self.task.taskStatus != TaskStatus.awaiting{ 89 | Button(action: { 90 | onStatusChanged(TaskStatus.awaiting) 91 | }) { 92 | Text("Await") 93 | Image(systemName: "tortoise") 94 | } 95 | } 96 | if self.task.taskStatus != TaskStatus.inProgress{ 97 | Button(action: { 98 | onStatusChanged(TaskStatus.inProgress) 99 | }) { 100 | Text("In Progress") 101 | Image(systemName: "hourglass") 102 | } 103 | } 104 | if self.task.taskStatus != TaskStatus.completed{ 105 | Button(action: { 106 | onStatusChanged(TaskStatus.completed) 107 | }) { 108 | Text("Complete") 109 | Image(systemName: "checkmark.shield") 110 | } 111 | } 112 | if self.task.taskStatus != TaskStatus.aborted{ 113 | Button(action: { 114 | onStatusChanged(TaskStatus.aborted) 115 | }) { 116 | Text("Abort") 117 | Image(systemName: "xmark.shield") 118 | } 119 | } 120 | Divider() 121 | Button(action: { 122 | onEditPressed() 123 | }) { 124 | Text("Edit") 125 | Image(systemName: "pencil") 126 | } 127 | Divider() 128 | Button(action: { 129 | onRemovePressed() 130 | }) { 131 | Text("Remove") 132 | Image(systemName: "trash") 133 | } 134 | }.onTapGesture { 135 | self.showDetailSheet.toggle() 136 | }.sheet(isPresented: $showDetailSheet){ 137 | let fullName = "\(self.userService.user?.firstName ?? "") \(self.userService.user?.lastName ?? "")" 138 | TaskDetailSheet(projectViewModel: projectViewModel, task: self.task, creatorName: fullName) 139 | } 140 | } 141 | } 142 | } 143 | 144 | struct TaskView_Previews: PreviewProvider { 145 | static var previews: some View { 146 | EmptyView() 147 | // VStack(alignment: .leading){ 148 | // TaskView(task: Task(id: "", title: "My task", content: "To get something done.", taskStatus: .awaiting, timestamp: Date().timeIntervalSince1970, dueTimestamp: Date().timeIntervalSince1970), onEditPressed: {}, onRemovePressed: { }, onStatusChanged: {_ in }) 149 | // TaskView(task: Task(id: "1", title: "My task", content: "To get something done.", taskStatus: .awaiting, timestamp: Date().timeIntervalSince1970), onEditPressed: {}, onRemovePressed: { }, onStatusChanged: {_ in }) 150 | // TaskView(task: Task(id: "2", title: "My task", content: "To get something done.", taskStatus: .awaiting, timestamp: Date().timeIntervalSince1970), onEditPressed: {}, onRemovePressed: { }, onStatusChanged: {_ in }) 151 | // } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Tasky/Repositories/ProjectRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProjectRepository.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 1/29/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import FirebaseFirestore 11 | import FirebaseFirestoreSwift 12 | import Combine 13 | 14 | class ProjectRepository: ObservableObject { 15 | private let path: String = "projects" 16 | private let testUserId: String = "testUserId" 17 | private let store = Firestore.firestore() 18 | 19 | @Published var projects: [Project] = [] 20 | 21 | var userId = "" 22 | 23 | private let authenticationService:AuthService 24 | 25 | private var cancellables: Set = [] 26 | 27 | init(authService: AuthService) { 28 | // authenticationService.$user 29 | // .compactMap { user in 30 | // if user?.uid.isEmpty ?? true { 31 | // return self.testUserId 32 | // }else{ 33 | // return user?.uid 34 | // } 35 | // } 36 | // .assign(to: \.userId, on: self) 37 | // .store(in: &cancellables) 38 | self.authenticationService = authService 39 | 40 | 41 | authenticationService.$user 42 | .compactMap { user in 43 | if user?.uid.isEmpty ?? true { 44 | return self.testUserId 45 | }else{ 46 | return user?.uid 47 | } 48 | }.receive(on: DispatchQueue.main) 49 | .sink(receiveValue: { 50 | self.userId = $0 51 | }) 52 | .store(in: &cancellables) 53 | 54 | authenticationService.$user 55 | .receive(on: DispatchQueue.main) 56 | .sink { [weak self] _ in 57 | self?.get() 58 | } 59 | .store(in: &cancellables) 60 | } 61 | 62 | func fetchProjectsWithoutTasks(){ 63 | store.collection("projects").whereField("managerId", isEqualTo: userId).addSnapshotListener{ querySnapshot, error in 64 | if let error = error { 65 | print("Error getting projects: \(error.localizedDescription)") 66 | return 67 | } 68 | 69 | self.projects = querySnapshot?.documents.compactMap { document in 70 | var project = try? document.data(as: Project.self) 71 | var tasks: [Task] = [] 72 | 73 | self.store.collection("projects").document(project!.id!).collection("tasks").getDocuments { snapshots, err in 74 | if let err = err { 75 | print("Error getting projects: \(err.localizedDescription)") 76 | return 77 | } 78 | 79 | tasks = snapshots?.documents.compactMap { doc in 80 | let task = try? doc.data(as: Task.self) 81 | return task 82 | } ?? [] 83 | 84 | project?.tasks = tasks 85 | 86 | //print("the project tasks are \(project?.tasks)") 87 | 88 | self.projects.removeAll { 89 | if project == $0 { 90 | return true 91 | } 92 | return false 93 | } 94 | self.projects.append(project!) 95 | } 96 | 97 | return project 98 | } ?? [] 99 | 100 | } 101 | } 102 | 103 | func get() { 104 | store.collection("projects").whereField("managerId", isEqualTo: userId).addSnapshotListener{ querySnapshot, error in 105 | if let error = error { 106 | print("Error getting projects: \(error.localizedDescription)") 107 | return 108 | } 109 | 110 | querySnapshot?.documents.compactMap { document in 111 | var project = try? document.data(as: Project.self) 112 | var tasks: [Task] = [] 113 | 114 | self.store.collection("projects").document(project!.id!).collection("tasks").getDocuments { snapshots, err in 115 | if let err = err { 116 | print("Error getting projects: \(err.localizedDescription)") 117 | return 118 | } 119 | 120 | tasks = snapshots?.documents.compactMap { doc in 121 | let task = try? doc.data(as: Task.self) 122 | return task 123 | } ?? [] 124 | 125 | project?.tasks = tasks 126 | 127 | self.projects.removeAll { 128 | if project == $0 { 129 | return true 130 | } 131 | return false 132 | } 133 | self.projects.append(project!) 134 | } 135 | 136 | return project 137 | } ?? [] 138 | } 139 | 140 | store.collection("projects").whereField("collaboratorIds", arrayContains: userId).addSnapshotListener{ querySnapshot, error in 141 | if let error = error { 142 | print("Error getting projects: \(error.localizedDescription)") 143 | return 144 | } 145 | 146 | querySnapshot?.documents.compactMap { document in 147 | var project = try? document.data(as: Project.self) 148 | var tasks: [Task] = [] 149 | 150 | self.store.collection("projects").document(project!.id!).collection("tasks").getDocuments { snapshots, err in 151 | if let err = err { 152 | print("Error getting projects: \(err.localizedDescription)") 153 | return 154 | } 155 | 156 | tasks = snapshots?.documents.compactMap { doc in 157 | let task = try? doc.data(as: Task.self) 158 | return task 159 | } ?? [] 160 | 161 | project?.tasks = tasks 162 | 163 | self.projects.removeAll { 164 | if project == $0 { 165 | return true 166 | } 167 | return false 168 | } 169 | self.projects.append(project!) 170 | } 171 | 172 | return project 173 | } ?? [] 174 | } 175 | } 176 | 177 | 178 | } 179 | 180 | extension ProjectRepository { 181 | static func add(_ project: Project) { 182 | let store = Firestore.firestore() 183 | //let userId = AuthService.currentUser!.uid 184 | 185 | do { 186 | let newProject = project 187 | print("managerId is \(newProject.managerId)") 188 | // if userId.isEmpty{ 189 | // newProject.managerId = "" 190 | // }else{ 191 | // newProject.managerId = userId 192 | // } 193 | let docRef = try store.collection("projects").addDocument(from: newProject) 194 | 195 | let uuid = UUID().uuidString 196 | let exampleTask = Task(id: uuid, title: "Your first task", content: "Get to know your project", taskStatus: .awaiting, timestamp: Date().timeIntervalSince1970, dueTimestamp: nil, creatorId: project.managerId, assigneesId: []) 197 | 198 | try docRef.collection("tasks").document(uuid).setData(from: exampleTask) 199 | 200 | //self.get() 201 | } catch { 202 | fatalError("Unable to add project: \(error.localizedDescription).") 203 | } 204 | } 205 | 206 | static func add(task: Task, to project: Project){ 207 | let store = Firestore.firestore() 208 | 209 | guard let projectId = project.id else { return } 210 | 211 | do { 212 | try store.collection("projects").document(projectId).collection("tasks").document(task.id).setData(from: task.self) 213 | store.collection("projects").document(projectId).updateData(["mock":Date().timeIntervalSince1970]) 214 | } catch { 215 | fatalError("Unable to update project: \(error.localizedDescription).") 216 | } 217 | } 218 | 219 | static func addTag(label: String, colorString: String, to project: Project){ 220 | let store = Firestore.firestore() 221 | 222 | guard let projectId = project.id else { return } 223 | 224 | store.collection("projects").document(projectId).setData(["tags":[label: colorString]], merge: true) 225 | } 226 | 227 | static func addTag(to task: Task, in project: Project, label: String, colorString: String){ 228 | let store = Firestore.firestore() 229 | 230 | guard let projectId = project.id else { return } 231 | 232 | store.collection("projects").document(projectId).collection("tasks").document(task.id).setData(["tags":[label: colorString]], merge: true) 233 | store.collection("projects").document(projectId).updateData(["mock":Date().timeIntervalSince1970]) 234 | } 235 | 236 | static func removeTag(label: String, from project: Project){ 237 | let store = Firestore.firestore() 238 | 239 | guard let projectId = project.id else { return } 240 | 241 | var map = project.tags! 242 | map.removeValue(forKey: label) 243 | 244 | for task in project.tasks.filter({ t in 245 | return t.tags?.keys.contains(label) ?? false 246 | }) { 247 | var subMap = task.tags! 248 | subMap.removeValue(forKey: label) 249 | 250 | store.collection("projects").document(projectId).collection("tasks").document(task.id).updateData(["tags" : subMap]) 251 | } 252 | 253 | store.collection("projects").document(projectId).updateData(["tags":map]) 254 | } 255 | 256 | static func removeTag(label: String, from task: Task, in project: Project){ 257 | let store = Firestore.firestore() 258 | 259 | guard let projectId = project.id else { return } 260 | 261 | var subMap = task.tags! 262 | subMap.removeValue(forKey: label) 263 | 264 | store.collection("projects").document(projectId).collection("tasks").document(task.id).updateData(["tags": subMap]) 265 | 266 | store.collection("projects").document(projectId).updateData(["mock":Date().timeIntervalSince1970]) 267 | } 268 | 269 | static func addCollaborator(userId: String, to projectId: String){ 270 | let store = Firestore.firestore() 271 | 272 | store.collection("projects").document(projectId).updateData(["collaboratorIds" : FieldValue.arrayUnion([userId])]) 273 | } 274 | 275 | static func removeCollaborator(userId: String, from projectId: String){ 276 | let store = Firestore.firestore() 277 | 278 | store.collection("projects").document(projectId).updateData(["collaboratorIds" : FieldValue.arrayRemove([userId])]) 279 | } 280 | 281 | static func update(_ project: Project) { 282 | let store = Firestore.firestore() 283 | 284 | guard let projectId = project.id else { return } 285 | 286 | do { 287 | for task in project.tasks { 288 | try store.collection("projects").document(projectId).collection("tasks").document(task.id).setData(from: task.self) 289 | } 290 | } catch { 291 | fatalError("Unable to update project: \(error.localizedDescription).") 292 | } 293 | } 294 | 295 | static func update(_ project: Project, task: Task){ 296 | let store = Firestore.firestore() 297 | 298 | guard let projectId = project.id else { return } 299 | 300 | do { 301 | try store.collection("projects").document(projectId).collection("tasks").document(task.id).setData(from: task.self) 302 | store.collection("projects").document(projectId).updateData(["mock":Date().timeIntervalSince1970]) 303 | } catch { 304 | fatalError("Unable to update project: \(error.localizedDescription).") 305 | } 306 | } 307 | 308 | static func update(_ project: Project, withName newName: String) { 309 | let store = Firestore.firestore() 310 | 311 | guard let projectId = project.id else { return } 312 | 313 | store.collection("projects").document(projectId).updateData(["name" : newName]) 314 | } 315 | 316 | static func remove(task: Task, from project: Project){ 317 | let store = Firestore.firestore() 318 | 319 | guard let projectId = project.id else { return } 320 | 321 | store.collection("projects").document(projectId).collection("tasks").document(task.id).delete { error in 322 | if let error = error { 323 | print("Unable to remove task: \(error.localizedDescription)") 324 | } 325 | 326 | print("removed task \(task.id) from \(String(describing: project.id))") 327 | 328 | //store.collection("projects").document(projectId).updateData(["mock":Date().timeIntervalSince1970]) 329 | } 330 | } 331 | 332 | func remove(_ project: Project) { 333 | let store = Firestore.firestore() 334 | 335 | guard let projectId = project.id else { return } 336 | 337 | guard let index = self.projects.firstIndex(where: {$0.id == project.id}) else { 338 | return 339 | } 340 | 341 | self.projects.remove(at: index) 342 | 343 | store.collection("projects").document(projectId).delete { error in 344 | if let error = error { 345 | print("Unable to remove project: \(error.localizedDescription)") 346 | } 347 | } 348 | 349 | store.collection("projects").document(projectId).collection("tasks").getDocuments { querySnapshot, err in 350 | if err != nil { 351 | print("Error deleting tasks from Project \(projectId)") 352 | } 353 | 354 | querySnapshot?.documents.forEach({ snapshot in 355 | print("deleting \(snapshot.data())") 356 | snapshot.reference.delete() 357 | }) 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /Tasky/Views/TaskListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TaskListView.swift 3 | // Tasky 4 | // 5 | // Created by Jiaqi Feng on 2/4/21. 6 | // 7 | 8 | import SwiftUI 9 | import StoreKit 10 | import SPAlert 11 | import FASwiftUI 12 | import ConfettiView 13 | 14 | fileprivate enum ActiveSheet: Identifiable { 15 | case newTaskSheet, newTagSheet, editProjectSheet, updateTaskSheet, peopleSheet, testSheet 16 | 17 | var id: Int { 18 | hashValue 19 | } 20 | } 21 | 22 | fileprivate enum ActiveActionSheet: Identifiable { 23 | case collabActionSheet, tagActionSheet 24 | 25 | var id: Int { 26 | hashValue 27 | } 28 | } 29 | 30 | fileprivate enum ActiveAlert: Identifiable { 31 | case deleteProjectAlert, removeCollabAlert, removeTagAlert 32 | 33 | var id: Int { 34 | hashValue 35 | } 36 | } 37 | 38 | var selectedTask: Task = testTask 39 | 40 | struct TaskListView: View { 41 | @ObservedObject var projectListViewModel: ProjectListViewModel 42 | @ObservedObject var userService: UserService = UserService() 43 | @ObservedObject var projectViewModel: ProjectViewModel 44 | @State fileprivate var activeSheet: ActiveSheet? 45 | @State fileprivate var activeActionSheet: ActiveActionSheet? 46 | @State fileprivate var activeAlert: ActiveAlert? 47 | @State var selectedTaskStatus: TaskStatus = .awaiting 48 | @State var showConfetti: Bool = false 49 | @State var progressValue: Float 50 | @State var selectedCollaborator: TaskyUser? 51 | @State var selectedTag: String? 52 | @State var pressedTag: String? 53 | var onDelete: (Project)->() 54 | @State var timer:Timer? 55 | 56 | init(projectListViewModel: ProjectListViewModel,projectViewModel: ProjectViewModel, onDelete: @escaping (Project)->()) { 57 | self.projectListViewModel = projectListViewModel 58 | self.projectViewModel = projectViewModel 59 | self.onDelete = onDelete 60 | let completedCount = Float(projectViewModel.project.tasks.filter { task -> Bool in 61 | if task.taskStatus == .completed { 62 | return true 63 | } 64 | return false 65 | }.count) 66 | let abortedCount = Float(projectViewModel.project.tasks.filter { task -> Bool in 67 | if task.taskStatus == .aborted { 68 | return true 69 | } 70 | return false 71 | }.count) 72 | let total = Float(projectViewModel.project.tasks.count) - abortedCount 73 | let val = total == 0 ? 0.0 : (completedCount/total) 74 | self._progressValue = State(initialValue: val) 75 | 76 | self.userService.fetchUserBy(id: projectViewModel.project.managerId!) 77 | 78 | guard let ids = projectViewModel.project.collaboratorIds else { return } 79 | 80 | self.userService.fetchUsersBy(ids: ids) 81 | } 82 | 83 | var tagMenu : some View{ 84 | let project = projectViewModel.project 85 | 86 | return Menu { 87 | if project.tags != nil { 88 | ForEach(project.tags!.sorted(by: >), id: \.key){ key, value in 89 | Button(action: { 90 | self.selectedTag = key 91 | self.activeActionSheet = .tagActionSheet 92 | }) { 93 | Text("\(key)") 94 | } 95 | } 96 | } 97 | 98 | if AuthService.currentUser!.uid == projectViewModel.project.managerId { 99 | Divider() 100 | Button(action: { self.activeSheet = .newTagSheet }) { 101 | Text("Add tag") 102 | Image(systemName: "plus.circle") 103 | } 104 | } 105 | 106 | } label:{ 107 | Image(systemName: "tag.fill").font(.system(size: 22)).foregroundColor(.blue) 108 | } 109 | } 110 | 111 | var collabMenu : some View { 112 | let participants = self.projectViewModel.project.collaboratorIds 113 | 114 | return Menu { 115 | Button(action: { 116 | 117 | }) { 118 | Text("\(self.userService.user!.fullName)") 119 | Image(systemName: "binoculars.fill") 120 | } 121 | 122 | if participants != nil { 123 | ForEach(self.userService.resultUsers){ taskyUser in 124 | Button(action: { 125 | if AuthService.currentUser!.uid == projectViewModel.project.managerId { 126 | self.selectedCollaborator = taskyUser 127 | self.activeActionSheet = .collabActionSheet 128 | } 129 | }) { 130 | Text("\(taskyUser.fullName)") 131 | } 132 | } 133 | } 134 | 135 | if AuthService.currentUser!.uid == projectViewModel.project.managerId { 136 | Divider() 137 | Button(action: { self.activeSheet = .peopleSheet }) { 138 | Text("Add collaborator") 139 | Image(systemName: "person.fill.badge.plus") 140 | } 141 | } 142 | 143 | } label:{ 144 | Image(systemName: "person.2.fill").font(.system(size: 22)).foregroundColor(.blue) 145 | } 146 | } 147 | 148 | var body: some View { 149 | GeometryReader{ geometry in 150 | ZStack{ 151 | VStack{ 152 | ProgressBar(value: $progressValue).frame(height: 24).padding(.horizontal) 153 | Picker(selection: $selectedTaskStatus.animation(), label: Text("Picker"), content: { 154 | Text("Awaiting").tag(TaskStatus.awaiting) 155 | Text("In Progress").tag(TaskStatus.inProgress) 156 | Text("Completed").tag(TaskStatus.completed) 157 | Text("Aborted").tag(TaskStatus.aborted) 158 | }).pickerStyle(SegmentedPickerStyle()).padding(.horizontal, 16) 159 | taskListOf(taskStatus: selectedTaskStatus) 160 | } 161 | if showConfetti { 162 | ConfettiView( confetti: [ 163 | .text("🎉"), 164 | .text("💪"), 165 | .shape(.circle), 166 | .shape(.triangle), 167 | ]).transition(.opacity) 168 | } 169 | } 170 | } 171 | .navigationBarTitle("\(projectViewModel.project.name)") 172 | .navigationBarItems(trailing: HStack{ 173 | tagMenu 174 | collabMenu 175 | Menu { 176 | Button(action: { activeSheet = .editProjectSheet }) { 177 | Image(systemName: "square.and.pencil") 178 | Text("Edit") 179 | } 180 | Button(action: { activeSheet = .newTaskSheet }) { 181 | Text("Add a task") 182 | Image(systemName: "plus") 183 | } 184 | if AuthService.currentUser!.uid == projectViewModel.project.managerId { 185 | Divider() 186 | Button(action: { activeAlert = .deleteProjectAlert }) { 187 | Text("Delete") 188 | Image(systemName: "trash") 189 | } 190 | } else { 191 | Divider() 192 | Button(action: { activeAlert = .removeCollabAlert }) { 193 | Text("Leave") 194 | Image(systemName: "figure.wave") 195 | } 196 | } 197 | } label:{ 198 | Image(systemName: "ellipsis").font(.system(size: 24)) 199 | } 200 | }.sheet(item: $activeSheet){ item in 201 | switch item { 202 | case .newTaskSheet: 203 | NewTaskSheet(projectViewModel: projectViewModel) 204 | case .newTagSheet: 205 | NewTagSheet(projectViewModel: projectViewModel) 206 | case .editProjectSheet: 207 | UpdateProjectForm(projectViewModel: projectViewModel) 208 | case .updateTaskSheet: 209 | UpdateTaskSheet(projectViewModel: projectViewModel) 210 | case .peopleSheet: 211 | PeopleSheet(projectViewModel: projectViewModel) 212 | case .testSheet: 213 | buildTaskSheet() 214 | } 215 | }.alert(item: $activeAlert, content: { item in 216 | switch item { 217 | case .deleteProjectAlert: 218 | return Alert(title: Text("Delete this project?"), message: Text("This project will be deleted permanently."), primaryButton: .default(Text("Cancel")), secondaryButton: .destructive(Text("Okay"), action: { 219 | self.onDelete(projectViewModel.project) 220 | })) 221 | case .removeCollabAlert: 222 | return Alert(title: Text("Remove yourself from this project?"), primaryButton: .default(Text("Cancel")), secondaryButton: .destructive(Text("Yes"), action: { 223 | projectViewModel.removeCollaborator(userId: AuthService.currentUser!.uid) 224 | //For some reasons, line above does not notify viewmodel with the change. 225 | //Below is the walk-around. 226 | projectListViewModel.remove(id: self.projectViewModel.project.id!) 227 | })) 228 | case .removeTagAlert: 229 | return Alert(title: Text("Remove \(selectedTag!) from this project?"), primaryButton: .default(Text("Cancel")), secondaryButton: .destructive(Text("Yes"), action: { 230 | projectViewModel.removeTag(label: selectedTag!) 231 | SPAlert.present(message: "Tag removed from Project", haptic: .error) 232 | })) 233 | } 234 | }).actionSheet(item: $activeActionSheet, content: { item in 235 | switch item { 236 | case .collabActionSheet: 237 | return ActionSheet(title: Text("\(selectedCollaborator!.fullName)"), buttons: [ 238 | .destructive(Text("Remove")) { 239 | projectViewModel.removeCollaborator(userId: selectedCollaborator!.id) 240 | SPAlert.present(message: "Removed from Project", haptic: .error) 241 | }, 242 | .cancel() 243 | ]) 244 | case .tagActionSheet: 245 | return ActionSheet(title: Text("\(selectedTag!)"), buttons: [ 246 | .destructive(Text("Remove \(selectedTag!)")) { 247 | self.activeAlert = .removeTagAlert 248 | }, 249 | .cancel() 250 | ]) 251 | } 252 | }) 253 | ).onReceive(pressedTag.publisher) { t in 254 | //print("received, is \(t)") 255 | } 256 | } 257 | 258 | func taskListOf(taskStatus: TaskStatus) -> some View { 259 | let filteredTasks = projectViewModel.project.tasks.filter({ task -> Bool in 260 | if task.taskStatus == taskStatus{ 261 | return true 262 | } 263 | return false 264 | }) 265 | 266 | return GeometryReader { geometry in 267 | ScrollView(.vertical) { 268 | VStack{ 269 | ForEach(filteredTasks) { task in 270 | TaskView(task: task, projectViewModel: projectViewModel,onEditPressed: { 271 | activeSheet = .updateTaskSheet 272 | projectViewModel.selected(task: task) 273 | }, onRemovePressed: { 274 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.6){ 275 | withAnimation(.easeInOut(duration: 0.20)) { 276 | projectViewModel.remove(task: task) 277 | } 278 | } 279 | }, onStatusChanged: { selectedStatus in 280 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.6){ 281 | let taskId = task.id 282 | withAnimation(.easeInOut(duration: 0.20)){ 283 | projectViewModel.updateTaskStatus(withId: taskId, to: selectedStatus) 284 | 285 | if selectedStatus == .completed { 286 | showConfetti.toggle() 287 | 288 | self.timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in 289 | withAnimation{ 290 | self.showConfetti.toggle() 291 | } 292 | 293 | } 294 | } 295 | 296 | let completedCount = Float(projectViewModel.project.tasks.filter { task -> Bool in 297 | if task.taskStatus == .completed { 298 | return true 299 | } 300 | return false 301 | }.count) 302 | let abortedCount = Float(projectViewModel.project.tasks.filter { task -> Bool in 303 | if task.taskStatus == .aborted { 304 | return true 305 | } 306 | return false 307 | }.count) 308 | let total = Float(projectViewModel.project.tasks.count) - abortedCount 309 | let val = completedCount/total 310 | self.progressValue = val 311 | } 312 | if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { 313 | SKStoreReviewController.requestReview(in: scene) 314 | } 315 | } 316 | 317 | }, onChipPressed: { pressedTag in 318 | self.pressedTag = pressedTag 319 | print("pressedTag is \(pressedTag)") 320 | self.activeSheet = .testSheet 321 | }) 322 | .padding([.leading, .trailing]).padding(.bottom, 8).transition(.slide) 323 | } 324 | }.frame(width: geometry.size.width, height: 128.0*CGFloat(filteredTasks.count), alignment: .leading) 325 | } 326 | } 327 | } 328 | 329 | func buildTaskSheet() -> some View { 330 | var tasks = getTasks(withTag: pressedTag ?? "") 331 | 332 | tasks.sort(by:{ lhs, rhs in 333 | return lhs.taskStatus.rawValue < rhs.taskStatus.rawValue 334 | }) 335 | 336 | return GeometryReader{geometry in 337 | ScrollView(.vertical){ 338 | Indicator().padding() 339 | HStack{ 340 | Text("\(tasks.count) tasks found with").font(.callout) 341 | SmallChip(color: Color(self.projectViewModel.project.tags![pressedTag!]!), label: pressedTag!, onPressed: {}) 342 | } 343 | VStack(alignment: .leading, spacing: 0){ 344 | ForEach(tasks) { task in 345 | TaskView(task: task, projectViewModel: projectViewModel,onEditPressed: { 346 | activeSheet = .updateTaskSheet 347 | projectViewModel.selected(task: task) 348 | }, onRemovePressed: { 349 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.6){ 350 | withAnimation(.easeInOut(duration: 0.20)) { 351 | projectViewModel.remove(task: task) 352 | } 353 | } 354 | }, onStatusChanged: { selectedStatus in 355 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.6){ 356 | let taskId = task.id 357 | withAnimation(.easeInOut(duration: 0.20)){ 358 | projectViewModel.updateTaskStatus(withId: taskId, to: selectedStatus) 359 | 360 | if selectedStatus == .completed { 361 | showConfetti.toggle() 362 | 363 | self.timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { _ in 364 | withAnimation{ 365 | self.showConfetti.toggle() 366 | } 367 | 368 | } 369 | } 370 | 371 | let completedCount = Float(projectViewModel.project.tasks.filter { task -> Bool in 372 | if task.taskStatus == .completed { 373 | return true 374 | } 375 | return false 376 | }.count) 377 | let abortedCount = Float(projectViewModel.project.tasks.filter { task -> Bool in 378 | if task.taskStatus == .aborted { 379 | return true 380 | } 381 | return false 382 | }.count) 383 | let total = Float(projectViewModel.project.tasks.count) - abortedCount 384 | let val = completedCount/total 385 | self.progressValue = val 386 | } 387 | if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { 388 | SKStoreReviewController.requestReview(in: scene) 389 | } 390 | } 391 | 392 | }, onChipPressed: { pressedTag in 393 | self.pressedTag = pressedTag 394 | withAnimation{ 395 | self.activeSheet = .testSheet 396 | } 397 | }).frame(height: 120) 398 | .padding([.leading, .trailing]).padding(.bottom, 8)//.background(Color.red) 399 | } 400 | }.transition(.fade).frame(width: geometry.size.width, height: 128.0*CGFloat(tasks.count), alignment: .leading).padding(.top, 0) 401 | }.frame(width: geometry.size.width, height: geometry.size.height, alignment: .leading) 402 | } 403 | } 404 | 405 | func getTasks(withTag tag: String) -> [Task]{ 406 | return self.projectViewModel.project.tasks.filter({ t in 407 | return t.tags?.contains(where: { key, val in key == tag}) ?? false 408 | }) 409 | } 410 | } 411 | 412 | struct TaskListView_Previews: PreviewProvider { 413 | static var previews: some View { 414 | EmptyView() 415 | // TaskListView(projectViewModel: ProjectViewModel(project: Project(name: "My project", tasks: [Task(id: "", title: "This is a task", content: "something needs to be done before blablabla", taskStatus: .awaiting, timestamp: NSDate().timeIntervalSince1970), Task(id: "1", title: "This is a task", content: "something needs to be done before blablabla", taskStatus: .awaiting, timestamp: NSDate().timeIntervalSince1970)], timestamp: Date().timeIntervalSince1970)), onDelete: {_ in}) 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /Tasky.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5E9849B325C401C200AD938C /* TaskyApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9849B225C401C200AD938C /* TaskyApp.swift */; }; 11 | 5E9849B525C401C200AD938C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9849B425C401C200AD938C /* ContentView.swift */; }; 12 | 5E9849B725C401C300AD938C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5E9849B625C401C300AD938C /* Assets.xcassets */; }; 13 | 5E9849BA25C401C300AD938C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5E9849B925C401C300AD938C /* Preview Assets.xcassets */; }; 14 | 5E9849BC25C401C300AD938C /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9849BB25C401C300AD938C /* Persistence.swift */; }; 15 | 5E9849BF25C401C300AD938C /* Tasky.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5E9849BD25C401C300AD938C /* Tasky.xcdatamodeld */; }; 16 | 5E9849CA25C401C400AD938C /* TaskyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9849C925C401C400AD938C /* TaskyTests.swift */; }; 17 | 5E9849D525C401C400AD938C /* TaskyUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E9849D425C401C400AD938C /* TaskyUITests.swift */; }; 18 | 5E9849E625C4021000AD938C /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5E9849E525C4021000AD938C /* GoogleService-Info.plist */; }; 19 | 5E9849E725C4021000AD938C /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5E9849E525C4021000AD938C /* GoogleService-Info.plist */; }; 20 | 5E9849E825C4021000AD938C /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5E9849E525C4021000AD938C /* GoogleService-Info.plist */; }; 21 | 5E9849F125C4024900AD938C /* FirebaseAppDistribution-Beta in Frameworks */ = {isa = PBXBuildFile; productRef = 5E9849F025C4024900AD938C /* FirebaseAppDistribution-Beta */; }; 22 | 5E9849F325C4024900AD938C /* FirebaseInstallations in Frameworks */ = {isa = PBXBuildFile; productRef = 5E9849F225C4024900AD938C /* FirebaseInstallations */; }; 23 | 5E9849F525C4024900AD938C /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 5E9849F425C4024900AD938C /* FirebaseMessaging */; }; 24 | 5E9849F725C4024900AD938C /* FirebaseDynamicLinks in Frameworks */ = {isa = PBXBuildFile; productRef = 5E9849F625C4024900AD938C /* FirebaseDynamicLinks */; }; 25 | 5E9849F925C4024900AD938C /* FirebaseFirestoreSwift-Beta in Frameworks */ = {isa = PBXBuildFile; productRef = 5E9849F825C4024900AD938C /* FirebaseFirestoreSwift-Beta */; }; 26 | 5E9849FB25C4024900AD938C /* FirebaseInAppMessaging-Beta in Frameworks */ = {isa = PBXBuildFile; productRef = 5E9849FA25C4024900AD938C /* FirebaseInAppMessaging-Beta */; }; 27 | 5E9849FD25C4024900AD938C /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 5E9849FC25C4024900AD938C /* FirebaseAuth */; }; 28 | 5E984A0125C4024900AD938C /* FirebaseStorage in Frameworks */ = {isa = PBXBuildFile; productRef = 5E984A0025C4024900AD938C /* FirebaseStorage */; }; 29 | 5E984A0325C4024900AD938C /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 5E984A0225C4024900AD938C /* FirebaseFirestore */; }; 30 | 5E984A0525C4024900AD938C /* FirebaseDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 5E984A0425C4024900AD938C /* FirebaseDatabase */; }; 31 | 5E984A0725C4024900AD938C /* FirebaseFunctions in Frameworks */ = {isa = PBXBuildFile; productRef = 5E984A0625C4024900AD938C /* FirebaseFunctions */; }; 32 | 5E984A0B25C4024900AD938C /* FirebaseRemoteConfig in Frameworks */ = {isa = PBXBuildFile; productRef = 5E984A0A25C4024900AD938C /* FirebaseRemoteConfig */; }; 33 | 5E984A0D25C4024900AD938C /* FirebaseStorageSwift-Beta in Frameworks */ = {isa = PBXBuildFile; productRef = 5E984A0C25C4024900AD938C /* FirebaseStorageSwift-Beta */; }; 34 | 5E984A1D25C402AA00AD938C /* Project.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A1C25C402AA00AD938C /* Project.swift */; }; 35 | 5E984A1E25C402AA00AD938C /* Project.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A1C25C402AA00AD938C /* Project.swift */; }; 36 | 5E984A1F25C402AA00AD938C /* Project.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A1C25C402AA00AD938C /* Project.swift */; }; 37 | 5E984A2A25C404B800AD938C /* ProjectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A2925C404B800AD938C /* ProjectViewModel.swift */; }; 38 | 5E984A2F25C404D400AD938C /* ProjectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A2E25C404D400AD938C /* ProjectView.swift */; }; 39 | 5E984A3425C404E500AD938C /* AuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A3325C404E500AD938C /* AuthService.swift */; }; 40 | 5E984A3925C404F000AD938C /* ProjectRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A3825C404F000AD938C /* ProjectRepository.swift */; }; 41 | 5E984A4E25C4091800AD938C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A4D25C4091800AD938C /* AppDelegate.swift */; }; 42 | 5E984A5625C40A1C00AD938C /* ProjectListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A5525C40A1C00AD938C /* ProjectListView.swift */; }; 43 | 5E984A5B25C40A4E00AD938C /* ProjectListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A5A25C40A4E00AD938C /* ProjectListViewModel.swift */; }; 44 | 5E984A6025C40AAE00AD938C /* NewProjectSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A5F25C40AAE00AD938C /* NewProjectSheet.swift */; }; 45 | 5E984A6925C4898F00AD938C /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E984A6825C4898F00AD938C /* LoginView.swift */; }; 46 | E5047FDD25CB89FD00DD6C32 /* iPhoneNumberField in Frameworks */ = {isa = PBXBuildFile; productRef = E5047FDC25CB89FD00DD6C32 /* iPhoneNumberField */; }; 47 | E5047FE225CB8A2C00DD6C32 /* PhoneLoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5047FE125CB8A2C00DD6C32 /* PhoneLoginView.swift */; }; 48 | E5047FED25CB9EF300DD6C32 /* Task.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5047FEC25CB9EF300DD6C32 /* Task.swift */; }; 49 | E5047FF225CB9F6200DD6C32 /* TaskyUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5047FF125CB9F6200DD6C32 /* TaskyUser.swift */; }; 50 | E5203D4125DB494400F25C53 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5203D4025DB494400F25C53 /* ImagePicker.swift */; }; 51 | E525647F25CE3BC200EFCEC1 /* ProgressBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E525647E25CE3BC200EFCEC1 /* ProgressBar.swift */; }; 52 | E52D112D25D4918E006F44BB /* EditNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52D112C25D4918E006F44BB /* EditNameView.swift */; }; 53 | E52D113225D4A1A1006F44BB /* ProfileSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52D113125D4A1A1006F44BB /* ProfileSheet.swift */; }; 54 | E52D113E25D4AB1A006F44BB /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E52D113D25D4AB1A006F44BB /* Indicator.swift */; }; 55 | E54ECFA325D378C700646421 /* FASwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = E54ECFA225D378C700646421 /* FASwiftUI */; }; 56 | E54ECFA825D3793C00646421 /* icons.json in Resources */ = {isa = PBXBuildFile; fileRef = E54ECFA725D3793C00646421 /* icons.json */; }; 57 | E54ED00E25D37AC400646421 /* Font Awesome 5 Pro-Light-300.otf in Resources */ = {isa = PBXBuildFile; fileRef = E54ED00A25D37AC400646421 /* Font Awesome 5 Pro-Light-300.otf */; }; 58 | E54FC37925CCCD8000DA0656 /* TaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54FC37825CCCD8000DA0656 /* TaskView.swift */; }; 59 | E54FC38E25CCD3DB00DA0656 /* TaskListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54FC38D25CCD3DB00DA0656 /* TaskListView.swift */; }; 60 | E54FC39325CCE35B00DA0656 /* NewTaskSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E54FC39225CCE35B00DA0656 /* NewTaskSheet.swift */; }; 61 | E56BF67125DB51B9005DD5E7 /* UpdateNameSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E56BF67025DB51B9005DD5E7 /* UpdateNameSheet.swift */; }; 62 | E56BF67F25DB7798005DD5E7 /* AvatarService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E56BF67E25DB7798005DD5E7 /* AvatarService.swift */; }; 63 | E56BF68B25DB9795005DD5E7 /* PartialSheet in Frameworks */ = {isa = PBXBuildFile; productRef = E56BF68A25DB9795005DD5E7 /* PartialSheet */; }; 64 | E56BF69025DB9EE8005DD5E7 /* UpdateTaskSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E56BF68F25DB9EE8005DD5E7 /* UpdateTaskSheet.swift */; }; 65 | E57C3C2526689CE500F7015E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = E57C3C2426689CE500F7015E /* GoogleService-Info.plist */; }; 66 | E588450125D24C5D0008A371 /* EmailSignInForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = E588450025D24C5D0008A371 /* EmailSignInForm.swift */; }; 67 | E588450625D364ED0008A371 /* UpdateProjectForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = E588450525D364ED0008A371 /* UpdateProjectForm.swift */; }; 68 | E588E8D525D383A3005F3903 /* UserService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E588E8D425D383A3005F3903 /* UserService.swift */; }; 69 | E588E8E025D3A186005F3903 /* Font Awesome 5 Brands-Regular-400.otf in Resources */ = {isa = PBXBuildFile; fileRef = E588E8DF25D3A186005F3903 /* Font Awesome 5 Brands-Regular-400.otf */; }; 70 | E588E8E525D3A199005F3903 /* Font Awesome 5 Pro-Regular-400.otf in Resources */ = {isa = PBXBuildFile; fileRef = E588E8E425D3A199005F3903 /* Font Awesome 5 Pro-Regular-400.otf */; }; 71 | E588E8EA25D3A1A1005F3903 /* Font Awesome 5 Pro-Solid-900.otf in Resources */ = {isa = PBXBuildFile; fileRef = E588E8E925D3A1A1005F3903 /* Font Awesome 5 Pro-Solid-900.otf */; }; 72 | E588E8EF25D3D5F5005F3903 /* TaskDetailSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E588E8EE25D3D5F5005F3903 /* TaskDetailSheet.swift */; }; 73 | E5A1833A25DCA18900E1616D /* PeopleSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5A1833925DCA18900E1616D /* PeopleSheet.swift */; }; 74 | E5A1833F25DCAE2800E1616D /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5A1833E25DCAE2800E1616D /* Avatar.swift */; }; 75 | E5C6FAF425DE2A72003CB408 /* SPAlert in Frameworks */ = {isa = PBXBuildFile; productRef = E5C6FAF325DE2A72003CB408 /* SPAlert */; }; 76 | E5C6FAFD25DE3136003CB408 /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = E5C6FAFC25DE3136003CB408 /* SDWebImageSwiftUI */; }; 77 | E5C6FB0625DE424D003CB408 /* SDWebImageSVGCoder in Frameworks */ = {isa = PBXBuildFile; productRef = E5C6FB0525DE424D003CB408 /* SDWebImageSVGCoder */; }; 78 | E5CADDEA25E0C052000C6F46 /* Chip.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CADDE925E0C052000C6F46 /* Chip.swift */; }; 79 | E5CADDEF25E0CAF8000C6F46 /* NewTagSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5CADDEE25E0CAF8000C6F46 /* NewTagSheet.swift */; }; 80 | E5F1D03625DA5AE500B6DA2A /* ConfettiView in Frameworks */ = {isa = PBXBuildFile; productRef = E5F1D03525DA5AE500B6DA2A /* ConfettiView */; }; 81 | /* End PBXBuildFile section */ 82 | 83 | /* Begin PBXContainerItemProxy section */ 84 | 5E9849C625C401C400AD938C /* PBXContainerItemProxy */ = { 85 | isa = PBXContainerItemProxy; 86 | containerPortal = 5E9849A725C401C200AD938C /* Project object */; 87 | proxyType = 1; 88 | remoteGlobalIDString = 5E9849AE25C401C200AD938C; 89 | remoteInfo = Tasky; 90 | }; 91 | 5E9849D125C401C400AD938C /* PBXContainerItemProxy */ = { 92 | isa = PBXContainerItemProxy; 93 | containerPortal = 5E9849A725C401C200AD938C /* Project object */; 94 | proxyType = 1; 95 | remoteGlobalIDString = 5E9849AE25C401C200AD938C; 96 | remoteInfo = Tasky; 97 | }; 98 | /* End PBXContainerItemProxy section */ 99 | 100 | /* Begin PBXFileReference section */ 101 | 5E9849AF25C401C200AD938C /* Tasky.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Tasky.app; sourceTree = BUILT_PRODUCTS_DIR; }; 102 | 5E9849B225C401C200AD938C /* TaskyApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskyApp.swift; sourceTree = ""; }; 103 | 5E9849B425C401C200AD938C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 104 | 5E9849B625C401C300AD938C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 105 | 5E9849B925C401C300AD938C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 106 | 5E9849BB25C401C300AD938C /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; 107 | 5E9849BE25C401C300AD938C /* Tasky.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Tasky.xcdatamodel; sourceTree = ""; }; 108 | 5E9849C525C401C400AD938C /* TaskyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TaskyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 109 | 5E9849C925C401C400AD938C /* TaskyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskyTests.swift; sourceTree = ""; }; 110 | 5E9849CB25C401C400AD938C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 111 | 5E9849D025C401C400AD938C /* TaskyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TaskyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 112 | 5E9849D425C401C400AD938C /* TaskyUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskyUITests.swift; sourceTree = ""; }; 113 | 5E9849D625C401C400AD938C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 114 | 5E9849E525C4021000AD938C /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 115 | 5E984A1C25C402AA00AD938C /* Project.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Project.swift; sourceTree = ""; }; 116 | 5E984A2925C404B800AD938C /* ProjectViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectViewModel.swift; sourceTree = ""; }; 117 | 5E984A2E25C404D400AD938C /* ProjectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectView.swift; sourceTree = ""; }; 118 | 5E984A3325C404E500AD938C /* AuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthService.swift; sourceTree = ""; }; 119 | 5E984A3825C404F000AD938C /* ProjectRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectRepository.swift; sourceTree = ""; }; 120 | 5E984A4D25C4091800AD938C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 121 | 5E984A5525C40A1C00AD938C /* ProjectListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectListView.swift; sourceTree = ""; }; 122 | 5E984A5A25C40A4E00AD938C /* ProjectListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProjectListViewModel.swift; sourceTree = ""; }; 123 | 5E984A5F25C40AAE00AD938C /* NewProjectSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewProjectSheet.swift; sourceTree = ""; }; 124 | 5E984A6425C487D700AD938C /* Tasky.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tasky.entitlements; sourceTree = ""; }; 125 | 5E984A6825C4898F00AD938C /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 126 | E5047FE125CB8A2C00DD6C32 /* PhoneLoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneLoginView.swift; sourceTree = ""; }; 127 | E5047FEC25CB9EF300DD6C32 /* Task.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Task.swift; sourceTree = ""; }; 128 | E5047FF125CB9F6200DD6C32 /* TaskyUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskyUser.swift; sourceTree = ""; }; 129 | E5203D4025DB494400F25C53 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; 130 | E525647E25CE3BC200EFCEC1 /* ProgressBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBar.swift; sourceTree = ""; }; 131 | E52D112C25D4918E006F44BB /* EditNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditNameView.swift; sourceTree = ""; }; 132 | E52D113125D4A1A1006F44BB /* ProfileSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSheet.swift; sourceTree = ""; }; 133 | E52D113D25D4AB1A006F44BB /* Indicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Indicator.swift; sourceTree = ""; }; 134 | E54ECFA725D3793C00646421 /* icons.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = icons.json; sourceTree = ""; }; 135 | E54ED00A25D37AC400646421 /* Font Awesome 5 Pro-Light-300.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Font Awesome 5 Pro-Light-300.otf"; sourceTree = ""; }; 136 | E54ED01325D37B2C00646421 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 137 | E54FC37825CCCD8000DA0656 /* TaskView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskView.swift; sourceTree = ""; }; 138 | E54FC38D25CCD3DB00DA0656 /* TaskListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListView.swift; sourceTree = ""; }; 139 | E54FC39225CCE35B00DA0656 /* NewTaskSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTaskSheet.swift; sourceTree = ""; }; 140 | E56BF67025DB51B9005DD5E7 /* UpdateNameSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateNameSheet.swift; sourceTree = ""; }; 141 | E56BF67E25DB7798005DD5E7 /* AvatarService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarService.swift; sourceTree = ""; }; 142 | E56BF68F25DB9EE8005DD5E7 /* UpdateTaskSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateTaskSheet.swift; sourceTree = ""; }; 143 | E57C3C2426689CE500F7015E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 144 | E588450025D24C5D0008A371 /* EmailSignInForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignInForm.swift; sourceTree = ""; }; 145 | E588450525D364ED0008A371 /* UpdateProjectForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateProjectForm.swift; sourceTree = ""; }; 146 | E588E8D425D383A3005F3903 /* UserService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserService.swift; sourceTree = ""; }; 147 | E588E8DF25D3A186005F3903 /* Font Awesome 5 Brands-Regular-400.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Font Awesome 5 Brands-Regular-400.otf"; sourceTree = ""; }; 148 | E588E8E425D3A199005F3903 /* Font Awesome 5 Pro-Regular-400.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Font Awesome 5 Pro-Regular-400.otf"; sourceTree = ""; }; 149 | E588E8E925D3A1A1005F3903 /* Font Awesome 5 Pro-Solid-900.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Font Awesome 5 Pro-Solid-900.otf"; sourceTree = ""; }; 150 | E588E8EE25D3D5F5005F3903 /* TaskDetailSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskDetailSheet.swift; sourceTree = ""; }; 151 | E5A1833925DCA18900E1616D /* PeopleSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeopleSheet.swift; sourceTree = ""; }; 152 | E5A1833E25DCAE2800E1616D /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; 153 | E5CADDE925E0C052000C6F46 /* Chip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chip.swift; sourceTree = ""; }; 154 | E5CADDEE25E0CAF8000C6F46 /* NewTagSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTagSheet.swift; sourceTree = ""; }; 155 | /* End PBXFileReference section */ 156 | 157 | /* Begin PBXFrameworksBuildPhase section */ 158 | 5E9849AC25C401C200AD938C /* Frameworks */ = { 159 | isa = PBXFrameworksBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | E5C6FAFD25DE3136003CB408 /* SDWebImageSwiftUI in Frameworks */, 163 | E56BF68B25DB9795005DD5E7 /* PartialSheet in Frameworks */, 164 | 5E9849F525C4024900AD938C /* FirebaseMessaging in Frameworks */, 165 | E5C6FB0625DE424D003CB408 /* SDWebImageSVGCoder in Frameworks */, 166 | E5047FDD25CB89FD00DD6C32 /* iPhoneNumberField in Frameworks */, 167 | 5E9849F125C4024900AD938C /* FirebaseAppDistribution-Beta in Frameworks */, 168 | E5F1D03625DA5AE500B6DA2A /* ConfettiView in Frameworks */, 169 | 5E984A0125C4024900AD938C /* FirebaseStorage in Frameworks */, 170 | 5E984A0B25C4024900AD938C /* FirebaseRemoteConfig in Frameworks */, 171 | 5E984A0725C4024900AD938C /* FirebaseFunctions in Frameworks */, 172 | 5E984A0D25C4024900AD938C /* FirebaseStorageSwift-Beta in Frameworks */, 173 | 5E984A0325C4024900AD938C /* FirebaseFirestore in Frameworks */, 174 | 5E9849F925C4024900AD938C /* FirebaseFirestoreSwift-Beta in Frameworks */, 175 | E5C6FAF425DE2A72003CB408 /* SPAlert in Frameworks */, 176 | 5E9849F325C4024900AD938C /* FirebaseInstallations in Frameworks */, 177 | 5E9849F725C4024900AD938C /* FirebaseDynamicLinks in Frameworks */, 178 | E54ECFA325D378C700646421 /* FASwiftUI in Frameworks */, 179 | 5E984A0525C4024900AD938C /* FirebaseDatabase in Frameworks */, 180 | 5E9849FD25C4024900AD938C /* FirebaseAuth in Frameworks */, 181 | 5E9849FB25C4024900AD938C /* FirebaseInAppMessaging-Beta in Frameworks */, 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | }; 185 | 5E9849C225C401C400AD938C /* Frameworks */ = { 186 | isa = PBXFrameworksBuildPhase; 187 | buildActionMask = 2147483647; 188 | files = ( 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | }; 192 | 5E9849CD25C401C400AD938C /* Frameworks */ = { 193 | isa = PBXFrameworksBuildPhase; 194 | buildActionMask = 2147483647; 195 | files = ( 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | }; 199 | /* End PBXFrameworksBuildPhase section */ 200 | 201 | /* Begin PBXGroup section */ 202 | 5E9849A625C401C200AD938C = { 203 | isa = PBXGroup; 204 | children = ( 205 | 5E9849B125C401C200AD938C /* Tasky */, 206 | 5E9849C825C401C400AD938C /* TaskyTests */, 207 | 5E9849D325C401C400AD938C /* TaskyUITests */, 208 | 5E9849B025C401C200AD938C /* Products */, 209 | ); 210 | sourceTree = ""; 211 | }; 212 | 5E9849B025C401C200AD938C /* Products */ = { 213 | isa = PBXGroup; 214 | children = ( 215 | 5E9849AF25C401C200AD938C /* Tasky.app */, 216 | 5E9849C525C401C400AD938C /* TaskyTests.xctest */, 217 | 5E9849D025C401C400AD938C /* TaskyUITests.xctest */, 218 | ); 219 | name = Products; 220 | sourceTree = ""; 221 | }; 222 | 5E9849B125C401C200AD938C /* Tasky */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | E57C3C2426689CE500F7015E /* GoogleService-Info.plist */, 226 | E588E8E925D3A1A1005F3903 /* Font Awesome 5 Pro-Solid-900.otf */, 227 | E588E8E425D3A199005F3903 /* Font Awesome 5 Pro-Regular-400.otf */, 228 | E588E8DF25D3A186005F3903 /* Font Awesome 5 Brands-Regular-400.otf */, 229 | E54ED01325D37B2C00646421 /* Info.plist */, 230 | E54ED00A25D37AC400646421 /* Font Awesome 5 Pro-Light-300.otf */, 231 | E54ECFA725D3793C00646421 /* icons.json */, 232 | 5E984A6425C487D700AD938C /* Tasky.entitlements */, 233 | 5E984A1B25C4029000AD938C /* Repositories */, 234 | 5E984A1A25C4028100AD938C /* Services */, 235 | 5E984A1625C4026500AD938C /* Views */, 236 | 5E984A1225C4025A00AD938C /* ViewModels */, 237 | 5E984A0E25C4025100AD938C /* Models */, 238 | 5E9849E525C4021000AD938C /* GoogleService-Info.plist */, 239 | 5E9849B225C401C200AD938C /* TaskyApp.swift */, 240 | 5E9849B425C401C200AD938C /* ContentView.swift */, 241 | 5E9849B625C401C300AD938C /* Assets.xcassets */, 242 | 5E9849BB25C401C300AD938C /* Persistence.swift */, 243 | 5E9849BD25C401C300AD938C /* Tasky.xcdatamodeld */, 244 | 5E9849B825C401C300AD938C /* Preview Content */, 245 | 5E984A4D25C4091800AD938C /* AppDelegate.swift */, 246 | ); 247 | path = Tasky; 248 | sourceTree = ""; 249 | }; 250 | 5E9849B825C401C300AD938C /* Preview Content */ = { 251 | isa = PBXGroup; 252 | children = ( 253 | 5E9849B925C401C300AD938C /* Preview Assets.xcassets */, 254 | ); 255 | path = "Preview Content"; 256 | sourceTree = ""; 257 | }; 258 | 5E9849C825C401C400AD938C /* TaskyTests */ = { 259 | isa = PBXGroup; 260 | children = ( 261 | 5E9849C925C401C400AD938C /* TaskyTests.swift */, 262 | 5E9849CB25C401C400AD938C /* Info.plist */, 263 | ); 264 | path = TaskyTests; 265 | sourceTree = ""; 266 | }; 267 | 5E9849D325C401C400AD938C /* TaskyUITests */ = { 268 | isa = PBXGroup; 269 | children = ( 270 | 5E9849D425C401C400AD938C /* TaskyUITests.swift */, 271 | 5E9849D625C401C400AD938C /* Info.plist */, 272 | ); 273 | path = TaskyUITests; 274 | sourceTree = ""; 275 | }; 276 | 5E984A0E25C4025100AD938C /* Models */ = { 277 | isa = PBXGroup; 278 | children = ( 279 | 5E984A1C25C402AA00AD938C /* Project.swift */, 280 | E5047FEC25CB9EF300DD6C32 /* Task.swift */, 281 | E5047FF125CB9F6200DD6C32 /* TaskyUser.swift */, 282 | ); 283 | path = Models; 284 | sourceTree = ""; 285 | }; 286 | 5E984A1225C4025A00AD938C /* ViewModels */ = { 287 | isa = PBXGroup; 288 | children = ( 289 | 5E984A2925C404B800AD938C /* ProjectViewModel.swift */, 290 | 5E984A5A25C40A4E00AD938C /* ProjectListViewModel.swift */, 291 | ); 292 | path = ViewModels; 293 | sourceTree = ""; 294 | }; 295 | 5E984A1625C4026500AD938C /* Views */ = { 296 | isa = PBXGroup; 297 | children = ( 298 | E5E3DF4825DC9E5D00846739 /* Sheets */, 299 | E52D113C25D4AB07006F44BB /* Components */, 300 | 5E984A2E25C404D400AD938C /* ProjectView.swift */, 301 | 5E984A5525C40A1C00AD938C /* ProjectListView.swift */, 302 | 5E984A6825C4898F00AD938C /* LoginView.swift */, 303 | E5047FE125CB8A2C00DD6C32 /* PhoneLoginView.swift */, 304 | E54FC37825CCCD8000DA0656 /* TaskView.swift */, 305 | E54FC38D25CCD3DB00DA0656 /* TaskListView.swift */, 306 | E588450025D24C5D0008A371 /* EmailSignInForm.swift */, 307 | E588450525D364ED0008A371 /* UpdateProjectForm.swift */, 308 | E52D112C25D4918E006F44BB /* EditNameView.swift */, 309 | ); 310 | path = Views; 311 | sourceTree = ""; 312 | }; 313 | 5E984A1A25C4028100AD938C /* Services */ = { 314 | isa = PBXGroup; 315 | children = ( 316 | 5E984A3325C404E500AD938C /* AuthService.swift */, 317 | E588E8D425D383A3005F3903 /* UserService.swift */, 318 | E56BF67E25DB7798005DD5E7 /* AvatarService.swift */, 319 | ); 320 | path = Services; 321 | sourceTree = ""; 322 | }; 323 | 5E984A1B25C4029000AD938C /* Repositories */ = { 324 | isa = PBXGroup; 325 | children = ( 326 | 5E984A3825C404F000AD938C /* ProjectRepository.swift */, 327 | ); 328 | path = Repositories; 329 | sourceTree = ""; 330 | }; 331 | E52D113C25D4AB07006F44BB /* Components */ = { 332 | isa = PBXGroup; 333 | children = ( 334 | E525647E25CE3BC200EFCEC1 /* ProgressBar.swift */, 335 | E52D113D25D4AB1A006F44BB /* Indicator.swift */, 336 | E5203D4025DB494400F25C53 /* ImagePicker.swift */, 337 | E5A1833E25DCAE2800E1616D /* Avatar.swift */, 338 | E5CADDE925E0C052000C6F46 /* Chip.swift */, 339 | ); 340 | path = Components; 341 | sourceTree = ""; 342 | }; 343 | E5E3DF4825DC9E5D00846739 /* Sheets */ = { 344 | isa = PBXGroup; 345 | children = ( 346 | 5E984A5F25C40AAE00AD938C /* NewProjectSheet.swift */, 347 | E54FC39225CCE35B00DA0656 /* NewTaskSheet.swift */, 348 | E588E8EE25D3D5F5005F3903 /* TaskDetailSheet.swift */, 349 | E52D113125D4A1A1006F44BB /* ProfileSheet.swift */, 350 | E56BF67025DB51B9005DD5E7 /* UpdateNameSheet.swift */, 351 | E56BF68F25DB9EE8005DD5E7 /* UpdateTaskSheet.swift */, 352 | E5A1833925DCA18900E1616D /* PeopleSheet.swift */, 353 | E5CADDEE25E0CAF8000C6F46 /* NewTagSheet.swift */, 354 | ); 355 | path = Sheets; 356 | sourceTree = ""; 357 | }; 358 | /* End PBXGroup section */ 359 | 360 | /* Begin PBXNativeTarget section */ 361 | 5E9849AE25C401C200AD938C /* Tasky */ = { 362 | isa = PBXNativeTarget; 363 | buildConfigurationList = 5E9849D925C401C400AD938C /* Build configuration list for PBXNativeTarget "Tasky" */; 364 | buildPhases = ( 365 | 5E9849AB25C401C200AD938C /* Sources */, 366 | 5E9849AC25C401C200AD938C /* Frameworks */, 367 | 5E9849AD25C401C200AD938C /* Resources */, 368 | ); 369 | buildRules = ( 370 | ); 371 | dependencies = ( 372 | ); 373 | name = Tasky; 374 | packageProductDependencies = ( 375 | 5E9849F025C4024900AD938C /* FirebaseAppDistribution-Beta */, 376 | 5E9849F225C4024900AD938C /* FirebaseInstallations */, 377 | 5E9849F425C4024900AD938C /* FirebaseMessaging */, 378 | 5E9849F625C4024900AD938C /* FirebaseDynamicLinks */, 379 | 5E9849F825C4024900AD938C /* FirebaseFirestoreSwift-Beta */, 380 | 5E9849FA25C4024900AD938C /* FirebaseInAppMessaging-Beta */, 381 | 5E9849FC25C4024900AD938C /* FirebaseAuth */, 382 | 5E984A0025C4024900AD938C /* FirebaseStorage */, 383 | 5E984A0225C4024900AD938C /* FirebaseFirestore */, 384 | 5E984A0425C4024900AD938C /* FirebaseDatabase */, 385 | 5E984A0625C4024900AD938C /* FirebaseFunctions */, 386 | 5E984A0A25C4024900AD938C /* FirebaseRemoteConfig */, 387 | 5E984A0C25C4024900AD938C /* FirebaseStorageSwift-Beta */, 388 | E5047FDC25CB89FD00DD6C32 /* iPhoneNumberField */, 389 | E54ECFA225D378C700646421 /* FASwiftUI */, 390 | E5F1D03525DA5AE500B6DA2A /* ConfettiView */, 391 | E56BF68A25DB9795005DD5E7 /* PartialSheet */, 392 | E5C6FAF325DE2A72003CB408 /* SPAlert */, 393 | E5C6FAFC25DE3136003CB408 /* SDWebImageSwiftUI */, 394 | E5C6FB0525DE424D003CB408 /* SDWebImageSVGCoder */, 395 | ); 396 | productName = Tasky; 397 | productReference = 5E9849AF25C401C200AD938C /* Tasky.app */; 398 | productType = "com.apple.product-type.application"; 399 | }; 400 | 5E9849C425C401C400AD938C /* TaskyTests */ = { 401 | isa = PBXNativeTarget; 402 | buildConfigurationList = 5E9849DC25C401C400AD938C /* Build configuration list for PBXNativeTarget "TaskyTests" */; 403 | buildPhases = ( 404 | 5E9849C125C401C400AD938C /* Sources */, 405 | 5E9849C225C401C400AD938C /* Frameworks */, 406 | 5E9849C325C401C400AD938C /* Resources */, 407 | ); 408 | buildRules = ( 409 | ); 410 | dependencies = ( 411 | 5E9849C725C401C400AD938C /* PBXTargetDependency */, 412 | ); 413 | name = TaskyTests; 414 | productName = TaskyTests; 415 | productReference = 5E9849C525C401C400AD938C /* TaskyTests.xctest */; 416 | productType = "com.apple.product-type.bundle.unit-test"; 417 | }; 418 | 5E9849CF25C401C400AD938C /* TaskyUITests */ = { 419 | isa = PBXNativeTarget; 420 | buildConfigurationList = 5E9849DF25C401C400AD938C /* Build configuration list for PBXNativeTarget "TaskyUITests" */; 421 | buildPhases = ( 422 | 5E9849CC25C401C400AD938C /* Sources */, 423 | 5E9849CD25C401C400AD938C /* Frameworks */, 424 | 5E9849CE25C401C400AD938C /* Resources */, 425 | ); 426 | buildRules = ( 427 | ); 428 | dependencies = ( 429 | 5E9849D225C401C400AD938C /* PBXTargetDependency */, 430 | ); 431 | name = TaskyUITests; 432 | productName = TaskyUITests; 433 | productReference = 5E9849D025C401C400AD938C /* TaskyUITests.xctest */; 434 | productType = "com.apple.product-type.bundle.ui-testing"; 435 | }; 436 | /* End PBXNativeTarget section */ 437 | 438 | /* Begin PBXProject section */ 439 | 5E9849A725C401C200AD938C /* Project object */ = { 440 | isa = PBXProject; 441 | attributes = { 442 | LastSwiftUpdateCheck = 1240; 443 | LastUpgradeCheck = 1240; 444 | TargetAttributes = { 445 | 5E9849AE25C401C200AD938C = { 446 | CreatedOnToolsVersion = 12.4; 447 | }; 448 | 5E9849C425C401C400AD938C = { 449 | CreatedOnToolsVersion = 12.4; 450 | TestTargetID = 5E9849AE25C401C200AD938C; 451 | }; 452 | 5E9849CF25C401C400AD938C = { 453 | CreatedOnToolsVersion = 12.4; 454 | TestTargetID = 5E9849AE25C401C200AD938C; 455 | }; 456 | }; 457 | }; 458 | buildConfigurationList = 5E9849AA25C401C200AD938C /* Build configuration list for PBXProject "Tasky" */; 459 | compatibilityVersion = "Xcode 9.3"; 460 | developmentRegion = en; 461 | hasScannedForEncodings = 0; 462 | knownRegions = ( 463 | en, 464 | Base, 465 | ); 466 | mainGroup = 5E9849A625C401C200AD938C; 467 | packageReferences = ( 468 | 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, 469 | E5047FDB25CB89FD00DD6C32 /* XCRemoteSwiftPackageReference "iPhoneNumberField" */, 470 | E54ECFA125D378C600646421 /* XCRemoteSwiftPackageReference "FASwiftUI" */, 471 | E5F1D03425DA5AE500B6DA2A /* XCRemoteSwiftPackageReference "ConfettiView" */, 472 | E56BF68925DB9795005DD5E7 /* XCRemoteSwiftPackageReference "PartialSheet" */, 473 | E5C6FAF225DE2A71003CB408 /* XCRemoteSwiftPackageReference "SPAlert" */, 474 | E5C6FAFB25DE3136003CB408 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */, 475 | E5C6FB0425DE424D003CB408 /* XCRemoteSwiftPackageReference "SDWebImageSVGCoder" */, 476 | ); 477 | productRefGroup = 5E9849B025C401C200AD938C /* Products */; 478 | projectDirPath = ""; 479 | projectRoot = ""; 480 | targets = ( 481 | 5E9849AE25C401C200AD938C /* Tasky */, 482 | 5E9849C425C401C400AD938C /* TaskyTests */, 483 | 5E9849CF25C401C400AD938C /* TaskyUITests */, 484 | ); 485 | }; 486 | /* End PBXProject section */ 487 | 488 | /* Begin PBXResourcesBuildPhase section */ 489 | 5E9849AD25C401C200AD938C /* Resources */ = { 490 | isa = PBXResourcesBuildPhase; 491 | buildActionMask = 2147483647; 492 | files = ( 493 | E54ED00E25D37AC400646421 /* Font Awesome 5 Pro-Light-300.otf in Resources */, 494 | 5E9849BA25C401C300AD938C /* Preview Assets.xcassets in Resources */, 495 | E57C3C2526689CE500F7015E /* GoogleService-Info.plist in Resources */, 496 | E588E8E525D3A199005F3903 /* Font Awesome 5 Pro-Regular-400.otf in Resources */, 497 | 5E9849E625C4021000AD938C /* GoogleService-Info.plist in Resources */, 498 | E54ECFA825D3793C00646421 /* icons.json in Resources */, 499 | 5E9849B725C401C300AD938C /* Assets.xcassets in Resources */, 500 | E588E8EA25D3A1A1005F3903 /* Font Awesome 5 Pro-Solid-900.otf in Resources */, 501 | E588E8E025D3A186005F3903 /* Font Awesome 5 Brands-Regular-400.otf in Resources */, 502 | ); 503 | runOnlyForDeploymentPostprocessing = 0; 504 | }; 505 | 5E9849C325C401C400AD938C /* Resources */ = { 506 | isa = PBXResourcesBuildPhase; 507 | buildActionMask = 2147483647; 508 | files = ( 509 | 5E9849E725C4021000AD938C /* GoogleService-Info.plist in Resources */, 510 | ); 511 | runOnlyForDeploymentPostprocessing = 0; 512 | }; 513 | 5E9849CE25C401C400AD938C /* Resources */ = { 514 | isa = PBXResourcesBuildPhase; 515 | buildActionMask = 2147483647; 516 | files = ( 517 | 5E9849E825C4021000AD938C /* GoogleService-Info.plist in Resources */, 518 | ); 519 | runOnlyForDeploymentPostprocessing = 0; 520 | }; 521 | /* End PBXResourcesBuildPhase section */ 522 | 523 | /* Begin PBXSourcesBuildPhase section */ 524 | 5E9849AB25C401C200AD938C /* Sources */ = { 525 | isa = PBXSourcesBuildPhase; 526 | buildActionMask = 2147483647; 527 | files = ( 528 | 5E984A6025C40AAE00AD938C /* NewProjectSheet.swift in Sources */, 529 | E56BF67125DB51B9005DD5E7 /* UpdateNameSheet.swift in Sources */, 530 | E52D113225D4A1A1006F44BB /* ProfileSheet.swift in Sources */, 531 | E588E8EF25D3D5F5005F3903 /* TaskDetailSheet.swift in Sources */, 532 | E54FC39325CCE35B00DA0656 /* NewTaskSheet.swift in Sources */, 533 | 5E9849BC25C401C300AD938C /* Persistence.swift in Sources */, 534 | 5E9849B525C401C200AD938C /* ContentView.swift in Sources */, 535 | 5E9849BF25C401C300AD938C /* Tasky.xcdatamodeld in Sources */, 536 | E5A1833A25DCA18900E1616D /* PeopleSheet.swift in Sources */, 537 | 5E984A3925C404F000AD938C /* ProjectRepository.swift in Sources */, 538 | E52D112D25D4918E006F44BB /* EditNameView.swift in Sources */, 539 | E5047FE225CB8A2C00DD6C32 /* PhoneLoginView.swift in Sources */, 540 | 5E984A5B25C40A4E00AD938C /* ProjectListViewModel.swift in Sources */, 541 | E5CADDEA25E0C052000C6F46 /* Chip.swift in Sources */, 542 | E5047FF225CB9F6200DD6C32 /* TaskyUser.swift in Sources */, 543 | 5E984A1D25C402AA00AD938C /* Project.swift in Sources */, 544 | E54FC37925CCCD8000DA0656 /* TaskView.swift in Sources */, 545 | E5047FED25CB9EF300DD6C32 /* Task.swift in Sources */, 546 | E52D113E25D4AB1A006F44BB /* Indicator.swift in Sources */, 547 | E5A1833F25DCAE2800E1616D /* Avatar.swift in Sources */, 548 | 5E984A5625C40A1C00AD938C /* ProjectListView.swift in Sources */, 549 | E5203D4125DB494400F25C53 /* ImagePicker.swift in Sources */, 550 | E588E8D525D383A3005F3903 /* UserService.swift in Sources */, 551 | E525647F25CE3BC200EFCEC1 /* ProgressBar.swift in Sources */, 552 | 5E984A6925C4898F00AD938C /* LoginView.swift in Sources */, 553 | 5E984A2F25C404D400AD938C /* ProjectView.swift in Sources */, 554 | E588450625D364ED0008A371 /* UpdateProjectForm.swift in Sources */, 555 | E588450125D24C5D0008A371 /* EmailSignInForm.swift in Sources */, 556 | E54FC38E25CCD3DB00DA0656 /* TaskListView.swift in Sources */, 557 | E56BF67F25DB7798005DD5E7 /* AvatarService.swift in Sources */, 558 | 5E984A2A25C404B800AD938C /* ProjectViewModel.swift in Sources */, 559 | E5CADDEF25E0CAF8000C6F46 /* NewTagSheet.swift in Sources */, 560 | 5E9849B325C401C200AD938C /* TaskyApp.swift in Sources */, 561 | 5E984A4E25C4091800AD938C /* AppDelegate.swift in Sources */, 562 | 5E984A3425C404E500AD938C /* AuthService.swift in Sources */, 563 | E56BF69025DB9EE8005DD5E7 /* UpdateTaskSheet.swift in Sources */, 564 | ); 565 | runOnlyForDeploymentPostprocessing = 0; 566 | }; 567 | 5E9849C125C401C400AD938C /* Sources */ = { 568 | isa = PBXSourcesBuildPhase; 569 | buildActionMask = 2147483647; 570 | files = ( 571 | 5E9849CA25C401C400AD938C /* TaskyTests.swift in Sources */, 572 | 5E984A1E25C402AA00AD938C /* Project.swift in Sources */, 573 | ); 574 | runOnlyForDeploymentPostprocessing = 0; 575 | }; 576 | 5E9849CC25C401C400AD938C /* Sources */ = { 577 | isa = PBXSourcesBuildPhase; 578 | buildActionMask = 2147483647; 579 | files = ( 580 | 5E9849D525C401C400AD938C /* TaskyUITests.swift in Sources */, 581 | 5E984A1F25C402AA00AD938C /* Project.swift in Sources */, 582 | ); 583 | runOnlyForDeploymentPostprocessing = 0; 584 | }; 585 | /* End PBXSourcesBuildPhase section */ 586 | 587 | /* Begin PBXTargetDependency section */ 588 | 5E9849C725C401C400AD938C /* PBXTargetDependency */ = { 589 | isa = PBXTargetDependency; 590 | target = 5E9849AE25C401C200AD938C /* Tasky */; 591 | targetProxy = 5E9849C625C401C400AD938C /* PBXContainerItemProxy */; 592 | }; 593 | 5E9849D225C401C400AD938C /* PBXTargetDependency */ = { 594 | isa = PBXTargetDependency; 595 | target = 5E9849AE25C401C200AD938C /* Tasky */; 596 | targetProxy = 5E9849D125C401C400AD938C /* PBXContainerItemProxy */; 597 | }; 598 | /* End PBXTargetDependency section */ 599 | 600 | /* Begin XCBuildConfiguration section */ 601 | 5E9849D725C401C400AD938C /* Debug */ = { 602 | isa = XCBuildConfiguration; 603 | buildSettings = { 604 | ALWAYS_SEARCH_USER_PATHS = NO; 605 | CLANG_ANALYZER_NONNULL = YES; 606 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 607 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 608 | CLANG_CXX_LIBRARY = "libc++"; 609 | CLANG_ENABLE_MODULES = YES; 610 | CLANG_ENABLE_OBJC_ARC = YES; 611 | CLANG_ENABLE_OBJC_WEAK = YES; 612 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 613 | CLANG_WARN_BOOL_CONVERSION = YES; 614 | CLANG_WARN_COMMA = YES; 615 | CLANG_WARN_CONSTANT_CONVERSION = YES; 616 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 617 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 618 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 619 | CLANG_WARN_EMPTY_BODY = YES; 620 | CLANG_WARN_ENUM_CONVERSION = YES; 621 | CLANG_WARN_INFINITE_RECURSION = YES; 622 | CLANG_WARN_INT_CONVERSION = YES; 623 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 624 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 625 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 626 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 627 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 628 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 629 | CLANG_WARN_STRICT_PROTOTYPES = YES; 630 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 631 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 632 | CLANG_WARN_UNREACHABLE_CODE = YES; 633 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 634 | COPY_PHASE_STRIP = NO; 635 | DEBUG_INFORMATION_FORMAT = dwarf; 636 | ENABLE_STRICT_OBJC_MSGSEND = YES; 637 | ENABLE_TESTABILITY = YES; 638 | GCC_C_LANGUAGE_STANDARD = gnu11; 639 | GCC_DYNAMIC_NO_PIC = NO; 640 | GCC_NO_COMMON_BLOCKS = YES; 641 | GCC_OPTIMIZATION_LEVEL = 0; 642 | GCC_PREPROCESSOR_DEFINITIONS = ( 643 | "DEBUG=1", 644 | "$(inherited)", 645 | ); 646 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 647 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 648 | GCC_WARN_UNDECLARED_SELECTOR = YES; 649 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 650 | GCC_WARN_UNUSED_FUNCTION = YES; 651 | GCC_WARN_UNUSED_VARIABLE = YES; 652 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 653 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 654 | MTL_FAST_MATH = YES; 655 | ONLY_ACTIVE_ARCH = YES; 656 | SDKROOT = iphoneos; 657 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 658 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 659 | }; 660 | name = Debug; 661 | }; 662 | 5E9849D825C401C400AD938C /* Release */ = { 663 | isa = XCBuildConfiguration; 664 | buildSettings = { 665 | ALWAYS_SEARCH_USER_PATHS = NO; 666 | CLANG_ANALYZER_NONNULL = YES; 667 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 668 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 669 | CLANG_CXX_LIBRARY = "libc++"; 670 | CLANG_ENABLE_MODULES = YES; 671 | CLANG_ENABLE_OBJC_ARC = YES; 672 | CLANG_ENABLE_OBJC_WEAK = YES; 673 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 674 | CLANG_WARN_BOOL_CONVERSION = YES; 675 | CLANG_WARN_COMMA = YES; 676 | CLANG_WARN_CONSTANT_CONVERSION = YES; 677 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 678 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 679 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 680 | CLANG_WARN_EMPTY_BODY = YES; 681 | CLANG_WARN_ENUM_CONVERSION = YES; 682 | CLANG_WARN_INFINITE_RECURSION = YES; 683 | CLANG_WARN_INT_CONVERSION = YES; 684 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 685 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 686 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 687 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 688 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 689 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 690 | CLANG_WARN_STRICT_PROTOTYPES = YES; 691 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 692 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 693 | CLANG_WARN_UNREACHABLE_CODE = YES; 694 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 695 | COPY_PHASE_STRIP = NO; 696 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 697 | ENABLE_NS_ASSERTIONS = NO; 698 | ENABLE_STRICT_OBJC_MSGSEND = YES; 699 | GCC_C_LANGUAGE_STANDARD = gnu11; 700 | GCC_NO_COMMON_BLOCKS = YES; 701 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 702 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 703 | GCC_WARN_UNDECLARED_SELECTOR = YES; 704 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 705 | GCC_WARN_UNUSED_FUNCTION = YES; 706 | GCC_WARN_UNUSED_VARIABLE = YES; 707 | IPHONEOS_DEPLOYMENT_TARGET = 14.4; 708 | MTL_ENABLE_DEBUG_INFO = NO; 709 | MTL_FAST_MATH = YES; 710 | SDKROOT = iphoneos; 711 | SWIFT_COMPILATION_MODE = wholemodule; 712 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 713 | VALIDATE_PRODUCT = YES; 714 | }; 715 | name = Release; 716 | }; 717 | 5E9849DA25C401C400AD938C /* Debug */ = { 718 | isa = XCBuildConfiguration; 719 | buildSettings = { 720 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 721 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 722 | CODE_SIGN_ENTITLEMENTS = Tasky/Tasky.entitlements; 723 | CODE_SIGN_IDENTITY = "Apple Development"; 724 | CODE_SIGN_STYLE = Automatic; 725 | CURRENT_PROJECT_VERSION = 1; 726 | DEVELOPMENT_ASSET_PATHS = "Tasky/avatar.png Tasky/Preview\\ Content"; 727 | DEVELOPMENT_TEAM = QMWX3X2NF7; 728 | ENABLE_PREVIEWS = YES; 729 | INFOPLIST_FILE = Tasky/Info.plist; 730 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 731 | LD_RUNPATH_SEARCH_PATHS = ( 732 | "$(inherited)", 733 | "@executable_path/Frameworks", 734 | ); 735 | MARKETING_VERSION = 0.0.5; 736 | OTHER_LDFLAGS = "-Objc"; 737 | PRODUCT_BUNDLE_IDENTIFIER = jiaqifeng.tasky; 738 | PRODUCT_NAME = "$(TARGET_NAME)"; 739 | PROVISIONING_PROFILE_SPECIFIER = ""; 740 | SWIFT_VERSION = 5.0; 741 | TARGETED_DEVICE_FAMILY = 1; 742 | }; 743 | name = Debug; 744 | }; 745 | 5E9849DB25C401C400AD938C /* Release */ = { 746 | isa = XCBuildConfiguration; 747 | buildSettings = { 748 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 749 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 750 | CODE_SIGN_ENTITLEMENTS = Tasky/Tasky.entitlements; 751 | CODE_SIGN_IDENTITY = "Apple Development"; 752 | CODE_SIGN_STYLE = Automatic; 753 | CURRENT_PROJECT_VERSION = 1; 754 | DEVELOPMENT_ASSET_PATHS = "Tasky/avatar.png Tasky/Preview\\ Content"; 755 | DEVELOPMENT_TEAM = QMWX3X2NF7; 756 | ENABLE_PREVIEWS = YES; 757 | INFOPLIST_FILE = Tasky/Info.plist; 758 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 759 | LD_RUNPATH_SEARCH_PATHS = ( 760 | "$(inherited)", 761 | "@executable_path/Frameworks", 762 | ); 763 | MARKETING_VERSION = 0.0.5; 764 | OTHER_LDFLAGS = "-Objc"; 765 | PRODUCT_BUNDLE_IDENTIFIER = jiaqifeng.tasky; 766 | PRODUCT_NAME = "$(TARGET_NAME)"; 767 | PROVISIONING_PROFILE_SPECIFIER = ""; 768 | SWIFT_VERSION = 5.0; 769 | TARGETED_DEVICE_FAMILY = 1; 770 | }; 771 | name = Release; 772 | }; 773 | 5E9849DD25C401C400AD938C /* Debug */ = { 774 | isa = XCBuildConfiguration; 775 | buildSettings = { 776 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 777 | BUNDLE_LOADER = "$(TEST_HOST)"; 778 | CODE_SIGN_STYLE = Automatic; 779 | DEVELOPMENT_TEAM = QMWX3X2NF7; 780 | INFOPLIST_FILE = TaskyTests/Info.plist; 781 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 782 | LD_RUNPATH_SEARCH_PATHS = ( 783 | "$(inherited)", 784 | "@executable_path/Frameworks", 785 | "@loader_path/Frameworks", 786 | ); 787 | PRODUCT_BUNDLE_IDENTIFIER = jiaqifeng.TaskyTests; 788 | PRODUCT_NAME = "$(TARGET_NAME)"; 789 | SWIFT_VERSION = 5.0; 790 | TARGETED_DEVICE_FAMILY = "1,2"; 791 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Tasky.app/Tasky"; 792 | }; 793 | name = Debug; 794 | }; 795 | 5E9849DE25C401C400AD938C /* Release */ = { 796 | isa = XCBuildConfiguration; 797 | buildSettings = { 798 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 799 | BUNDLE_LOADER = "$(TEST_HOST)"; 800 | CODE_SIGN_STYLE = Automatic; 801 | DEVELOPMENT_TEAM = QMWX3X2NF7; 802 | INFOPLIST_FILE = TaskyTests/Info.plist; 803 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 804 | LD_RUNPATH_SEARCH_PATHS = ( 805 | "$(inherited)", 806 | "@executable_path/Frameworks", 807 | "@loader_path/Frameworks", 808 | ); 809 | PRODUCT_BUNDLE_IDENTIFIER = jiaqifeng.TaskyTests; 810 | PRODUCT_NAME = "$(TARGET_NAME)"; 811 | SWIFT_VERSION = 5.0; 812 | TARGETED_DEVICE_FAMILY = "1,2"; 813 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Tasky.app/Tasky"; 814 | }; 815 | name = Release; 816 | }; 817 | 5E9849E025C401C400AD938C /* Debug */ = { 818 | isa = XCBuildConfiguration; 819 | buildSettings = { 820 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 821 | CODE_SIGN_STYLE = Automatic; 822 | DEVELOPMENT_TEAM = QMWX3X2NF7; 823 | INFOPLIST_FILE = TaskyUITests/Info.plist; 824 | LD_RUNPATH_SEARCH_PATHS = ( 825 | "$(inherited)", 826 | "@executable_path/Frameworks", 827 | "@loader_path/Frameworks", 828 | ); 829 | PRODUCT_BUNDLE_IDENTIFIER = jiaqifeng.TaskyUITests; 830 | PRODUCT_NAME = "$(TARGET_NAME)"; 831 | SWIFT_VERSION = 5.0; 832 | TARGETED_DEVICE_FAMILY = "1,2"; 833 | TEST_TARGET_NAME = Tasky; 834 | }; 835 | name = Debug; 836 | }; 837 | 5E9849E125C401C400AD938C /* Release */ = { 838 | isa = XCBuildConfiguration; 839 | buildSettings = { 840 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 841 | CODE_SIGN_STYLE = Automatic; 842 | DEVELOPMENT_TEAM = QMWX3X2NF7; 843 | INFOPLIST_FILE = TaskyUITests/Info.plist; 844 | LD_RUNPATH_SEARCH_PATHS = ( 845 | "$(inherited)", 846 | "@executable_path/Frameworks", 847 | "@loader_path/Frameworks", 848 | ); 849 | PRODUCT_BUNDLE_IDENTIFIER = jiaqifeng.TaskyUITests; 850 | PRODUCT_NAME = "$(TARGET_NAME)"; 851 | SWIFT_VERSION = 5.0; 852 | TARGETED_DEVICE_FAMILY = "1,2"; 853 | TEST_TARGET_NAME = Tasky; 854 | }; 855 | name = Release; 856 | }; 857 | /* End XCBuildConfiguration section */ 858 | 859 | /* Begin XCConfigurationList section */ 860 | 5E9849AA25C401C200AD938C /* Build configuration list for PBXProject "Tasky" */ = { 861 | isa = XCConfigurationList; 862 | buildConfigurations = ( 863 | 5E9849D725C401C400AD938C /* Debug */, 864 | 5E9849D825C401C400AD938C /* Release */, 865 | ); 866 | defaultConfigurationIsVisible = 0; 867 | defaultConfigurationName = Release; 868 | }; 869 | 5E9849D925C401C400AD938C /* Build configuration list for PBXNativeTarget "Tasky" */ = { 870 | isa = XCConfigurationList; 871 | buildConfigurations = ( 872 | 5E9849DA25C401C400AD938C /* Debug */, 873 | 5E9849DB25C401C400AD938C /* Release */, 874 | ); 875 | defaultConfigurationIsVisible = 0; 876 | defaultConfigurationName = Release; 877 | }; 878 | 5E9849DC25C401C400AD938C /* Build configuration list for PBXNativeTarget "TaskyTests" */ = { 879 | isa = XCConfigurationList; 880 | buildConfigurations = ( 881 | 5E9849DD25C401C400AD938C /* Debug */, 882 | 5E9849DE25C401C400AD938C /* Release */, 883 | ); 884 | defaultConfigurationIsVisible = 0; 885 | defaultConfigurationName = Release; 886 | }; 887 | 5E9849DF25C401C400AD938C /* Build configuration list for PBXNativeTarget "TaskyUITests" */ = { 888 | isa = XCConfigurationList; 889 | buildConfigurations = ( 890 | 5E9849E025C401C400AD938C /* Debug */, 891 | 5E9849E125C401C400AD938C /* Release */, 892 | ); 893 | defaultConfigurationIsVisible = 0; 894 | defaultConfigurationName = Release; 895 | }; 896 | /* End XCConfigurationList section */ 897 | 898 | /* Begin XCRemoteSwiftPackageReference section */ 899 | 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { 900 | isa = XCRemoteSwiftPackageReference; 901 | repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; 902 | requirement = { 903 | kind = upToNextMajorVersion; 904 | minimumVersion = 7.5.0; 905 | }; 906 | }; 907 | E5047FDB25CB89FD00DD6C32 /* XCRemoteSwiftPackageReference "iPhoneNumberField" */ = { 908 | isa = XCRemoteSwiftPackageReference; 909 | repositoryURL = "https://github.com/MojtabaHs/iPhoneNumberField.git"; 910 | requirement = { 911 | kind = upToNextMajorVersion; 912 | minimumVersion = 0.5.4; 913 | }; 914 | }; 915 | E54ECFA125D378C600646421 /* XCRemoteSwiftPackageReference "FASwiftUI" */ = { 916 | isa = XCRemoteSwiftPackageReference; 917 | repositoryURL = "https://github.com/mattmaddux/FASwiftUI.git"; 918 | requirement = { 919 | kind = upToNextMajorVersion; 920 | minimumVersion = 1.0.4; 921 | }; 922 | }; 923 | E56BF68925DB9795005DD5E7 /* XCRemoteSwiftPackageReference "PartialSheet" */ = { 924 | isa = XCRemoteSwiftPackageReference; 925 | repositoryURL = "https://github.com/AndreaMiotto/PartialSheet"; 926 | requirement = { 927 | kind = upToNextMajorVersion; 928 | minimumVersion = 2.1.11; 929 | }; 930 | }; 931 | E5C6FAF225DE2A71003CB408 /* XCRemoteSwiftPackageReference "SPAlert" */ = { 932 | isa = XCRemoteSwiftPackageReference; 933 | repositoryURL = "https://github.com/varabeis/SPAlert.git"; 934 | requirement = { 935 | kind = upToNextMajorVersion; 936 | minimumVersion = 3.0.3; 937 | }; 938 | }; 939 | E5C6FAFB25DE3136003CB408 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = { 940 | isa = XCRemoteSwiftPackageReference; 941 | repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI"; 942 | requirement = { 943 | kind = upToNextMajorVersion; 944 | minimumVersion = 1.5.0; 945 | }; 946 | }; 947 | E5C6FB0425DE424D003CB408 /* XCRemoteSwiftPackageReference "SDWebImageSVGCoder" */ = { 948 | isa = XCRemoteSwiftPackageReference; 949 | repositoryURL = "https://github.com/SDWebImage/SDWebImageSVGCoder"; 950 | requirement = { 951 | kind = upToNextMajorVersion; 952 | minimumVersion = 1.6.0; 953 | }; 954 | }; 955 | E5F1D03425DA5AE500B6DA2A /* XCRemoteSwiftPackageReference "ConfettiView" */ = { 956 | isa = XCRemoteSwiftPackageReference; 957 | repositoryURL = "https://github.com/ziligy/ConfettiView"; 958 | requirement = { 959 | kind = upToNextMajorVersion; 960 | minimumVersion = 1.1.0; 961 | }; 962 | }; 963 | /* End XCRemoteSwiftPackageReference section */ 964 | 965 | /* Begin XCSwiftPackageProductDependency section */ 966 | 5E9849F025C4024900AD938C /* FirebaseAppDistribution-Beta */ = { 967 | isa = XCSwiftPackageProductDependency; 968 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 969 | productName = "FirebaseAppDistribution-Beta"; 970 | }; 971 | 5E9849F225C4024900AD938C /* FirebaseInstallations */ = { 972 | isa = XCSwiftPackageProductDependency; 973 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 974 | productName = FirebaseInstallations; 975 | }; 976 | 5E9849F425C4024900AD938C /* FirebaseMessaging */ = { 977 | isa = XCSwiftPackageProductDependency; 978 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 979 | productName = FirebaseMessaging; 980 | }; 981 | 5E9849F625C4024900AD938C /* FirebaseDynamicLinks */ = { 982 | isa = XCSwiftPackageProductDependency; 983 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 984 | productName = FirebaseDynamicLinks; 985 | }; 986 | 5E9849F825C4024900AD938C /* FirebaseFirestoreSwift-Beta */ = { 987 | isa = XCSwiftPackageProductDependency; 988 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 989 | productName = "FirebaseFirestoreSwift-Beta"; 990 | }; 991 | 5E9849FA25C4024900AD938C /* FirebaseInAppMessaging-Beta */ = { 992 | isa = XCSwiftPackageProductDependency; 993 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 994 | productName = "FirebaseInAppMessaging-Beta"; 995 | }; 996 | 5E9849FC25C4024900AD938C /* FirebaseAuth */ = { 997 | isa = XCSwiftPackageProductDependency; 998 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 999 | productName = FirebaseAuth; 1000 | }; 1001 | 5E984A0025C4024900AD938C /* FirebaseStorage */ = { 1002 | isa = XCSwiftPackageProductDependency; 1003 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 1004 | productName = FirebaseStorage; 1005 | }; 1006 | 5E984A0225C4024900AD938C /* FirebaseFirestore */ = { 1007 | isa = XCSwiftPackageProductDependency; 1008 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 1009 | productName = FirebaseFirestore; 1010 | }; 1011 | 5E984A0425C4024900AD938C /* FirebaseDatabase */ = { 1012 | isa = XCSwiftPackageProductDependency; 1013 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 1014 | productName = FirebaseDatabase; 1015 | }; 1016 | 5E984A0625C4024900AD938C /* FirebaseFunctions */ = { 1017 | isa = XCSwiftPackageProductDependency; 1018 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 1019 | productName = FirebaseFunctions; 1020 | }; 1021 | 5E984A0A25C4024900AD938C /* FirebaseRemoteConfig */ = { 1022 | isa = XCSwiftPackageProductDependency; 1023 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 1024 | productName = FirebaseRemoteConfig; 1025 | }; 1026 | 5E984A0C25C4024900AD938C /* FirebaseStorageSwift-Beta */ = { 1027 | isa = XCSwiftPackageProductDependency; 1028 | package = 5E9849EF25C4024900AD938C /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; 1029 | productName = "FirebaseStorageSwift-Beta"; 1030 | }; 1031 | E5047FDC25CB89FD00DD6C32 /* iPhoneNumberField */ = { 1032 | isa = XCSwiftPackageProductDependency; 1033 | package = E5047FDB25CB89FD00DD6C32 /* XCRemoteSwiftPackageReference "iPhoneNumberField" */; 1034 | productName = iPhoneNumberField; 1035 | }; 1036 | E54ECFA225D378C700646421 /* FASwiftUI */ = { 1037 | isa = XCSwiftPackageProductDependency; 1038 | package = E54ECFA125D378C600646421 /* XCRemoteSwiftPackageReference "FASwiftUI" */; 1039 | productName = FASwiftUI; 1040 | }; 1041 | E56BF68A25DB9795005DD5E7 /* PartialSheet */ = { 1042 | isa = XCSwiftPackageProductDependency; 1043 | package = E56BF68925DB9795005DD5E7 /* XCRemoteSwiftPackageReference "PartialSheet" */; 1044 | productName = PartialSheet; 1045 | }; 1046 | E5C6FAF325DE2A72003CB408 /* SPAlert */ = { 1047 | isa = XCSwiftPackageProductDependency; 1048 | package = E5C6FAF225DE2A71003CB408 /* XCRemoteSwiftPackageReference "SPAlert" */; 1049 | productName = SPAlert; 1050 | }; 1051 | E5C6FAFC25DE3136003CB408 /* SDWebImageSwiftUI */ = { 1052 | isa = XCSwiftPackageProductDependency; 1053 | package = E5C6FAFB25DE3136003CB408 /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */; 1054 | productName = SDWebImageSwiftUI; 1055 | }; 1056 | E5C6FB0525DE424D003CB408 /* SDWebImageSVGCoder */ = { 1057 | isa = XCSwiftPackageProductDependency; 1058 | package = E5C6FB0425DE424D003CB408 /* XCRemoteSwiftPackageReference "SDWebImageSVGCoder" */; 1059 | productName = SDWebImageSVGCoder; 1060 | }; 1061 | E5F1D03525DA5AE500B6DA2A /* ConfettiView */ = { 1062 | isa = XCSwiftPackageProductDependency; 1063 | package = E5F1D03425DA5AE500B6DA2A /* XCRemoteSwiftPackageReference "ConfettiView" */; 1064 | productName = ConfettiView; 1065 | }; 1066 | /* End XCSwiftPackageProductDependency section */ 1067 | 1068 | /* Begin XCVersionGroup section */ 1069 | 5E9849BD25C401C300AD938C /* Tasky.xcdatamodeld */ = { 1070 | isa = XCVersionGroup; 1071 | children = ( 1072 | 5E9849BE25C401C300AD938C /* Tasky.xcdatamodel */, 1073 | ); 1074 | currentVersion = 5E9849BE25C401C300AD938C /* Tasky.xcdatamodel */; 1075 | path = Tasky.xcdatamodeld; 1076 | sourceTree = ""; 1077 | versionGroupType = wrapper.xcdatamodel; 1078 | }; 1079 | /* End XCVersionGroup section */ 1080 | }; 1081 | rootObject = 5E9849A725C401C200AD938C /* Project object */; 1082 | } 1083 | --------------------------------------------------------------------------------