iOS Bild herunterladen und in App speichern

78

Kann ich ein Bild von der Website herunterladen und dauerhaft in meiner App speichern? Ich habe wirklich keine Ahnung, aber es wäre eine nette Funktion für meine App.

Samuli Lehtonen
quelle

Antworten:

92

Obwohl es stimmt, dass die anderen Antworten hier funktionieren, sind sie wirklich keine Lösungen, die jemals im Produktionscode verwendet werden sollten . (zumindest nicht ohne Änderung)

Probleme

Das Problem bei diesen Antworten besteht darin, dass sie den Hauptthread beim Herunterladen und Speichern des Bildes blockieren, wenn sie unverändert implementiert werden und nicht von einem Hintergrundthread aufgerufen werden. Das ist schlecht .

Wenn der Hauptthread blockiert ist, werden UI-Updates erst durchgeführt, wenn das Herunterladen / Speichern des Bildes abgeschlossen ist. Angenommen, Sie fügen Ihrer App eine UIActivityIndicatorView hinzu, um dem Benutzer anzuzeigen, dass der Download noch läuft (ich werde dies in dieser Antwort als Beispiel verwenden), mit dem folgenden groben Kontrollfluss:

  1. Das für den Start des Downloads verantwortliche Objekt wird geladen.
  2. Weisen Sie die Aktivitätsanzeige an, mit der Animation zu beginnen.
  3. Starten Sie den synchronen Download-Prozess mit +[NSData dataWithContentsOfURL:]
  4. Speichern Sie die Daten (Bild), die gerade heruntergeladen wurden.
  5. Weisen Sie die Aktivitätsanzeige an, die Animation zu beenden.

Dies scheint ein vernünftiger Kontrollfluss zu sein, verschleiert jedoch ein kritisches Problem.

Wenn Sie die startAnimating-Methode des Aktivitätsindikators im Hauptthread (UI) aufrufen, werden die UI-Aktualisierungen für dieses Ereignis erst beim nächsten Aktualisieren der Hauptlaufschleife durchgeführt. Hier liegt das erste große Problem.

Bevor dieses Update durchgeführt werden kann, wird der Download ausgelöst. Da es sich um einen synchronen Vorgang handelt, wird der Hauptthread blockiert, bis der Download abgeschlossen ist (das Speichern hat das gleiche Problem). Dadurch wird tatsächlich verhindert, dass die Aktivitätsanzeige ihre Animation startet. Danach rufen Sie die stopAnimating-Methode des Aktivitätsindikators auf und erwarten, dass alle gut sind, dies ist jedoch nicht der Fall.

An diesem Punkt werden Sie sich wahrscheinlich Folgendes fragen.

Warum wird meine Aktivitätsanzeige nie angezeigt?

Nun, denken Sie so darüber nach. Sie weisen den Indikator an, zu starten, aber er erhält keine Chance, bevor der Download beginnt. Nach Abschluss des Downloads weisen Sie die Anzeige an, die Animation zu beenden. Da der Haupt-Thread während des gesamten Vorgangs blockiert war, entspricht das tatsächlich angezeigte Verhalten eher dem Hinweis, dass der Indikator gestartet und sofort gestoppt werden soll, obwohl dazwischen eine (möglicherweise) große Download-Aufgabe lag.

Im besten Fall führt dies nur zu einer schlechten Benutzererfahrung (immer noch sehr schlecht). Selbst wenn Sie der Meinung sind, dass dies keine große Sache ist, weil Sie nur ein kleines Bild herunterladen und der Download fast sofort erfolgt, ist dies nicht immer der Fall. Einige Ihrer Benutzer haben möglicherweise langsame Internetverbindungen, oder auf der Serverseite stimmt möglicherweise etwas nicht, sodass der Download nicht sofort / überhaupt nicht gestartet werden kann.

In beiden Fällen kann die App keine UI-Updates verarbeiten oder sogar Ereignisse berühren, während Ihre Download-Aufgabe mit den Daumen herumwirbelt und darauf wartet, dass der Download abgeschlossen wird oder der Server auf seine Anfrage reagiert.

Dies bedeutet, dass Sie durch synchrones Herunterladen vom Hauptthread möglicherweise nichts implementieren können, um dem Benutzer anzuzeigen, dass gerade ein Download ausgeführt wird. Und da Berührungsereignisse auch im Haupt-Thread verarbeitet werden, besteht die Möglichkeit, dass auch jede Art von Abbrechen-Schaltfläche hinzugefügt werden kann.

Im schlimmsten Fall erhalten Sie dann Absturzberichte mit den folgenden Angaben.

Ausnahmetyp: 00000020 Ausnahmecodes: 0x8badf00d

