2

RSS フィードからデータ (タイトル、画像など) を解析し、テーブルビューに表示する単純な iPhone アプリがあります。

viewDidLoad には、フィードの最初のページに到達し、fetchEntriesNew メソッドを呼び出して tableview にロードするための初期カウンター値があります。

- (void)viewDidLoad
{
    [super viewDidLoad];        
    counter = 1;    
    [self fetchEntriesNew:counter];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(dataSaved:)
                                              name:@"DataSaved" object:nil];
}


- (void) fetchEntriesNew:(NSInteger )pageNumber
{    
    channel = [[TheFeedStore sharedStore] fetchWebService:pageNumber withCompletion:^(RSSChannel *obj, NSError *err){

            if (!err) {
                int currentItemCount = [[channel items] count];
                channel = obj;
                int newItemCount = [[channel items] count];
                NSLog(@"Total Number Of Entries Are: %d", newItemCount);
                counter = (newItemCount / 10) + 1;
                NSLog(@"New Counter Should Be %d", counter);


                int itemDelta = newItemCount - currentItemCount;
                if (itemDelta > 0) {

                    NSMutableArray *rows = [NSMutableArray array];

                    for (int i = 0; i < itemDelta; i++) {
                        NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
                        [rows addObject:ip];
                    }
                    [[self tableView] insertRowsAtIndexPaths:rows withRowAnimation:UITableViewRowAnimationBottom];
                    [aiView stopAnimating];

                }
            }        
    }];
    [[self tableView] reloadData];
}

ユーザーがテーブルビューの下部に到達すると、次を使用してフィードの次のページに到達し、最初に読み込まれた最初のページの下部に読み込みます:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
    if (endScrolling >= scrollView.contentSize.height)
    {
        NSLog(@"Scroll End Called");
        NSLog(@"New Counter NOW is %d", counter);
        [self fetchEntriesNew:counter];
    }
}

更新 2: これは、私が解決できない問題のわかりやすい説明です: たとえば、RSS フィードの各ページに 10 のエントリがあります。アプリが起動し、タイトルやその他のラベルがすぐに読み込まれ、画像が遅延して読み込まれ、最後に終了します。ここまでは順調ですね。ユーザーはスクロールして一番下に到達し、一番下に到達するとスクロール デリゲート メソッドが使用され、カウンターが 1 から 2 に増加し、fetchEntriesNew メソッドに RSS フィードの 2 ページ目に到達するように指示します。プログラムは、以前にフェッチした最初の 10 個のエントリの最後にある次の 10 個のエントリのロードを開始します。これが続くと、ユーザーがスクロールして一番下に到達するたびに、プログラムはさらに 10 個のエントリを取得し、新しい行は以前に取得した行の下に配置されます。ここまでは順調ですね。

ここで、ユーザーが現在、画像が完全に読み込まれた 3 ページ目にいるとします。ページ 3 が完全に読み込まれるため、現在、テーブルビューには 30 のエントリがあることを意味します。ユーザーが一番下までスクロールすると、カウンターがインクリメントされ、最初の 30 エントリの一番下にある RSS フィードのページ 4 から、テーブルビューが新しい行の入力を開始します。タイトルはすぐに読み込まれるため、行が作成され、画像がダウンロードされている間 (まだ完全にはダウンロードされていません)、ユーザーはすぐに一番下に移動します。現在ダウンロードの途中で、4番目のもののロードを再開します。

すべきことは、前のページの画像がダウンロード中かどうかに関係なく、ユーザーがテーブルビューの一番下に到達したときに、次のページからタイトルなどを保持することです。

プロジェクトのデータのダウンロードと永続化に問題はなく、すべてのデータはアプリケーションの実行間で永続化されます。

誰かが私を正しい方向に向けるのを手伝ってくれますか? 前もって感謝します。

更新 3: @Sergio の回答に基づいて、これは私がしたことです:

1) archiveRootObject [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath] への別の呼び出しを追加しました。after [channelCopy addItemsFromChannel:obj];

この時点で、同じバッチを何度も破棄して再ロードすることはありません。まさに私が望んでいたことです。ただし、前のページの画像が完全に読み込まれずに次のページに到達するために複数回スクロールすると、画像が保持されません。

2) 彼が答えで説明したように、Bool の使い方がわかりません。これは私がしたことです: @property Bool myBool を追加しました。TheFeedStore で合成し、新しく追加された archiveRootObject:channelCopy の後に NO に設定し、fetchEntries メソッドの最初の段階で ListViewController で YES に設定します。うまくいきませんでした。

3)私はまた、私が問題全体に対処している方法は、パフォーマンスの悪徳であることに気付きました. キャッシュ外の画像を使用して、一種のキャッシュとして処理する方法はわかりませんが。画像に別のアーカイブ ファイルを使用することをお勧めしますか?

私の問題を解決するために貢献してくれたすべての人に感謝します。

4

