关于ios:为什么有些东西会立即更新而有些则不会?

Why does some stuff get updated immediately and some doesnt?

我已经为此苦苦挣扎了几个小时和几天。当发生更改时,我的视图控制器中的某些内容会立即更新,而有些则不会。举个例子,我会尽量复制尽可能多的代码让你理解。

我正在为我的公司开发预算应用。三个实体是:MainCategorySpendingCategoryExpenditures。一个支出类别有几个支出,一个主要类别有几个支出类别。

MainCategory <-->> SpendingCategory <-->> Expenditures

编辑和添加事务发生在 TransactionViewController 中。作为模型,我有一个 SpendingCategory。您可以从主类别视图控制器访问此视图控制器,然后在 prepareForSegue 方法中设置 SpendingCategory

这里是 TransactionViewController 所需的所有内容(我省略了 viewDidLoad,因为那里只设置了标签和格式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#pragma mark Model initialization
- (void)setSpendingCategory:(SpendingCategory *)spendingCategory
{
    _spendingCategory = spendingCategory;
    [self setupFetchedResultsController];
    self.title = NSLocalizedString(@"Transactions", nil);
}

- (SpendingCategory *)spendingCategory
{
    return _spendingCategory;
}

- (void)setupFetchedResultsController
{
    NSDate *startDate = [NSDate startDateForCostPeriod:[self.spendingCategory getBiggestCostPeriod]];
   
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Expenditures"];
    request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"date" ascending:NO]];
    request.predicate = [NSPredicate predicateWithFormat:@"forSpendingCategory = %@ AND date >=%@", self.spendingCategory, startDate];
   
    self.fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request
                                                                       managedObjectContext:self.spendingCategory.managedObjectContext
                                                                        sectionNameKeyPath:nil                                                                                      cacheName:nil];
}

    - (void)viewWillAppear:(BOOL)animated
    {
        [super viewWillAppear:animated];
        [self updateBudgetAndBudgetLeft];
        [self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:NO];
    }
   
    - (void)updateBudgetAndBudgetLeft
    {
        self.budgetLabel.text = [NSString stringWithFormat:@"%@/%@",[self.spendingCategory convertCostToBiggestCostPeriodAsString], [self.spendingCategory getBiggestCostPeriodAsString]];
       
        double moneySpent = [[self.spendingCategory getExpendituresAmountForCostPeriod:[self.spendingCategory getBiggestCostPeriod]] doubleValue];
        double budget = [[self.spendingCategory convertCostToBiggestCostPeriod] doubleValue];
        //Money spent will be ADDED because negative values are already stored as negative
        double budgetLeftCalc = budget + moneySpent;
       
        if(budgetLeftCalc >= 0){
            NSNumber *budgetLeft = [NSNumber numberWithDouble:budgetLeftCalc];
            self.budgetLeftLabel.text = [NSString stringWithFormat:@"%@ %@", [budgetLeft getLocalizedCurrencyStringWithDigits:0], NSLocalizedString(@"left", nil)];
        } else {
            NSNumber *budgetLeft = [NSNumber numberWithDouble:-budgetLeftCalc];
            self.budgetLeftLabel.text = [NSString stringWithFormat:@"%@ %@", [budgetLeft getLocalizedCurrencyStringWithDigits:0], NSLocalizedString(@"over", nil)];
            self.budgetLeftLabel.textColor = [UIColor redColor];
        }
       
        if (moneySpent < 0) {
            self.progressBar.progress = -moneySpent/budget;
        } else {
            self.progressBar.progress = 0;
        }
   
    }