Diese sind leicht an dem Ausnahmecode zu erkennen 0x8badf00d, der als "schlechtes Essen gegessen" gelesen werden kann. Diese Ausnahme wird vom Watchdog-Timer ausgelöst, dessen Aufgabe es ist, auf lange laufende Aufgaben zu achten, die den Hauptthread blockieren, und die fehlerhafte App zu beenden, wenn dies zu lange dauert. Dies ist wahrscheinlich immer noch ein Problem mit der Benutzererfahrung, aber wenn dies auftritt, hat die App die Grenze zwischen schlechter Benutzererfahrung und schrecklicher Benutzererfahrung überschritten.

Weitere Informationen dazu, was dazu führen kann, finden Sie in den technischen Fragen und Antworten von Apple zu synchronen Netzwerken (der Kürze halber verkürzt).

Die häufigste Ursache für Watchdog-Timeout-Abstürze in einer Netzwerkanwendung ist das synchrone Netzwerk im Hauptthread. Hier tragen vier Faktoren bei:

  1. Synchrones Netzwerk - Hier stellen Sie eine Netzwerkanforderung und blockieren das Warten auf die Antwort.
  2. Hauptthread - Synchrones Netzwerk ist im Allgemeinen nicht ideal, verursacht jedoch bestimmte Probleme, wenn Sie es im Hauptthread ausführen. Denken Sie daran, dass der Hauptthread für die Ausführung der Benutzeroberfläche verantwortlich ist. Wenn Sie den Hauptthread für einen längeren Zeitraum blockieren, reagiert die Benutzeroberfläche inakzeptabel nicht mehr.
  3. lange Zeitüberschreitungen - Wenn das Netzwerk gerade verschwindet (z. B. der Benutzer in einem Zug sitzt, der in einen Tunnel fährt), schlägt eine ausstehende Netzwerkanforderung erst nach Ablauf einer bestimmten Zeitüberschreitung fehl.

...

  1. Watchdog - Um die Benutzeroberfläche ansprechbar zu halten, enthält iOS einen Watchdog-Mechanismus. Wenn Ihre Anwendung nicht rechtzeitig auf bestimmte Ereignisse der Benutzeroberfläche (Starten, Anhalten, Fortsetzen, Beenden) reagiert, beendet der Watchdog Ihre Anwendung und generiert einen Absturzbericht zum Timeout des Watchdogs. Die Zeit, die der Watchdog Ihnen zur Verfügung stellt, ist nicht offiziell dokumentiert, aber immer kürzer als ein Netzwerk-Timeout.

Ein schwieriger Aspekt dieses Problems ist, dass es stark von der Netzwerkumgebung abhängt. Wenn Sie Ihre Anwendung immer in Ihrem Büro testen, wo die Netzwerkverbindung gut ist, werden Sie diese Art von Absturz nie sehen. Sobald Sie jedoch mit der Bereitstellung Ihrer Anwendung für Endbenutzer beginnen, die sie in allen möglichen Netzwerkumgebungen ausführen, kommt es häufig zu solchen Abstürzen.

An diesem Punkt werde ich aufhören, darüber nachzudenken, warum die bereitgestellten Antworten problematisch sein könnten, und einige alternative Lösungen anbieten. Beachten Sie, dass ich in diesen Beispielen die URL eines kleinen Bildes verwendet habe und Sie einen größeren Unterschied feststellen werden, wenn Sie ein Bild mit höherer Auflösung verwenden.


Lösungen

Ich werde zunächst eine sichere Version der anderen Antworten anzeigen und zusätzlich erläutern, wie mit UI-Updates umgegangen wird. Dies ist das erste von mehreren Beispielen, bei denen davon ausgegangen wird, dass die Klasse, in der sie implementiert sind, gültige Eigenschaften für eine UIImageView, eine UIActivityIndicatorView sowie die documentsDirectoryURLMethode für den Zugriff auf das Dokumentenverzeichnis aufweist. Im Produktionscode möchten Sie möglicherweise Ihre eigene Methode implementieren, um auf das Dokumentenverzeichnis als Kategorie in NSURL zuzugreifen, um die Wiederverwendbarkeit des Codes zu verbessern. In diesen Beispielen ist dies jedoch in Ordnung.

- (NSURL *)documentsDirectoryURL
{
    NSError *error = nil;
    NSURL *url = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory
                                                        inDomain:NSUserDomainMask
                                               appropriateForURL:nil
                                                          create:NO
                                                           error:&error];
    if (error) {
        // Figure out what went wrong and handle the error.
    }
    
    return url;
}

