从 NSUserDefault 恢复后,NSMutableArray 中的奇怪自定义对象行为(复制一个对象,删除另一个对象)



我对Objective-C(和一般的编码)很陌生,我正在开发一个iOS项目来熟悉自定义对象的持久存储。

在开发的第一阶段一切都很顺利(也要感谢我在StackOverflow上找到的许多答案)直到几天前,我遇到了一个奇怪的行为,我无法理解也无法解决。我将尽我所能描述这个应用程序,并解释我遇到的问题(抱歉,英语不是我的主要语言)。

应用程序的结构已经有:

  • 重量对象的自定义类(每个对象都有几个属性:NSDate, int表示千克,int表示克,float表示千克,等等);
  • 一个MainViewController,包含一个UIPickerView与两个组件(公斤和克),一个"保存选定的重量"按钮,添加当前的重量对象(实际上,它的一个确切的副本-我实现了NSCoping协议)到NSMutableArray插入它在索引0,一个"历史"按钮,触发segue到FlipSideViewController和一对标签显示的日期最后记录的重量和它的值;
  • 一个包含TableView的FlipSideViewController,我在其中加载了NSMutableArray的值。
  • 还有一个SettingsViewController,但它还没有做任何事情,它与这个问题无关(我提到它只是因为有一些参考它的代码我稍后包括)。

我通过NSUserDefaults实现数据持久性,并在我的自定义权重类中添加NSCoding方法(initWithCoder和encodeWithCoder):我编码和解码包含自定义权重对象(NSKeyedArchiver archivedDataWithRootObject:obj)的数组,因为,如果我正确地得到,NSUserDefaults不允许存储未在NSData中编码的自定义对象。

基本的交互已经起作用了:

  • 如果我开始干净,不恢复以前存储的数据,我可以创建许多新的权重对象,这是添加到NSMutableArray和显示在FlipSideViewController的TableView。
  • 一切似乎都很好(但事实并非如此,我将稍后解释),即使我恢复以前记录的数据:在MainViewController中的标签显示以前记录的最后一个对象的值,PickerView的行匹配最后选择的值,在FlipSideViewController中的TableView显示数组的恢复值…

但是有一个但是:当我恢复以前的数据(上面的第2点)时,即使一切看起来都是正确的,当我点击"保存按钮"添加新记录时,数组中的值被打乱了。

也许用一个例子更容易解释它们是如何搞砸的:

假设在第一次训练中,我保存了3个不同的体重值:第一次是12,0 kg,然后是25,0 kg,最后是70,0 kg。然后TableView在顶部显示70,0kg,第二个值为25,0kg,第三个值为12,0;MainViewController上的标签显然显示70,0kg,因为它是最后记录的值。现在,如果我退出应用程序并再次启动它,在它加载后,一切看起来都和我退出前一样:标签显示70,0kg TableView有相同的3个值,顺序相同 问题开始了,如果我想添加一个新值,比如100,5kg,按下保存按钮:之后,在TableView中显示的值以一种我不理解的方式改变:从上到下,值现在是100,5 - 100,5(是的,它是重复的,而不是显示70,0),25,0,12,0

我不知道这个问题;也许在重新创建数组时,数组的索引发生了一些变化,但我不明白为什么和如何。

我想为了清晰起见,我最好也提供代码;请原谅我的变量名和方法名通常是意大利语(我不会再这样做了,我现在意识到,如果我必须用英语问一些事情,最好我的代码也不包含意大利语的痕迹)。

我不知道包含所有代码是否更好;由于我的行为看起来很奇怪,我想问题是在我在其他地方做的事情,所以我希望它是好的,如果我包括一切:

自定义类:Peso.h

#import <Foundation/Foundation.h>
@interface Peso : NSObject <NSCopying,NSCoding>
@property int pesoChili;
@property int pesoGrammi;
@property float pesoInChiloGrammi;
@property NSDate * dataPesatura;
@property NSString * pesoChilogrammi;
@property NSString * DataPesaturaStringa;
-(id)initWithCoder:(NSCoder *)aDecoder;
-(void)encodeWithCoder:(NSCoder *)aCoder;
@end

自定义类:Peso.m

#import "Peso.h"
@implementation Peso

-(id)copyWithZone:(NSZone *)zone{
    Peso *copy = [[Peso alloc] init];
    if (copy)
    {
        copy.PesoChili = self.pesoChili;
        copy.pesoGrammi = self.pesoGrammi;
        copy.pesoInChiloGrammi = self.pesoInChiloGrammi;
        copy.dataPesatura = self.dataPesatura;
        copy.pesoChilogrammi = self.pesoChilogrammi;
        copy.DataPesaturaStringa = self.DataPesaturaStringa;
    }
    return copy;
}

-(id)initWithCoder:(NSCoder *)aDecoder{
    _pesoChili = [aDecoder decodeIntForKey:@"pesoChili"];
    _pesoGrammi = [aDecoder decodeIntForKey:@"pesoGrammi"];
    _pesoInChiloGrammi = [aDecoder decodeFloatForKey:@"pesoInChiloGrammi"];
    _dataPesatura = [aDecoder decodeObjectForKey:@"dataPesatura"];
    _pesoChilogrammi = [aDecoder decodeObjectForKey:@"pesoChilogrammi"];
    _DataPesaturaStringa = [aDecoder decodeObjectForKey:@"DataPesaturaStringa"];
    return self;
}

