How to asynchronous update image in custom CNContactViewController - ios

My app has a local database of contacts not stored in the internal contact store (users can however choose to add the contacts to the internal contact store)
I'm using the CNContactViewController to show the details of the contacts - but the image is not (always) stored in the database and has to be loaded (asynchronous) on each request.
A minimalized version of the ContactModel class:
class ContactModel {
var id: String
var givenName: String?
var thumbnail: Data?
init?(_ identifier: String?, givenName: String? = nil)
{
if ((identifier ?? "").isEmpty) {
return nil
}
self.id = identifier
}
func toMutableContact() -> CNMutableContact
{
let contact = CNMutableContact()
contact.contactType = .person
contact.givenName = givenName
if let thumbnail = thumbnail {
contact.imageData = thumbnail
}
return contact
}
}
The following code shows the contact details and gets the thumbnail from an online service
func showContact(_ model : ContactModel)
{
// Create a 'CNMutableContact' from the 'ContactModel' object
let contact : CNMutableContact = model.toMutableContact()
let store = CNContactStore()
let cvc = CNContactViewController(forUnknownContact: contact)
cvc.delegate = self
cvc.contactStore = CNContactStore()
cvc.allowsEditing = false
self.navigationController?.pushViewController(cvc, animated: true)
var hasThumbnail: Bool = false
if let _ = model.thumbnail {
hasThumbnail = true
}
if !hasThumbnail {
// Get the thumbnail from the online service
getThumbnail(id: model.id) { (data, error) in
// Everything above works as expected!
// This does not work either
contact.givenName = "XXX"
if let data = data {
// How can I update the image ?
print("imageDataAvailable (before): \(contact.imageDataAvailable)") // returns: false
contact.imageData = data
print("imageDataAvailable (after): \(contact.imageDataAvailable)") // returns: true
}
}
}
}
I'm not going to save the contact to the internal contact store - just using the CNContactViewController to view the details.
If the image is set in the ContactModel then the image is shown perfectly - but if any values are changed afterwards then the view is not updated.
(The user can choose not to save the thumbnails to the local database due to space consumption)

You can't do that. The contact displayed by the CNContactViewController is an immutable contact with no connection to the CNMutableContact you created at the outset (it's a copy).

Related

iOS - CoreData CloudKit sharing - participants adding children

