Как отображать веб-изображение в SwiftUI - PullRequest
0 голосов
/ 02 декабря 2019

SwiftUI кажется классным, но некоторые вещи кажутся мне сложными. Несмотря на это, я бы лучше понял, как лучше сделать что-то по-SwiftUI, чем оборачивать контроллеры pre-swiftui и делать что-то по-старому. Итак, позвольте мне начать с простой проблемы - показа веб-изображения по URL. Есть решения, но их не так просто найти и не так просто понять.

У меня есть решение, и я хотел бы получить некоторую обратную связь. Ниже приведен пример того, что я хотел бы сделать (изображения из Open Images).

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 - если он изменится, я хочу обновить свой вид. Существует также переменная myimage с оберткой свойства @State - когда она устанавливается, я также хочу обновить свое представление. Если все идет хорошо с загрузкой изображения, myimage будет установлен, и изображение отобразится. Первоначальная проблема заключается в том, что изменение состояния в теле приведет к тому, что представление будет признано недействительным и будет запускать еще одну загрузку до бесконечности. Решение кажется простым (код ниже). Просто проверьте imagePath и посмотрите, изменился ли он с момента последней загрузки. Обратите внимание, что при загрузке я сразу установил prev, что вызывает другое выполнение bodyУсловие приводит к тому, что изменение состояния игнорируется.

Я где-то читал, что @State проверяет равенство и игнорирует наборы, если значение не изменяется. Этот вид проверки на равенство не удастся для UIImage. Я ожидаю три вызова тела: начальный вызов, вызов, когда я устанавливаю prev, и вызов, когда я устанавливаю изображение. Я полагаю, я мог бы добавить изменяемое значение для prev (то есть, простой класс) и избежать второго вызова.

Обратите внимание, что загрузка веб-контента могла быть выполнена с использованием расширения и замыканий, но это другая проблема. Это привело бы к сокращению 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 голосов
/ 03 декабря 2019

Я бы предложил две вещи. Во-первых, вы, как правило, хотите разрешить просмотр заполнителя при загрузке изображения. Во-вторых, вы должны кэшировать изображение, иначе, если у вас есть что-то вроде 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()
  }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...