How to load UIImage from url? - swift

I have the following problem:
fatal error: unexpectedly found nil while unwrapping an Optional value
#IBOutlet weak var DetailImageView1: UIImageView!
#IBOutlet weak var DetailLabel: UILabel!
#IBOutlet weak var DetailLabel2: UILabel!
var SentData1: String!
var SentData2: String!
var SentData3: NSURL!
override func viewDidLoad() {
super.viewDidLoad()
DetailLabel.text = SentData1
DetailLabel2.text = SentData2
let url = NSURL(string: "\(SentData3)")
let data = NSData(contentsOfURL: url!)
DetailImageView1.image = UIImage(data: data!)
I am taking the picture from a url and segue the url link from my previous view controller to this one. Then I created SentData3: NSURL!
Now I have to show the picture in the DetailImageView1.image, but when I try to test the app I get an error.
I would be glad if someone can show me the mistake.

This works for me...
var image: UIImage?
let urlString = "https://example.com/filename"
let url = NSURL(string: urlString)! as URL
if let imageData: NSData = NSData(contentsOf: url) {
image = UIImage(data: imageData as Data)
}
..."image", will either be nil if there was an error, or it will contain the new UIImage object.

You can try that for SwiftUI:
import SwiftUI
struct RemoteImage: View {
private enum LoadState {
case loading, success, failure
}
private class Loader: ObservableObject {
var data = Data()
var state = LoadState.loading
init(url: String) {
guard let parsedURL = URL(string: url) else {
fatalError("Invalid URL: \(url)")
}
URLSession.shared.dataTask(with: parsedURL) { data, response, error in
if let data = data, data.count > 0 {
self.data = data
self.state = .success
} else {
self.state = .failure
}
DispatchQueue.main.async {
self.objectWillChange.send()
}
}.resume()
}
}
#StateObject private var loader: Loader
var loading: Image
var failure: Image
var body: some View {
selectImage()
.resizable()
}
init(url: String, loading: Image = Image(systemName: "photo"), failure: Image = Image(systemName: "multiply.circle")) {
_loader = StateObject(wrappedValue: Loader(url: url))
self.loading = loading
self.failure = failure
}
private func selectImage() -> Image {
switch loader.state {
case .loading:
return loading
case .failure:
return failure
default:
if let image = UIImage(data: loader.data) {
return Image(uiImage: image)
} else {
return failure
}
}
}
}
Then use it in your view:
struct ContentView: View {
let jsonURL = "https://cf.geekdo-images.com/thumb/img/sD_qvrzIbvfobJj0ZDAaq-TnQPs=/fit-in/200x150/pic2649952.jpg"
var body: some View {
RemoteImage(url: jsonURL)
.aspectRatio(contentMode: .fit)
.frame(width: 200)
}
}

If SentData3 is already a URL, you can just simply insert it. Don't force unwrap a variable when you are not sure whether it will return nil or not.
if let imageData: NSData = NSData(contentsOfURL: SentData3) {
DetailImageView1.image = UIImage(data: imageData)
}

Related

SwiftUI Network Image show different views on loading and error

I would like to make sure that when the image is being downloaded the ProgressView is visible and if the url being passed is invalid (empty), a placeholder is put.
How can I do this?
Code:
import Foundation
import SwiftUI
// Download image from URL
struct NetworkImage: View {
public let url: URL?
var body: some View {
Group {
if let url = url, let imageData = try? Data(contentsOf: url),
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
}
else {
ProgressView()
}
}
}
}
struct NetworkImage_Previews: PreviewProvider {
static var previews: some View {
let url = [
"",
"http://google.com",
"https://yt3.ggpht.com/a/AATXAJxAbUTyYnKsycQjZzCdL_gWVbJYVy4mVaVGQ8kRMQ=s176-c-k-c0x00ffffff-no-rj"
]
NetworkImage(url: URL(string: url[0])!)
}
}
You can create a ViewModel to handle the downloading logic:
extension NetworkImage {
class ViewModel: ObservableObject {
#Published var imageData: Data?
#Published var isLoading = false
private var cancellables = Set<AnyCancellable>()
func loadImage(from url: URL?) {
isLoading = true
guard let url = url else {
isLoading = false
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.replaceError(with: nil)
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.imageData = $0
self?.isLoading = false
}
.store(in: &cancellables)
}
}
}
And modify your NetworkImage to display a placeholder image as well:
struct NetworkImage: View {
#StateObject private var viewModel = ViewModel()
let url: URL?
var body: some View {
Group {
if let data = viewModel.imageData, let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
} else if viewModel.isLoading {
ProgressView()
} else {
Image(systemName: "photo")
}
}
.onAppear {
viewModel.loadImage(from: url)
}
}
}
Then you can use it like:
NetworkImage(url: URL(string: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png"))
(Note that the url parameter is not force unwrapped).

SwiftUI Image from URL not showing

I have a problem when I want to show an Image from a URL. I created a class for downloading data and publishing the data forward - ImageLoader:
class ImageLoader: ObservableObject {
var didChange = PassthroughSubject<Data, Never>()
var data = Data() {
didSet {
didChange.send(data)
}
}
func loadData(from urlString: String?) {
if let urlString = urlString {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.data = data
}
}
task.resume()
}
}
}
Therefore, I use it inside a ImageView struct which I use inside my screen.
struct ImageView: View {
var urlString: String
#ObservedObject var imageLoader: ImageLoader = ImageLoader()
#State var image: UIImage = UIImage(named: "homelessDogsCats")!
var body: some View {
ZStack() {
Image(uiImage: image)
.resizable()
.onReceive(imageLoader.didChange) { data in
self.image = UIImage(data: data) ?? UIImage()
}
}.onAppear {
self.imageLoader.loadData(from: urlString)
}
}
}
My problem is that if I just run my project, the image doesn't change and by default appears only image UIImage(named: "homelessDogsCats").
If I add a breakpoint inside
onAppear {
self.imageLoader.loadData(from: urlString)
}
and just step forward, the image is showing.
I have the same problem in another view which usually doesn't display the Image from URL, but sometimes it does.
Try using #Published - then you don't need a custom PassthroughSubject:
class ImageLoader: ObservableObject {
// var didChange = PassthroughSubject<Data, Never>() <- remove this
#Published var data: Data?
...
}
and use it in your view:
struct ImageView: View {
var urlString: String
#ObservedObject var imageLoader = ImageLoader()
#State var image = UIImage(named: "homelessDogsCats")!
var body: some View {
ZStack() {
Image(uiImage: image)
.resizable()
.onReceive(imageLoader.$data) { data in
guard let data = data else { return }
self.image = UIImage(data: data) ?? UIImage()
}
}.onAppear {
self.imageLoader.loadData(from: urlString)
}
}
}
Note: if you're using SwiftUI 2, you can use #StateObject instead of #ObservedObject and onChange instead of onReceive.
struct ImageView: View {
var urlString: String
#StateObject var imageLoader = ImageLoader()
#State var image = UIImage(named: "homelessDogsCats")!
var body: some View {
ZStack() {
Image(uiImage: image)
.resizable()
.onChange(of: imageLoader.data) { data in
guard let data = data else { return }
self.image = UIImage(data: data) ?? UIImage()
}
}.onAppear {
self.imageLoader.loadData(from: urlString)
}
}
}
Working version for iOS 13, 14 (AsyncImage 1 liner is introduced in iOS 15, and the solution below is for the versions prior to that in case your min deployment target is not iOS 15 yet ) and with the latest property wrappers - Observed, Observable and Publisher ( without having to use PassthroughSubject<Data, Never>()
Main View
import Foundation
import SwiftUI
import Combine
struct TransactionCardRow: View {
var transaction: Transaction
var body: some View {
CustomImageView(urlString: "https://stackoverflow.design/assets/img/logos/so/logo-stackoverflow.png") // This is where you extract urlString from Model ( transaction.imageUrl)
}
}
Creating CustomImageView
struct CustomImageView: View {
var urlString: String
#ObservedObject var imageLoader = ImageLoaderService()
#State var image: UIImage = UIImage()
var body: some View {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width:100, height:100)
.onReceive(imageLoader.$image) { image in
self.image = image
}
.onAppear {
imageLoader.loadImage(for: urlString)
}
}
}
Creating a service layer to download the Images from url string, using a Publisher
class ImageLoaderService: ObservableObject {
#Published var image: UIImage = UIImage()
func loadImage(for urlString: String) {
guard let url = URL(string: urlString) else { return }
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else { return }
DispatchQueue.main.async {
self.image = UIImage(data: data) ?? UIImage()
}
}
task.resume()
}
}