-(void)encodeWithCoder:(NSCoder *)aCoder{
    [aCoder encodeInt:_pesoChili forKey:@"pesoChili"];
    [aCoder encodeInt:_pesoGrammi forKey:@"pesoGrammi"];
    [aCoder encodeFloat:_pesoInChiloGrammi forKey:@"pesoInChiloGrammi"];
    [aCoder encodeObject:_dataPesatura forKey:@"dataPesatura"];
    [aCoder encodeObject:_pesoChilogrammi forKey:@"pesoChilogrammi"];
    [aCoder encodeObject:_DataPesaturaStringa forKey:@"DataPesaturaStringa"];
}
@end

MainViewController.h

#import "FlipsideViewController.h"
#import "SettingsViewController.h"
#import "Peso.h"
@interface MainViewController : UIViewController <FlipsideViewControllerDelegate,UIPickerViewDataSource,UIPickerViewDelegate,SettingsViewControllerDelegate>
@end

MainViewController.m

#define kPercorso @"percorsoArrayCronologiaPeso30"
#import "MainViewController.h"
@interface MainViewController ()
@property (weak, nonatomic) IBOutlet UILabel *dataPesoCorrente;
@property (weak, nonatomic) IBOutlet UILabel *labelPesoCorrente;
@property (weak, nonatomic) IBOutlet UIButton *bottoneRegistraPeso;
@property (weak, nonatomic) IBOutlet UIButton *bottoneHistory;
@property (weak, nonatomic) IBOutlet UIPickerView *thePicker;
@end
@implementation MainViewController {
    NSMutableArray *cronologiaPeso;
    Peso * pesoRilevato;
    NSUserDefaults *defaults;
}

- (IBAction)salvaPesoCorrente:(id)sender {
    pesoRilevato.pesoChilogrammi = [NSString stringWithFormat:@"%d,%d kg",pesoRilevato.pesoChili,pesoRilevato.pesoGrammi];
    pesoRilevato.dataPesatura = [NSDate date];
    NSDateFormatter * formatter = [[NSDateFormatter alloc] init];
    [formatter setDateFormat:@"EEEE, MMMM dd yyyy - hh:mm:ss a"];
    pesoRilevato.DataPesaturaStringa = [formatter stringFromDate:pesoRilevato.dataPesatura];
    self.labelPesoCorrente.text = pesoRilevato.pesoChilogrammi;
    self.dataPesoCorrente.text = pesoRilevato.DataPesaturaStringa;
    self.bottoneHistory.hidden = NO;
    [self nuoviValori];
}

