如何在SwiftUI中显示基于Web的图像

时间:2019-12-01 21:03:04

标签: image download swiftui

SwiftUI看起来很酷,但是有些事情对我来说似乎很难。即使这样,我还是想了解如何最好地以SwiftUI的方式进行操作,而不是包装swiftui之前的控制器并以旧的方式进行操作。因此,让我从一个简单的问题开始-显示具有URL的网络图像。有解决方案,但不是很容易找到,也不是那么容易理解。

我有一个解决方案,希望得到一些反馈。以下是我要执行的操作的示例(图像来自“打开图像”)。

struct ContentView: View {
    @State var imagePath: String = "https://farm2.staticflickr.com/440/19711210125_6c12414d8f_o.jpg"

    var body: some View {
        WebImage(imagePath: $imagePath).scaledToFit()
    }
}

我的解决方案是在主体顶部放一些代码以开始下载图像。图像路径具有@Binding属性包装器-如果更改,我想更新视图。还有一个带有@State属性包装器的myimage变量-设置它时,我也想更新我的视图。如果图像加载一切正常,将设置myimage并显示图像。最初的问题是,更改主体内的状态将导致视图无效,并触发另一个无限下载。解决方案似乎很简单(下面的代码)。只需检查imagePath,看看自上次加载某些东西以来它是否已更改。请注意,在下载中,我会立即设置prev,这将触发body的另一次执行。有条件的导致状态更改被忽略。

我读到某处@State检查是否相等,如果值不变则将忽略集合。这种对UIImage的相等性检查将失败。我希望对body进行三个调用:初始调用,设置prev时的调用和设置image时的调用。我想我可以为prev添加一个可变值(即一个简单的类),并避免第二次调用。

请注意,使用扩展名和闭包可以完成Web内容的加载,但这是另一个问题。这样做会使WebImage缩减为仅几行代码。

那么,有没有更好的方法来完成此任务?

//
//  ContentView.swift
//  Learn
//
//  Created by John Morris on 11/26/19.
//  Copyright © 2019 John Morris. All rights reserved.
//

import SwiftUI

struct WebImage: View {
    @Binding var imagePath: String?
    @State var prev: String?
    @State var myimage: UIImage?
    @State var message: String?

    var body: some View {
        if imagePath != prev {
            self.downloadImage(from: imagePath)
        }
        return VStack {
            myimage.map({Image(uiImage: $0).resizable()})
            message.map({Text("\($0)")})
        }
    }

    init?(imagePath: Binding<String?>) {
        guard imagePath.wrappedValue != nil else {
            return nil
        }

        self._imagePath = imagePath
        guard let _ = URL(string: self.imagePath!) else {
            return nil
        }

    }

    func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
        URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
    }

    func downloadImage(from imagePath: String?) {
        DispatchQueue.main.async() {
            self.prev = imagePath
        }

        guard let imagePath = imagePath, let url = URL(string: imagePath) else {
            self.message = "Image path is not URL"
            return
        }

        getData(from: url) { data, response, error in
            if let error = error {
                self.message = error.localizedDescription
                return
            }

            guard let httpResponse = response as? HTTPURLResponse else {
                self.message = "No Response"
                return
            }

            guard (200...299).contains(httpResponse.statusCode) else {
                if httpResponse.statusCode == 404 {
                    self.message = "Page, \(url.absoluteURL), not found"
                } else {
                    self.message = "HTTP Status Code \(httpResponse.statusCode)"
                }
                return
            }

            guard let mimeType = httpResponse.mimeType else {
                self.message = "No mimetype"
                return
            }

            guard mimeType == "image/jpeg" else {
                self.message = "Wrong mimetype"
                return
            }

            print(response.debugDescription)
            guard let data = data else {
                self.message = "No Data"
                return
            }

            if let image = UIImage(data: data) {
                DispatchQueue.main.async() {
                    self.myimage = image
                }
            }
        }
    }
}

struct ContentView: View {
    var images = ["https://c1.staticflickr.com/3/2260/5744476392_5d025d6a6a_o.jpg",
                  "https://c1.staticflickr.com/9/8521/8685165984_e0fcc1dc07_o.jpg",
                  "https://farm1.staticflickr.com/204/507064030_0d0cbc850c_o.jpg",
                  "https://farm2.staticflickr.com/440/19711210125_6c12414d8f_o.jpg"
    ]
    @State var imageURL: String?
    @State var count = 0

    var body: some View {
        VStack {
            WebImage(imagePath: $imageURL).scaledToFit()
            Button(action: {
                self.imageURL = self.images[self.count]
                self.count += 1
                if self.count >= self.images.count {
                    self.count = 0
                }
            }) {
                Text("Next")
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

1 个答案:

答案 0 :(得分:0)

我建议两件事。首先,您通常希望在下载图像时允许一个占位符视图。其次,您应该缓存图像,否则,如果有tableView之类的东西在屏幕上滚动并回到屏幕上,则您将继续重新下载图像。这是我的一个应用程序中如何解决该问题的示例:

import SwiftUI
import Combine
import UIKit

class ImageCache {
  enum Error: Swift.Error {
    case dataConversionFailed
    case sessionError(Swift.Error)
  }
  static let shared = ImageCache()
  private let cache = NSCache<NSURL, UIImage>()
  private init() { }
  static func image(for url: URL) -> AnyPublisher<UIImage?, ImageCache.Error> {
    guard let image = shared.cache.object(forKey: url as NSURL) else {
      return URLSession
        .shared
        .dataTaskPublisher(for: url)
        .tryMap { (tuple) -> UIImage in
          let (data, _) = tuple
          guard let image = UIImage(data: data) else {
            throw Error.dataConversionFailed
          }
          shared.cache.setObject(image, forKey: url as NSURL)
          return image
        }
        .mapError({ error in Error.sessionError(error) })
        .eraseToAnyPublisher()
    }
    return Just(image)
      .mapError({ _ in fatalError() })
      .eraseToAnyPublisher()
  }
}

class ImageModel: ObservableObject {
  @Published var image: UIImage? = nil
  var cacheSubscription: AnyCancellable?
  init(url: URL) {
    cacheSubscription = ImageCache
      .image(for: url)
      .replaceError(with: nil)
      .receive(on: RunLoop.main, options: .none)
      .assign(to: \.image, on: self)
  }
}

struct RemoteImage : View {
  @ObservedObject var imageModel: ImageModel
  init(url: URL) {
    imageModel = ImageModel(url: url)
  }
  var body: some View {
    imageModel
      .image
      .map { Image(uiImage:$0).resizable() }
      ?? Image(systemName: "questionmark").resizable()
  }
}