How to get and display contact photos on iOS with SwiftUI? - swiftui

I have a running script to read and display all contacts on iOS with SwiftUI but I am struggling to show the contact images as well. When I debug the code, imageData seems empty for all contacts and hasImageData is false. Generally the contacts are synchronized from Google to the iPhone, but for test purpose I have added photos on the iPhone to some contacts with the same result - no result.
Hopefully anybody have a clue what's going wrong :-)
import SwiftUI
import Contacts
struct FetchedContact {
var id: String
var firstName: String
var lastName: String
var telephone: String
}
func getContacts() -> [FetchedContact] {
var contacts = [FetchedContact]()
let store = CNContactStore()
store.requestAccess(for: .contacts) { (granted, error) in
if let error = error {
print("failed to request access", error)
return
}
if granted {
let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey]
let request = CNContactFetchRequest(keysToFetch: keys as [CNKeyDescriptor])
do {
try store.enumerateContacts(with: request, usingBlock: { (contact, stopPointer) in
contacts.append(FetchedContact(id: contact.identifier, firstName: contact.givenName, lastName: contact.familyName, telephone: contact.phoneNumbers.first?.value.stringValue ?? ""))
})
} catch let error {
print("Failed to enumerate contact", error)
}
} else {
print("access denied")
}
}
contacts.sort {
$0.firstName < $1.firstName
}
contacts.sort {
$0.lastName < $1.lastName
}
return contacts
}
struct ContactListView: View {
#State var contacts = [FetchedContact]()
var body: some View {
VStack {
Button(action: {
self.contacts = getContacts()
}) {
Text("Update contacts")
}
.padding()
Text("\(self.contacts.count) contacts found")
.padding()
List(self.contacts, id: \.id) { contact in
Text("\(contact.lastName), \(contact.firstName)")
}
.padding(.vertical)
}
.navigationTitle("Contact List View")
}
}
struct ContactListView_Previews: PreviewProvider {
static var previews: some View {
NavigationView() {
ContactListView()
}
}
}

Solution:
Add required keys to the query
let keys = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey, CNContactImageDataKey, CNContactImageDataAvailableKey, CNContactThumbnailImageDataKey]
Modify the image display
Image(uiImage: UIImage(data: contact.image ?? Data()) ?? UIImage())

Related

Why is my app failing to find a user during authentication?

