Page 1 of 1

Accessing Mac CNContactStore

Posted: Mon May 15, 2023 2:14 pm
by Kukulkan
Hi,

I used this code for accessing the Mac address book for a while, but since MacOS Catalina (or even before) this is no longer working. Forum user Wilbert found that this is outdated and now the CNContactStore should be used.

Did anyone use this before with PureBasic and CocoaMessage()? Any code examples available?

Re: Accessing Mac CNContactStore

Posted: Sat Nov 23, 2024 6:32 pm
by Shardik
The following example is working to access the CNContactStore and list all contacts with phone numbers and/or eMail addresses. I have tested the example successfully in these MacOS versions:
  • 10.11.6 'El Capitan'
  • 10.12.6 'Sierra'
  • 10.13.6 'High Sierra'
  • 10.14.6 'Mojave'
  • 10.15.7 'Catalina'
  • 11.7.10 'Big Sur'
  • 12.7.6 'Monterey'
  • 13.7.1 'Ventura'
  • 14.7.1 'Sonoma'
  • 15.1.1 'Sequoia'
In MacOS 10.10.5 'Yosemite' this example doesn't work because the contacts framework was introduced in 'El Capitan'.

It took me many many hours to program a working example in PureBasic. I programmed more than 40 not working test programs in PureBasic and even a dozen test programs in Objective C and Swift which sometimes kind of worked but also displayed several severe warnings. Because of Apple's security settings (which also became more strict with newer MacOS versions) this was a minefield and I paused several times because I didn't seem to come any further. As a last resort in my despair I tried to find help by opening a session with ChatGPT 4o mini and against all odds I was able to find a first PureBasic example which finally worked. Of course the examples proposed by ChatGPT didn't work but by asking ChatGPT to correct the errors, to use CocoaMessage() instead of importing API calls and adding other crucial code lines by myself after about 20 minutes of cooperative work I unbelievably had a first working example!

In order to work with contacts, the program has to ask the user to allow access to the AddressBook. To test the sourcecode in the PureBasic IDE, the program in the IDE also has to ask for this allowance in an automatic requester (by the OS). In Yosemite to Ventura this should work. In Sonoma and Sequoia this didn't work. After compiling the example code to an app, the started app also should display the requester for allowance to access the AddressBook. This worked relatively reliably for Yosemite to Sequioa.

