Search code examples
imageswiftuiforeachcloudkitckasset

SwiftUI, CloudKit and Images


I'm really stumped by something I think that should be relatively easy, so i need a little bump in the right direction. I've searched in a lot of places and I get either the wrong information, or outdated information (a lot!).

I am working with Core Data and CloudKit to sync data between the user's devices. Images I save as CKAsset attached to a CKRecord. That works well. The problem is with retrieving the images. I need the images for each unique enitity (Game) in a list. So I wrote a method on my viewModel that retrieves the record with the CKAsset. This works (verified), but I have no idea how to get the image out and assign that to a SwiftUI Image() View. My current method returns a closure with a UIImage, how do I set that image to an Image() within a foreach. Or any other solution is appreciated. Musn't be that hard to get the image?

/// Returns the saved UIImage from CloudKit for the game or the default Image!
func getGameImageFromCloud(for game: Game, completion: @escaping (UIImage) -> Void ) {
    // Every game should always have an id (uuid)!
    if let imageURL = game.iconImageURL {
        let recordID = CKRecord.ID(recordName: imageURL)
        var assetURL = ""

        CKContainer.default().privateCloudDatabase.fetch(withRecordID: recordID) { record, error in
            if let error = error {
                print(error.getCloudKitError())
                return
            } else {
                if let record = record {
                    if let asset = record["iconimage"] as? CKAsset {
                        assetURL = asset.fileURL?.path ?? ""
                        DispatchQueue.main.async {
                            completion(UIImage(contentsOfFile: assetURL) ?? AppImages.gameDefaultImage)
                        }
                    }
                }
            }
        }
    } else {
        completion(AppImages.gameDefaultImage)
    }
}

This is the ForEach I want to show the Image for each game (but this needed in multiple places:

        //Background Tab View
        TabView(selection: $gamesViewModel.currentIndex) {
            ForEach(gamesViewModel.games.indices, id: \.self) { index in
                GeometryReader { proxy in
                    Image(uiImage: gamesViewModel.getGameImageFromCloud(for: gamesViewModel.games[index], completion: { image in
                         
                    }))
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(width: proxy.size.width, height: proxy.size.height)
                        .cornerRadius(1)
                }
                .ignoresSafeArea()
                .offset(y: -100)
            }
            .onAppear(perform: loadImage)
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
        .animation(.easeInOut, value: gamesViewModel.currentIndex)
        .overlay(
            LinearGradient(colors: [
                Color.clear,
                Color.black.opacity(0.2),
                Color.white.opacity(0.4),
                Color.white,
                Color.systemPurple,
                Color.systemPurple
            ], startPoint: .top, endPoint: .bottom)
        )
        .ignoresSafeArea()

TIA!


Solution

  • So, let's go... extract ForEach image dependent internals into subview, like (of course it is not testable, just idea):

    ForEach(gamesViewModel.games.indices, id: \.self) { index in
        GeometryReader { proxy in
            GameImageView(model: gamesViewModel, index: index)     // << here !!
                .frame(width: proxy.size.width, height: proxy.size.height)
                .cornerRadius(1)
                //.onDisappear { // if you think about cancelling
                //   gamesViewModel.cancelLoad(for: index)
                //}
        }
        .ignoresSafeArea()
        .offset(y: -100)
    }
    .onAppear(perform: loadImage)
    

    and now subview itself

    struct GameImageView: View {
      var model: Your_model_type_here
      var index: Int
    
      @State private var image: UIImage?    // << here !!
    
      var body: some View {
        Group {
          if let loadedImage = image {
            Image(uiImage: loadedImage)          // << here !!
              .resizable()
              .aspectRatio(contentMode: .fill)
          } else {
            Text("Loading...")
          }
        }.onAppear {
           model.getGameImageFromCloud(for: model.games[index]) { image in
             self.image = image
           }
        }
      }
    }