3 に答える 3

3

この古い質問と私が提案した解決策を検討すれば、あなたの問題を理解できます。

特に重要な点は、情報 (RSS 情報 + 画像) を永続化する方法に関係しています。これは、全体channelをディスク上のファイルにアーカイブすることによって行われます。

        [channelCopy addItemsFromChannel:obj];
        [NSKeyedArchiver archiveRootObject:channelCopy toFile:pathOfCache];

さて、 を見るとfetchEntriesNew:、そこで最初にすることは、現在のチャンネルを破壊することです。チャネルがディスクに永続化される前にこれが発生すると、一種の無限ループに入ります。

画像のダウンロードの最後に、現在 (私の最初の提案に従って) チャネルを保持していることを理解しています。

すべきことは、フィードが読み取られた直後で、画像のダウンロードを開始する前にチャネルを永続化することです (もちろん、画像のダウンロードの最後にも永続化する必要があります)。

したがって、私の古い要点からこのスニペットを取得すると、次のようになります。

[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {

    if (!err) {

        [channelCopy addItemsFromChannel:obj];

        // ADDED
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_group_wait(obj.imageDownloadGroup, DISPATCH_TIME_FOREVER);
            [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
        });
    }
    block(channelCopy, err);

あなたがすべきことは、もう1つのarchiveRootObject呼び出しを追加することです:

[connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {

    if (!err) {

        [channelCopy addItemsFromChannel:obj];
        [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];

        // ADDED
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            dispatch_group_wait(obj.imageDownloadGroup, DISPATCH_TIME_FOREVER);
            [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
        });
    }
    block(channelCopy, err);

これは、フィード (画像なし) が読み込まれる前にチャネルが破棄されるほど速くスクロールしない限り、機能します。これを修正するには、TheFeedStoreクラスにbool を追加して、YES呼び出し時に設定しfetchWebService、新しく追加されarchiveRootObject:channelCopyた .

これで問題が解決します。

また、設計/アーキテクチャの観点から、永続性の管理方法に大きな問題があることも言わせてください。実際、ディスク上に単一のファイルがあり、 を使用してアトミックに書き込みますarchiveRootObject。このアーキテクチャは、マルチスレッドの観点から本質的に「危険」であり、共有ストアへの同時アクセスが破壊的な影響を及ぼさないようにする方法も考案する必要があります (例: 同時にページ 4 のチャネルをディスクにアーカイブします)。ページ 1 の画像が完全にダウンロードされているため、それらの画像も同じファイルに保持しようとします)。

画像処理のもう 1 つの方法は、アーカイブ ファイルの外部に画像を保存し、それらを一種のキャッシュとして扱うことです。これにより、並行性の問題が修正され、チャネルをページごとに 2 回アーカイブすることによるパフォーマンスの低下も解消されます (フィードが最初に読み取られたときと、後で画像が読み込まれたとき)。

お役に立てれば。

アップデート:

この時点で、同じバッチを何度も破棄して再ロードすることはありません。まさに私が望んでいたことです。ただし、前のページの画像が完全に読み込まれずに次のページに到達するために複数回スクロールすると、画像が保持されません。

これはまさに、あなたのアーキテクチャ (共有アーカイブ/同時アクセス) がおそらく問題を引き起こすだろうと私が言いたかったことです.

いくつかのオプションがあります。Core Data/sqlite を使用します。または、より簡単に、各画像を独自のファイルに保存します。後者の場合、次のことができます。

  1. 取得時に、各画像にファイル名 (これはフィード エントリの ID または連番など) を割り当て、そこに画像データを保存します。

  2. 画像の URL と保存先のファイル名の両方をアーカイブに保存します。

  3. 画像にアクセスする必要がある場合、アーカイブされた辞書から直接取得することはありません。代わりに、それからファイル名を取得し、ディスクからファイルを読み取ります (利用可能な場合)。

  4. この変更は、RSS/画像取得の現在の実装に影響を与えることはありませんが、画像を永続化し、必要に応じてアクセスする方法にのみ影響します (つまり、かなり簡単な変更のようです)。

2) 彼が答えで説明したように、Bool の使い方がわかりません。

  1. isDownloadingBool を TheFeedStore に追加します。

  2. 実行する直前に、メソッドでに設定YESします;fetchWebService:[connection start]

  3. 最初にフィードをアーカイブした直後にNO接続オブジェクトに渡す完了ブロック (再び) に設定します (これは既に行っています)。fetchWebService:

  4. で、scrollViewDidEndDecelerating:最初に次のことを行います。

        if ([TheFeedStore sharedStore].isDownloading)
            return;
    

    更新中に RSS フィードを更新しないようにします。

これが役立つかどうか教えてください。

新しいアップデート:

画像をファイルに保存する方法をスケッチしましょう。

RSSItem クラスで、次を定義します。

@property (nonatomic, readonly) UIImage *thumbnail;
@property (nonatomic, strong) NSString *thumbFile;

