Failed to send custom object using WatchConnectivity (swift) - ios

I was trying to pass my swift object from the iOS app to the Watch. However, I found it works for basic types like NSString, but my custom object type.
My custom object is able to cast to NSData
I've made my object implement NSObject and NSCoding, which works well. I can do following without problem:
let encodedChordProgression = NSKeyedArchiver.archivedDataWithRootObject(chordProgressions[1])
let decodedChordProgression = NSKeyedUnarchiver.unarchiveObjectWithData(encodedChordProgression) as! ChordProgression
NSLog("get decodedChordProgression = \(decodedChordProgression.description)")
WatchConnectivity code works for NSString
In iPhone:
try WatchSessionManager.sharedManager.updateApplicationContext(["data": "mystringishere"])
with Watch:
dispatch_async(dispatch_get_main_queue()) { [weak self] in
self?.dataSourceChangedDelegates.forEach { $0.dataSourceDidUpdate(applicationContext["data"] as! NSString)}
}
works.
My custom object with WatchConnectivity Failed
However, when I switch the object to my own object, it failed by not calling the dataSourceChangedDelegates callback function. That is:
In iPhone:
let encodedChordProgression = NSKeyedArchiver.archivedDataWithRootObject(chordProgressions[1])
try WatchSessionManager.sharedManager.updateApplicationContext(["data": encodedChordProgression])
with Watch:
dispatch_async(dispatch_get_main_queue()) { [weak self] in
self?.dataSourceChangedDelegates.forEach { $0.dataSourceDidUpdate(applicationContext["data"] as! NSData)}
}
and
func dataSourceDidUpdate(encodedChordProgression: NSData) {
let chordProgression = NSKeyedUnarchiver.unarchiveObjectWithData(encodedChordProgression) as! ChordProgression
NSLog("get something here: \(chordProgression.description)")
}
What I've tried & my problem
I've tried to read the system.log of both the iPhone app and Watch app, but I couldn't find any clue, which is the biggest problem I have now.
The full code is: here (checkout 7f2a72c6004f6580e2a38a2d7fd0ed2cef8a2b2e)

NSKeyedArchiver/NSKeyedUnarchiver won't work in this way unfortunately. This is because even though you may share class files between your watchkit and iOS targets, they are essentially different classes to the compiler because they are compiled for different architectures.
What I have done to get around this issue myself (because I initially tried to do the same thing) is serialize my custom objects to a json dictionary (or json NSData if you like) and send that. Here is a github project I have made that automatically serializes your swift objects to json for you (specifically with this use case in mind).

I tried with "NSKeyedArchiver/NSKeyedUnarchiver" and this is working perfectly.
no need to go for serialization and all.
your dictionary should have same type of data and Archiver is doing it very well.

Related

Storing a PKDrawing object to Core Data

