【问题标题】:Odd custom objects behavior (duplicates one object, removes another) in NSMutableArray after it's restored from NSUserDefaults从 NSUserDefaults 恢复后 NSMutableArray 中的奇怪自定义对象行为(复制一个对象,删除另一个对象)
【发布时间】:2014-07-23 14:30:10
【问题描述】:

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

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

应用的结构已经有:

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

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

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

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

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

用一个例子来解释他们是如何搞砸的可能更容易:

假设在第一个会话中,我保存了 3 个不同的重量值:第一个 12.0 公斤,然后 25.0 公斤,最后 70.0 公斤。 然后 TableView 在顶部显示 70,0 kg,25,0 kg 作为第二个值,12,0 作为第三个值; MainViewController 上的标签显然显示 70,0 kg,因为它是记录的最后一个值。 现在,如果我退出应用程序并再次启动它,在它加载后一切看起来都与我退出前完全一样:标签显示 70,0 公斤,而 TableView 具有相同的 3 个值以相同的顺序。 如果我想通过按保存按钮添加一个新值,比如 100.5 公斤,问题就开始了:之后,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

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

提前致谢,

切萨雷

【问题讨论】:

  • 你是在设备还是模拟器上测试的?如果您在设备上测试过,请尝试删除该应用并重新构建。
  • @Bejibun 我在模拟器上测试过,我还没有苹果开发者账号。
  • @Cesare,我的建议是从小处着手。创建一个没有 UI 的新项目,只有你的 Peso 类。启动,在 NSUserDefaults 中查找 Pesos 数组。如果找不到,请在代码中创建一个并将其保存到 NSUserDefaults。如果您确实找到了,请添加一些比索并重新保存。 NSLog 每一步。如果你有问题,你可以来这里发布一个小得多的问题。
  • 谢谢@danh,您的建议非常有道理:我担心我试图同时做太多事情,也许从小处着手可以帮助我找到问题的原因大批。我会告诉你的,再次感谢!
  • 嗨@danh,这很疯狂,但我在没有用户界面且功能较少的简化项目中也回到了第一方。我想我更好地理解了问题所在,但我无法解决它:基本上,在从 NSUSerDefaults 恢复数组之后(并且仅在之后),将新对象添加到可变数组的简单操作会导致:新对象按预期推送到索引 0,索引 1 处的对象(在新保存之前位于索引 0 处)消失并被索引 0 处相同对象的副本替换。相反,如果我尝试在最后添加新对象...

标签: ios objective-c nsmutablearray nsuserdefaults


【解决方案1】:

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

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

希望对你有所帮助:)

【讨论】:

  • 感谢您的帮助@Pavlusha。我保存的对象非常小,所以我认为速度不是问题。我实际上想尝试使用 NSUserDefaults 因为我是 Objective-C 的新手,我想看看我是否能够使用它(我已经在上一个项目中使用了 Mutable Dictionary,我正在尝试看看最多完成任务的方法)。我还不熟悉全局单例对象,现在我很好奇,我会研究一下。再次感谢您花时间帮助我。
【解决方案2】:

看起来像我遇到的问题,我为此发布了这个问题(将对象添加到从 NSUserDefaults 恢复的 NSMutableArray 会导致奇怪的行为:新对象在索引 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];

//...

我希望这篇文章对其他人有用...

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-28
    • 1970-01-01
    • 1970-01-01
    • 2013-04-03
    • 2011-02-07
    • 1970-01-01
    相关资源
    最近更新 更多