In diesen Beispielen wird auch davon ausgegangen, dass der Thread, mit dem sie beginnen, der Hauptthread ist. Dies ist wahrscheinlich das Standardverhalten, es sei denn, Sie starten Ihre Download-Aufgabe von einem Ort wie dem Rückrufblock einer anderen asynchronen Aufgabe aus. Wenn Sie den Download an einem typischen Ort starten, z. B. bei einer Lebenszyklusmethode eines View Controllers (z. B. viewDidLoad, viewWillAppear: usw.), wird das erwartete Verhalten erzeugt.

In diesem ersten Beispiel wird die +[NSData dataWithContentsOfURL:]Methode verwendet, jedoch mit einigen wesentlichen Unterschieden. Zum einen werden Sie feststellen, dass in diesem Beispiel der erste Aufruf, den wir tätigen, darin besteht, die Aktivitätsanzeige anzuweisen, mit der Animation zu beginnen. Dann gibt es einen unmittelbaren Unterschied zwischen diesem und den synchronen Beispielen. Sofort verwenden wir dispatch_async () und übergeben die globale gleichzeitige Warteschlange, um die Ausführung in den Hintergrundthread zu verschieben.

Zu diesem Zeitpunkt haben Sie Ihre Download-Aufgabe bereits erheblich verbessert. Da jetzt alles im Block dispatch_async () außerhalb des Hauptthreads stattfindet, wird Ihre Benutzeroberfläche nicht mehr blockiert und Ihre App kann auf Berührungsereignisse reagieren.

Was hier zu beachten ist, ist, dass der gesamte Code in diesem Block im Hintergrund-Thread ausgeführt wird, bis zu dem Punkt, an dem das Herunterladen / Speichern des Bildes erfolgreich war. An diesem Punkt möchten Sie möglicherweise den Aktivitätsindikator anweisen, die Animation zu stoppen oder wenden Sie das neu gespeicherte Bild auf eine UIImageView an. In beiden Fällen handelt es sich um Aktualisierungen der Benutzeroberfläche. Dies bedeutet, dass Sie den Hauptthread mithilfe von dispatch_get_main_queue () zurücksenden müssen, um sie auszuführen. Andernfalls kommt es zu undefiniertem Verhalten, das dazu führen kann, dass die Benutzeroberfläche nach einem unerwarteten Zeitraum aktualisiert wird oder sogar abstürzt. Stellen Sie immer sicher, dass Sie zum Haupt-Thread zurückkehren, bevor Sie UI-Updates durchführen.

// Start the activity indicator before moving off the main thread
[self.activityIndicator startAnimating];
// Move off the main thread to start our blocking tasks.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // Create the image URL from a known string.
    NSURL *imageURL = [NSURL URLWithString:@"http://www.google.com/images/srpr/logo3w.png"];
    
    NSError *downloadError = nil;
    // Create an NSData object from the contents of the given URL.
    NSData *imageData = [NSData dataWithContentsOfURL:imageURL
                                              options:kNilOptions
                                                error:&downloadError];
    // ALWAYS utilize the error parameter!
    if (downloadError) {
        // Something went wrong downloading the image. Figure out what went wrong and handle the error.
        // Don't forget to return to the main thread if you plan on doing UI updates here as well.
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.activityIndicator stopAnimating];
            NSLog(@"%@",[downloadError localizedDescription]);
        });
    } else {
        // Get the path of the application's documents directory.
        NSURL *documentsDirectoryURL = [self documentsDirectoryURL];
        // Append the desired file name to the documents directory path.
        NSURL *saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:@"GCD.png"];

        NSError *saveError = nil;
        BOOL writeWasSuccessful = [imageData writeToURL:saveLocation
                                                options:kNilOptions
                                                  error:&saveError];
        // Successful or not we need to stop the activity indicator, so switch back the the main thread.
        dispatch_async(dispatch_get_main_queue(), ^{
            // Now that we're back on the main thread, you can make changes to the UI.
            // This is where you might display the saved image in some image view, or
            // stop the activity indicator.
            
            // Check if saving the file was successful, once again, utilizing the error parameter.
            if (writeWasSuccessful) {
                // Get the saved image data from the file.
                NSData *imageData = [NSData dataWithContentsOfURL:saveLocation];
                // Set the imageView's image to the image we just saved.
                self.imageView.image = [UIImage imageWithData:imageData];
            } else {
                NSLog(@"%@",[saveError localizedDescription]);
                // Something went wrong saving the file. Figure out what went wrong and handle the error.
            }
            
            [self.activityIndicator stopAnimating];
        });
    }
});

Denken Sie jetzt daran, dass die oben gezeigte Methode immer noch keine ideale Lösung ist, da sie nicht vorzeitig abgebrochen werden kann. Sie gibt keinen Hinweis auf den Fortschritt des Downloads und kann keine Authentifizierungsaufforderung bewältigen Es wird kein bestimmtes Zeitlimit usw. angegeben (viele, viele Gründe). Ich werde im Folgenden einige der besseren Optionen behandeln.

