I've spent the better part of a day and half now trying to debug this issue I'm seeing when trying to unarchive a Data blob stored locally (this issue also appears when retrieving it via iCloud, but since they run through the same codepath, I assume they're related).
Background
I originally built this app four years ago, and for reasons that have since been lost to time (but probably because I was more of a novice back then), I relied on the AutoCoding library to get objects in my data model to automagically adopt NSCoding (although I did implement the protocol myself in some places -- like I said, I was an novice) and FCFileManager for saving these objects to the local documents directory. The data model itself is fairly simple: custom NSObjects that have various properties of NSString, NSArray, and other custom NSObject classes (but I will note there are a number of circular references; most of them declared as strong and nonatomic in header files). This combination has (and still does) work well in the production version of the app.
However, in a future update, I'm planning on adding saving/loading files from iCloud. While I've been building that out, I've been looking to trim down my list of third-party dependencies and update older code to iOS 13+ APIs. It so happens that FCFileManager relies on the now-deprecated +[NSKeyedUnarchiver unarchiveObjectWithFile:] and +[NSKeyedArchiver archiveRootObject:toFile:], so I've focused on rewriting what I need from that library using more modern APIs.
I was able to get saving files working pretty easily using this:
#objc static func save(_ content: NSCoding, at fileName: String, completion: ((Bool, Error?) -> ())?) {
CFCSerialQueue.processingQueue.async { // my own serial queue
measureTime(operation: "[LocalService Save] Saving") { // just measures the time it takes for the logic in the closure to process
do {
let data: Data = try NSKeyedArchiver.archivedData(withRootObject: content, requiringSecureCoding: false)
// targetDirectory here is defined earlier in the class as the local documents directory
try data.write(to: targetDirectory!.appendingPathComponent(fileName), options: .atomicWrite)
if (completion != nil) {
completion!(true, nil)
}
} catch {
if (completion != nil) {
completion!(false, error)
}
}
}
}
}
And this works great -- pretty fast and can still be loaded by FCFileManager's minimal wrapper around +[NSKeyedUnarchiver unarchiveObjectWithFile:].
Problem
But loading this file back from the local documents directory has proved to be a massive challenge. Here's what I'm working with right now:
#objc static func load(_ fileName: String, completion: #escaping ((Any?, Error?) -> ())) {
CFCSerialQueue.processingQueue.async {// my own serial queue
measureTime(operation: "[LocalService Load] Loading") {
do {
// targetDirectory here is defined earlier in the class as the local documents directory
let combinedUrl: URL = targetDirectory!.appendingPathComponent(fileName)
if (FileManager.default.fileExists(atPath: combinedUrl.path)) {
let data: Data = try Data(contentsOf: combinedUrl)
let obj: Any? = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
completion(obj, nil)
} else {
completion(nil, ServiceError.generic(message: "Data not found at URL \(combinedUrl)"))
}
} catch {
completion(nil, error)
}
}
}
}
I've replaced FCFileManager's +[NSKeyedUnarchiver unarchiveObjectWithFile:] with the new +[NSKeyedUnarchiver unarchiveTopLevelObjectWithData:], but I'm running into EXC_BAD_ACCESS code=2 crashes when getting execution flowing through that line. The stacktrace is never particularly helpful; it's usually ~1500 frames long and jumping between various custom -[NSObject initWithCoder:] implementations. Here's an example (comments added for context, clarity, and conciseness):
#implementation Game
#synthesize AwayKStats,AwayQBStats,AwayRB1Stats,AwayRB2Stats,AwayWR1Stats,AwayWR2Stats,AwayWR3Stats,awayTOs,awayTeam,awayScore,awayYards,awayQScore,awayStarters,gameName,homeTeam,hasPlayed,homeYards,HomeKStats,superclass,HomeQBStats,HomeRB1Stats,HomeRB2Stats,homeStarters,HomeWR1Stats,HomeWR2Stats,HomeWR3Stats,homeScore,homeQScore,homeTOs,numOT,AwayTEStats,HomeTEStats, gameEventLog,HomeSStats,HomeCB1Stats,HomeCB2Stats,HomeCB3Stats,HomeDL1Stats,HomeDL2Stats,HomeDL3Stats,HomeDL4Stats,HomeLB1Stats,HomeLB2Stats,HomeLB3Stats,AwaySStats,AwayCB1Stats,AwayCB2Stats,AwayCB3Stats,AwayDL1Stats,AwayDL2Stats,AwayDL3Stats,AwayDL4Stats,AwayLB1Stats,AwayLB2Stats,AwayLB3Stats,homePlays,awayPlays,playEffectiveness, homeStarterSet, awayStarterSet;
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
// ...lots of other decoding...
// stack trace says the BAD_ACCESS is flowing through these decoding lines
// #property (atomic) Team *homeTeam;
homeTeam = [aDecoder decodeObjectOfClass:[Team class] forKey:#"homeTeam"];
// #property (atomic) Team *awayTeam;
// there's no special reason for this line using a different decoding method;
// I was just trying to test out both
awayTeam = [aDecoder decodeObjectForKey:#"awayTeam"];
// ...lots of other decoding...
}
return self;
}
Each Game object has a home and away Team; each team has an NSMutableArray of Game objects called gameSchedule, defined as such:
#property (strong, atomic) NSMutableArray<Game*> *gameSchedule;
Here's Team's initWithCoder: implementation:
-(id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
if (teamHistory.count > 0) {
if (teamHistoryDictionary == nil) {
teamHistoryDictionary = [NSMutableDictionary dictionary];
}
if (teamHistoryDictionary.count < teamHistory.count) {
for (int i = 0; i < teamHistory.count; i++) {
[teamHistoryDictionary setObject:teamHistory[i] forKey:[NSString stringWithFormat:#"%ld",(long)([HBSharedUtils currentLeague].baseYear + i)]];
}
}
}
if (state == nil) {
// set the home state here
}
if (playersTransferring == nil) {
playersTransferring = [NSMutableArray array];
}
if (![coder containsValueForKey:#"projectedPollScore"]) {
if (teamOLs != nil && teamQBs != nil && teamRBs != nil && teamWRs != nil && teamTEs != nil) {
FCLog(#"[Team Attributes] Adding Projected Poll Score to %#", self.abbreviation);
projectedPollScore = [self projectPollScore];
} else {
projectedPollScore = 0;
}
}
if (![coder containsValueForKey:#"teamStrengthOfLosses"]) {
[self updateStrengthOfLosses];
}
if (![coder containsValueForKey:#"teamStrengthOfSchedule"]) {
[self updateStrengthOfSchedule];
}
if (![coder containsValueForKey:#"teamStrengthOfWins"]) {
[self updateStrengthOfWins];
}
}
return self;
}
Pretty simple other than for the backfilling of some properties. However, this class imports AutoCoding, which hooks into -[NSObject initWithCoder:] like so:
- (void)setWithCoder:(NSCoder *)aDecoder
{
BOOL secureAvailable = [aDecoder respondsToSelector:#selector(decodeObjectOfClass:forKey:)];
BOOL secureSupported = [[self class] supportsSecureCoding];
NSDictionary *properties = self.codableProperties;
for (NSString *key in properties)
{
id object = nil;
Class propertyClass = properties[key];
if (secureAvailable)
{
object = [aDecoder decodeObjectOfClass:propertyClass forKey:key]; // where the EXC_BAD_ACCESS seems to be coming from
}
else
{
object = [aDecoder decodeObjectForKey:key];
}
if (object)
{
if (secureSupported && ![object isKindOfClass:propertyClass] && object != [NSNull null])
{
[NSException raise:AutocodingException format:#"Expected '%#' to be a %#, but was actually a %#", key, propertyClass, [object class]];
}
[self setValue:object forKey:key];
}
}
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
[self setWithCoder:aDecoder];
return self;
}
I did some code tracing and found that execution flows the -[NSCoder decodeObject:forKey:] call above. Based on some logging I added, it seems like propertyClass somehow gets deallocated before getting passed to -[NSCoder decodeObjectOfClass:forKey:]. However, Xcode shows that propertyClass has a value when the crash occurs (see screenshot: https://imgur.com/a/J0mgrvQ)
The property in question in that frame is defined:
#property (strong, nonatomic) Record *careerFgMadeRecord;
and has the following properties itself:
#interface Record : NSObject
#property (strong, nonatomic) NSString *title;
#property (nonatomic) NSInteger year;
#property (nonatomic) NSInteger statistic;
#property (nonatomic) Player *holder;
#property (nonatomic) HeadCoach *coachHolder;
// … some functions
#end
This class also imports AutoCoding, but has no custom initWithCoder: or setWithCoder: implementation.
Curiously, replacing the load method I’ve written with FCFileManager’s version also crashes in the same fashion, so this could be more of an issue with how the data is being archived than how it’s being retrieved. But again, this all works fine when using FCFileManager’s methods to load/save files, so my guess is that there’s some lower-level difference between the implementation of archives in iOS 11 (when FCFileManager was last updated) and iOS 12+ (when the NSKeyedArchiver APIs were updated).
Per some suggestions I've found online (like this one), I also tried this:
#objc static func load(_ fileName: String, completion: #escaping ((Any?, Error?) -> ())) {
CFCSerialQueue.processingQueue.async {
measureTime(operation: "[LocalService Load] Loading") {
do {
let combinedUrl: URL = targetDirectory!.appendingPathComponent(fileName)
if (FileManager.default.fileExists(atPath: combinedUrl.path)) {
let data: Data = try Data(contentsOf: combinedUrl)
let unarchiver: NSKeyedUnarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
unarchiver.requiresSecureCoding = false;
let obj: Any? = try unarchiver.decodeTopLevelObject(forKey: NSKeyedArchiveRootObjectKey)
completion(obj, nil)
} else {
completion(nil, ServiceError.generic(message: "Data not found at URL \(combinedUrl)"))
}
} catch {
completion(nil, error)
}
}
}
}
However, this still throws the same EXC_BAD_ACCESS when trying to decode.
Does anyone have any insight into where I might be going wrong here? I’m sure it’s something simple, but I just can’t seem to figure it out. I have no problem providing more source code if needed to dive deeper.
Thanks for your help!
I (somehow) got past this by relying on AutoCoding to manage the NSCoding implementation for the Game class. Peeling back the layers, it seems like there was some issue with using -[NSMutableArray arrayWithObjects:] in -[Game initWithCoder:] that caused the EXC_BAD_ACCESS, but even that seemed to go away when falling back to AutoCoding. Not sure what effect this will have on backwards compatibility, but I guess I'll cross that bridge when I get there.
Related
I'm trying to debug a mysterious crash I'm seeing in Crashlytics, but haven't been able to reproduce myself.
The error message looks like this:
Fatal Exception: NSInvalidArgumentException
-[NSNull compare:]: unrecognized selector sent to instance 0x1e911bc30
-[NSOrderedSet initWithSet:copyItems:]
Here is the full stacktrack if interested
Because I haven't been able to pinpoint the origin of the crash, I thought I would add a new method to NSNull in order to further debug it via logging.
However I'm not sure how to do it. I think I'd need to add a compare method to NSNull, but I have limited knowledge of objc. I got the idea from this answer. The proposed solution for a similar problem looks like this
BOOL canPerformAction(id withSender) {
return false;
}
- (void)viewDidLoad {
[super viewDidLoad];
Class class = NSClassFromString(#"UIThreadSafeNode");
class_addMethod(class, #selector(canPerformAction:withSender:), (IMP)canPerformAction, "##:");
}
How could I do this in Swift for adding compare to NSNull?
You could add a compare method to NSNull like this:
Objective-C:
#import <objc/runtime.h>
static inline NSComparisonResult compareNulls(id self, SEL _cmd, NSNull *other) {
if([other isKindOfClass:[NSNull class]]) {
return NSOrderedSame; // Nulls are always the same.
}
return NSOrderedDescending;
}
#implementation NSNull (Comparisons)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
const char *encoding = [[NSString stringWithFormat:#"%s#:#", #encode(NSComparisonResult)] UTF8String];
class_addMethod([self class], #selector(compare:), (IMP)compareNulls, encoding);
});
}
#end
Swift:
// Add this code to your AppDelegate.swift file:
import ObjectiveC
fileprivate func compareNulls(_ self: AnyObject, _ _cmd: Selector, _ other: AnyObject) -> ComparisonResult {
if other is NSNull {
return .orderedSame
}
return .orderedDescending
}
fileprivate func addNSNullCompareImplementationIfNecessary() {
let sel = NSSelectorFromString("compareNulls:")
guard class_getMethodImplementation(NSNull.self, sel) == nil else {
return
}
let types = "i#:#"
class_addMethod(NSNull.self, sel, imp_implementationWithBlock(compareNulls), types)
}
// Add this line to your -didFinishLaunching: function:
addNSNullCompareImplementationIfNecessary()
This is only a temporary solution that will stop the crashes.
I would nevertheless encourage you to a) file a bug report, and b) continue investigating why this happened - clearly having an NSNull in this case wasn't expected by Parse...
I have a problem with keeping a reference to a RPPreviewViewController in ReplayKit with ObjectiveC and I'm wondering what am I doing wrong.
The .h file:
#interface ReplayKitHelper : NSObject <RPPreviewViewControllerDelegate, RPScreenRecorderDelegate>
-(void)startRecording;
-(void)stopRecording;
-(void)previewRecording;
#property(strong) RPPreviewViewController* previewViewControllerRef;
#end
The .mm file:
#implementation ReplayKitHelper
#synthesize previewViewControllerRef;
-(void)startRecording
{
RPScreenRecorder* recorder = RPScreenRecorder.sharedRecorder;
recorder.delegate = self;
[recorder startRecordingWithMicrophoneEnabled : true handler : ^ (NSError *error)
{
}];
}
-(void)stopRecording
{
RPScreenRecorder* recorder = RPScreenRecorder.sharedRecorder;
[recorder stopRecordingWithHandler : ^ (RPPreviewViewController * previewViewController, NSError * error)
{
if (error == nil)
{
if (previewViewController != nil)
{
previewViewControllerRef = previewViewController;
}
}
}];
}
-(void)previewRecording
{
if (previewViewControllerRef != nil)
{
previewViewControllerRef.modalPresentationStyle = UIModalPresentationFullScreen;
previewViewControllerRef.previewControllerDelegate = self;
[[IOSAppDelegate GetDelegate].IOSController presentViewController : previewViewControllerRef animated : YES completion : nil];
// IOSController is my main UIViewController
}
}
#end
During the runtime I launch methods startRecording, stopRecording and previewRecording, in that order. Everything is going fine, until previewRecording, where it looks like the previewViewControllerRef is no longer valid (it's not nil, but it crashes when I'm trying to refer to it).
When I try to run [self previewRecording] inside the stopRecordingWithHandler block, after I pass the reference - everything works fine.
It looks like the previewViewController from the handler is released right after app leaves the block.
Most of the examples are written in Swift, unfortunatelly I'm condemned to ObjectiveC. In Swift examples the reference to previeViewController is just passed to the variable, but in ObjectiveC it seems to not working.
Do You have any ideas what's wrong here?
I'm going to assume that you are using ARC, if so there is no need to synthesize properties anymore.
Change your RPPreviewViewController in the Interface file to:
#property (nonatomic, strong) RPPreviewViewController *RPPreviewViewController;
Drop the #synthesize.
then in the stopRecording handler you can keep a reference to the available RPPreviewViewController like so:
- (void)stopScreenRecording {
RPScreenRecorder *sharedRecorder = RPScreenRecorder.sharedRecorder;
[sharedRecorder stopRecordingWithHandler:^(RPPreviewViewController *previewViewController, NSError *error) {
if (error) {
NSLog(#"stopScreenRecording: %#", error.localizedDescription);
}
if (previewViewController) {
self.previewViewController = previewViewController;
}
}];
}
In my experience ReplayKit is still buggy and there isn't a lot of documentation about it yet.
Apple says
There should typically be little need to subclass NSMutableDictionary.
If you do need to customize behavior, it is often better to consider
composition rather than subclassing.
(See https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSMutableDictionary_Class/)
They should probably make this a little stronger and say pursue this at your own risk.
However, there are situations where it can be important to subclass NSMutableDictionary. In my case, notationally, it really was relevant to my code. There are quite a few hurdles to overcome. There are other web and SO entries on this, but I encountered some seemingly new issues on my travels through this, so wanted to write this up for my memory and help others. So, I'll post my answer to this. Feel free to contribute your own additional findings.
1) There are no proxy objects. At the outset, for some reason, Apple seems to have made NSMutableDictionary different in some unusual ways than NSMutableSet. My underlying need to subclass NSMutableDictionary really stems from a need to know about mutation changes to an NSMutableDictionary instance. NSMutableSets, for example, make this some what easier. NSMutableSets give you access to a "proxy" object: mutableSetValueForKey. This gives you a mechanism to know when the set contents mutate. See https://www.objc.io/issues/7-foundation/key-value-coding-and-observing/ for some details. What you'd expect to see would be something like mutableDictValueForKey but that seems to not exist.
2) Implement init in your subclass methods! Apple tells you you need to override methods:
In a subclass, you must override both of its primitive methods:
setObject:forKey:
removeObjectForKey:
You must also override the primitive methods of the NSDictionary
class.
and the NSDictionary primitive methods are:
initWithObjects:forKeys:count:
#property count
objectForKey:
keyEnumerator:
BUT, you must also override the init method!
3) Doing this in Swift doesn't work yet! At least as of the date I was trying this (about 10/8/15, and Xcode 7), you must do make your NSMutableDictionary subclass in Objective-C, not Swift. See Cannot override initializer of NSDictionary in Swift
4) NSCoding doesn't work with NSMutableDictionary subclasses! In my NSMutableDictionary subclass, I tried implementing the NSCoding protocol, but couldn't get it work in the context of keyed archivers. The keyed archiver would generate an empty NSMutableDictionary (when decoded), not my own subclass, and I don't know why. Some special NSMutableDictionary magic?
5) subscript in Swift may not cut it. I tried only implementing the subscript method for Swift (see https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Subscripts.html), but notationally this left much to be desired. I really wanted a type that was fully interoperable with NSDictionary/NSMutableDictionary, which seems to require a subclass.
6) Don't just implement the methods; you need your own data! If you just try to override the methods as above, and invoke "super" your code will not work. You need to use "composition" to internally implement an NSMutableDictionary property. Or whatever other mechanism you want for implementing your dictionary. Again, some class cluster magic going on. See my dict property in the .m file below.
Here's what I have to date in terms of my Objective-C code:
//
// SMMutableDictionary.h
// Dictionary
//
// Created by Christopher Prince on 10/6/15.
// Copyright © 2015 Spastic Muffin, LLC. All rights reserved.
//
/* I subclassed NSMutableDictionary because:
1) because I needed a way to know when a key was set or removed. With other mutable objects you can use proxy objects (e.g., see https://www.objc.io/issues/7-foundation/key-value-coding-and-observing/), but a proxy object doesn't seem to be provided by Apple for NSMutableDictionary's.
2) for notational convenience in some other code that I was writing.
*/
// QUESTION: Can I set up an observer to detect any changes to the value of the key's within the dictionary? We'd have to remove this KVO observer if the object was removed. Presumably, with this interface, the way that the object would be removed would be (a) setting with nil, and (b) deallocation of this SMMutableDictionary itself.
#import <Foundation/Foundation.h>
#class SMMutableDictionary;
#protocol SMMutableDictionaryDelegate <NSObject>
#required
// Reports on the assignment to a keyed value for this dictionary and the removal of a key: setObject:forKey: and removeObjectForKey:
- (void) dictionaryWasChanged: (SMMutableDictionary * _Nonnull) dict;
#end
#interface SMMutableDictionary : NSMutableDictionary
// For some reason (more of the ugliness associated with having an NSMutableDictionary subclass), when you unarchive a keyed archive of an SMMutableDictionary, it doesn't give you back the SMMutableDictionary, it gives you an NSMutableDictionary. So, this method is for your convenience. AND, almost even better, when you use a keyed archiver to archive, it uses our encoder method, but doesn't actually generate an archive containing our dictionary!! SO, don't use keyed archiver methods directly, use the following two methods:
- (NSData * _Nullable) archive;
+ (instancetype _Nullable) unarchiveFromData: (NSData * _Nonnull) keyedArchiverData;
// Optional delegate
#property (nonatomic, weak, nullable) id<SMMutableDictionaryDelegate> delegate;
#end
Here's the .m file:
//
// SMMutableDictionary.m
// Dictionary
//
// Created by Christopher Prince on 10/6/15.
// Copyright © 2015 Spastic Muffin, LLC. All rights reserved.
//
// I wanted to make this a Swift NSMutableDictionary subclass, but run into issues...
// See https://stackoverflow.com/questions/28636598/cannot-override-initializer-of-nsdictionary-in-swift
// http://www.cocoawithlove.com/2008/12/ordereddictionary-subclassing-cocoa.html
// See also https://stackoverflow.com/questions/10799444/nsdictionary-method-only-defined-for-abstract-class-my-app-crashed
// I tried only implementing the subscript method for Swift (see https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Subscripts.html), but notationally this left much to be desired. I really wanted a type that was fully interoperable with NSDictionary/NSMutableDictionary, which seems to require a subclass.
// See also http://www.smackie.org/notes/2007/07/11/subclassing-nsmutabledictionary/
#import "SMMutableDictionary.h"
#interface SMMutableDictionary()
#property (nonatomic, strong) NSMutableDictionary *dict;
#end
// See this for methods you have to implement to subclass: https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Reference/Foundation/Classes/NSMutableDictionary_Class/index.html
// HOWEVER, while they didn't say you have to subclass the init method, it did't work for me without doing that. i.e., I needed to have [1] below.
#implementation SMMutableDictionary
- (instancetype) initWithObjects:(const id _Nonnull __unsafe_unretained *)objects forKeys:(const id<NSCopying> _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt;
{
self = [super init];
if (self) {
self.dict = [[NSMutableDictionary alloc] initWithObjects:objects forKeys:keys count:cnt];
}
return self;
}
// [1].
- (instancetype) init;
{
self = [super init];
if (self) {
self.dict = [NSMutableDictionary new];
}
return self;
}
// Both of these are useless. See the keyed archiver/unarchiver methods on the .h interface.
/*
- (void)encodeWithCoder:(NSCoder *)aCoder;
{
//[aCoder encodeObject:self.dict];
[aCoder encodeObject:self.dict forKey:#"dict"];
}
*/
/*
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;
{
self = [super initWithCoder:aDecoder];
if (self) {
//self.dict = [aDecoder decodeObject];
self.dict = [aDecoder decodeObjectForKey:#"dict"];
}
return self;
}
*/
- (NSData * _Nullable) archive;
{
return [NSKeyedArchiver archivedDataWithRootObject:self.dict];
}
+ (instancetype _Nullable) unarchiveFromData: (NSData * _Nonnull) keyedArchiverData;
{
NSMutableDictionary *dict = [NSKeyedUnarchiver unarchiveObjectWithData:keyedArchiverData];
if (nil == dict) return nil;
return [[SMMutableDictionary alloc] initWithDictionary:dict];
}
- (NSUInteger) count;
{
return self.dict.count;
}
- (id) objectForKey:(id)aKey;
{
return [self.dict objectForKey:aKey];
}
- (NSEnumerator *)keyEnumerator;
{
return [self.dict keyEnumerator];
}
- (void) setObject:(id)anObject forKey:(id<NSCopying>)aKey;
{
[self.dict setObject:anObject forKey:aKey];
if (self.delegate) {
[self.delegate dictionaryWasChanged:self];
}
}
- (void) removeObjectForKey:(id)aKey;
{
[self.dict removeObjectForKey:aKey];
if (self.delegate) {
[self.delegate dictionaryWasChanged:self];
}
}
#end
Update on 10/9/15
To clarify what I meant by "mutation changes" (responding to #quelish below), here's a KVO example with an NSMutableDictionary. Note that the output of this does not reflect Test 1 below. I.e., a change to a key is not indicated by KVO. This example is adapted from https://developer.apple.com/library/prerelease/mac/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-XID_5
If you do know all of the keys to your dictionary, you may be able to use KVO. See Observing NSMutableDictionary changes
//
// ViewController.swift
// Dictionary2
//
// Created by Christopher Prince on 10/9/15.
// Copyright © 2015 Spastic Muffin, LLC. All rights reserved.
//
import UIKit
private var myContext = 0
class ViewController: UIViewController {
var obj = MyObserver()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
print("Test 1")
obj.objectToObserve.myDict["key1"] = "value1"
print("Test 2")
obj.objectToObserve.myDict = NSMutableDictionary()
}
}
class MyObjectToObserve: NSObject {
dynamic var myDict = NSMutableDictionary()
override var description : String {
return "\(myDict)"
}
}
class MyObserver: NSObject {
var objectToObserve = MyObjectToObserve()
override init() {
super.init()
objectToObserve.addObserver(self, forKeyPath: "myDict", options: NSKeyValueObservingOptions(rawValue: 0), context: &myContext)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if context == &myContext {
//let newValue = change?[NSKeyValueChangeNewKey]
print("change: \(change)")
print("object: \(object)")
} else {
super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
}
}
deinit {
objectToObserve.removeObserver(self, forKeyPath: "myDate", context: &myContext)
}
}
I need to check during parsing data from json file if there will be NSArray or NSDictionary model because bug on server side. If there is Array I need to ignore this model.
#property (strong, nonatomic) NSDictionary <Optional,RCTruckInningsModel> *innings;
+(BOOL)propertyIsOptional:(NSString*)propertyName
{
return YES;
}
I use jsnomodel.com
This is not working
Thanks
You are correctly defining "innings" as Optional, but this is not how Optional works. Properties defined as Optional can be either their specified type or nil.
You have a very non-standard situation on your hands and it will require a special solution. Off top of my head you can do the following:
1) define an ignored property (i.e. JSONModel does not process it when importing JSON)
#property (strong, nonatomic) NSDictionary<Ignore>* innings;
2) then import the property value yourself by overriding initWithDictionary (in case you use initWithDictionary):
-(id)initWithDictionary:(NSDictionary*)dict error:(NSError**)err
{
self = [super initWithDictionary:dict error:err];
if (self) {
if ([dict[#"innings"] isKindOfClass:[NSDictionary class]]) {
NSDictionary* d = dict[#"innings"];
NSMutableDictionary* md = [#{} mutableCopy];
for (NSString* key in d.allKeys) {
RCTruckInningsModel* model = [[RCTruckInningsModel alloc] initWithDictionary: d[key]];
if (model) {
md[key] = model;
}
}
self.innings = [md copy];
}
}
return self;
}
I didn't actually try the code in Xcode, but that's what I think the general solution should be - you'll need to try it yourself and finish it up for your own JSON structure, etc. But it should push you in the right direction
Accept the answer if it helps you to solve your problem
I have the following 2 classes:
EventDispatcher:
#interface EventDispatcher()
-(id)initEventDispatcher;
-(NSMutableArray*)getSubscriptionsToEvent:(EVENT_TYPE)eventType;
-(NSNumber*)getKeyToEvent:(EVENT_TYPE)eventType;
#end
#implementation EventDispatcher
static EventDispatcher* eventDispatcher;
// Singleton.
+(EventDispatcher*)instance
{
if (eventDispatcher == nil)
{
eventDispatcher = [[EventDispatcher alloc] initEventDispatcher];
}
return eventDispatcher;
}
-(id)initEventDispatcher
{
self = [super init];
if (self)
{
eventSubscriptions = [[NSMutableDictionary alloc] init];
}
return self;
}
// Let anyone subscribe to an event. Return the EventSubscriber so they can dispatch events if needed, and to be able to unsubscribe.
-(EventSubscriber*)subscribe:(EVENT_TYPE)eventType :(void(^)(id package))operateEvent
{
// Create the object.
EventSubscriber* eventSubscriber = [[EventSubscriber alloc] initEventSubscriber:eventType :operateEvent];
// Now get the list it belongs to (we sort subscriptions in a dictionary so that when we dispatch an event, it's fast (we don't need to iterate through all EventSubscribers to find who subscribe to an event).
NSMutableArray* subscriptionsToThisEvent = [self getSubscriptionsToEvent:eventType];
if(subscriptionsToThisEvent == nil)
{
// If the list is nil, no one has subscribed to it yet, so make that list and add it to the dictionary.
subscriptionsToThisEvent = [[NSMutableArray alloc] init];
NSNumber* key = [self getKeyToEvent:eventType];
[eventSubscriptions setObject:subscriptionsToThisEvent forKey:key];
[subscriptionsToThisEvent release];
}
// Add the EventSubscriber to the subscription list.
[subscriptionsToThisEvent addObject:eventSubscriber];
[eventSubscriber release];
return eventSubscriber;
}
-(void)unsubscribe:(EventSubscriber*)eventSubscriber
{
// Get the list it belongs to, and remove it from that list.
EVENT_TYPE eventType = [eventSubscriber getEventType];
NSMutableArray* subscriptionsToThisEvent = [self getSubscriptionsToEvent:eventType];
if (subscriptionsToThisEvent != nil)
{
[subscriptionsToThisEvent removeObject:eventSubscriber];
}
}
-(void)dispatch:(EVENT_TYPE)eventType :(id)package
{
NSMutableArray* subscriptionsToThisEvent = [self getSubscriptionsToEvent:eventType];
// If no one has subscribed to this event, it could be nil, so do nothing.
if (subscriptionsToThisEvent != nil)
{
// Otherwise, let them all know that the event was dispatched!
for (EventSubscriber* eventSubscriber in subscriptionsToThisEvent)
[eventSubscriber dispatch:package];
}
}
// Helper methods to get stuff (lists, keys) from event types.
-(NSMutableArray*)getSubscriptionsToEvent:(EVENT_TYPE)eventType
{
NSNumber* key = [self getKeyToEvent:eventType];
NSMutableArray* subscriptionsToThisEvent = [eventSubscriptions objectForKey:key];
return subscriptionsToThisEvent;
}
-(NSNumber*)getKeyToEvent:(EVENT_TYPE)eventType
{
return [NSNumber numberWithInt:eventType];
}
-(void)dealloc
{
[eventSubscriptions release];
[super dealloc];
}
#end
EventSubscriber:
#import "EventSubscriber.h"
#implementation EventSubscriber
-(id)initEventSubscriber:(EVENT_TYPE)newEventType :(void(^)(id package))newOperateEvent
{
self = [super init];
if (self)
{
operateEvent = [newOperateEvent copy];
eventType = newEventType;
}
return self;
}
-(void)dispatch:(id)package
{
operateEvent(package);
}
-(EVENT_TYPE)getEventType
{
return eventType;
}
-(void)dealloc
{
[operateEvent release];
[super dealloc];
}
#end
Onto the big question: How do I unburden a programmer who is using this system with having to unsubscribe from an event during deallocation? When multiple classes are using this system, programmers will have to remember to unsubscribe during deallocation (if not an earlier time), or REALLY bad/weird/unexpected things could happen (I would prefer a compile-time check, or a big, obvious, debuggable crash, but more-so the former). Ideally, I'd like to restructure this system (or do anything) so that when an object is deallocated, the EventDispatcher gracefully handles it.
One quick fix is to have objects allocate EventSubscribers directly, then in the EventSubscriber constructor, it subscribes itself to EventDispatcher (but that's obviously bad, maybe make EventDispatcher's stuff static? Ugh now we're just getting worse).
Side notes:
I'm not using ARC, but, that does not matter here (at least I think it does not, if there are ARC-based solutions, I'd like to hear them).
I do plan on adding a method in EventDispatcher to be able to remove EventSubscribers by those who did the subscription (so now when subscribing, objects will have to pass 'self'). I also plan on making the enumerated EVENT_TYPE use strings, but that's a different topic altogether.
I also plan on translating a lot of my code (including these classes) to C++. So I'd appreciate a conceptual solution as opposed to Objective-C specific solutions.
So, is this possible?
Thanks a bunch!