Sam*_*nen 77 iphone cocoa-touch ipad ios
我可以从网站下载图像并将其永久保存在我的应用程序中吗?我真的不知道,但它会为我的应用程序提供一个很好的功能.
Mic*_*lum 92
Although it is true that the other answers here will work, they really aren't solutions that should ever be used in production code. (at least not without modification)
The problem with these answers is that if they are implemented as is and are not called from a background thread, they will block the main thread while downloading and saving the image. This is bad.
If the main thread is blocked, UI updates won't happen until the downloading/saving of the image is complete. As an example of what this means, say you add a UIActivityIndicatorView to your app to show the user that the download is still in progress (I will be using this as an example throughout this answer) with the following rough control flow:
+[NSData dataWithContentsOfURL:]现在,这似乎是合理的控制流程,但它掩盖了一个关键问题.
当您在主(UI)线程上调用活动指示器的startAnimating方法时,直到下次主运行循环更新时才会实际发生此事件的UI 更新,这是第一个主要问题.
在此更新有可能发生之前,将触发下载,并且由于这是同步操作,因此它会阻止主线程直到完成下载(保存具有相同的问题).这实际上会阻止活动指示器开始动画.之后,您调用活动指示器的stopAnimating方法并期望一切都很好,但事实并非如此.
在这一点上,你可能会发现自己想知道以下内容.
为什么我的活动指标没有出现?
好吧,想想这样吧.您告诉指示器启动但在下载开始之前没有机会.下载完成后,您告诉指示器停止动画.由于主线程在整个操作中被阻塞,因此您实际看到的行为更多的是告诉指示器启动然后立即告诉它停止,即使它们之间存在(可能)大型下载任务.
现在,在最好的情况下,所有这些都会导致糟糕的用户体验(仍然非常糟糕).即使你认为这不是什么大问题,因为你只是下载一个小图像并且下载几乎是瞬间完成的,但情况并非总是如此.您的某些用户可能网络连接速度较慢,或者服务器端可能存在错误,导致下载无法立即启动/根本无法启动.
在这两种情况下,应用程序将无法处理UI更新,甚至触摸事件,同时您的下载任务围绕着拇指等待下载完成或服务器响应其请求.
这意味着从主线程同步下载会阻止您执行任何操作以向用户指示当前正在进行下载.并且由于触摸事件也在主线程上处理,因此这也增加了添加任何类型的取消按钮的可能性.
然后在最坏的情况下,您将开始收到崩溃报告,说明以下内容.
异常类型:00000020异常代码:0x8badf00d
这些很容易通过异常代码识别0x8badf00d,可以理解为"吃不好的食物".看门狗定时器抛出此异常,其作用是监视阻塞主线程的长时间运行任务,并且如果持续时间过长则终止有问题的应用程序.可以说,这仍然是一个糟糕的用户体验问题,但如果这种情况开始发生,应用程序已经跨越了糟糕的用户体验和糟糕的用户体验之间的界限.
以下是有关可能导致这种情况发生的更多信息,这些信息来自Apple关于同步网络的技术问答(为简洁而缩短).
在网络应用程序中看门狗超时崩溃的最常见原因是主线程上的同步网络.这里有四个因素:
- 同步网络 - 这是您发出网络请求并阻止等待响应的地方.
- 主线程 - 同步网络通常不太理想,但如果在主线程上执行,则会导致特定问题.请记住,主线程负责运行用户界面.如果您在任何大量时间内阻止主线程,则用户界面变得无法接受地无响应.
- 超时 - 如果网络刚刚消失(例如,用户在火车上进入隧道),任何待处理的网络请求都不会失败,直到某个超时到期为止....
...
- 看门狗 - 为了保持用户界面的响应,iOS包括一个看门狗机制.如果您的应用程序未能及时响应某些用户界面事件(启动,暂停,恢复,终止),则监视程序将终止您的应用程序并生成监视程序超时崩溃报告.看门狗给你的时间没有正式记录,但总是小于网络超时.
这个问题的一个棘手问题是它高度依赖于网络环境.如果您总是在办公室测试您的应用程序,那么网络连接良好,您将永远不会看到这种类型的崩溃.但是,一旦您开始将应用程序部署到最终用户 - 他们将在各种网络环境中运行它 - 这样的崩溃将变得很常见.
现在,在这一点上,我将停止讨论为什么提供的答案可能有问题,并将开始提供一些替代解决方案.请记住,我在这些示例中使用了小图像的URL,并且在使用更高分辨率的图像时您会注意到更大的差异.
I'll start by showing a safe version of the other answers, with the addition of how to handle UI updates. This will be the first of several examples, all of which will assume that the class in which they are implemented has valid properties for a UIImageView, a UIActivityIndicatorView, as well as the documentsDirectoryURL method to access the documents directory. In production code, you may want to implement your own method to access the documents directory as a category on NSURL for better code reusability, but for these examples, this will be fine.
- (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;
}
Run Code Online (Sandbox Code Playgroud)
These examples will also assume that the thread that they start off on is the main thread. This will likely be the default behavior unless you start your download task from somewhere like the callback block of some other asynchronous task. If you start your download in a typical place, like a lifecycle method of a view controller (i.e. viewDidLoad, viewWillAppear:, etc.) this will produce the expected behavior.
This first example will use the +[NSData dataWithContentsOfURL:] method, but with some key differences. For one, you'll notice that in this example, the very first call we make is to tell the activity indicator to start animating, then there is an immediate difference between this and the synchronous examples. Immediately, we use dispatch_async(), passing in the global concurrent queue to move execution to the background thread.
At this point, you've already greatly improved your download task. Since everything within the dispatch_async() block will now happen off the main thread, your interface will no longer lock up, and your app will be free to respond to touch events.
What is important to notice here is that all of the code within this block will execute on the background thread, up until the point where the downloading/saving of the image was successful, at which point you might want to tell the activity indicator to stopAnimating, or apply the newly saved image to a UIImageView. Either way, these are updates to the UI, meaning you must dispatch back the the main thread using dispatch_get_main_queue() to perform them. Failing to do so results in undefined behavior, which may cause the UI to update after an unexpected period of time, or may even cause a crash. Always make sure you move back to the main thread before performing UI updates.
// 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];
});
}
});
Run Code Online (Sandbox Code Playgroud)
Now keep in mind, that the method shown above is still not an ideal solution considering it can't be cancelled prematurely, it gives you no indication of the progress of the download, it can't handle any kind of authentication challenge, it can't be given a specific timeout interval, etc. (lots and lots of reasons). I'll cover a few of the better options below.
In these examples, I'll only be covering solutions for apps targeting iOS 7 and up considering (at time of writing) iOS 8 is the current major release, and Apple is suggesting only supporting versions N and N-1. If you need to support older iOS versions, I recommend looking into the NSURLConnection class, as well as the 1.0 version of AFNetworking. If you look at the revision history of this answer, you can find basic examples using NSURLConnection and ASIHTTPRequest, although it should be noted that ASIHTTPRequest is no longer being maintained, and should not be used for new projects.
Lets start with NSURLSession, which was introduced in iOS 7, and greatly improves the ease with which networking can be done in iOS. With NSURLSession, you can easily perform asynchronous HTTP requests with a callback block and handle authentication challenges with its delegate. But what makes this class really special is that it also allows for download tasks to continue running even if the application is sent to the background, gets terminated, or even crashes. Here's a basic example of its usage.
// 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];
Run Code Online (Sandbox Code Playgroud)
From this you'll notice that the downloadTaskWithURL: completionHandler: method returns an instance of NSURLSessionDownloadTask, on which an instance method -[NSURLSessionTask resume] is called. This is the method that actually tells the download task to start. This means that you can spin up your download task, and if desired, hold off on starting it (if needed). This also means that as long as you store a reference to the task, you can also utilize its cancel and suspend methods to cancel or pause the task if need be.
What's really cool about NSURLSessionTasks is that with a little bit of KVO, you can monitor the values of its countOfBytesExpectedToReceive and countOfBytesReceived properties, feed these values to an NSByteCountFormatter, and easily create a download progress indicator to your user with human readable units (e.g. 42 KB of 100 KB).
Before I move away from NSURLSession though, I'd like to point out that the ugliness of having to dispatch_async back to the main threads at several different points in the download's callback block can be avoided. If you chose to go this route, you can initialize the session with its initializer that allows you to specify the delegate, as well as the delegate queue. This will require you to use the delegate pattern instead of the callback blocks, but this may be beneficial because it is the only way to support background downloads.
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
Run Code Online (Sandbox Code Playgroud)
If you've never heard of AFNetworking, it is IMHO the end-all of networking libraries. It was created for Objective-C, but it works in Swift as well. In the words of its author:
AFNetworking is a delightful networking library for iOS and Mac OS X. It's built on top of the Foundation URL Loading System, extending the powerful high-level networking abstractions built into Cocoa. It has a modular architecture with well-designed, feature-rich APIs that are a joy to use.
AFNetworking 2.0 supports iOS 6 and up, but in this example, I will be using its AFHTTPSessionManager class, which requires iOS 7 and up due to its usage of all the new APIs around the NSURLSession class. This will become obvious when you read the example below, which shares a lot of code with the NSURLSession example above.
There are a few differences that I'd like to point out though. To start off, instead of creating your own NSURLSession, you'll create an instance of AFURLSessionManager, which will internally manage a NSURLSession. Doing so allows you take advantage of some of its convenience methods like -[AFURLSessionManager downloadTaskWithRequest:progress:destination:completionHandler:]. What is interesting about this method is that it lets you fairly concisely create a download task with a given destination file path, a completion block, and an input for an NSProgress pointer, on which you can observe information about the progress of the download. Here's an example.
// 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];
Run Code Online (Sandbox Code Playgroud)
Of course since we've added the class containing this code as an observer to one of the NSProgress instance's properties, you'll have to implement the -[NSObject observeValueForKeyPath:ofObject:change:context:] method. In this case, I've included an example of how you might update a progress label to display the download's progress. It's really easy. NSProgress has an instance method localizedDescription which will display progress information in a localized, human readable format.
- (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];
}
}
Run Code Online (Sandbox Code Playgroud)
Don't forget, if you want to use AFNetworking in your project, you'll need to follow its installation instructions and be sure to #import <AFNetworking/AFNetworking.h>.
And finally, I'd like to give a final example using Alamofire. This is a the library that makes networking in Swift a cake-walk. I'm out of characters to go into great detail about the contents of this sample, but it does pretty much the same thing as the last examples, just in an arguably more beautiful way.
// 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)
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
小智 39
您无法在应用程序包中保存任何内容,但您可以使用+[NSData dataWithContentsOfURL:]该图像存储在应用程序的文档目录中,例如:
NSData *imageData = [NSData dataWithContentsOfURL:myImageURL];
NSString *imagePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:@"/myImage.png"];
[imageData writeToFile:imagePath atomically:YES];
Run Code Online (Sandbox Code Playgroud)
不完全是永久性的,但它至少会在用户删除应用程序之前保持不变.
cem*_*cem 13
这是主要概念.玩得开心 ;)
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];
Run Code Online (Sandbox Code Playgroud)
由于我们现在使用的是IO5,因此您不再需要将映像写入磁盘.
您现在可以在coredata二进制属性上设置"允许外部存储".根据苹果发行说明,它意味着以下内容:
像图像缩略图这样的小数据值可以有效地存储在数据库中,但是大型照片或其他媒体最好由文件系统直接处理.您现在可以指定托管对象属性的值可以存储为外部记录 - 请参阅setAllowsExternalBinaryDataStorage: 启用后,如果核心数据应该直接在数据库中保存数据或存储URI,则会根据每个值进行启发式决策到它为您管理的单独文件.如果使用此选项,则无法基于二进制数据属性的内容进行查询.
| 归档时间: |
|
| 查看次数: |
137413 次 |
| 最近记录: |