In diesen Beispielen werde ich nur Lösungen für Apps behandeln, die auf iOS 7 und höher abzielen, da (zum Zeitpunkt des Schreibens) iOS 8 die aktuelle Hauptversion ist und Apple nur die Versionen N und N-1 unterstützt . Wenn Sie ältere iOS-Versionen unterstützen müssen, empfehlen wir Ihnen , sich die NSURLConnection- Klasse sowie die 1.0-Version von AFNetworking anzusehen. Wenn Sie sich den Revisionsverlauf dieser Antwort ansehen , finden Sie grundlegende Beispiele für die Verwendung von NSURLConnection und ASIHTTPRequest. Beachten Sie jedoch, dass ASIHTTPRequest nicht mehr verwaltet wird und nicht für neue Projekte verwendet werden sollte.


NSURLSession

Beginnen wir mit NSURLSession , das in iOS 7 eingeführt wurde und die Benutzerfreundlichkeit von Netzwerken in iOS erheblich verbessert. Mit NSURLSession können Sie problemlos asynchrone HTTP-Anforderungen mit einem Rückrufblock ausführen und Authentifizierungsprobleme mit dem Delegaten lösen. Das Besondere an dieser Klasse ist jedoch, dass Download-Aufgaben auch dann weiter ausgeführt werden können, wenn die Anwendung in den Hintergrund gesendet wird, beendet wird oder sogar abstürzt. Hier ist ein grundlegendes Beispiel für seine Verwendung.

// Start the activity indicator before starting the download task.
[self.activityIndicator startAnimating];

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// Use a session with a custom configuration
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
// Create the image URL from some known string.
NSURL *imageURL = [NSURL URLWithString:@"http://www.google.com/images/srpr/logo3w.png"];
// Create the download task passing in the URL of the image.
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:imageURL completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
    // Get information about the response if neccessary.
    if (error) {
        NSLog(@"%@",[error localizedDescription]);
        // Something went wrong downloading the image. Figure out what went wrong and handle the error.
        // Don't forget to return to the main thread if you plan on doing UI updates here as well.
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.activityIndicator stopAnimating];
        });
    } else {
        NSError *openDataError = nil;
        NSData *downloadedData = [NSData dataWithContentsOfURL:location
                                                       options:kNilOptions
                                                         error:&openDataError];
        if (openDataError) {
            // Something went wrong opening the downloaded data. Figure out what went wrong and handle the error.
            // Don't forget to return to the main thread if you plan on doing UI updates here as well.
            dispatch_async(dispatch_get_main_queue(), ^{
                NSLog(@"%@",[openDataError localizedDescription]);
                [self.activityIndicator stopAnimating];
            });
        } else {
            // Get the path of the application's documents directory.
            NSURL *documentsDirectoryURL = [self documentsDirectoryURL];
            // Append the desired file name to the documents directory path.
            NSURL *saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:@"NSURLSession.png"];
            NSError *saveError = nil;
            
            BOOL writeWasSuccessful = [downloadedData writeToURL:saveLocation
                                                          options:kNilOptions
                                                            error:&saveError];
            // Successful or not we need to stop the activity indicator, so switch back the the main thread.
            dispatch_async(dispatch_get_main_queue(), ^{
                // Now that we're back on the main thread, you can make changes to the UI.
                // This is where you might display the saved image in some image view, or
                // stop the activity indicator.
                
                // Check if saving the file was successful, once again, utilizing the error parameter.
                if (writeWasSuccessful) {
                    // Get the saved image data from the file.
                    NSData *imageData = [NSData dataWithContentsOfURL:saveLocation];
                    // Set the imageView's image to the image we just saved.
                    self.imageView.image = [UIImage imageWithData:imageData];
                } else {
                    NSLog(@"%@",[saveError localizedDescription]);
                    // Something went wrong saving the file. Figure out what went wrong and handle the error.
                }
                
                [self.activityIndicator stopAnimating];
            });
        }
    }
}];

// Tell the download task to resume (start).
[task resume];

Daraus erkennen Sie, dass die downloadTaskWithURL: completionHandler:Methode eine Instanz von NSURLSessionDownloadTask zurückgibt, für die eine Instanzmethode -[NSURLSessionTask resume]aufgerufen wird. Dies ist die Methode, mit der die Download-Aufgabe tatsächlich gestartet wird. Dies bedeutet, dass Sie Ihre Download-Aufgabe starten und bei Bedarf beim Starten warten können (falls erforderlich). Dies bedeutet auch, dass Sie, solange Sie einen Verweis auf die Aufgabe speichern, auch deren cancelund suspendMethoden verwenden können, um die Aufgabe bei Bedarf abzubrechen oder anzuhalten.