I've had a problem on and off for the past week where my else statement is executing in the MainTabView upon login/signup (meaning it can't find the currentuser??) So rather than logging in and showing the main navigation, I see a white "loading.." screen after logging in. It's odd cause some log in's have worked fine and others crash the app. Any help is very appreciated!
I don't think the problem is within EmailAuth or CreateAccountAuth but let me know if you'd like to see the code for those too.
AuthViewModel:
import SwiftUI
import FirebaseAuth
import FirebaseCore
import FirebaseStorage
import FirebaseFirestore
import FirebaseFirestoreSwift
class AuthViewModel: NSObject, ObservableObject {
#Published var userSession: FirebaseAuth.User?
#Published var currentUser: User?
#Published var selectedImage: UIImage?
private let service = UserService()
static let shared = AuthViewModel()
override init() {
super.init()
userSession = Auth.auth().currentUser
fetchUser()
}
func login(withEmail email: String, password: String) {
Auth.auth().signIn(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to sign in with error \(error.localizedDescription)")
return
}
self.userSession = result?.user
self.fetchUser()
}
}
func register(withEmail email: String, password: String, fullname: String) {
Auth.auth().createUser(withEmail: email, password: password) { result, error in
if let error = error {
print("DEBUG: Failed to register with error \(error.localizedDescription)")
return
}
guard let user = result?.user else { return }
self.userSession = user
let data: [String: Any] = ["email": email,
"fullname": fullname]
COLLECTION_USERS
.document(user.uid)
.setData(data)
self.uploadProfileImage(self.selectedImage)
}
}
func signOut() {
// sets user session to nil so we show login view
self.userSession = nil
// signs user out on server
try? Auth.auth().signOut()
}
func uploadProfileImage(_ image: UIImage?) {
guard let uid = userSession?.uid else { return }
ImageUploader.uploadImage(image: image) { profileImageUrl in
COLLECTION_USERS
.document(uid)
.updateData(["profileImageUrl": profileImageUrl])
//{ _ in self.userSession = user }
}
}
func fetchUser() {
guard let uid = userSession?.uid else { return }
COLLECTION_USERS.document(uid).getDocument { snapshot, _ in
guard let user = try? snapshot?.data(as: User.self) else { return }
self.currentUser = user
}
}
}
App File:
struct Page_TurnerApp: App {
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
NavigationView {
ContentView().environmentObject(AuthViewModel())
}
}
}
}
ContentView
struct ContentView: View {
#EnvironmentObject var viewModel: AuthViewModel
var body: some View {
Group {
if viewModel.userSession != nil {
MainTabView()
} else {
EmailAuth()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
MainTabView
struct MainTabView: View {
#State private var selectedIndex = 0
#EnvironmentObject var viewModel: AuthViewModel
var body: some View {
if let user = viewModel.currentUser {
TabView(selection: $selectedIndex) {
ExploreView()
.onTapGesture {
self.selectedIndex = 0
}
.tabItem {
Image(systemName: "house")
}.tag(0)
SearchView()
.onTapGesture {
self.selectedIndex = 1
}
.tabItem {
Image(systemName: "magnifyingglass")
}.tag(1)
ConversationsView()
.onTapGesture {
self.selectedIndex = 2
}
.tabItem {
Image(systemName: "message")
}.tag(2)
AccountView(user: user)
.onTapGesture {
self.selectedIndex = 3
}
.tabItem {
Image(systemName: "person.crop.circle")
}.tag(3)
}
} else {
Text("loading...")
}
}
}
My problem was that my User wasn't being created unless there was a profile image. That's why it worked for certain users and crashed for others. I changed let profileImageUrl: String
to let profileImageUrl: String? and all users can log in now

SWIFTUI Displaying JSON Data in ContentView

I have been having trouble displaying my JSON into my content view. I can decode the data and save it into a dictionary as I have printed and seen. However when its time to display it in ContentView with a ForEach. I'm getting this error Cannot convert value of type '[String : String]' to expected argument type 'Binding' Below is my code for my ContentView, Struct and ApiCall. I have read other solutions on stackoverflow and tried them all but they do not work.
struct ContentView: View {
#StateObject var api = APICALL()
var body: some View {
let country = api.storedData.countries
VStack(alignment: .leading) {
ForEach(country.id, id: \.self) { country in
HStack(alignment: .top) {
Text("\(country.countries)")
}
}
.onAppear {
api.loadData()
}
}
}
}
My ApiCall class which loads the data, as well as the struct.
// MARK: - Country
struct Country: Codable, Identifiable {
let id = UUID()
var countries: [String: String]
enum CodingKeys: String, CodingKey {
case countries = "countries"
}
}
class APICALL: ObservableObject {
#Published var storedData = Country(countries: [:])
func loadData() {
let apikey = ""
guard let url = URL(string:"https://countries-cities.p.rapidapi.com/location/country/list?rapidapi-key=\(apikey)") else {
print("Your Api end point is Invalid")
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode(Country.self, from: data) {
DispatchQueue.main.async {
self.storedData.countries = response.countries
print(self.storedData.countries)
}
return
}
}
}
.resume()
}
}
Any Point in the right direction would be absolutely helpful.
you could try this approach to display your countries data:
struct ContentView: View {
#StateObject var api = APICALL()
var body: some View {
VStack(alignment: .leading) {
// -- here --
ForEach(Array(api.storedData.countries.enumerated()), id: \.0) { index, country in
HStack(alignment: .top) {
Text("\(country.key) \(country.value)")
}
}
.onAppear {
api.loadData()
}
}
}
}
you can also use this, if you prefer:
ForEach(api.storedData.countries.sorted(by: >), id: \.key) { key, value in
HStack(alignment: .top) {
Text("\(key) \(value)")
}
}

Published/Observed var not updating in view swiftui w/ called function

Struggling to get a simple example up and running in swiftui:
Load default list view (working)
click button that launches picker/filtering options (working)
select options, then click button to dismiss and call function with selected options (call is working)
display new list of objects returned from call (not working)
I'm stuck on #4 where the returned query isn't making it to the view. I suspect I'm creating a different instance when making the call in step #3 but it's not making sense to me where/how/why that matters.
I tried to simplify the code some, but it's still a bit, sorry for that.
Appreciate any help!
Main View with HStack and button to filter with:
import SwiftUI
import FirebaseFirestore
struct TestView: View {
#ObservedObject var query = Query()
#State var showMonPicker = false
#State var monFilter = "filter"
var body: some View {
VStack {
HStack(alignment: .center) {
Text("Monday")
Spacer()
Button(action: {
self.showMonPicker.toggle()
}, label: {
Text("\(monFilter)")
})
}
.padding()
ScrollView(.horizontal) {
LazyHStack(spacing: 35) {
ForEach(query.queriedList) { menuItems in
MenuItemView(menuItem: menuItems)
}
}
}
}
.sheet(isPresented: $showMonPicker, onDismiss: {
//optional function when picker dismissed
}, content: {
CuisineTypePicker(selectedCuisineType: $monFilter)
})
}
}
The Query() file that calls a base query with all results, and optional function to return specific results:
import Foundation
import FirebaseFirestore
class Query: ObservableObject {
#Published var queriedList: [MenuItem] = []
init() {
baseQuery()
}
func baseQuery() {
let queryRef = Firestore.firestore().collection("menuItems").limit(to: 50)
queryRef
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
self.queriedList = querySnapshot?.documents.compactMap { document in
try? document.data(as: MenuItem.self)
} ?? []
}
}
}
func filteredQuery(category: String?, glutenFree: Bool?) {
var filtered = Firestore.firestore().collection("menuItems").limit(to: 50)
// Sorting and Filtering Data
if let category = category, !category.isEmpty {
filtered = filtered.whereField("cuisineType", isEqualTo: category)
}
if let glutenFree = glutenFree, !glutenFree {
filtered = filtered.whereField("glutenFree", isEqualTo: true)
}
filtered
.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
self.queriedList = querySnapshot?.documents.compactMap { document in
try? document.data(as: MenuItem.self);
} ?? []
print(self.queriedList.count)
}
}
}
}
Picker view where I'm calling the filtered query:
import SwiftUI
struct CuisineTypePicker: View {
#State private var cuisineTypes = ["filter", "American", "Chinese", "French"]
#Environment(\.presentationMode) var presentationMode
#Binding var selectedCuisineType: String
#State var gfSelected = false
let query = Query()
var body: some View {
VStack(alignment: .center) {
//Buttons and formatting code removed to simplify..
}
.padding(.top)
Picker("", selection: $selectedCuisineType) {
ForEach(cuisineTypes, id: \.self) {
Text($0)
}
}
Spacer()
Button(action: {
self.query.filteredQuery(category: selectedCuisineType, glutenFree: gfSelected)
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text( "apply filters")
})
}
.padding()
}
}
I suspect that the issue stems from the fact that you aren't sharing the same instance of Query between your TestView and your CuisineTypePicker. So, when you start a new Firebase query on the instance contained in CuisineTypePicker, the results are never reflected in the main view.
Here's an example of how to solve that (with the Firebase code replaced with some non-asynchronous sample code for now):
struct MenuItem : Identifiable {
var id = UUID()
var cuisineType : String
var title : String
var glutenFree : Bool
}
struct ContentView: View {
#ObservedObject var query = Query()
#State var showMonPicker = false
#State var monFilter = "filter"
var body: some View {
VStack {
HStack(alignment: .center) {
Text("Monday")
Spacer()
Button(action: {
self.showMonPicker.toggle()
}, label: {
Text("\(monFilter)")
})
}
.padding()
ScrollView(.horizontal) {
LazyHStack(spacing: 35) {
ForEach(query.queriedList) { menuItem in
Text("\(menuItem.title) - \(menuItem.cuisineType)")
}
}
}
}
.sheet(isPresented: $showMonPicker, onDismiss: {
//optional function when picker dismissed
}, content: {
CuisineTypePicker(query: query, selectedCuisineType: $monFilter)
})
}
}
class Query: ObservableObject {
#Published var queriedList: [MenuItem] = []
private let allItems: [MenuItem] = [.init(cuisineType: "American", title: "Hamburger", glutenFree: false),.init(cuisineType: "Chinese", title: "Fried Rice", glutenFree: true)]
init() {
baseQuery()
}
func baseQuery() {
self.queriedList = allItems
}
func filteredQuery(category: String?, glutenFree: Bool?) {
queriedList = allItems.filter({ item in
if let category = category {
return item.cuisineType == category
} else {
return true
}
}).filter({item in
if let glutenFree = glutenFree {
return item.glutenFree == glutenFree
} else {
return true
}
})
}
}
struct CuisineTypePicker: View {
#ObservedObject var query : Query
#Binding var selectedCuisineType: String
#State private var gfSelected = false
private let cuisineTypes = ["filter", "American", "Chinese", "French"]
#Environment(\.presentationMode) private var presentationMode
var body: some View {
VStack(alignment: .center) {
//Buttons and formatting code removed to simplify..
}
.padding(.top)
Picker("", selection: $selectedCuisineType) {
ForEach(cuisineTypes, id: \.self) {
Text($0)
}
}
Spacer()
Button(action: {
self.query.filteredQuery(category: selectedCuisineType, glutenFree: gfSelected)
self.presentationMode.wrappedValue.dismiss()
}, label: {
Text( "apply filters")
})
}
}

