I have a class containing data that is being produced on the Apple Watch. I use the following method to archive the class, store the data in a file and then send the file to the iPhone.
func send(file counter: CounterModel) {
let session = WCSession.default
let fm = FileManager.default
let documentsDirectory = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
let transferStore = documentsDirectory.appendingPathComponent("transferfile").appendingPathExtension("cnt")
do {
let counterData = try NSKeyedArchiver.archivedData(
withRootObject: counter,
requiringSecureCoding: false
)
try counterData.write(to: transferStore)
if session.activationState == .activated {
session.transferFile(transferStore, metadata: nil)
}
} catch {
print("Oops")
}
}
Sending the file to the iPhone works fine, the delegate method is being called and the file is received. However, I can't unarchive the data and get the error message "The data couldn’t be read because it isn’t in the correct format." The delegate is simple:
func session(_ session: WCSession, didReceive file: WCSessionFile) {
do {
let contents = try Data(contentsOf: file.fileURL)
if let newValue = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(contents) as? CounterModel {
listOfCounters.append(newValue)
} else {
print("The content could not be decoded.")
}
} catch {
print("Failed to retrieve the file with error \(error.localizedDescription).")
}
}
Apparently, I'm doing something wrong. The un-archiving of the data on the iPhone works, so this is not the problem. Perhaps the file send has another format, but I can't get any information on that.
I opened the problem as a ticket to DTS and got the following answer:
The culprit is that your Model class has a different (full) class name in different targets. A Swift class has a module name, which by default is tied to the target name. When your Model class is compiled for your WatchKit extension, its full name is “TagetName_watchkit_extension.Model”; when it is compiled for your iOS app, it becomes “TargetName.Model”.
When your WatchKit extension archives an object Model, it uses “Target_watchkit_extension.Model” as the class name, which is not recognized by your iOS app, and triggers the failure.
You can use #objc to give your Model class a full name, which prevents the compiler from adding the module name, like below:
#objc(Model)
class Model: NSObject, NSCoding, ObservableObject {
I implemented this advice and it worked. However, on my MacBook I got an error message from the preview, that stated, that I needed to change some methods of my model with a prefix of "#objc dynamic". This might, however, happen, because DTS at Apple, didn't get this error.
The response on the problem was:
“#objc dynamic” is required for KVO (key-value observation) support. Since a “#Published" variable relies on KVO as well, adding that does sound reasonable for me.
This solved my problem and I'm happy.
Related
I've followed along the Apple Developer Code-Along videos (https://developer.apple.com/news/?id=yv6so7ie) as well as looked at this repo with different Widget types (https://github.com/pawello2222/WidgetExamples) in search on how to populate a dynamic intent with items from Core Data.
My question is: How can I used the saved Core Data objects from the user to have them as "filters" or options in widget settings?
My Core Data model is called Favourite and it is a Class Definition CodeGen file.
I have added the Intent target to my project and can get it to appear in the Widget settings, but when I tap in to "Choose" the list is empty. However, in my Core Data there are 3 saved items.
I have tried doing simple things like print(CoreDM.shared.getAllFavourites()) to see if I am even retrieving them all, but not listing in the settings, but the console prints out:
<NextDeparturesIntent: 0x283490bd0> {
favourite = <null>;
}
At this point I'm stuck on understanding on how I can get my Favourites visible and then usable. It seems everything else is hooked up and working or ready but the retrieval.
I have also tried re-adding into the Info.plist of the intent the IntentSupported of the intent's name: NextDeparturesIntentHandling:but that had no success.
Files
Core Data Model - Favourite - FavouritesCDModel
In my Core Data model I have more options but for this example:
UUID
beginName
finishName
Widget Intent - NextDepartures.intentdefinition
This is what has been set up as followed by Apple's Code Along videos:
app-name-intent - IntentHandler.swift
import Intents
class IntentHandler: INExtension, NextDeparturesIntentHandling {
let coreDM = CoreDataManager.shared
func provideFavouriteOptionsCollection(
for intent: NextDeparturesIntent,
with completion: #escaping (INObjectCollection<FavouriteRoutes>?, Error?) -> Void
) {
dump( coreDM.getAllFavourites() ) // <--- debug line
let favouriteRoutes = coreDM.getAllFavourites().map {
FavouriteRoutes(identifier: $0.uniqueId, display: $0.departingStopName!)
}
let collection = INObjectCollection(items: favouriteRoutes)
completion(collection, nil)
}
override func handler(for intent: INIntent) -> Any {
return self
}
}
CoreDataManager
import CoreData
final class CoreDataManager {
static let shared = CoreDataManager()
private init() {}
private let persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "FavouritesCDModel")
container.loadPersistentStores(completionHandler: { description, error in
if let error = error as NSError? {
fatalError("Core Data Store failed \(error.localizedDescription)")
}
})
return container
}()
var managedObjectContext: NSManagedObjectContext {
persistentContainer.viewContext
}
func getAllFavourites() -> [Favourite] {
let fetchRequest: NSFetchRequest<Favourite> = Favourite.fetchRequest()
do {
return try managedObjectContext.fetch(fetchRequest)
} catch {
return []
}
}
}
I didn't realise that the app, widget, and other targets are all sandboxed.
I incorrectly assumed everything within the same app ecosystem would be allowed access to the same items.
In order to get the above code to work is adding the file to the App Groups and FileManager.
CoreDataManager
Inside the persistentContainer add in the storeURL and descriptions:
let storeURL = FileManager.appGroupContainerURL.appendingPathComponent("COREDATAFILE.sqlite")
container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeURL)]
FileManager+Ext
Create a FileManager extension for the container url:
extension FileManager {
static let appGroupContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.domain.app")!
}
Info.plist
Make sure that the Info.plist files have access to the app group
Signing and Capabilities
Make sure you add the App Groups capability to each target that needs it, and add it in App Store Connect
I have an Action Extension to which I'm trying to share PDF-files.
I'm using the boilerplate code for ActionRequestHandler.swift that was autogenerated for me:
func beginRequest(with context: NSExtensionContext) {
// Do not call super in an Action extension with no user interface
self.extensionContext = context
for item in context.inputItems as! [NSExtensionItem] {
if let attachments = item.attachments {
for itemProvider in attachments {
...
...
}
}
}
}
Working from other apps
When exporting from every application except Safari, this is what I get:
This is all ok, I can verify that it's an pdf by checking the com.adobe.pdf and then I use the public.file-url to fetch the shared file.
Failing from Safari
But when exporting from Safari (doesn't matter if I choose "Automatic" or "Pdf" for file type), I instead only get com.apple.property-list:
Further info
Both dropbox and OneDrive works, so it's doable in some sort of way.
Also I realised that sharing an PDF from a url that's protected by some sort of login doesn't work with "Public.file-url" since that URL wont be accessible from inside swift-code.
That leads me to think that the java-script preprocessor might be the way to go? Fetch the pdf-contents with JS and pass it on to code?
Question
How do I use the com.apple.property-list to fetch the file?
Or is some config I did faulty, since I get this property-list instead of the pdf/url combo?
While I didn't manage to figure out a solution to the original question, I did manage to solve the problem.
When adding an Action Extension, one gets to choose Action type:
Presents user interface
No user interface
I choosed No user interfacesince that was what I wanted.
That gave me an Action.js file and ActionRequestHandler.swift:
class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
...
}
These files seem to work around a system where the Action.js is supposed to fetch/manipulate the source page and then send information to the backing Swift code. As stated in my original question, when sharing a PDF from Safari, no PDF-URL gets attached.
A working solution
If I instead choose Presents user interface, I got another setup, ActionViewController.swift:
class ActionViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Get the item[s] we're handling from the extension context.
for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
for provider in item.attachments! {
if provider.hasItemConformingToTypeIdentifier(kUTTypePDF as String) {
provider.loadItem(forTypeIdentifier: kUTTypePDF as String, options: nil, completionHandler: { (pdfUrl, error) in
OperationQueue.main.addOperation {
if let pdfUrl = pdfUrl as? URL {
// pdfUrl now contains the path to the shared pdf data
}
}
}
}
}
This file / solution works as expected, the extensionContext gets populated with one attachment that conforms to kUTTypePDF as expected.
Why this works, while the "no gui"-approach doesn't, I have no idea. Bug or feature?
I have not found any documentation of how/why this is supposed to work in Apple's developer section, the "share extension" documentation is very light.
I have seen many SO question curious about this case but still I am posting this as many of developers out there may also want to know this another reason is that no solution is working for me .
I have used following code but it only works when My app is in background. but I am not notified when my app is killed and meanwhile user has updated the info of any contact. So in this case I am not sure how to do it.
What I am doing: here is a code snippet what I am trying to do
From iOS 9 you can register your class to observe CNContactStoreDidChangeNotification
NSNotificationCenter.defaultCenter().addObserver(
self,
selector: #selector(addressBookDidChange),
name: NSNotification.Name.CNContactStoreDidChange,
object: nil)
And then:
#objc func addressBookDidChange(notification: NSNotification){
//Handle event here...
}
I found this solution over here:
Whats Happening: Through this way I am able to get my app notified once the user has updated his contact while app is in background.
What I want: I just want to know that if the user has updated any contact even though my app was killed then How to get my app notified with updated contacts?
Please let me know if you have solution of this issue in advance.
UPDATE: I have seen Whatsapp doing this. Is there anyone who can tell me how Whatsapp is doing this?
To check if a contact has changed you can use a custom hash function because the native one only checks for the identifier:
extension CNContact {
var customHash : Int {
var hasher = Hasher()
hasher.combine(identifier)
hasher.combine(contactType)
hasher.combine(namePrefix)
hasher.combine(givenName)
hasher.combine(middleName)
hasher.combine(familyName)
hasher.combine(previousFamilyName)
hasher.combine(nameSuffix)
hasher.combine(nickname)
hasher.combine(organizationName)
hasher.combine(departmentName)
hasher.combine(jobTitle)
hasher.combine(phoneticGivenName)
hasher.combine(phoneticMiddleName)
hasher.combine(phoneticFamilyName)
if #available(iOS 10.0, *) {
hasher.combine(phoneticOrganizationName)
}
hasher.combine(note)
hasher.combine(imageData)
hasher.combine(thumbnailImageData)
if #available(iOS 9.0, *) {
hasher.combine(imageDataAvailable)
}
hasher.combine(phoneNumbers)
hasher.combine(emailAddresses)
hasher.combine(postalAddresses)
hasher.combine(urlAddresses)
hasher.combine(contactRelations)
hasher.combine(socialProfiles)
hasher.combine(instantMessageAddresses)
hasher.combine(birthday)
hasher.combine(nonGregorianBirthday)
hasher.combine(dates)
return hasher.finalize()
}
}
(You can remove fields you don't care)
Then you have to keep a dictionary inside your app to store the hash values of all the contacts, to build it just do:
let hashedContacts = [String:Int]()
for contact in allContacts {
hashedContacts[contact.identifier] = contact.customHash
}
You have to store it on the file system.
Whenever a contact is updated, you update it:
hashedContacts[updatedContact.identifier] = updatedContact.customHash
Then at every launch, you load the saved dictionary, and you check for differences:
for contact in allContacts {
if contact.customHash != savedHashedValues[contact.identifier] {
// This contact has changed since last launch
...
}
}
And voilà!
EDIT:
How to save the hash map on disk...
var hashedContacts = ...
guard let name = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("hashedContacts")
else { return }
try? (hashedContacts as NSDictionary).write(to: name)
How to load the hash map from disk...
guard
let name = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent("hashedContacts"),
let loadedContacts = (try? NSDictionary(contentsOf: name, error: ())) as? [String:Int]
else { return }
// Do whatever you want with loaded contacts...
Whenever you open your app you need to get all the contacts from the contact list and can compare to previous one which is saved inside of your app. After that you can push your contact list to server.
What you can do is send an update notification to your application on launch screen. This might have an illusion to your user that you have done the changes while in background.
I'm trying to write a simple Quick Look Preview Extension for my UIDocument-based iOS app.
The problem is that in my implementation of preparePreviewOfFile(at:completionHandler:) my attempt to open the UIDocument based on the URL I'm being handed is failing. I instantiate my document with the file URL and call open(completionHandler:) but I'm not getting any data, and I'm seeing a console message that the file coordinator has crashed.
All of this works fine in my actual app; it's just the Quick Look Preview Extension implementation that's having trouble. Is there something special I have to do to open a UIDocument from inside a Quick Look Preview Extension? Apple doesn't provide any sample code; in WWDC 2017 video 229 they just gloss over the whole thing.
EDIT: Curiouser and curiouser. I created a simplified testbed app that displays a Quick Look preview with UIDocumentInteractionController, along with my custom Quick Look Preview Extension. On the Simulator, the preview works! On the device, it doesn't. It looks like, when I tell my document to open, its load(fromContents:ofType) is never even called; instead, we are getting a pair of error messages like this:
The connection to service named com.apple.FileCoordination was invalidated.
A process invoked one of the -[NSFileCoordinator coordinate...] methods but filecoordinationd crashed. Returning an error.
I was able to work around the issue by not calling open on my UIDocument. Instead, I call read directly, on a background thread, like this:
func preparePreviewOfFile(at url: URL, completionHandler handler: #escaping (Error?) -> Void) {
DispatchQueue.global(qos: .background).async {
let doc = MyDocument(fileURL: url)
do {
try doc.read(from: url)
DispatchQueue.main.async {
// update interface here!
}
handler(nil)
} catch {
handler(error)
}
}
}
I have no idea if that's even legal. You'd think that just reading the document straight in, without the use of a file coordinator, would be Bad. But it does seem to work!
I found yet another workaround, using NSFileCoordinator and calling load manually to get the UIDocument to process the data:
let fc = NSFileCoordinator()
let intent = NSFileAccessIntent.readingIntent(with: url)
fc.coordinate(with: [intent], queue: .main) { err in
do {
let data = try Data(contentsOf: intent.url)
let doc = MyDocument(fileURL: url)
try doc.load(fromContents: data, ofType: nil)
self.lab.text = doc.string
handler(nil)
} catch {
handler(error)
}
}
Again, whether that's legal, I have no idea, but I feel better about it than calling read directly, because at least I'm passing through a file coordinator.
I need to Display and modify my data structure from both Apple Watch and iPhone.
The Database:
I am currently using a simple Realm Structure where I have an Object A and an Object B which can hold lots of A's.
So on iPhone the user can create a B and add A's and view of course all A's and B's.
I want the Apple watch to show all A's of the current B and give the users the chance to add new A's to their current B.
The way I have tried to do it:
I wanted to move the hole Realm file from iPhone to the watch or the other way. (That was a tip from the Internet)
iPhone Code:
override func viewDidLoad() {
super.viewDidLoad()
if WCSession.isSupported() { //makes sure it's not an iPad or iPod
let watchSession = WCSession.default()
watchSession.delegate = self
watchSession.activate()
transferRealmFile()
if watchSession.isWatchAppInstalled {
do {
try watchSession.updateApplicationContext(["foo": "bar"])
} catch let error as NSError {
print(error.description)
}
}
}
}
func transferRealmFile(){
if let path = Realm.Configuration().fileURL {
WCSession.default().transferFile(path, metadata: nil)
}
}
WathcKit Extension:
func session(_ session: WCSession, didReceive file: WCSessionFile) {
//set the recieved file to default Realm file
var config = Realm.Configuration()
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
let realmURL = documentsDirectory.appendingPathComponent("data.realm")
if FileManager.default.fileExists(atPath: realmURL.path){
try! FileManager.default.removeItem(at: realmURL)
}
try! FileManager.default.copyItem(at: file.fileURL, to: realmURL)
config.fileURL = realmURL
Realm.Configuration.defaultConfiguration = config
}
Then I call transferRealmFile() every time I write to Realm. This works but I can't solve this Problems:
Problems:
It doesn't work if only watchKit App is started.
Apple Watch to iPhone doesn't work the same way. (I think I need to change the didRecived code, but I don't know what)
Question:
Do you know who to solve this 2 Problems or do you maybe know a better way to handle the situation or will the way we interact between iPhone an Watch change in WathcOS 3?
With watchOS1 it was possible to use AppGroups to share resources (even your Realm database) between an iOS app and its Watch extension. However, Apple removed this in watchOS 2, so now the only way to share data between your iOS and watchOS apps is via WatchConnectivity. Have a look at this answer.
Sadly the WatchConnectivity framework requires the WCSession to be active on both devices for transferring data, so you can't really get around problem 1.
In my opinion it is a better solution to only communicate the changes between the two apps and not send the whole Realm file, since your Realm file can get quite big and hence sending it forward and backward can take a lot of time and resources, while just sending the changes should be way faster.