Was an NSURLSessionTasks wirklich cool ist, ist, dass Sie mit ein wenig KVO die Werte der Eigenschaften countOfBytesExpectedToReceive und countOfBytesReceived überwachen, diese Werte einem NSByteCountFormatter zuführen und Ihrem Benutzer auf einfache Weise eine Anzeige für den Download-Fortschritt mit lesbaren Einheiten erstellen können (z. B. 42) KB von 100 KB).

Bevor ich mich jedoch von NSURLSession entferne, möchte ich darauf hinweisen, dass die Hässlichkeit, an mehreren verschiedenen Stellen im Rückrufblock des Downloads_async zurück zu den Hauptthreads senden zu müssen, vermieden werden kann. Wenn Sie sich für diese Route entschieden haben, können Sie die Sitzung mit ihrem Initialisierer initialisieren, mit dem Sie den Delegaten sowie die Delegatenwarteschlange angeben können. Dies erfordert, dass Sie das Delegatenmuster anstelle der Rückrufblöcke verwenden. Dies kann jedoch von Vorteil sein, da nur so Hintergrund-Downloads unterstützt werden können.

NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
                                                      delegate:self
                                                 delegateQueue:[NSOperationQueue mainQueue]];

AFNetworking 2.0

Wenn Sie noch nie von AFNetworking gehört haben , ist es meiner Meinung nach das Ende aller Netzwerkbibliotheken. Es wurde für Objective-C erstellt, funktioniert aber auch in Swift. In den Worten seines Autors:

AFNetworking ist eine reizvolle Netzwerkbibliothek für iOS und Mac OS X. Sie basiert auf dem Foundation URL Loading System und erweitert die leistungsstarken Netzwerkabstraktionen auf hoher Ebene, die in Cocoa integriert sind. Es verfügt über eine modulare Architektur mit gut gestalteten, funktionsreichen APIs, deren Verwendung eine Freude macht.

AFNetworking 2.0 unterstützt iOS 6 und höher. In diesem Beispiel verwende ich jedoch die AFHTTPSessionManager-Klasse, für die iOS 7 und höher erforderlich ist, da alle neuen APIs rund um die NSURLSession-Klasse verwendet werden. Dies wird deutlich, wenn Sie das folgende Beispiel lesen, das viel Code mit dem obigen NSURLSession-Beispiel teilt.

Es gibt jedoch einige Unterschiede, auf die ich hinweisen möchte. Anstatt Ihre eigene NSURLSession zu erstellen, erstellen Sie zunächst eine Instanz von AFURLSessionManager, die eine NSURLSession intern verwaltet. Auf diese Weise können Sie einige der praktischen Methoden wie nutzen -[AFURLSessionManager downloadTaskWithRequest:progress:destination:completionHandler:]. Das Interessante an dieser Methode ist, dass Sie damit eine Download-Aufgabe mit einem bestimmten Zieldateipfad , einem Abschlussblock und einer Eingabe für einen NSProgress- Zeiger ziemlich präzise erstellen können , auf der Sie Informationen über den Fortschritt des Downloads beobachten können. Hier ist ein Beispiel.

// Use the default session configuration for the manager (background downloads must use the delegate APIs)
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
// Use AFNetworking's NSURLSessionManager to manage a NSURLSession.
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];

// Create the image URL from some known string.
NSURL *imageURL = [NSURL URLWithString:@"http://www.google.com/images/srpr/logo3w.png"];
// Create a request object for the given URL.
NSURLRequest *request = [NSURLRequest requestWithURL:imageURL];
// Create a pointer for a NSProgress object to be used to determining download progress.
NSProgress *progress = nil;

// Create the callback block responsible for determining the location to save the downloaded file to.
NSURL *(^destinationBlock)(NSURL *targetPath, NSURLResponse *response) = ^NSURL *(NSURL *targetPath, NSURLResponse *response) {
    // Get the path of the application's documents directory.
    NSURL *documentsDirectoryURL = [self documentsDirectoryURL];
    NSURL *saveLocation = nil;
    
    // Check if the response contains a suggested file name
    if (response.suggestedFilename) {
        // Append the suggested file name to the documents directory path.
        saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:response.suggestedFilename];
    } else {
        // Append the desired file name to the documents directory path.
        saveLocation = [documentsDirectoryURL URLByAppendingPathComponent:@"AFNetworking.png"];
    }

    return saveLocation;
};