从数据库中获取每期成本的函数是:(而且这个函数显然没有及时得到正确的结果):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
- (NSNumber *)getExpendituresAmountForCostPeriod:(CostPeriod)costPeriod
{
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Expenditures"];
    [fetchRequest setResultType:NSDictionaryResultType];
   
    NSDate *startDate = [NSDate startDateForCostPeriod:[self getBiggestCostPeriod]];
    fetchRequest.predicate = [NSPredicate predicateWithFormat:@"forSpendingCategory = %@ AND date >= %@", self, startDate];;
   
   
    //Define what we want
    NSExpression *keyPathExpression = [NSExpression expressionForKeyPath: @"amount"];
    NSExpression *sumExpression = [NSExpression expressionForFunction: @"sum:"
                                                            arguments: [NSArray arrayWithObject:keyPathExpression]];
   
    //Defining the result type (name etc.)
    NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
    [expressionDescription setName: @"totalExpenditures"];
    [expressionDescription setExpression: sumExpression];
    [expressionDescription setExpressionResultType: NSDoubleAttributeType];
   
    // Set the request's properties to fetch just the property represented by the expressions.
    [fetchRequest setPropertiesToFetch:[NSArray arrayWithObject:expressionDescription]];
   
   
    // Execute the fetch.
    NSError *error = nil;
    NSArray *objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
    if (objects == nil) {
        return [NSNumber numberWithDouble:0];
    } else {
        if ([objects count] > 0) {
            return [[objects objectAtIndex:0] valueForKey:@"totalExpenditures"];
        } else {
            return [NSNumber numberWithDouble:0];
        }
    }
}

如果你点击一个交易,你会进入ExpenditureViewController,它有一个Expenditure对象*支出的模型。该模型具有三个属性:dateamountdescription.

如果您现在更改某些事务或插入新事务,此 updateBudgetAndBudgetLeft 不会做任何事情!奇怪的是,它在一段时间后确实如此。如果您等待 20 秒并重新加载我的 TransactionViewController,则会发生更改。然而,显示在我的 TransactionViewController 中的交易立即完全准确!为什么?为什么会延迟?为什么我什至需要做一个单独的函数 updateBudgetAndBudgetLeft 因为我认为我的 NSFetchedResultsController 会自动执行?!在整个核心数据丛林中,我错过了什么?

编辑 2:

Complete CoredDataViewController(tableView 是一个属性,因为我使用嵌入式表格视图)

pragma mark - 获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
- (void)performFetch
{
    if (self.fetchedResultsController) {
        if (self.fetchedResultsController.fetchRequest.predicate) {
            if (self.debug) NSLog(@"[%@ %@] fetching %@ with predicate: %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), self.fetchedResultsController.fetchRequest.entityName, self.fetchedResultsController.fetchRequest.predicate);
        } else {
            if (self.debug) NSLog(@"[%@ %@] fetching all %@ (i.e., no predicate)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), self.fetchedResultsController.fetchRequest.entityName);
        }
        NSError *error;
        [self.fetchedResultsController performFetch:&error];
        if (error) NSLog(@"[%@ %@] %@ (%@)", NSStringFromClass([self class]), NSStringFromSelector(_cmd), [error localizedDescription], [error localizedFailureReason]);
    } else {
        if (self.debug) NSLog(@"[%@ %@] no NSFetchedResultsController (yet?)", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
    }
    [self.tableView reloadData];
}

- (void)setFetchedResultsController:(NSFetchedResultsController *)newfrc
{
    NSFetchedResultsController *oldfrc = _fetchedResultsController;
    if (newfrc != oldfrc) {
        _fetchedResultsController = newfrc;
        newfrc.delegate = self;
        if ((!self.title || [self.title isEqualToString:oldfrc.fetchRequest.entity.name]) && (!self.navigationController || !self.navigationItem.title)) {
            self.title = newfrc.fetchRequest.entity.name;
        }
        if (newfrc) {
            if (self.debug) NSLog(@"[%@ %@] %@", NSStringFromClass([self class]), NSStringFromSelector(_cmd), oldfrc ? @"updated" : @"set");
            [self performFetch];
        } else {
            if (self.debug) NSLog(@"[%@ %@] reset to nil", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
            [self.tableView reloadData];
        }
    }
}

#pragma mark - UITableViewDataSource

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[self.fetchedResultsController sections] count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects];
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
    return [[[self.fetchedResultsController sections] objectAtIndex:section] name];
}

- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index
{
    return [self.fetchedResultsController sectionForSectionIndexTitle:title atIndex:index];
}

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView
{
    return [self.fetchedResultsController sectionIndexTitles];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return nil;
}

#pragma mark - NSFetchedResultsControllerDelegate

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    if(!self.reordering){
        if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext) {
            [self.tableView beginUpdates];
            self.beganUpdates = YES;
        }
    }
}

