From f8f3c6bea3e127097b809a442521dc8e33407b87 Mon Sep 17 00:00:00 2001 From: talksik Date: Wed, 15 Dec 2021 17:58:08 -0800 Subject: [PATCH] decent working showing contacts sheet to user --- nirvana-ios.xcodeproj/project.pbxproj | 16 ++++ nirvana-ios/Models/Contact.swift | 15 ---- nirvana-ios/Services/ContactService.swift | 12 +++ .../Views/Contacts/ContactsPickerView.swift | 76 +++++++++++++++++ nirvana-ios/Views/Contacts/ContactsView.swift | 39 +++++---- .../Views/Contacts/ContactsViewModel.swift | 84 +++++++++---------- 6 files changed, 169 insertions(+), 73 deletions(-) create mode 100644 nirvana-ios/Services/ContactService.swift create mode 100644 nirvana-ios/Views/Contacts/ContactsPickerView.swift diff --git a/nirvana-ios.xcodeproj/project.pbxproj b/nirvana-ios.xcodeproj/project.pbxproj index 9e67f33..79147f8 100644 --- a/nirvana-ios.xcodeproj/project.pbxproj +++ b/nirvana-ios.xcodeproj/project.pbxproj @@ -40,6 +40,8 @@ 89D99D2E2766FD6B00237814 /* Liquid in Frameworks */ = {isa = PBXBuildFile; productRef = 89D99D2D2766FD6B00237814 /* Liquid */; }; 89F05B9B276A87920002E9C7 /* ContactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F05B9A276A87920002E9C7 /* ContactsViewModel.swift */; }; 89F05B9F276A8D360002E9C7 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F05B9E276A8D360002E9C7 /* Contact.swift */; }; + 89F05BA4276ACC060002E9C7 /* ContactService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F05BA3276ACC060002E9C7 /* ContactService.swift */; }; + 89F05BA6276ACD830002E9C7 /* ContactsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F05BA5276ACD830002E9C7 /* ContactsPickerView.swift */; }; 89F1386D2767EB19006680ED /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F1386C2767EB19006680ED /* SignInView.swift */; }; 89F138702767F55F006680ED /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = 89F1386F2767F55F006680ED /* GoogleSignIn */; }; /* End PBXBuildFile section */ @@ -75,6 +77,8 @@ 89BDCDF02769E64200577829 /* Firestore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firestore.swift; sourceTree = ""; }; 89F05B9A276A87920002E9C7 /* ContactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsViewModel.swift; sourceTree = ""; }; 89F05B9E276A8D360002E9C7 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; + 89F05BA3276ACC060002E9C7 /* ContactService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactService.swift; sourceTree = ""; }; + 89F05BA5276ACD830002E9C7 /* ContactsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsPickerView.swift; sourceTree = ""; }; 89F1386C2767EB19006680ED /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -114,6 +118,7 @@ 891A15DF27653A7000B26659 /* nirvana-ios */ = { isa = PBXGroup; children = ( + 89F05BA2276ACBFA0002E9C7 /* Services */, 89B46E6F276893C900B74255 /* Auth */, 891A160C27657CD600B26659 /* Models */, 891A1609276572C600B26659 /* Globals */, @@ -248,10 +253,19 @@ children = ( 89BDCDEE2769B50C00577829 /* ContactsView.swift */, 89F05B9A276A87920002E9C7 /* ContactsViewModel.swift */, + 89F05BA5276ACD830002E9C7 /* ContactsPickerView.swift */, ); path = Contacts; sourceTree = ""; }; + 89F05BA2276ACBFA0002E9C7 /* Services */ = { + isa = PBXGroup; + children = ( + 89F05BA3276ACC060002E9C7 /* ContactService.swift */, + ); + path = Services; + sourceTree = ""; + }; 89F1386B2767EB07006680ED /* SignIn */ = { isa = PBXGroup; children = ( @@ -353,6 +367,7 @@ 892179E62765FECD001E6C60 /* FooterControlsViewModel.swift in Sources */, 891A15F5276557AB00B26659 /* colors.swift in Sources */, 89288ECB27675B9F00E962C4 /* OnboardingTemplateView.swift in Sources */, + 89F05BA4276ACC060002E9C7 /* ContactService.swift in Sources */, 89B46E71276893E200B74255 /* AuthSessionStore.swift in Sources */, 891A15F9276563CA00B26659 /* WelcomeView.swift in Sources */, 89BDCDEF2769B50C00577829 /* ContactsView.swift in Sources */, @@ -362,6 +377,7 @@ 89F05B9B276A87920002E9C7 /* ContactsViewModel.swift in Sources */, 891A15E127653A7000B26659 /* nirvana_iosApp.swift in Sources */, 891A160127656FB200B26659 /* FooterView.swift in Sources */, + 89F05BA6276ACD830002E9C7 /* ContactsPickerView.swift in Sources */, 89BDCDEA27697F9C00577829 /* UserMenu.swift in Sources */, 891A15FF27656F9200B26659 /* HeaderView.swift in Sources */, 892179E42765FEB8001E6C60 /* FooterControlsView.swift in Sources */, diff --git a/nirvana-ios/Models/Contact.swift b/nirvana-ios/Models/Contact.swift index 70aaf0e..334fbbe 100644 --- a/nirvana-ios/Models/Contact.swift +++ b/nirvana-ios/Models/Contact.swift @@ -15,18 +15,3 @@ struct Contact: Identifiable, Hashable { var phoneNumbers: [String] var emailAddresses: [String] } - -enum PermissionsError: Identifiable { - var id: String { UUID().uuidString } - case userError - case fetchError(_: Error) - var description: String { - switch self { - case .userError: - return "Please change permissions in settings" - case .fetchError(let error): - return error.localizedDescription - } - } - -} diff --git a/nirvana-ios/Services/ContactService.swift b/nirvana-ios/Services/ContactService.swift new file mode 100644 index 0000000..1edd150 --- /dev/null +++ b/nirvana-ios/Services/ContactService.swift @@ -0,0 +1,12 @@ +// +// ContactService.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/15/21. +// + +import Foundation + +public class ContactService { + +} diff --git a/nirvana-ios/Views/Contacts/ContactsPickerView.swift b/nirvana-ios/Views/Contacts/ContactsPickerView.swift new file mode 100644 index 0000000..667c402 --- /dev/null +++ b/nirvana-ios/Views/Contacts/ContactsPickerView.swift @@ -0,0 +1,76 @@ +// +// ContactsPickerView.swift +// nirvana-ios +// +// Created by Arjun Patel on 12/15/21. +// + +import SwiftUI +import Contacts + +struct ContactPickerView: View { + @Binding var showPicker: Bool + @Binding var selectedContact: CNContact? + var contacts: [String: [CNContact]] = loadContactsGrouped() + + var body: some View { + let keys = (Array(contacts.keys) as [String]).sorted() + NavigationView { + List { + ForEach(keys.sorted(), id: \.self) { key in + Section(header: Text(key)) { + ForEach(contacts[key] ?? [CNContact](), id: \.identifier) { contact in + ContactRow(contact: contact, showPicker: self.$showPicker, selectedContact: self.$selectedContact) + } + } + } + } + .listStyle(GroupedListStyle()) + .navigationTitle("All Contacts".localized) + .navigationBarItems(trailing: Button(action: {self.showPicker = false}, label: { + Text("Cancel".localized) + })) + } + } + + static func loadContactsGrouped() -> [String: [CNContact]] { + let provider = ContactsViewModel() + let contacts = provider.fetchContacts() + + let group = Dictionary(grouping: contacts) { (contact) -> String in + return String(contact.familyName.first ?? "?") + } + + return group + } +} + + +struct ContactRow: View { + var contact: CNContact + @Binding var showPicker: Bool + @Binding var selectedContact: CNContact? + + var body: some View { + Button(action: { + selectContact() + }) { + HStack { + Text("\(contact.familyName)").fontWeight(.bold) + Text("\(contact.givenName) \(contact.middleName)") + } + .foregroundColor(.black) + } + } + + func selectContact() { + self.selectedContact = self.contact + self.showPicker = false + } +} + +extension String { + var localized: String { + return NSLocalizedString(self, comment: "") + } +} diff --git a/nirvana-ios/Views/Contacts/ContactsView.swift b/nirvana-ios/Views/Contacts/ContactsView.swift index dc0c126..199ebdc 100644 --- a/nirvana-ios/Views/Contacts/ContactsView.swift +++ b/nirvana-ios/Views/Contacts/ContactsView.swift @@ -18,27 +18,36 @@ struct ContactsView: View { TestUser(_profilePic: "rohan", _firstN: "Rohan", _lastN: "Chadha") ] - @ObservedObject var contactsViewModel = ContactsViewModel() + @State var showPicker = false + @State var selectedContact: CNContact? var body: some View { - VStack(spacing: 0) { + VStack(alignment: .trailing, spacing: 0) { OnboardingTemplateView(imgName: "undraw_grades_re_j7d6", mainLeadingActText: "Add your ", mainHighlightedActText: "closest folks.", mainTrailingActText: "", subActText: "besties, bf or gf, siblings, parents, etc.") - Text("Add contacts") - .padding() - .foregroundColor(NirvanaColor.teal) - .overlay( - Rectangle() - .strokeBorder(style: StrokeStyle(lineWidth: 4, dash: [10])) - .foregroundColor(NirvanaColor.teal) - ) + Button(action: { + self.showPicker = true + }, label: { + Label("Add Contacts", systemImage: "plus.circle") + .frame(width: 50, height: 50, alignment: .center) + .labelStyle(.iconOnly) + .font(.largeTitle) + .foregroundColor(NirvanaColor.white) + .background(NirvanaColor.teal) + .padding(.vertical, 20) + .clipShape(Circle()) + .shadow(radius:10) + }) + + Text(selectedContact != nil ? "Selected: \((selectedContact?.familyName)!) \((selectedContact?.givenName)!)" : "Nothing selected".localized) + + Spacer() } + .sheet(isPresented: self.$showPicker) { + ContactPickerView(showPicker: self.$showPicker, selectedContact: self.$selectedContact) + } + .padding() .background(NirvanaColor.bgLightGrey) - .alert(item: $contactsViewModel.permissionsError) {_ in - Alert(title: Text("Permissions Needed"), - message: Text(contactsViewModel.permissionsError?.description ?? "unknown error"), - dismissButton: .default(Text("Ok"), action: contactsViewModel.openSettings)) - } } } diff --git a/nirvana-ios/Views/Contacts/ContactsViewModel.swift b/nirvana-ios/Views/Contacts/ContactsViewModel.swift index e60216e..f792014 100644 --- a/nirvana-ios/Views/Contacts/ContactsViewModel.swift +++ b/nirvana-ios/Views/Contacts/ContactsViewModel.swift @@ -9,60 +9,58 @@ import Foundation import Contacts import SwiftUI -final public class ContactsViewModel : ObservableObject { - @Published var contacts : [Contact] = [] - @Published var permissionsError: PermissionsError? = .none +class ContactsViewModel : ObservableObject { + @Published var showPermissionAlert = false + + let store: CNContactStore + let keys: [CNKeyDescriptor] init() { - self.permissions() + store = CNContactStore() + + keys = [CNContactImageDataKey as CNKeyDescriptor, + CNContactPhoneNumbersKey as CNKeyDescriptor, + CNContactEmailAddressesKey as CNKeyDescriptor, + CNContactFormatter.descriptorForRequiredKeys(for: .fullName)] } func openSettings() { - self.permissionsError = .none - guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return } - if UIApplication.shared.canOpenURL(settingsURL) { UIApplication.shared.open(settingsURL)} + guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else { return } + if UIApplication.shared.canOpenURL(settingsURL) { UIApplication.shared.open(settingsURL)} } - private func fetchContacts() { - //remove all current contacts from vm - contacts.removeAll() + func requestAccess() { + store.requestAccess(for: .contacts) { + (granted, error) in + return + } + // TODO do something useful with completionHandler + } + + func fetchContacts(sortOrder: CNContactSortOrder = .userDefault) -> [CNContact] { + let authStatus = CNContactStore.authorizationStatus(for: .contacts) + if authStatus == .notDetermined { + requestAccess() + } else if authStatus == .restricted { + // TODO prompt user that app is useless without access to contacts + self.showPermissionAlert = true + } - let store = CNContactStore() + let fetchRequest = CNContactFetchRequest(keysToFetch: keys) + fetchRequest.sortOrder = sortOrder + + var fetchedContacts = [CNContact]() do { - let predicate = CNContact.predicateForContacts(matchingName: "sarth") - //specific contact details: ex: name, email, etc. - let keysToFetch = [CNContactGivenNameKey, CNContactFamilyNameKey] as [CNKeyDescriptor] - - let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keysToFetch) - - print("Fetched contacts: \(contacts)") - } catch { - print("Failed to fetch contacts, error: \(error)") - // Handle the error - self.permissionsError = .fetchError(error) - } - } - - private func permissions() { - switch CNContactStore.authorizationStatus(for: .contacts) { - case.authorized: - print("fetching contacts") - fetchContacts() - case .notDetermined, .restricted, .denied: - CNContactStore().requestAccess(for: .contacts) {granted, error in - switch granted { - case true: - self.fetchContacts() - case false: - DispatchQueue.main.async { - print("unable to get contacts") - self.permissionsError = .userError - } - } + try store.enumerateContacts(with: fetchRequest) { + (contact, stop) in + fetchedContacts.append(contact) } - default: - fatalError("Unknown Error!") + } catch { + print("Unable to fetch contacts. \(error)") + } + + return fetchedContacts } }