= []
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 
2 |
3 |
4 | 
5 | [](https://apps.apple.com/us/app/tasky-task-made-easy/id1552534120)
6 | [](https://img.shields.io/badge/Price-Free-orange)
7 | [](https://badges.pufler.dev)
8 | [](https://img.shields.io/github/stars/livinglist/Tasky?style=social)
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
--------------------------------------------------------------------------------