- (void)controller:(NSFetchedResultsController *)controller
  didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo
           atIndex:(NSUInteger)sectionIndex
     forChangeType:(NSFetchedResultsChangeType)type
{
    if(!self.reordering){
        if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext)
        {
            switch(type)
            {
                case NSFetchedResultsChangeInsert:
                    [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
                    break;
                   
                case NSFetchedResultsChangeDelete:
                    [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
                    break;
            }
        }
    }
}


- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    if(!self.reordering){
        if (!self.suspendAutomaticTrackingOfChangesInManagedObjectContext)
        {
            switch(type)
            {
                case NSFetchedResultsChangeInsert:
                    [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
                    break;
                   
                case NSFetchedResultsChangeDelete:
                    [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                    break;
                   
                case NSFetchedResultsChangeUpdate:
                    [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                    break;
                   
                case NSFetchedResultsChangeMove:
                    [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                    [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
                    break;
            }
        }
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    if(!self.reordering){
        if (self.beganUpdates) [self.tableView endUpdates];
    }
}

- (void)endSuspensionOfUpdatesDueToContextChanges
{
    _suspendAutomaticTrackingOfChangesInManagedObjectContext = NO;
}

- (void)setSuspendAutomaticTrackingOfChangesInManagedObjectContext:(BOOL)suspend
{
    if (suspend) {
        _suspendAutomaticTrackingOfChangesInManagedObjectContext = YES;
    } else {
        [self performSelector:@selector(endSuspensionOfUpdatesDueToContextChanges) withObject:0 afterDelay:0];
    }
}

-(void)stopSuspendAutomaticTrackingOfChangesInManagedObjectContextWithDelay:(double)delay
{
    //the delay is necessary in my app after row editing
    [self performSelector:@selector(endSuspensionOfUpdatesDueToContextChanges) withObject:0 afterDelay:delay];
}


我会尝试给出一些提示,但这里的逻辑很难理解,因为代码太多。

首先,你告诉过 updateBudgetAndBudgetLeft 不做任何事情,但我看不出这个方法在任何地方被调用。如果 viewWillAppear 是唯一调用该方法的地方,则重新计算数据是不够的。事实上,你可以改变一些东西,但控制器不应该从屏幕上消失a€|这只是我的假设。

其次,在

1
2
3
4
5
6
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    if(!self.reordering){
        if (self.beganUpdates) [self.tableView endUpdates];
    }
}

您错过了检查 self.suspendAutomaticTrackingOfChangesInManagedObjectContext 布尔值。

第三,为了隔离问题,我建议省略 CoreDataViewController。这个类,我认为它被用作基类,可能会引入难以调试的问题。特别是对于 Core Data 的新手。所以,暂时忘记它。

基于这些假设,我想说的是应该显式调用 updateBudgetAndBudgetLeft。当您使用 NSFetchedResultsController 时,它会跟踪您设置的实体(NSFetchedResultsController 陷阱中对此有一个有趣的解释)。我猜在这种情况下 Expenditures 但没有人会调用 updatea€| 方法。
例如,我的建议是在 - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller 中调用该方法。您确定更改已完成以进行跟踪。

编辑 1

you missed to check
self.suspendAutomaticTrackingOfChangesInManagedObjectContext bool
value. What do you mean here?

如果您已暂停更改跟踪,则不应调用 endUpdates 方法。如果我没记错的话,斯坦福代码也做了类似的事情。检查它以确保。

So you're suggestion is to overwrite this controllerDidChangeContent
in every view controller (which is subclassing CoreDataViewController)
and call updateBudgetAndBudgetLeft there?

是的。这可能是一个选择。同样调用 super 应该可以工作,但在这种情况下,这取决于您是否要更新,即使更改没有被跟踪。无论如何,我会在没有那个基类的情况下工作。

Why isn't it sufficient to call this method in viewWillAppear? I mean
it should be called each time the view gets visible or not?

这取决于您何时重新计算数据。如果您执行它并且控制器保持可见,则不会调用 updatea€|

编辑 2

根据文档并基于这个较新的问题 iOS FetchRequest 关于聚合函数:How to include pending changes?,如果 updatea€| 方法与 NSDictionaryResultType 类型一起使用,则需要执行 首先。之后就可以正常调用它了。换句话说,每次修改 Expenditures 项时,都应该先做一个 save,然后再做一个 updatea€|。显然,保存太多会影响磁盘,因此可能导致应用程序运行缓慢,但在这里您需要进行测量。否则,您可以在内存级别执行聚合。为此,只需过滤正确的实体即可。这可以在 controllerDidChangeContentviewWillAppear.

轻松完成