// Create the completion block that will be called when the image is done downloading/saving.
void (^completionBlock)(NSURLResponse *response, NSURL *filePath, NSError *error) = ^void (NSURLResponse *response, NSURL *filePath, NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        // There is no longer any reason to observe progress, the download has finished or cancelled.
        [progress removeObserver:self
                      forKeyPath:NSStringFromSelector(@selector(fractionCompleted))];
        
        if (error) {
            NSLog(@"%@",error.localizedDescription);
            // Something went wrong downloading or saving the file. Figure out what went wrong and handle the error.
        } else {
            // Get the data for the image we just saved.
            NSData *imageData = [NSData dataWithContentsOfURL:filePath];
            // Get a UIImage object from the image data.
            self.imageView.image = [UIImage imageWithData:imageData];
        }
    });
};

// Create the download task for the image.
NSURLSessionDownloadTask *task = [manager downloadTaskWithRequest:request
                                                         progress:&progress
                                                      destination:destinationBlock
                                                completionHandler:completionBlock];
// Start the download task.
[task resume];

// Begin observing changes to the download task's progress to display to the user.
[progress addObserver:self
           forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
              options:NSKeyValueObservingOptionNew
              context:NULL];

Da wir die Klasse, die diesen Code enthält, als Beobachter zu einer der Eigenschaften der NSProgress-Instanz hinzugefügt haben, müssen Sie die -[NSObject observeValueForKeyPath:ofObject:change:context:]Methode natürlich implementieren . In diesem Fall habe ich ein Beispiel beigefügt, wie Sie eine Fortschrittsbezeichnung aktualisieren können, um den Fortschritt des Downloads anzuzeigen. Es ist wirklich einfach. NSProgress verfügt über eine Instanzmethode, mit localizedDescriptionder Fortschrittsinformationen in einem lokalisierten, für Menschen lesbaren Format angezeigt werden.

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    // We only care about updates to fractionCompleted
    if ([keyPath isEqualToString:NSStringFromSelector(@selector(fractionCompleted))]) {
        NSProgress *progress = (NSProgress *)object;
        // localizedDescription gives a string appropriate for display to the user, i.e. "42% completed"
        self.progressLabel.text = progress.localizedDescription;
    } else {
        [super observeValueForKeyPath:keyPath
                             ofObject:object
                               change:change
                              context:context];
    }
}

Vergessen Sie nicht, wenn Sie AFNetworking in Ihrem Projekt verwenden möchten, müssen Sie die Installationsanweisungen befolgen und dies unbedingt tun #import <AFNetworking/AFNetworking.h>.

Alamofire

Und zum Schluss möchte ich noch ein letztes Beispiel mit Alamofire geben . Dies ist die Bibliothek, die das Networking in Swift zu einem Kinderspiel macht. Ich habe keine Charaktere mehr, um detailliert auf den Inhalt dieses Beispiels einzugehen, aber es macht so ziemlich das Gleiche wie die letzten Beispiele, nur auf eine wohl schönere Art und Weise.

// Create the destination closure to pass to the download request. I haven't done anything with them
// here but you can utilize the parameters to make adjustments to the file name if neccessary.
let destination = { (url: NSURL!, response: NSHTTPURLResponse!) -> NSURL in
    var error: NSError?
    // Get the documents directory
    let documentsDirectory = NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory,
        inDomain: .UserDomainMask,
        appropriateForURL: nil,
        create: false,
        error: &error
    )
    
    if let error = error {
        // This could be bad. Make sure you have a backup plan for where to save the image.
        println("\(error.localizedDescription)")
    }
    
    // Return a destination of .../Documents/Alamofire.png
    return documentsDirectory!.URLByAppendingPathComponent("Alamofire.png")
}

Alamofire.download(.GET, "http://www.google.com/images/srpr/logo3w.png", destination)
    .validate(statusCode: 200..<299) // Require the HTTP status code to be in the Successful range.
    .validate(contentType: ["image/png"]) // Require the content type to be image/png.
    .progress { (bytesRead, totalBytesRead, totalBytesExpectedToRead) in
        // Create an NSProgress object to represent the progress of the download for the user.
        let progress = NSProgress(totalUnitCount: totalBytesExpectedToRead)
        progress.completedUnitCount = totalBytesRead
        
        dispatch_async(dispatch_get_main_queue()) {
            // Move back to the main thread and update some progress label to show the user the download is in progress.
            self.progressLabel.text = progress.localizedDescription
        }
    }
    .response { (request, response, _, error) in
        if error != nil {
            // Something went wrong. Handle the error.
        } else {
            // Open the newly saved image data.
            if let imageData = NSData(contentsOfURL: destination(nil, nil)) {
                dispatch_async(dispatch_get_main_queue()) {
                    // Move back to the main thread and add the image to your image view.
                    self.imageView.image = UIImage(data: imageData)
                }
            }
        }
    }