I am trying to add Apple Pencil support to my mind mapping app. I already use Core Data within my app for data persistence which all works fine without any bugs.
I have my Apple Pencil features working fine but I'm having trouble storing the Apple Pencil data.
Ive tried to add a PKDrawing object to my MindMap data model but I keep getting a compile time error 'cannot find type 'PKDrawing' in scope'
As far as I am away I can store the PKDrawing object into core data and then fetch it back out when the app loads. But I'm obviously not doing something right.
Any help is greatly appreciated folks.
Thank you
Update:
So I've used:
func convertToData(pkdrawing: PKDrawing) -> Data {
let data = pkdrawing.dataRepresentation()
return data
}
Then updated my model data and savedContext which all seems to work ok. The problem is when I try and initialise the data on opening the app. I have:
func createDrawing(data: Data) -> PKDrawing {
var loadedDrawing: PKDrawing
do {
try loadedDrawing = PKDrawing.init(from: data as! Decoder)
return loadedDrawing
} catch {
print("Error loading drawing object")
return PKDrawing()
}
}
which gives me error:
Could not cast value of type 'Foundation.Data' (0x1f3851a98) to 'Swift.Decoder' (0x1ee5381a8).
2021-09-24 06:22:58.912173+0100 MindMappingApp[1137:612856] Could not cast value of type 'Foundation.Data' (0x1f3851a98) to 'Swift.Decoder' (0x1ee5381a8).
I tried:
try loadedDrawing = PKDrawing.init(from: data as! Decoder)
as:
try loadedDrawing = PKDrawing.init(from: data)
but I kept getting:
Argument type 'Data' does not conform to expected type 'Decoder'
any ideas? Thanks in advance :)
Instead of trying to store the PKDrawing directly, store the Data associated with it instead.
Your Core Data model will have a field with the type Data (or Binary Data if you're using the GUI).
When you need to actually use that data and covert to/from the PKDrawing, you can use:
init(data:) https://developer.apple.com/documentation/pencilkit/pkdrawing/3281882-init
dataRepresentation() https://developer.apple.com/documentation/pencilkit/pkdrawing/3281878-datarepresentation

NSKeyedArchiver and sharing a custom class between targets

My app uses a custom class as its data model:
class Drug: NSObject, NSCoding {
// Properties, methods etc...
}
I have just created a Today extension and need to access the user’s data from it, so I use NSCoding to persist my data in both the app container and the shared container. These are the save and load functions in the main app:
func saveDrugs() {
// Save to app container
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.ArchiveURL.path)
if isSuccessfulSave {
print("Drugs successfully saved locally")
} else {
print("Error saving drugs locally")
}
// Save to shared container for extension
let isSuccessfulSaveToSharedContainer = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.SharedArchiveURL.path)
if isSuccessfulSaveToSharedContainer {
print("Drugs successfully saved to shared container")
} else {
print("Error saving drugs to shared container")
}
}
func loadDrugs() -> [Drug]? {
return NSKeyedUnarchiver.unarchiveObject(withFile: Drug.ArchiveURL.path) as? [Drug]
}
I encountered the problem of class namespacing where the NSKeyedUnarchiver in my Today extension could not decode the object properly, so I used this answer and added #objc before the class definition:
#objc(Drug)
class Drug: NSObject, NSCoding {
// Properties, methods etc...
}
This solved the problem perfectly. However, this will be version 1.3 of my app, and it seems this breaks the unarchiving process for pre-existing data (as I thought it might).
What is the best way to handle this scenario, as if I just make this change, the new version of the app will crash for existing users!
I cannot find any other answers about this, and I am not sure that the NSKeyedArchiver.setClass() method is relevant, nor am I sure where to use it.
Any help would be gratefully received. Thanks.
This is exactly the use case for NSKeyedUnarchiver.setClass(_:forClassName:) — you can migrate old classes forward by associating the current classes for their old names:
let unarchiver = NSKeyedUnarchiver(...)
unarchiver.setClass(Drug.self, forClassName: "myApp.Drug")
// unarchive as appropriate
Alternatively, you can provide a delegate conforming to NSKeyedUnarchiverDelegate and providing a unarchiver(_:cannotDecodeObjectOfClassName:originalClasses:), but that's likely overkill for this scenario.
Keep in mind that this works for migrating old data forward. If you have newer versions of the app that need to send data to old versions of the app, what you'll need to do is similar on the archive side — keep encoding the new class with the old name with NSKeyedArchiver.setClassName(_:for:):
let archiver = NSKeyedArchiver(...)
archiver.setClassName("myApp.Drug", for: Drug.self)
// archive as appropriate
If you have this backwards compatibility issue, then unfortunately, it's likely you'll need to keep using the old name as long as there are users of the old version of the app.

What does this Swift iPad crash log mean? [duplicate]

This question already has an answer here:
`save(to:for:completionHandler:)` of `UIDocument` crashes
(1 answer)
Closed 5 years ago.
App runs fine on every device that qualifies for deployment target of 9.3+ except iPad2. The url is good. Works on every iPhone and every other iPad. The crash is on a physical iPad2 and Simulator iPad2 iOS 9.3.
doc.save(to: target, for: .forCreating, completionHandler: {(success) in
if (success) {
print("Save succeeded")
}
} else {
print("Save failed")
}
})
This is where it crashes. Get to a breakpoint at this line, and do not get to breakpoints in completion handler or either print. Again, just the one model of just iPad.
The crash log is over my head. Does it make sense to any of you? Thank you.
Edit: expanded the crash log
tl;dr: It's a bug in the Swift Data type. Solution: In your UIDocument contents(forType:) implementation, change this:
return data
to this:
return NSData(data:data)
More info: Note that merely casting data to NSData won't help:
return data as NSData
That doesn't get us any further because it's the bridging that's the problem. We're already bridging to NSData and it isn't helping. You have to create a completely new object that is not a Swift Data object.
Even more info: For future generations who come along and want to test this, the crash can be reliably reproduced as follows. Make a new Single View project, with an app delegate and a ViewController as usual. In the ViewController.swift file, put this:
import UIKit
class WhatsUpDoc: UIDocument {
var array = [String]()
override func load(fromContents contents: Any, ofType typeName: String?) throws {}
override func contents(forType typeName: String) throws -> Any {
let data = NSKeyedArchiver.archivedData(withRootObject: array)
return data // comment out this line to avoid the crash
return NSData(data:data)
}
}
class ViewController: UIViewController {
var doc : WhatsUpDoc?
override func viewDidLoad() {
super.viewDidLoad()
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let baseURL = documentsDirectory.appendingPathComponent("Untitled")
let doc = WhatsUpDoc(fileURL: baseURL)
self.doc = doc
self.doc!.save(to:self.doc!.fileURL, for: .forCreating)
}
}
Configure your project to have a deployment target of 9.0, and make sure you've got a 9.0 simulator SDK on hand. In Window > Devices, give yourself an iPad 2 simulator. Set that simulator as the project's destination. Run, and crash. Comment out the line that says to comment it out, and don't crash.
Post-analysis Q&A:
Wait, so what exactly _is_ the bug? You're not saying you can't write a Swift Data object to disk, are you?
No, it's got something to do with the peculiar threaded nature of writing during a UIDocument file save. Note that we are crashing in a background thread (thread 10 or 11 in the OP's screen shots). And what we are crashing in is a method of SwiftNSData, the NSData wrapped by a Swift Data. We're trying to enumerate the data's bytes on this background thread, and we can't; presumably this isn't thread-safe on certain device types, i.e. 32-bit devices. My solution is to get Swift Data out of the picture completely.
Okay, so it turns out this bug is known; the answer is given here: https://stackoverflow.com/a/41163395/341994 How about that?
Okay, but I figured it out independently in response to the current question. I didn't find out about the other question and answer until later, when it occurred to me to do a search. So I've marked the current question as a duplicate, but I'm leaving my answer for historical purposes, esp. as it gives a clear test case (which the other question does not).
By the way, I do not know why using NSMutableData(data:) would be better than my solution of using NSData(data:), but that's what Apple says to do, so let's just take it as gospel.