Saving favorites to UserDefaults using struct id

I'm trying to save the users favorite cities in UserDefaults. Found this solution saving the struct ID - builds and runs but does not appear to be saving: On app relaunch, the previously tapped Button is reset.
I'm pretty sure I'm missing something…
Here's my data struct and class:
struct City: Codable {
var id = UUID().uuidString
var name: String
}
class Favorites: ObservableObject {
private var cities: Set<String>
let defaults = UserDefaults.standard
var items: [City] = [
City(name: "London"),
City(name: "Paris"),
City(name: "Berlin")
]
init() {
let decoder = PropertyListDecoder()
if let data = defaults.data(forKey: "Favorites") {
let cityData = try? decoder.decode(Set<String>.self, from: data)
self.cities = cityData ?? []
return
} else {
self.cities = []
}
}
func getTaskIds() -> Set<String> {
return self.cities
}
func contains(_ city: City) -> Bool {
cities.contains(city.id)
}
func add(_ city: City) {
objectWillChange.send()
cities.contains(city.id)
save()
}
func remove(_ city: City) {
objectWillChange.send()
cities.remove(city.id)
save()
}
func save() {
let encoder = PropertyListEncoder()
if let encoded = try? encoder.encode(tasks) {
defaults.setValue(encoded, forKey: "Favorites")
}
}
}
and here's the TestDataView
struct TestData: View {
#StateObject var favorites = Favorites()
var body: some View {
ForEach(self.favorites.items, id: \.id) { item in
VStack {
Text(item.title)
Button(action: {
if self.favorites.contains(item) {
self.favorites.remove(item)
} else {
self.favorites.add(item)
}
}) {
HStack {
Image(systemName: self.favorites.contains(item) ? "heart.fill" : "heart")
.foregroundColor(self.favorites.contains(item) ? .red : .white)
}
}
}
}
}
}
There were a few issues, which I'll address below. Here's the working code:
struct ContentView: View {
#StateObject var favorites = Favorites()
var body: some View {
VStack(spacing: 10) {
ForEach(Array(self.favorites.cities), id: \.id) { item in
VStack {
Text(item.name)
Button(action: {
if self.favorites.contains(item) {
self.favorites.remove(item)
} else {
self.favorites.add(item)
}
}) {
HStack {
Image(systemName: self.favorites.contains(item) ? "heart.fill" : "heart")
.foregroundColor(self.favorites.contains(item) ? .red : .black)
}
}
}
}
}
}
}
struct City: Codable, Hashable {
var id = UUID().uuidString
var name: String
}
class Favorites: ObservableObject {
#Published var cities: Set<City> = []
#Published var favorites: Set<String> = []
let defaults = UserDefaults.standard
var initialItems: [City] = [
City(name: "London"),
City(name: "Paris"),
City(name: "Berlin")
]
init() {
let decoder = PropertyListDecoder()
if let data = defaults.data(forKey: "Cities") {
cities = (try? decoder.decode(Set<City>.self, from: data)) ?? Set(initialItems)
} else {
cities = Set(initialItems)
}
self.favorites = Set(defaults.array(forKey: "Favorites") as? [String] ?? [])
}
func getTaskIds() -> Set<String> {
return self.favorites
}
func contains(_ city: City) -> Bool {
favorites.contains(city.id)
}
func add(_ city: City) {
favorites.insert(city.id)
save()
}
func remove(_ city: City) {
favorites.remove(city.id)
save()
}
func save() {
let encoder = PropertyListEncoder()
if let encoded = try? encoder.encode(self.cities) {
self.defaults.set(encoded, forKey: "Cities")
}
self.defaults.set(Array(self.favorites), forKey: "Favorites")
defaults.synchronize()
}
}
Issues with the original:
The biggest issue was that items was getting recreated on each new launch and City has an id that is assigned a UUID on creation. This guaranteed that every new launch, each batch of cities would have different UUIDs, so a saving situation would never work.
There were some general typos and references to properties that didn't actually exist.
What I did:
Made cities and favorites both #Published properties so that you don't have to call objectWillChange.send by hand
On init, load both the cities and the favorites. That way, the cities, once initially created, will always have the same UUIDs, since they're getting loaded from a saved state
On save, I save both Sets -- the favorites and the cities
In the original ForEach, I iterate through all of the cities and then only mark the ones that are part of favorites
Important note: While testing this, I discovered that at least on Xcode 12.3 / iOS 14.3, syncing to UserDefaults is slow, even when using the now-unrecommended synchronize method. I kept wondering why my changes weren't reflected when I killed and then re-opened the app. Eventually figured out that everything works if I give it about 10-15 seconds to sync to UserDefaults before killing the app and then opening it again.