I have implemented sharing of items from my Core Data store, using the NSPersistentCloudKitContainer api, with limited success. The objects I share (a TripsLog) have child objects (Trip), which appear on participants' devices. I've implemented more or less a direct copy of Apple's example code, except using my own core data entities.
The problem is that any new trips I create on participants' devices don't appear in the owner's database, whereas any I create on the owner's device appear for all participants, updating their displays pretty much instantly. If I edit any of the original trips on a participant's device, the changes are successfully shared, it's just the new ones which are problematic.
I'm getting a lot of noise in the console -
"Attempt to map database failed: permission was denied. This attempt will not be retried"
"Permission Failure" (10/2007); server message = "Invalid bundle ID for container"
etc
which really aren't helpful. The bundle ID & containers are set up properly and it all works fine for syncing across a single user's devices.
Although my code is more-or-less just the same as Apple's, except for the container identifier & the Core Data entities, here's most of the relevant stuff -
// sharing protocol
protocol CloudKitSharable: NSManagedObject {
static var entityName: String { get }
var identifier: String { get }
var sharedTitle: String { get }
var sharedSubject: String? { get }
var thumbnailImage: UIImage? { get }
}
class PersistenceController: NSObject {
enum CoreDataError: Error {
case modelURLNotFound(forResourceName: String)
case modelLoadingFailed(forURL: URL)
}
static let shared = PersistenceController()
private static let containerIdentifier = "iCloud.com.containerIdentifier"
private var _privatePersistentStore: NSPersistentStore?
var privatePersistentStore: NSPersistentStore {
return _privatePersistentStore!
}
private var _sharedPersistentStore: NSPersistentStore?
var sharedPersistentStore: NSPersistentStore {
return _sharedPersistentStore!
}
lazy var cloudKitContainer: CKContainer = {
return CKContainer(identifier: Self.containerIdentifier)
}()
lazy var persistentContainer: NSPersistentCloudKitContainer = {
let container: NSPersistentCloudKitContainer = try! mainDatabaseContainer()
guard let localDatabaseURL = localDatabaseURL, let cloudDatabaseURL = cloudDatabaseURL else {
fatalError("#\(#function): Failed to get local database URLs")
}
// Set up the database which will sync over the cloud
let cloudStoreDescription = NSPersistentStoreDescription(url: cloudDatabaseURL)
cloudStoreDescription.configuration = "Cloud"
cloudStoreDescription.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
setupStoreDescription(cloudStoreDescription)
let cloudKitContainerOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: Self.containerIdentifier)
cloudKitContainerOptions.databaseScope = .private
cloudStoreDescription.cloudKitContainerOptions = cloudKitContainerOptions
// Setting up for sharing
let sharedStoreDescription = cloudStoreDescription.copy() as! NSPersistentStoreDescription
sharedStoreDescription.url = sharedDatabaseURL
let sharedStoreOptions = NSPersistentCloudKitContainerOptions(containerIdentifier: Self.containerIdentifier)
sharedStoreOptions.databaseScope = .shared
sharedStoreDescription.cloudKitContainerOptions = sharedStoreOptions
// Set up the stuff which we don't want to sync to the cloud
let localStoreDescription = NSPersistentStoreDescription(url: localDatabaseURL)
localStoreDescription.configuration = "Local"
setupStoreDescription(localStoreDescription)
// finish setting up the container
container.persistentStoreDescriptions = [
cloudStoreDescription,
localStoreDescription,
sharedStoreDescription
]
loadPersistentStores(for: container)
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
container.viewContext.transactionAuthor = TransactionAuthor.app
/**
Automatically merge the changes from other contexts.
*/
container.viewContext.automaticallyMergesChangesFromParent = true
/**
Pin the viewContext to the current generation token and set it to keep itself up-to-date with local changes.
*/
do {
try container.viewContext.setQueryGenerationFrom(.current)
} catch {
fatalError("#\(#function): Failed to pin viewContext to the current generation:\(error)")
}
/**
Observe the following notifications:
- The remote change notifications from container.persistentStoreCoordinator.
- The .NSManagedObjectContextDidSave notifications from any context.
- The event change notifications from the container.
*/
NotificationCenter.default.addObserver(self, selector: #selector(storeRemoteChange(_:)),
name: .NSPersistentStoreRemoteChange,
object: container.persistentStoreCoordinator)
NotificationCenter.default.addObserver(self, selector: #selector(containerEventChanged(_:)),
name: NSPersistentCloudKitContainer.eventChangedNotification,
object: container)
return container
}()
func mergeTransactions(_ transactions: [NSPersistentHistoryTransaction], to context: NSManagedObjectContext) {
context.perform {
for transaction in transactions {
context.mergeChanges(fromContextDidSave: transaction.objectIDNotification())
}
}
}
private var cloudDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("CloudDatabase.sqlite")
}
private var localDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("Database.sqlite")
}
private var sharedDatabaseURL: URL? {
return UIApplication.applicationSupportDirectory?.appendingPathComponent("Shared.sqlite")
}
private func setupStoreDescription(_ description: NSPersistentStoreDescription) {
description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
description.shouldInferMappingModelAutomatically = true
}
private func model(name: String) throws -> NSManagedObjectModel {
return try loadModel(name: name, bundle: Bundle.main)
}
private func loadModel(name: String, bundle: Bundle) throws -> NSManagedObjectModel {
guard let modelURL = bundle.url(forResource: name, withExtension: "momd") else {
throw CoreDataError.modelURLNotFound(forResourceName: name)
}
guard let model = NSManagedObjectModel(contentsOf: modelURL) else {
throw CoreDataError.modelLoadingFailed(forURL: modelURL)
}
return model
}
private func mainDatabaseContainer() throws -> NSPersistentCloudKitContainer {
return NSPersistentCloudKitContainer(name: "Database", managedObjectModel: try model(name: "Database"))
}
private func locationsContainer() throws -> NSPersistentContainer {
return NSPersistentContainer(name: "Locations", managedObjectModel: try model(name: "Locations"))
}
private func loadPersistentStores(for container: NSPersistentContainer) {
container.loadPersistentStores { [unowned self] storeDescription, error in
if let error = error {
fatalError("#\(#function): Failed to load persistent stores:\(error)")
} else {
print("Database store ok: ", storeDescription)
if let containerOptions = storeDescription.cloudKitContainerOptions, let url = self.sharedDatabaseURL {
if containerOptions.databaseScope == .shared {
let sharedStore = container.persistentStoreCoordinator.persistentStore(for: url)
self._sharedPersistentStore = sharedStore
}
} else if let url = self.cloudDatabaseURL {
let privateStore = container.persistentStoreCoordinator.persistentStore(for: url)
self._privatePersistentStore = privateStore
}
}
}
}
/**
An operation queue for handling history-processing tasks: watching changes, deduplicating tags, and triggering UI updates, if needed.
*/
lazy var historyQueue: OperationQueue = {
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
return queue
}()
}
// Sharing extensions
extension PersistenceController {
func presentCloudSharingController<T: CloudKitSharable>(for item: T) {
/**
Grab the share if the item is already shared.
*/
var itemToShare: CKShare?
if let shareSet = try? persistentContainer.fetchShares(matching: [item.objectID]), let (_, share) = shareSet.first {
itemToShare = share
}
let sharingController: UICloudSharingController
if let itemToShare = itemToShare {
sharingController = UICloudSharingController(share: itemToShare, container: cloudKitContainer)
} else {
sharingController = newSharingController(for: item)
}
sharingController.delegate = self
/**
Setting the presentation style to .formSheet so there's no need to specify sourceView, sourceItem, or sourceRect.
*/
if let viewController = rootViewController {
sharingController.modalPresentationStyle = .formSheet
viewController.present(sharingController, animated: true)
}
}
func presentCloudSharingController(share: CKShare) {
let sharingController = UICloudSharingController(share: share, container: cloudKitContainer)
sharingController.delegate = self
/**
Setting the presentation style to .formSheet so there's no need to specify sourceView, sourceItem, or sourceRect.
*/
if let viewController = rootViewController {
sharingController.modalPresentationStyle = .formSheet
viewController.present(sharingController, animated: true)
}
}
private func newSharingController<T: CloudKitSharable>(for unsharedItem: T) -> UICloudSharingController {
return UICloudSharingController { (_, completion: #escaping (CKShare?, CKContainer?, Error?) -> Void) in
/**
The app doesn't specify a share intentionally, so Core Data creates a new share (zone).
CloudKit has a limit on how many zones a database can have, so this app provides an option for users to use an existing share.
If the share's publicPermission is CKShareParticipantPermissionNone, only private participants can accept the share.
Private participants mean the participants an app adds to a share by calling CKShare.addParticipant.
If the share is more permissive, and is, therefore, a public share, anyone with the shareURL can accept it,
or self-add themselves to it.
The default value of publicPermission is CKShare.ParticipantPermission.none.
*/
self.persistentContainer.share([unsharedItem], to: nil) { objectIDs, share, container, error in
if let share = share {
self.configure(share: share, with: unsharedItem)
}
completion(share, container, error)
}
}
}
private var rootViewController: UIViewController? {
for scene in UIApplication.shared.connectedScenes {
if scene.activationState == .foregroundActive,
let sceneDelegate = (scene as? UIWindowScene)?.delegate as? UIWindowSceneDelegate,
let window = sceneDelegate.window {
return window?.rootViewController
}
}
print("\(#function): Failed to retrieve the window's root view controller.")
return nil
}
}
extension PersistenceController: UICloudSharingControllerDelegate {
/**
CloudKit triggers the delegate method in two cases:
- An owner stops sharing a share.
- A participant removes themselves from a share by tapping the Remove Me button in UICloudSharingController.
After stopping the sharing, purge the zone or just wait for an import to update the local store.
This sample chooses to purge the zone to avoid stale UI. That triggers a "zone not found" error because UICloudSharingController
deletes the zone, but the error doesn't really matter in this context.
Purging the zone has a caveat:
- When sharing an object from the owner side, Core Data moves the object to the shared zone.
- When calling purgeObjectsAndRecordsInZone, Core Data removes all the objects and records in the zone.
To keep the objects, deep copy the object graph you want to keep and make sure no object in the new graph is associated with any share.
The purge API posts an NSPersistentStoreRemoteChange notification after finishing its job, so observe the notification to update
the UI, if necessary.
*/
func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) {
if let share = csc.share {
purgeObjectsAndRecords(with: share)
}
}
func cloudSharingControllerDidSaveShare(_ csc: UICloudSharingController) {
if let share = csc.share, let persistentStore = share.persistentStore {
persistentContainer.persistUpdatedShare(share, in: persistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
}
}
}
func cloudSharingController(_ csc: UICloudSharingController, failedToSaveShareWithError error: Error) {
print("\(#function): Failed to save a share: \(error)")
}
func itemTitle(for csc: UICloudSharingController) -> String? {
return csc.share?.title ?? "Airframe Logbook"
}
}
extension PersistenceController {
func shareObject<T: CloudKitSharable>(_ unsharedObject: T, to existingShare: CKShare?, completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)? = nil) {
persistentContainer.share([unsharedObject], to: existingShare) { (objectIDs, share, container, error) in
guard error == nil, let share = share else {
print("\(#function): Failed to share an object: \(error!))")
completionHandler?(share, error)
return
}
/**
Deduplicate tags, if necessary, because adding a photo to an existing share moves the whole object graph to the associated
record zone, which can lead to duplicated tags.
*/
if existingShare != nil {
/*
if let tagObjectIDs = objectIDs?.filter({ $0.entity.name == "Tag" }), !tagObjectIDs.isEmpty {
self.deduplicateAndWait(tagObjectIDs: Array(tagObjectIDs))
}
*/
} else {
self.configure(share: share, with: unsharedObject)
}
/**
Synchronize the changes on the share to the private persistent store.
*/
self.persistentContainer.persistUpdatedShare(share, in: self.privatePersistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
/**
Delete the Core Data objects and the records in the CloudKit record zone associated with the share.
*/
func purgeObjectsAndRecords(with share: CKShare, in persistentStore: NSPersistentStore? = nil) {
guard let store = (persistentStore ?? share.persistentStore) else {
print("\(#function): Failed to find the persistent store for share. \(share))")
return
}
persistentContainer.purgeObjectsAndRecordsInZone(with: share.recordID.zoneID, in: store) { (zoneID, error) in
if let error = error {
print("\(#function): Failed to purge objects and records: \(error)")
}
}
}
func existingShare(for item: NSManagedObject) -> CKShare? {
if let shareSet = try? persistentContainer.fetchShares(matching: [item.objectID]),
let (_, share) = shareSet.first {
return share
}
return nil
}
func share(with title: String) -> CKShare? {
let stores = [privatePersistentStore, sharedPersistentStore]
let shares = try? persistentContainer.fetchShares(in: stores)
let share = shares?.first(where: { $0.title == title })
return share
}
func shareTitles() -> [String] {
let stores = [privatePersistentStore, sharedPersistentStore]
let shares = try? persistentContainer.fetchShares(in: stores)
return shares?.map { $0.title } ?? []
}
private func configure<T: CloudKitSharable>(share: CKShare, with item: T) {
share[CKShare.SystemFieldKey.title] = item.sharedTitle
}
}
extension PersistenceController {
func addParticipant(emailAddress: String, permission: CKShare.ParticipantPermission = .readWrite, share: CKShare,
completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?) {
/**
Use the email address to look up the participant from the private store. Return if the participant doesn't exist.
Use privatePersistentStore directly because only the owner may add participants to a share.
*/
let lookupInfo = CKUserIdentity.LookupInfo(emailAddress: emailAddress)
let persistentStore = privatePersistentStore //share.persistentStore!
persistentContainer.fetchParticipants(matching: [lookupInfo], into: persistentStore) { (results, error) in
guard let participants = results, let participant = participants.first, error == nil else {
completionHandler?(share, error)
return
}
participant.permission = permission
participant.role = .privateUser
share.addParticipant(participant)
self.persistentContainer.persistUpdatedShare(share, in: persistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
func deleteParticipant(_ participants: [CKShare.Participant], share: CKShare,
completionHandler: ((_ share: CKShare?, _ error: Error?) -> Void)?) {
for participant in participants {
share.removeParticipant(participant)
}
/**
Use privatePersistentStore directly because only the owner may delete participants to a share.
*/
persistentContainer.persistUpdatedShare(share, in: privatePersistentStore) { (share, error) in
if let error = error {
print("\(#function): Failed to persist updated share: \(error)")
}
completionHandler?(share, error)
}
}
}
// Core Data models
extension TripsLog {
#NSManaged var name: String
#NSManaged var identifier: String
#NSManaged var entries: NSSet
}
extension Trip {
#NSManaged var identifier: String
#NSManaged var name: String
#NSManaged var date: Date
#NSManaged var comments: String?
#NSManaged var leaderName: String
#NSManaged var images: NSSet?
}
If anyone is able to shed any light on this I'd really appreciate it, as Apple's own documentation is somewhat lacking. Many thanks!

I tried to connect and fetch mails using mailcore2 in iOS. But I got error

Error Domain=MCOErrorDomain Code=5 "Unable to authenticate with the current session's credentials." UserInfo={NSLocalizedDescription=Unable to authenticate with the current session's credentials.}
I put this code in my project.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
prepareImapSession()
}
var imapsession:MCOIMAPSession = MCOIMAPSession()
var error : Error? = nil
func prepareImapSession()
{
// CONFIGURE THAT DEPENDING OF YOUR NEEDS
imapsession.hostname = "imap.gmail.com" // String
imapsession.username = my email // String
imapsession.password = password // String
imapsession.port = 993 // UInt32 number
imapsession.authType = MCOAuthType.saslLogin
imapsession.connectionType = MCOConnectionType.TLS
DispatchQueue.main.asyncAfter(deadline: .now() + 8) { [self] in
self.useImapWithUIDS()
}
imapsession.connectOperation()
}
func useImapWithUIDS() {
// There is more than one option here, explore depending of your needs
// let kind = MCOIMAPMessagesRequestKind()
// let headers = kind.union(MCOIMAPMessagesRequestKind.headers)
// let request = headers.union(MCOIMAPMessagesRequestKind.flags)
let requestKind: MCOIMAPMessagesRequestKind = [.headers, .flags]
let folder : String = "INBOX"
// HERE ALSO EXPLORE DEPENDING OF YOUR NEEDS, RANGE IT IS THE RANGE OF THE UIDS THAT YOU WANT TO FETCH, I SUGGEST TO YOU TO CHANGE THE // NUMBER ONE IF YOU HAVE A LOWER BOUND TO FETCH EMAIL
let uids : MCOIndexSet = MCOIndexSet(range: MCORangeMake(1, UINT64_MAX))
let fetchOperation = imapsession.fetchMessagesOperation(withFolder: folder, requestKind: requestKind, uids: uids)
fetchOperation?.start
{ [self] (err, msg, vanished) -> Void in
if (err != nil)
{
self.error = err
NSLog((err?.localizedDescription)!)
}
else
{
guard let msgs = msg as? [MCOIMAPMessage]
else
{
print("ERROR GETTING THE MAILS")
return
}
for i in 0..<msgs.count
{
// THE SUBJECT
let subject = msgs[i].header.subject
// THE uid for this email. The uid is unique for one email
let uid = msgs[i].uid
self.useImapFetchContent(uidToFetch: uid)
// The sequenceNumber like the nomber say it is the sequence for the emails in the INBOX from the first one // (sequenceNumber = 1) to the last one , it not represent always the same email. Because if you delete one email then //next one will get the sequence number of that email that was deleted
let sequenceNumber = msgs[i].sequenceNumber
}
}
}
}
// MARK: - EXTRACT THE CONTENT OF ONE EMAIL, IN THIS FUNCTION YOU NEED THE uid, THE UNIQUE NUMBER FOR ONE EMAIL
func useImapFetchContent(uidToFetch uid: UInt32) {
let operation: MCOIMAPFetchContentOperation = imapsession.fetchMessageOperation(withFolder: "INBOX", uid: uid)
operation.start { (Error, data) in
if (Error != nil)
{
NSLog("ERROR")
return
}
let messageParser: MCOMessageParser = MCOMessageParser(data: data)
// IF YOU HAVE ATTACHMENTS USE THIS
let attachments = messageParser.attachments() as? [MCOAttachment]
// THEN YOU NEED THIS PROPERTIE, IN THIS EXAMPLE I TAKE THI FIRST, USE WHAT EVER YOU WANT
let attachData = attachments?.first?.data
// FOR THE MESSAGEPARSER YOU CAN EPLORE MORE THAN ONE OPTION TO OBTAIN THE TEXT
let msgPlainBody = messageParser.plainTextBodyRendering()
}
}
}
I using the mailcore2 framework. I got error description Unable to authenticate with the current session's credentials.
It can be related with 2nd factor authentication on your account. It was described on Apple forum.

Swift UITableViewController `await` until all data is loaded before rendering, or re-render after data has been loaded

I am on Swift 4. The goal is to load all the data in an address book, before render the address book in view. In a different language such as js, I may use await in each item in the loop, before telling the view to render the rows. I am looking for the canonical way to solve this issue in Swift 4 with UITableViewController.
Right now the address book is stored in backend with Amplify and GraphQL. I have a User model of form
type User #Model {
id: ID!
name: String!
bio : String!
}
and Contact of form
type Contact #model {
ownerId: ID!
userId: ID!
lastOpened: String
}
In ContactController: UITableViewController.viewDidLoad I fetch all Contact in database where the ownerId is my user's id-token, I then create an object using this contact information. And then for each Contact object instance, I get its corresponding User in database when the object is initialized. Per this post: Wait until swift for loop with asynchronous network requests finishes executing, I am using Dispatch group, and then reload the UITableView after the loop completes and the Dispatch group has ended. But when I print to console, I see that the loop completes before the Contact object has loaded its User information.
Code snippets:
class ContactsController: UITableViewController, UISearchResultsUpdating {
var dataSource : [Contact] = []
override func viewDidLoad() {
super.viewDidLoad()
let fetchContactGrp = DispatchGroup()
fetchContactGrp.enter()
self.getMyContacts(){ contacts in
for contact in contacts {
let _contactData = Contact(
userId : contact.userId
, contactId : contact.id
, timeStamp : contact.timeStamp
, lastOpened : contact.lastOpened
, haveAccount: true
)
_contactData.loadData()
self.dataSource.append(_contactData)
}
}
fetchContactGrp.leave()
DispatchQueue.main.async{
self.tableView.reloadData()
}
}
}
The function self.getMyContacts is just a standard GraphQL query:
func getMyContacts( callBack: #escaping ([Contact]) -> Void ){
let my_token = AWSMobileClient.default().username
let contact = Contact.keys
let predicate = contact.ownerId == my_token!
_ = Amplify.API.query(from: Contact.self, where: predicate) { (event) in
switch event {
case .completed(let result):
switch result {
case .success(let cts):
/// #On success, output a user list
callBack(cts)
case .failure(let error):
break
}
case .failed(let error):
break
default:
break
}
}
}
And the Contact object loads the User data from database:
class Contact {
let userId: String!
let contactId: String!
var name : String
var bio : String
var website: String
let timeStamp: String
let lastOpened: String
init( userId: String, contactId: String, timeStamp: String, lastOpened: String, haveAccount: Bool){
self.userId = userId
self.contactId = contactId
self.timeStamp = timeStamp
self.lastOpened = lastOpened
self.haveAccount = haveAccount
self.name = ""
self.bio = ""
self.website = ""
}
func loadData(){
/// #use: fetch user data from db and populate field on initation
let _ = Amplify.API.query(from: User.self, byId: self.userId) { (event) in
switch event {
case .completed(let res):
switch res{
case .success (let musr):
if (musr != nil){
let userData = musr!
let em = genEmptyString()
self.name = (userData.name == em) ? "" : userData.name
self.bio = (userData.bio == em) ? "" : userData.bio
self.website = (userData.website == em) ? "" : userData.website
print(">> amplify.query: \(self.name)")
} else {
break
}
default:
break
}
default:
print("failed")
}
}
}
}
It's because the function getMyContacts() is performing an Async task and the control goes over that and execute the leave statement. You need to call the leave statement inside the getMyContacts() function outside the for loop.
Try the following code:
override func viewDidLoad() {
super.viewDidLoad()
let fetchContactGrp = DispatchGroup()
fetchContactGrp.enter()
self.getMyContacts(){ contacts in
for contact in contacts {
let _contactData = Contact(
userId : contact.userId
, contactId : contact.id
, timeStamp : contact.timeStamp
, lastOpened : contact.lastOpened
, haveAccount: true
)
_contactData.loadData()
self.dataSource.append(_contactData)
}
fetchContactGrp.leave()
}
fetchContactGrp.wait()
DispatchQueue.main.async{
self.tableView.reloadData()
}
}
I posted a more general version of this question here: Using `DispatchGroup` or some concurency construct to load data and populate cells in `UITableViewController` sequentially
And it has been resolved.

how to save chats for a unique user in core data and fetch when needed swift

According to my requirement I have to save all chat of the user in core data with userid and when again visit users page then have to fetch the previous chat via user ID.
I have Chats Entity that is having msg, userType and time Attributes.
which I have to save in Users entity as like each message(Chats object) of the user should save in Users and can be refetch using Unique ID of user from core data.
Below code I am using for saving the data.
func saveDataToModel(msg:MessagesData)
{
let chats = Chats(context: context!)
chats.msg = msg.message
chats.userType = msg.userType
users?.userChats = chats
if let ids = viewModel?.userSelected?.id
{
users?.id = Int16(ids)
}
//saveContext()
do {
try context?.save()
} catch {
print("Failed saving")
}
}
And Below code for fetching
func getDataFromCoreData() {
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Users")
request.returnsObjectsAsFaults = false
do {
let result = try context?.fetch(request)
if (result != nil) {
for data in (result as? [NSManagedObject])! {
print("data",data)
}
}
} catch {
print("Failed")
}
}
Below is the struct
struct MessagesData {
var message:String?
var userType:String?
}
Thanks for any help

Fetched data from core data missing some info

I have two entities in my core data model: CardCollection (class name CardCollectionMO) and Card (CardMO). A CardCollection can have many cards. Here's my code for saving the CardCollection with a set of new cards, which seems to be working correctly.
func saveCardInfo() {
let currentDate = createDate()
let collectionNumber = arc4random_uniform(1000000)
let cardcollection = CardCollectionMO.insertNewCollection(collectionNumber: Int64(collectionNumber), collectionTitle: collectionTitle.text!, collectionDescription: collectionDescription.text!, numberOfCards: Int16(cards.count), dateCreated: String(currentDate))
var newCard = Set<CardMO>()
for card in cards {
if let addedCard = CardMO.insertNewCard(questionText: card.questionText, answerText: card.answerText, cardNum: Int16(card.cardNum), questionImage: UIImageJPEGRepresentation(card.questionImage, 1)! as NSData) {
addedCard.cardcollection = cardcollection
newCard.insert(addedCard)
}
}
cardcollection?.addToCards(newCard as NSSet)
}
But, strange things happen when fetching the data back. Here's the code.
func fetchData() {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let managedObjectContext = appDelegate.persistentContainer.viewContext
let request = NSFetchRequest<CardCollectionMO>(entityName: "CardCollection")
do {
let results = try managedObjectContext.fetch(request)
if results.count > 0 {
for result in results {
if let collectionTitle = result.collectionTitle {
print(collectionTitle)
}
if let listOfCards = result.cards?.allObjects as? [CardMO] {
for card in listOfCards {
if let questionText = card.questionText {
print(questionText)
}
}
The code above works fine while the app is running but if I stop and restart the app, the last card in each collection is removed. I've done a listOfCards.count and its like it doesn't exist. But, if I just fetch all the cards from the Card entity, they're there. Any thoughts on what I'm doing wrong and why the last card in the collection isn't being fetched?

Resources