Mick MacCallum
quelle
Wie empfehlen Sie, dass wir die documentsDirectoryUrl für die AFNetworking-Lösung erhalten?
SleepsOnNewspapers
2
@HomelessPeopleCanCode Ganz oben in meiner Antwort unter der Überschrift "Lösungen" habe ich diese Methode eingefügt und in allen meinen Objective-C-Beispielen verwendet. Es stehen jedoch weitere Optionen zur Verfügung . Mit den meisten davon erhalten Sie den Pfad zum Dokumentenverzeichnis in Form eines NSStrings. Sie müssen ihn daher in eine NSURL konvertieren, um sie mit meinen Beispielen verwenden zu können, ohne sie ändern zu müssen dies : NSURL *filePathURL = [NSURL fileURLWithPath:filePathString];.
Mick MacCallum
gut erklären. Wie speichere ich in Fotos? über Alamofire. Was ist als Parameter in 'Ziel' zu übergeben?
Khunshan
Beeindruckend! Geniale Antwort, sehr informativ. Vielen Dank!
Koushik Ravikumar
Eine der besten Antworten, die ich auf dieser Seite gelesen habe. Sehr nützlich und informativ. Vielen Dank, dass Sie sich die Zeit genommen haben, uns Plebls beizubringen;)
Alexandre G.
39

Sie können nichts im App-Bundle +[NSData dataWithContentsOfURL:]speichern , aber Sie können das Bild im Dokumentenverzeichnis Ihrer App speichern, z. B.:

NSData *imageData = [NSData dataWithContentsOfURL:myImageURL];
NSString *imagePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"/myImage.png"];
[imageData writeToFile:imagePath atomically:YES];

Nicht gerade permanent , aber es bleibt mindestens so lange dort, bis der Benutzer die App löscht.


quelle
3
Diese Antwort ist besser als die akzeptierte, denn wenn Sie sie mit der UIImage UIImageJPEGRepresentation oder UIImagePNGRepresentation als PNG oder JPEG speichern, ist die Datengröße auf der iPhone-Festplatte doppelt so groß wie die des Originals. Mit diesem Code speichern Sie nur die Originaldaten.
Jcesarmobile
13

Das ist das Hauptkonzept. Habe Spaß ;)

NSURL *url = [NSURL URLWithString:@"http://example.com/yourImage.png"];
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
path = [path stringByAppendingString:@"/yourLocalImage.png"];
[data writeToFile:path atomically:YES];
cem
quelle
7

Da wir jetzt auf IO5 sind, müssen Sie keine Bilder mehr auf die Festplatte schreiben.
Sie können jetzt "externen Speicher zulassen" für ein Coredata-Binärattribut festlegen. Laut den Versionshinweisen für Äpfel bedeutet dies Folgendes:

Kleine Datenwerte wie Bildminiaturen können effizient in einer Datenbank gespeichert werden, große Fotos oder andere Medien werden jedoch am besten direkt vom Dateisystem verarbeitet. Sie können jetzt angeben, dass der Wert eines verwalteten Objektattributs als externer Datensatz gespeichert werden darf - siehe setAllowsExternalBinaryDataStorage: Wenn diese Option aktiviert ist, entscheidet Core Data heuristisch pro Wert, ob die Daten direkt in der Datenbank gespeichert oder ein URI gespeichert werden sollen in eine separate Datei, die es für Sie verwaltet. Sie können nicht basierend auf dem Inhalt einer Binärdateneigenschaft abfragen, wenn Sie diese Option verwenden.

Alexander
quelle
3

Wie andere Leute sagten, gibt es viele Fälle, in denen Sie ein Bild im Hintergrund-Thread herunterladen sollten, ohne die Benutzeroberfläche zu blockieren

In diesen Fällen besteht meine Lieblingslösung darin, eine bequeme Methode mit Blöcken wie diesen zu verwenden: (Kredit -> iOS: So laden Sie Bilder asynchron herunter (und machen Sie Ihren UITableView-Bildlauf schnell) )

- (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
{
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
                               if ( !error )
                               {
                                   UIImage *image = [[UIImage alloc] initWithData:data];
                                   completionBlock(YES,image);
                               } else{
                                   completionBlock(NO,nil);
                               }
                           }];
}

Und nenne es wie

NSURL *imageUrl = //...

[[MyUtilManager sharedInstance] downloadImageWithURL:[NSURL URLWithString:imageURL] completionBlock:^(BOOL succeeded, UIImage *image) {
    //Here you can save the image permanently, update UI and do what you want...
}];
andreacipriani
quelle
1

So lade ich ein Werbebanner herunter. Am besten im Hintergrund, wenn Sie ein großes Bild oder eine Reihe von Bildern herunterladen.