For obtaining the OS access request in the IDE on El Capitan to Sierra it was possible to execute my example code in PB 6.04. In Big Sur to Ventura I had to run the example code in PB 5.73 (PB 6.04 or 6.12 don't trigger the requester). In Sonoma to Sequoia I was not able to trigger the access request in the IDE at all because PB 5.73 doesn't run anymore on Sonoma and Sequoia and PB 6.04 and 6.12 don't trigger the access requester.

When having problems with the access to the AddressBook you may take a look into the MacOS securty settings:
System settings > Privacy & Security > Contacts
(or in German:
Systemeinstellungen > Datenschutz & Sicherheit > Kontakte)

You are able to activate or deactivate each app by using a switch in the contacts list. But you are not able to delete a program entry. To delete an entry you have to open a console and use the tccutil console program:

Code: Select all

- Remove all apps for the category Contacts:
  > tccutil reset AddressBook

- Remove single app (for example PureBasic IDE) from Contacts:
  - Get Bundle-ID of PureBasic IDE (will return com.fantaisiesoftware.purebasicide):
    > osascript -e 'id of app"PureBASIC-6.11-x64"'
  - Remove PureBasic IDE from Contacts:
    > tccutil reset AddressBook com.fantaisiesoftware.purebasicide
The app name PureBASIC-6.11-x64 may be different in your case because it depends on which has been the first PureBasic version you installed on your MacOS. But all PureBasic versions work with this registered version name. They all result in the AppID com.fantaisiesoftware.purebasicide. So for the PureBasic IDE the osascript step wouldn't be necessary.

Code: Select all

EnableExplicit

#KeyList = "givenName+familyName+phoneNumbers+emailAddresses"

ImportC "-framework Contacts"
EndImport

Define i.I
Define Info.S
Define Result.S

NewList Key.S()

Procedure.S GetContactsFromAddressBook(KeyList.S)
  Protected ClassName.S
  Protected Contact.I
  Protected ContactArray.I
  Protected ContactCount.I
  Protected ContactInfo.S
  Protected ContactStore.I
  Protected Error.S
  Protected i.I
  Protected Item.I
  Protected ItemCount.I
  Protected ItemArray.I
  Protected j.I
  Protected k.I
  Protected Key.S
  Protected KeyArray.I
  Protected KeyCount.I

  ContactStore = CocoaMessage(0, 0, "CNContactStore new")

  If ContactStore = 0
    ProcedureReturn "ERROR: Failed to create CNContactStore instance!"
  EndIf

  KeyCount = CountString(KeyList, "+") + 1

  ; ----- Create mutable array with 1st key name 

  Key = StringField(KeyList, 1, "+")
  KeyArray = CocoaMessage(0, 0, "NSMutableArray arrayWithObject:$", @Key)

  If KeyCount > 1
    ; ----- Add keys 2 to n to mutable array

    For i = 2 To KeyCount
      Key = StringField(KeyList, i, "+")
      CocoaMessage(0, KeyArray, "addObject:$", @Key)
    Next i
  EndIf

  ; ----- Fetch all unified contacts containing the keys in key array

  ContactArray = CocoaMessage(0, ContactStore,
    "unifiedContactsMatchingPredicate:", 0,
    "keysToFetch:", KeyArray,
    "error:", @Error)

  If ContactArray = 0
    CocoaMessage(0, ContactStore, "release")
    ProcedureReturn "ERROR: Fetching of the contacts failed!"
  EndIf

  ContactCount = CocoaMessage(0, ContactArray, "count")
  ContactInfo = "Contacts found: " + ContactCount + #CR$ + #CR$

  If ContactCount > 0
    For k = 1 To ContactCount
      Contact = CocoaMessage(0, ContactArray, "objectAtIndex:", k - 1)
      ContactInfo + "Contact " + Str(k) + #CR$

      For j = 1 To KeyCount
        Key = StringField(KeyList, j, "+")
        Item = CocoaMessage(0, Contact, Key)
        ClassName = PeekS(CocoaMessage(0, CocoaMessage(0, Item, "className"),
          "UTF8String"), -1, #PB_UTF8)

        ; ----- Check whether the returned item is an array

        If FindString(ClassName, "Array") = 0
          ContactInfo + Key + ": " + PeekS(CocoaMessage(0, CocoaMessage(0,
            Contact, Key), "UTF8String"), -1, #PB_UTF8) + #CR$
        Else
          ItemArray = Item
          ItemCount = CocoaMessage(0, ItemArray, "count")
          ContactInfo + Key + ":" + #CR$

          For i = 1 To ItemCount
            Item = CocoaMessage(0, ItemArray, "objectAtIndex:", i - 1)

            If FindString("phoneNumbers", Key)
              ContactInfo + "- " + PeekS(CocoaMessage(0, CocoaMessage(0,
                CocoaMessage(0, Item, "value"), "stringValue"), "UTF8String"),
                -1, #PB_UTF8) + #CR$
            ElseIf FindString("emailAddresses", Key)
              ContactInfo + "- " + PeekS(CocoaMessage(0, CocoaMessage(0,
                Item, "value"), "UTF8String"), -1, #PB_UTF8) + #CR$
            Else
              CocoaMessage(0, ContactStore, "release")
              ProcedureReturn "ERROR: Array evaluation for key " + Key +
                "is currently not implemented!"
            EndIf
          Next i
        EndIf
      Next j

      ContactInfo + #CR$
    Next k
  EndIf

  ; ----- Release the CNContactStore object
  CocoaMessage(0, contactStore, "release")

  ProcedureReturn ContactInfo
EndProcedure

Result = GetContactsFromAddressBook(#KeyList)

If Left(Result, 5) = "ERROR"
  MessageRequester("ERROR", Mid(Result, 7))
  End
EndIf

If Result = ""
  Info = "No Contacts were found!"
Else
  Info = Result
EndIf

OpenWindow(0, 0, 0, 270, 300, "Address book contacts",
  #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
EditorGadget(0, 5, 5, WindowWidth(0) - 10, WindowHeight(0) - 10)
AddGadgetItem(0, 0, Info)

Repeat
Until WaitWindowEvent() = #PB_Event_CloseWindow

Re: Accessing Mac CNContactStore

Posted: Mon Nov 25, 2024 8:04 am
by Kukulkan
Great, thanks for sharing! :D

Re: Accessing Mac CNContactStore

Posted: Sun Jan 19, 2025 11:17 am
by Shardik
Shardik wrote: Sat Nov 23, 2024 6:32 pm For obtaining the OS access request in the IDE on El Capitan to Sierra it was possible to execute my example code in PB 6.04. In Big Sur to Ventura I had to run the example code in PB 5.73 (PB 6.04 or 6.12 don't trigger the requester). In Sonoma to Sequoia I was not able to trigger the access request in the IDE at all because PB 5.73 doesn't run anymore on Sonoma and Sequoia and PB 6.04 and 6.12 don't trigger the access requester.
Now I have also found a solution to obtain the access requester for contacts in the IDE of Sonoma and Sequoia. Load my example from above in PB 5.73 and add the following line above ImportC:

Code: Select all

Import "-fno-pie" : EndImport

Unfortunately this solution still doesn't work in PB 6.12 and 6.20 Beta 3 (Intel processor) either with Asm or C backend. The access requester is only triggered when running my example with PB 5.73.

Here you find a detailed analysis why the Asm backend with Intel processor doesn't work in PB 5.73, 6.04 and 6.12 up to 6.20 Beta 2. In PB 6.20 Beta 3 Fred implemented my Import line, so that the Asm backend is working again without the above Import line.