SwiftUI List is working but not the picker

I have this view. The list works fine by showing the data. The picker is not working. It does not display any data. Both use the same objects and functions. I do not know the reason for the picker not showing data. I want to use the picker. I placed the List just to try to determine the problem but I still don't know.
import SwiftUI
struct GameListPicker: View {
#ObservedObject var gameListViewModel = GameListViewModel()
#State private var selectedGameList = ""
var body: some View {
VStack{
List(gameListViewModel.gameList){gameList in
HStack {
Text(gameList.gameName)
}
}
.onAppear() {self.gameListViewModel.fetchData()}
Picker(selection: $selectedGameList, label: Text("")){
ForEach(gameListViewModel.gameList) { gameList in
Text(gameList.gameName)
}
}
.onAppear() {self.gameListViewModel.fetchData()}}
}
}
This is the GameListViewModel
import Foundation
import Firebase
class GameListViewModel: ObservableObject{
#Published var gameList = [GameListModel]()
let db = Firestore.firestore()
func fetchData() {
db.collection("GameData").addSnapshotListener {(querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.gameList = documents.map { queryDocumentSnapshot -> GameListModel in
let data = queryDocumentSnapshot.data()
let gameName = data["GameName"] as? String ?? ""
return GameListModel(id: gameName, gameName: gameName)
}
}
}
}
This is the GameListModel
import Foundation
struct GameListModel: Codable, Hashable,Identifiable {
var id: String
//var id: String = UUID().uuidString
var gameName: String
}
Thanks in advance for any help
Picker is not designed to be dynamic, so it is not refreshed when data updated, try to force-rebuild picker when data changed, like below
Picker(selection: $selectedGameList, label: Text("")){
ForEach(gameListViewModel.gameList) { gameList in
Text(gameList.gameName)
}
}.id(gameListViewModel.gameList) // << here !!