0

I am new to Swiftui and I struggle to understand how to properly retain data created in ObservableObject when rendering views? Or a completely different approach to the problem maybe?

More specifically, it is about getting HTTP data in each row in a List().

Right now, it makes the HTTP call far too often when parent views are rendered, which causes all rows to be reloaded.

The same issue can be found here: Keep reference on view/data model after View update

public class VideoFetcher: ObservableObject {
    @Published var video: VideoResponse?
    @Published var coverImage: UIImage?
    @Published var coverImageLoading = false
    @Published var categories: String?
    @Published var loading = false
    @Published var error = false

    func load(mediaItemSlug: String = "", broadcasterSlug: String = "") {
        self.loading = true

        Video.findBySlug(
            mediaItemSlug: mediaItemSlug,
            broadcasterSlug: broadcasterSlug,
            successCallback: {video -> Void in                
                self.video = video
                self.loading = false

                self.setCategories()
                self.loadCoverImage()
            },
            errorCallback: {(error, _) -> Void in
                self.loading = false
                self.error = true
            })
    }

    func loadCoverImage() {
        guard self.video!.coverImageUrl != "" else {
            return
        }

        self.coverImageLoading = true

        let downloader = ImageDownloader()
        let urlRequest = URLRequest(url: URL(string: self.video!.coverImageUrl)!)
        let filter = AspectScaledToFillSizeFilter(size: CGSize(width: 520.0, height: 292.499999963))

        downloader.download(urlRequest, filter: filter) { response in
            if case .success(let image) = response.result {
                self.coverImage = image
                self.coverImageLoading = false
            }
        }
    }

    func setCategories() {
        if (self.video!.broadcaster.categories.count > 0) {
            let categoryNames = self.video!.broadcaster.categories.map { category in
                return category.name == "" ? "(no name)" : category.name
            }

            self.categories = categoryNames.joined(separator: " • ");
        }
    }
}

List() row:

struct VideoCard: View {
    @ObservedObject var fetcher = VideoFetcher()
    ...
    init() {
        // Causes reload each render
        self.fetcher.load()
    }

    var body: some View {
        ...
        .onAppear {
            // Loads that on appear but fetcher.video is nil after view re-rendered because load() wasn't called
            self.fetcher.load()
        }
    }
}
Stefan Edberg
  • 231
  • 2
  • 4
  • 15
  • you already found the link where he makes suggestions....and his suggestions are ok - i think. and 2nd - i do not think it is a problem of SwiftUI but a problem of data handling - so you should think about caches.... – Chris May 05 '20 at 09:25

1 Answers1

0

Thanks, Chris. I thought I was doing something wrong on an architectural level but I added caching and that solved my problem.

import Alamofire
import AlamofireImage
import Cache

public class VideoFetcher: ObservableObject {
    @Published var video: VideoResponse?
    @Published var coverImage: UIImage?
    @Published var coverImageLoading = false
    @Published var broadcasterImage: UIImage?
    @Published var categories: String?
    @Published var loading = false
    @Published var error = false

    func load(mediaItemSlug: String = "", broadcasterSlug: String = "") {
        let videoCache = try? AppCache.video!.object(forKey: mediaItemSlug)

        if (videoCache != nil) {
            self.video = videoCache
            self.setCategories()
            self.loadCoverImage()

            return
        }

        self.loading = true

        Video.findBySlug(
            mediaItemSlug: mediaItemSlug,
            broadcasterSlug: broadcasterSlug,
            successCallback: {video -> Void in
                try? AppCache.video!.setObject(video, forKey: mediaItemSlug)

                self.video = video
                self.loading = false

                self.setCategories()
                self.loadCoverImage()
                self.loadBroadcasterImage()
            },
            errorCallback: {(error, _) -> Void in
                self.loading = false
                self.error = true
            })
    }

    func loadCoverImage() {
        let coverImageUrl = self.video!.coverImageUrl

        guard coverImageUrl != "" else {
            return
        }

        let urlRequest = URLRequest(url: URL(string: coverImageUrl)!)
        let cachedImage = AppCache.image!.image(for: urlRequest, withIdentifier: coverImageUrl)

        if (cachedImage != nil) {
            self.coverImage = cachedImage
            return
        }

        self.coverImageLoading = true

        let downloader = ImageDownloader(imageCache: AppCache.image!)
        let filter = AspectScaledToFillSizeFilter(size: CGSize(width: 520.0, height: 292.499999963))

        downloader.download(urlRequest, filter: filter) { response in
            if case .success(let image) = response.result {
                AppCache.image!.add(image, for: urlRequest, withIdentifier: coverImageUrl)

                self.coverImage = image
                self.coverImageLoading = false
            }
        }
    }

    func loadBroadcasterImage() {
        let broadcasterImage = self.video!.broadcaster.avatarImageUrl

        guard broadcasterImage != "" else {
            return
        }

        let urlRequest = URLRequest(url: URL(string: broadcasterImage)!)
        let cachedImage = AppCache.image!.image(for: urlRequest, withIdentifier: broadcasterImage)

        if (cachedImage != nil) {
            self.broadcasterImage = cachedImage
            return
        }

        let downloader = ImageDownloader(imageCache: AppCache.image!)
        let filter = AspectScaledToFillSizeFilter(size: CGSize(width: 16, height: 16))

        downloader.download(urlRequest, filter: filter) { response in
            if case .success(var image) = response.result {
                image = image.af.imageRoundedIntoCircle()
                AppCache.image!.add(image, for: urlRequest, withIdentifier: broadcasterImage)
                self.broadcasterImage = image
            }
        }
    }

    func setCategories() {
        let categories = self.video!.broadcaster.categories

        if (categories.count > 0) {
            let categoryNames = categories.map { category in
                return category.name == "" ? "(no name)" : category.name
            }

            self.categories = categoryNames.joined(separator: " • ");
        }
    }
}
Stefan Edberg
  • 231
  • 2
  • 4
  • 15