- (void)viewDidLoad {
    [super viewDidLoad];

    [self performSelectorInBackground:@selector(loadImageIntoMemory) withObject:nil];

}
- (void)loadImageIntoMemory {
    NSString *temp_Image_String = [[NSString alloc] initWithFormat:@"http://yourwebsite.com/MyImageName.jpg"];
    NSURL *url_For_Ad_Image = [[NSURL alloc] initWithString:temp_Image_String];
    NSData *data_For_Ad_Image = [[NSData alloc] initWithContentsOfURL:url_For_Ad_Image];
    UIImage *temp_Ad_Image = [[UIImage alloc] initWithData:data_For_Ad_Image];
    [self saveImage:temp_Ad_Image];
    UIImageView *imageViewForAdImages = [[UIImageView alloc] init];
    imageViewForAdImages.frame = CGRectMake(0, 0, 320, 50);
    imageViewForAdImages.image = [self loadImage];
    [self.view addSubview:imageViewForAdImages];
}
- (void)saveImage: (UIImage*)image {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString* path = [documentsDirectory stringByAppendingPathComponent: @"MyImageName.jpg" ];
    NSData* data = UIImagePNGRepresentation(image);
    [data writeToFile:path atomically:YES];
}
- (UIImage*)loadImage {
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString* path = [documentsDirectory stringByAppendingPathComponent:@"MyImageName.jpg" ];
    UIImage* image = [UIImage imageWithContentsOfFile:path];
    return image;
}
Bobby
quelle
1

Hier ist Code, um ein Bild asynchron von der URL herunterzuladen und dann in Objective-C an der gewünschten Stelle zu speichern: ->

    + (void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
        {
            NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
            [NSURLConnection sendAsynchronousRequest:request
                                               queue:[NSOperationQueue mainQueue]
                                   completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
                                       if ( !error )
                                       {
                                           UIImage *image = [[UIImage alloc] initWithData:data];
                                           completionBlock(YES,image);
                                       } else{
                                           completionBlock(NO,nil);
                                       }
                                   }];
        }
Mohd. Als ob
quelle
0

Wenn Sie die AFNetworking-Bibliothek zum Herunterladen von Bildern verwenden und diese Bilder in UITableview verwenden, können Sie den folgenden Code in cellForRowAtIndexPath verwenden

 [self setImageWithURL:user.user_ProfilePicturePath toControl:cell.imgView]; 
 
-(void)setImageWithURL:(NSURL*)url toControl:(id)ctrl
{
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request imageProcessingBlock:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
        if (image) {
            if([ctrl isKindOfClass:[UIButton class]])
            {
                UIButton btn =(UIButton)ctrl;
                [btn setBackgroundImage:image forState:UIControlStateNormal];
            }
            else
            {
                UIImageView imgView = (UIImageView)ctrl;
                imgView.image = image;
            }

} } failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) { NSLog(@"No Image"); }]; [operation start];}
ASHISHT
quelle
0

Mit NSURLSessionDataTask können Sie Bilder herunterladen, ohne die Benutzeroberfläche zu blockieren.

+(void)downloadImageWithURL:(NSURL *)url completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
 {
 NSURLSessionDataTask*  _sessionTask = [[NSURLSession sharedSession] dataTaskWithRequest:[NSURLRequest requestWithURL:url]
                                            completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if (error != nil)
        {
          if ([error code] == NSURLErrorAppTransportSecurityRequiresSecureConnection)
                {
                    completionBlock(NO,nil);
                }
         }
    else
     {
      [[NSOperationQueue mainQueue] addOperationWithBlock: ^{
                        dispatch_async(dispatch_get_main_queue(), ^{
                        UIImage *image = [[UIImage alloc] initWithData:data];
                        completionBlock(YES,image);

                        });

      }];

     }

                                            }];
    [_sessionTask resume];
}
Mohd. Als ob
quelle
0

Hier ist eine Swift 5- Lösung zum Herunterladen und Speichern eines Bildes oder allgemein einer Datei im Dokumentenverzeichnis unter Verwendung von Alamofire:

func dowloadAndSaveFile(from url: URL) {
    let destination: DownloadRequest.DownloadFileDestination = { _, _ in
        var documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
        documentsURL.appendPathComponent(url.lastPathComponent)
        return (documentsURL, [.removePreviousFile])
    }
    let request = SessionManager.default.download(url, method: .get, to: destination)
    request.validate().responseData { response in
        switch response.result {
        case .success:
            if let destinationURL = response.destinationURL {
                print(destinationURL)
            }
        case .failure(let error):
            print(error.localizedDescription)
        }
    }
}
Tanaschita
quelle