thumbFileイメージをホストするローカル ファイルへのパスです。画像の URL ( getFirstImageUrl) を取得したら、たとえばその MD5 ハッシュを取得して、これをローカルの画像ファイル名として使用できます。

NSString* imageURLString = [self getFirstImageUrl:someString];
....
self.thumbFile = [imageURLString MD5String];

(MD5String は、Google で検索できるカテゴリです)。

次に、 でdownloadThumbnailsイメージ ファイルをローカルに保存します。

    NSMutableData *tempData = [NSData dataWithContentsOfURL:finalUrl];
    [tempData writeToFile:[self cachedFileURLFromFileName:self.thumbFile] atomically:YES];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"DataSaved" object:nil];

さて、トリックは、thumbnailプロパティにアクセスするときに、ファイルから画像を読み取って返すことです。

- (UIImage *)thumbnail
{
    NSData* d = [NSData dataWithContentsOfURL:[self cachedFileURLFromFileName:self.thumbFile]];
    return [[UIImage alloc] initWithData:d];
}

このスニペットでcachedFileURLFromFileName:は、次のように定義されています。

- (NSURL*)cachedFileURLFromFileName:(NSString*)filename {

NSFileManager *fileManager = [[NSFileManager alloc] init];
NSArray *fileArray = [fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask];

NSURL* cacheURL = (NSURL*)[fileArray lastObject];
if(cacheURL)
{
    return [cacheURL URLByAppendingPathComponent:filename];
}
return nil;
}

もちろん、thumbFileこれが機能するためには永続化する必要があります。

ご覧のとおり、このアプローチは実装が非常に「簡単」です。これは最適化されたソリューションではなく、アプリを現在のアーキテクチャで動作させる簡単な方法です。

完全を期すために、MD5Stringカテゴリ:

@interface NSString (MD5)

- (NSString *)MD5String;

@end

@implementation NSString (MD5)

- (NSString *)MD5String {
const char *cstr = [self UTF8String];
unsigned char result[16];
CC_MD5(cstr, strlen(cstr), result);

return [NSString stringWithFormat:
        @"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
        result[0], result[1], result[2], result[3],
        result[4], result[5], result[6], result[7],
        result[8], result[9], result[10], result[11],
        result[12], result[13], result[14], result[15]
        ];  
}

@end
于 2013-04-24T19:14:29.197 に答える
2

あなたが実際にやろうとしているのは、ページングを実装することですUITableView

これは非常に簡単で、デリゲートメソッドでこれを行うのではなく、UITableViewデリゲートメソッドでページングを実装することをお勧めします。cellForRowAtIndexPathUIScrollView scrollViewDidEndDecelerating

これが私のページングの実装であり、あなたにも完全に機能するはずです。

まず、ページングに関連する実装定数があります。

//paging step size (how many items we get each time)
#define kPageStep 30
//auto paging offset (this means when we reach offset autopaging kicks in, i.e. 10 items before the end of list)
#define kPageBegin 10

これを行う理由は、.m ファイルのページング パラメーターを簡単に変更するためです。

ページングの方法は次のとおりです。

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSUInteger row = [indexPath row];
    int section = indexPath.section-1;

    while (section>=0) {
        row+= [self.tableView numberOfRowsInSection:section];
        section--;
    }

    if (row+kPageBegin>=currentItems && !isLoadingNewItems && currentItems+1<maxItems) {
        //begin request
        [self LoadMoreItems];
    }
......
}
  • currentItemstableView データソースの現在のアイテムの数を持つ整数です。

  • isLoadingNewItems現時点でアイテムがフェッチされているかどうかを示すブール値であるため、サーバーから次のバッチをロードしている間、別のリクエストをインスタンス化することはありません。

  • maxItemsページングをいつ停止するかを示す整数で、サーバーから取得して最初のリクエストで設定する値です。

制限を設けたくない場合は、maxItems チェックを省略できます。

ページング読み込みコードでは、isLoadingNewItemsフラグを true に設定し、サーバーからデータを取得した後に false に戻します。

したがって、あなたの状況では、これは次のようになります。

- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSUInteger row = [indexPath row];
    int section = indexPath.section-1;

    while (section>=0) {
        row+= [self.tableView numberOfRowsInSection:section];
        section--;
    }

    if (row+kPageBegin>=counter && !isDowloading) {
        //begin request
        isDowloading = YES;
        [self fetchEntriesNew:counter];
    }
......
}

また、新しい行を追加した後にテーブル全体をリロードする必要はありません。これを使用するだけです:

for (int i = 0; i < itemDelta; i++) {
    NSIndexPath *ip = [NSIndexPath indexPathForRow:i inSection:0];
    [rows addObject:ip];
}

[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:rows withRowAnimation:UITableViewRowAnimationBottom];
[self.tableView endUpdates];
于 2013-04-24T23:17:54.887 に答える