iOS 8 share extension doesn't work with iBooks

I try to share selected/highlighted text in iBooks with my custom extension, but it doen't have anything inside self.extensionContext
<NSExtensionContext: 0x17541d90> - UUID: <__NSConcreteUUID 0x1765e860> D69F0393-C5F1-4DEB-9A97-B479C2BC0C95 - _isHost: NO
items:
(
)
so after i choose my extension in provided list it just pops up empty SLComposeServiceViewController
Mail, iMessages, Twitter etc. works as expected. Is there any additional magic i must do to handle this?
The interesting things about this are
When sharing from iBooks, self.extensionContext.inputItems is an empty array. It's not giving you anything to share.
Sharing from iBooks only works normally with extensions that were provided by Apple. On my iPhone other extensions are available from iBooks-- Evernote, Things, and others-- but none of them work normally. They all come up with empty content.
My take: Sharing from Apple's extensions relies on some undocumented secret behavior, and there's no extra magic you can apply that would get through the app store approval process.
If you set your activation rule to TRUEPREDICATE (which means that the extension should always show up) or something very lenient, your extension will show up in iBooks. But it doesn't look like you can get any content to share right now. I'd file a bug with Apple about it.
It does seem like that the issue has been resolved with iOS 9, the following code (in Swift) correctly returns the contents of the selection in iBooks:
for item: AnyObject in self.extensionContext!.inputItems {
let inputItem = item as! NSExtensionItem
for provider: AnyObject in inputItem.attachments! {
let itemProvider = provider as! NSItemProvider
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
itemProvider.loadItemForTypeIdentifier(kUTTypeText as String, options: nil, completionHandler: { (txt, error) in
NSOperationQueue.mainQueue().addOperationWithBlock {
//Doing stuff with txt
}
})
}
}
}

parsing json in swift

I'm trying to read the Linkedin response in swift.
My object is something like this ["positions":["values":["data1","data2","data3"]]]
if let positions: NSDictionary = info["positions"] as NSDictionary!{
if let positionsInfo: [NSDictionary] = positions["values"] as? [NSDictionary]{
for position : NSDictionary! in positionsInfo {
dosomething(position, person:usr)
}
}
}
If I do a StepOver line by line it works correctly. But if I run it i'll get a EXC_BAD_ADDRESS(code=1,address=0x7966b04) I enabled Zombie objects and ran it on Instruments. I'm pretty sure this is the code which is causing the problem. But not sure what is wrong with it.
The moment you used ! you opened yourself up for crashes if there were any problem. You must use as? to make sure that the data is actually what you think it is.
There are many blog posts out there on how to safely parse JSON into Swift data structures. It's now almost a rite of passage for Swift bloggers.
http://robots.thoughtbot.com/efficient-json-in-swift-with-functional-concepts-and-generics
http://chris.eidhof.nl/posts/json-parsing-in-swift.html
https://github.com/owensd/json-swift
https://github.com/lingoer/SwiftyJSON
Of course the many packages: https://github.com/search?q=%5Bswift%5D+json
http://robnapier.net/functional-wish-fulfillment - My own version on top of all the others

Resources