-(NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView{
    return 2;
}

-(NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component{
    switch (component) {
        case 0:
            return 201;
            break;
        case 1:
            return 10;
            break;
        default:
            return 0;
    }
}

-(NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component{
    switch (component) {
        case 0:
            return [NSString stringWithFormat:@"%d", row];
            break;
        case 1:
            return [NSString stringWithFormat:@"%d", row];
            break;
        default:
            return @"error";
            break;
    }
}

-(void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component{
    if (component == 0) {
        pesoRilevato.pesoChili = row;
    }
    if (component == 1) {
        pesoRilevato.pesoGrammi = row;
    }
    [self combinaChiliEGrammi];
}

-(void)combinaChiliEGrammi{
    pesoRilevato.pesoInChiloGrammi = pesoRilevato.pesoGrammi;
    while (pesoRilevato.pesoInChiloGrammi >= 1) {
        pesoRilevato.pesoInChiloGrammi /= 10;
    }
    pesoRilevato.pesoInChiloGrammi += pesoRilevato.pesoChili;
    self.bottoneRegistraPeso.hidden = NO;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.bottoneRegistraPeso.hidden = YES;
    self.bottoneHistory.hidden = YES;
    if ([[[NSUserDefaults standardUserDefaults] dictionaryRepresentation].allKeys containsObject:kPercorso]) {
        cronologiaPeso = [[NSMutableArray alloc]initWithArray:[self caricaCronologiaPesoCodificatoConKey:kPercorso]];
        self.bottoneHistory.hidden = NO;
        self.bottoneRegistraPeso.hidden = NO;
        self.labelPesoCorrente.text = [[cronologiaPeso objectAtIndex:0]pesoChilogrammi];
        self.dataPesoCorrente.text = [[cronologiaPeso objectAtIndex:0]DataPesaturaStringa];
        [self.thePicker selectRow:[[cronologiaPeso objectAtIndex:0]pesoChili] inComponent:0 animated:YES];
        [self.thePicker selectRow:[[cronologiaPeso objectAtIndex:0]pesoGrammi] inComponent:1 animated:YES];
        if (!pesoRilevato){
            pesoRilevato = [cronologiaPeso objectAtIndex:0];
        }
    } else {
        pesoRilevato = [[Peso alloc]init];
        cronologiaPeso = [[NSMutableArray alloc]init];
    }
}

-(void)nuoviValori{
    [cronologiaPeso insertObject:[pesoRilevato copy] atIndex:0];
    //    [cronologiaPeso insertObject:[pesoRilevato copy] atIndex:pesoRilevato.contatoreOggettiPeso];
    [self salvaArrayCronologiaPesoCodificandolo:cronologiaPeso];
}

-(void)salvaArrayCronologiaPesoCodificandolo:(NSMutableArray*)obj{
    defaults = [NSUserDefaults standardUserDefaults];
    NSData *myEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:obj];
    [defaults setObject:myEncodedObject forKey:kPercorso];
    [defaults synchronize];
}

-(NSArray*)caricaCronologiaPesoCodificatoConKey:(NSString*)key{
    defaults = [NSUserDefaults standardUserDefaults];
    NSData *myEncodedObject = [defaults objectForKey: key];
    NSArray* obj = (NSArray*)[NSKeyedUnarchiver unarchiveObjectWithData: myEncodedObject];
    return obj;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

#pragma mark - Flipside View
- (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)settingsViewControllerDidFinish:(SettingsViewController *)controller
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"showAlternate"]) {
        [[segue destinationViewController] setDelegate:self];
        FlipsideViewController * nextViewController = [segue destinationViewController];
        [nextViewController setCronologiaPeso:cronologiaPeso];

    }
    if ([[segue identifier] isEqualToString:@"showSettings"]) {
        [[segue destinationViewController] setDelegate:self];
    }

}
@end

* * FlipsideViewController.h * *

#import <UIKit/UIKit.h>
#import "Peso.h"
@class FlipsideViewController;
@protocol FlipsideViewControllerDelegate
- (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller;
@end
@interface FlipsideViewController : UIViewController <UITableViewDataSource,UITableViewDelegate>
@property (weak, nonatomic) id <FlipsideViewControllerDelegate> delegate;
@property NSMutableArray *cronologiaPeso;
- (IBAction)done:(id)sender;
@end

* * FlipsideViewController.m * *

#import "FlipsideViewController.h"
@interface FlipsideViewController ()
@end
@implementation FlipsideViewController{
    NSUserDefaults *defaults;
}

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return self.cronologiaPeso.count;
}

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cella" forIndexPath:indexPath];
    Peso * currentPeso = [self.cronologiaPeso objectAtIndex:indexPath.row];
    cell.textLabel.text = currentPeso.pesoChilogrammi;
    cell.detailTextLabel.text = currentPeso.DataPesaturaStringa;
    return cell;
}
// NOT YET IMPLEMENTED
//-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
//    return YES;
//}
//
//
//-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{
//    if (editingStyle == UITableViewCellEditingStyleDelete) {
//        [self.cronologiaPeso removeObjectAtIndex:indexPath.row];
//        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
////        [defaults synchronize]
//    }
//}

- (void)viewDidLoad
{
    [super viewDidLoad];        
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

#pragma mark - Actions
- (IBAction)done:(id)sender
{
    [self.delegate flipsideViewControllerDidFinish:self];
}
@end

如果这个问题太长,可能不太清楚,如果我包含了所有的代码,请原谅;显然,我在保存或恢复数组时做错了什么,但我不知道如何以及在哪里……或者可能是我所遵循的保存和恢复数据的整个模式是错误的,我是自学的,我大约一个月前开始编码,所以我确信我的逻辑有严重的问题。

提前感谢,

凯撒

我不确定我是否正确理解了你的问题,但是使用NSUserDefaults时的问题之一可能是因为它需要一些时间来保存所有数据。它基本上在内部使用一个。plist文件,所以如果你的数据非常大,更新的信息可能无法立即可用,例如,如果你想在相同的毫秒内显示在表视图上。

我的建议是在你需要的类中使用一些NSMutableDictionary属性(或者在全局单例对象中更好,所以它在任何地方都是可用的),并且每当数据更新时,首先更新字典属性,这样视图就会显示正确的数据。之后你也可以在NSUserDefaults中保存更改。另一个好的做法是使用[[NSUserDefaults standardUserDefaults] synchronize];

我希望它能以某种方式帮助你:)

看起来像我的问题,我张贴了这个问题(添加一个对象到NSMutableArray,从NSUserDefaults恢复导致一个奇怪的行为:新对象被添加到数组在索引0和索引1,完全取代的对象,而不是应该在索引1)可以很容易地修复。

在我的代码(这是包含在上面的问题),在MainViewController。m -> viewDidLoad之前我写过:

if ([[[NSUserDefaults standardUserDefaults] dictionaryRepresentation].allKeys containsObject:kPercorso]) {
    cronologiaPeso = [[NSMutableArray alloc]initWithArray:[self caricaCronologiaPesoCodificatoConKey:kPercorso]];
/...

证明添加copyItems:YES到initWithArray:完全解决了这个问题。正确的代码是:

//...
cronologiaPeso = [[NSMutableArray alloc]initWithArray:[self caricaCronologiaPesoCodificatoConKey:kPercorso] copyItems:YES];
//...

最新更新