SwiftUI Image causing abort trap 6

When I run the following code I just get one error: abort trap: 6. Anyone have any idea? I already tried deleting the derived data file.
class ImageViewModel: ObservableObject {
#Published var image: UIImage = nil
let session = URLSession.shared
func loadImage() {
guard let url = URL(string: "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png") else {
print("Invalid URL")
return
}
let request = URLRequest(url: url)
session.dataTask(with: request) {data, response, error in
guard let data = data else {return}
DispatchQueue.main.async() {
self.image = UIImage(data: data)
}
}
}
}
struct ImageView: View {
#ObservedObject var viewModel = ImageViewModel()
var body: some View {
Image(uiImage: viewModel.image)
.onAppear(perform: loadImage)
// Text("hi")
}
func loadImage() {
viewModel.loadImage()
}
}
No worries, it was because of this:
var image: UIImage = nil
And because I didn't add:
.resume()

Updating image view in SwiftUI after downloading image

On the click of a button I am trying to download a new random image and update the view. When the app loads it displays the downloaded image. When the button is clicked the image seems to download but the view is never updated and displays the place holder image. Am I missing something here, any ideas? Here is a simplified version.
import SwiftUI
struct ContentView : View {
#State var url = "https://robohash.org/random.png"
var body: some View {
VStack {
Button(action: {
self.url = "https://robohash.org/\(Int.random(in:0 ..< 10)).png"
}) {
Text("Get Random Robot Image")
}
URLImage(url: url)
}
}
}
class ImageLoader: BindableObject {
var downloadedImage: UIImage?
let didChange = PassthroughSubject<ImageLoader?, Never>()
func load(url: String) {
guard let imageUrl = URL(string: url) else {
fatalError("Image URL is not correct")
}
URLSession.shared.dataTask(with: imageUrl) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async {
self.didChange.send(nil)
}
return
}
self.downloadedImage = UIImage(data: data)
DispatchQueue.main.async {
print("downloaded image")
self.didChange.send(self)
}
}.resume()
}
}
import SwiftUI
struct URLImage : View {
#ObjectBinding private var imageLoader = ImageLoader()
var placeholder: Image
init(url: String, placeholder: Image = Image(systemName: "photo")) {
self.placeholder = placeholder
self.imageLoader.load(url: url)
}
var body: some View {
if let uiImage = self.imageLoader.downloadedImage {
print("return downloaded image")
return Image(uiImage: uiImage)
} else {
return placeholder
}
}
}
The problem seems to be related to some kind of lost synchronization between the ContentView and the ImageURL (that happens after the button click event).
A possible workaround is making the ImageURL a #State property of the ContentView.
After that, inside the scope of the button click event, we can call the image.imageLoader.load(url: ) method. As the download of the image ends, the publisher (didChange) will notify the ImageURL and then the change is correctly propagated to the ContentView.
import SwiftUI
import Combine
enum ImageURLError: Error {
case dataIsNotAnImage
}
class ImageLoader: BindableObject {
/*
init(url: URL) {
self.url = url
}
private let url: URL */
let id: String = UUID().uuidString
var didChange = PassthroughSubject<Void, Never>()
var image: UIImage? {
didSet {
DispatchQueue.main.async {
self.didChange.send()
}
}
}
func load(url: URL) {
print(#function)
self.image = nil
URLSession.shared.dataTask(with: url) { (data, res, error) in
guard error == nil else {
return
}
guard
let data = data,
let image = UIImage(data: data)
else {
return
}
self.image = image
}.resume()
}
}
URLImage view:
struct URLImage : View {
init() {
self.placeholder = Image(systemName: "photo")
self.imageLoader = ImageLoader()
}
#ObjectBinding var imageLoader: ImageLoader
var placeholder: Image
var body: some View {
imageLoader.image == nil ?
placeholder : Image(uiImage: imageLoader.image!)
}
}
ContentView:
struct ContentView : View {
#State var url: String = "https://robohash.org/random.png"
#State var image: URLImage = URLImage()
var body: some View {
VStack {
Button(action: {
self.url = "https://robohash.org/\(Int.random(in: 0 ..< 10)).png"
self.image.imageLoader.load(url: URL(string: self.url)!)
}) {
Text("Get Random Robot Image")
}
image
}
}
}
Anyway I will try to investigate the problem and if I will know something new I will modify my answer.

SwiftUI and Combine not working smoothly when downloading image asynchrously

When I tried to use SwiftUI & Combine to download image asynchrously, it works fine. Then, I try to implement this into a dynamic list, and I found out there is only one row(the last row) will be show correctly, images in other cells are missing. I have trace the code with breakpoints and I'm sure the image download process is success in others, but only the last row will trigger the #ObjectBinding to update image. Please check my sample code and let me know if there's any wrong. Thanks!
struct UserView: View {
var name: String
#ObjectBinding var loader: ImageLoader
init(name: String, loader: ImageLoader) {
self.name = name
self.loader = loader
}
var body: some View {
HStack {
Image(uiImage: loader.image ?? UIImage())
.onAppear {
self.loader.load()
}
Text("\(name)")
}
}
}
struct User {
let name: String
let imageUrl: String
}
struct ContentView : View {
#State var users: [User] = []
var body: some View {
NavigationView {
List(users.identified(by: \.name)) { user in
UserView(name: user.name, loader: ImageLoader(with: user.imageUrl))
}
.navigationBarTitle(Text("Users"))
.navigationBarItems(trailing:
Button(action: {
self.didTapAddButton()
}, label: {
Text("+").font(.system(size: 36.0))
}))
}
}
func didTapAddButton() {
fetchUser()
}
func fetchUser() {
API.fetchData { (user) in
self.users.append(user)
}
}
}
class ImageLoader: BindableObject {
let didChange = PassthroughSubject<UIImage?, Never>()
var urlString: String
var task: URLSessionDataTask?
var image: UIImage? = UIImage(named: "user") {
didSet {
didChange.send(image)
}
}
init(with urlString: String) {
print("init a new loader")
self.urlString = urlString
}
func load() {
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
if error == nil {
DispatchQueue.main.async {
self.image = UIImage(data: data!)
}
}
}
task.resume()
self.task = task
}
func cancel() {
if let task = task {
task.cancel()
}
}
}
class API {
static func fetchData(completion: #escaping (User) -> Void) {
let request = URLRequest(url: URL(string: "https://randomuser.me/api/")!)
let task = URLSession.shared.dataTask(with: request) { (data, _, error) in
guard error == nil else { return }
do {
let json = try JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any]
guard
let results = json!["results"] as? [[String: Any]],
let nameDict = results.first!["name"] as? [String: String],
let pictureDict = results.first!["picture"] as? [String: String]
else { return }
let name = "\(nameDict["last"]!) \(nameDict["first"]!)"
let imageUrl = pictureDict["thumbnail"]
let user = User(name: name, imageUrl: imageUrl!)
DispatchQueue.main.async {
completion(user)
}
} catch let error {
print(error.localizedDescription)
}
}
task.resume()
}
}
every images should be downloaded successfully no matter how many items in the list.
There seems to be a bug in #ObjectBinding. I am not sure and I cannot confirm yet. I want to create a minimal example code to be sure, and if so, report a bug to Apple. It seems that sometimes SwiftUI does not invalidate a view, even if the #ObjectBinding it is based upon has its didChange.send() called. I posted my own question (#BindableObject async call to didChange.send() does not invalidate its view (and never updates))
In the meantime, I try to use EnvironmentObject whenever I can, as the bug doesn't seem to be there.
Your code then works with very few changes. Instead of using ObjectBinding, use EnvironmentObject:
Code Replacing #ObjectBinding with #EnvironmentObject:
import SwiftUI
import Combine
struct UserView: View {
var name: String
#EnvironmentObject var loader: ImageLoader
init(name: String) {
self.name = name
}
var body: some View {
HStack {
Image(uiImage: loader.image ?? UIImage())
.onAppear {
self.loader.load()
}
Text("\(name)")
}
}
}
struct User {
let name: String
let imageUrl: String
}
struct ContentView : View {
#State var users: [User] = []
var body: some View {
NavigationView {
List(users.identified(by: \.name)) { user in
UserView(name: user.name).environmentObject(ImageLoader(with: user.imageUrl))
}
.navigationBarTitle(Text("Users"))
.navigationBarItems(trailing:
Button(action: {
self.didTapAddButton()
}, label: {
Text("+").font(.system(size: 36.0))
}))
}
}
func didTapAddButton() {
fetchUser()
}
func fetchUser() {
API.fetchData { (user) in
self.users.append(user)
}
}
}
class ImageLoader: BindableObject {
let didChange = PassthroughSubject<UIImage?, Never>()
var urlString: String
var task: URLSessionDataTask?
var image: UIImage? = UIImage(named: "user") {
didSet {
didChange.send(image)
}
}
init(with urlString: String) {
print("init a new loader")
self.urlString = urlString
}
func load() {
let url = URL(string: urlString)!
let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
if error == nil {
DispatchQueue.main.async {
self.image = UIImage(data: data!)
}
}
}
task.resume()
self.task = task
}
func cancel() {
if let task = task {
task.cancel()
}
}
}
class API {
static func fetchData(completion: #escaping (User) -> Void) {
let request = URLRequest(url: URL(string: "https://randomuser.me/api/")!)
let task = URLSession.shared.dataTask(with: request) { (data, _, error) in
guard error == nil else { return }
do {
let json = try JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any]
guard
let results = json!["results"] as? [[String: Any]],
let nameDict = results.first!["name"] as? [String: String],
let pictureDict = results.first!["picture"] as? [String: String]
else { return }
let name = "\(nameDict["last"]!) \(nameDict["first"]!)"
let imageUrl = pictureDict["thumbnail"]
let user = User(name: name, imageUrl: imageUrl!)
DispatchQueue.main.async {
completion(user)
}
} catch let error {
print(error.localizedDescription)
}
}
task.resume()
}
}