Assign a random Unsplash photo when loading local JSON to CoreData

Hello all,

I’ve managed to make my current app read from a locally packaged JSON file and store its contents in CoreData. Additionally, I have a working Unsplash class that gets 1 random image. What I’d like to be able to do now is, during the iteration of the local JSON entries,

  1. call the Unsplash function,
  2. generate a random photo
  3. assign said photo to the current AphorismEntity record, along with its URL, User and Username

Unfortunately, although the project compiles without any errors, when I run the application in the simulator, all I can see is the fallback local image wave assigned to all entries.

As far as I could see when debugging, the photo constant on line 56 in AphorismModel is nil.

Here’s the packaged project (I’ve removed the Unplash API token, sorry).

Could someone please have a look and tell me what I’m doing wrong?

Alternatively, I’m open to suggestions how to accomplish the same in a better way (instead of generating a random image at each iteration). I was considering an additional function in Unsplash to get a number of random images that equals the number of JSON objects. However, because I intend to have ~300 entries when I complete the app, I thought getting 300 images all at once might take too long… I don’t know, I’m new to app development, be merciful, please.

Thank you!

@d3f7

Hi Borislav,

For the purposes of the exercise I registered for an API access key on Unsplash (I have a login anyway). I changed the Bundle identifier to suit my Developer account. It works.

In your UnsplashModel, the API definitely retrieves an image and stores it in the Published array unsplashPhoto BUT at the time that you run the preloadLocalData in your AphorismModel, that array is not yet populated since the API call is on a background thread and the code has already preloaded the local data before the API call has completed. That’s why you are getting the default image assigned to each record.

Have a look at the attached project that I have updated.

In UnsplashModel I changed the way getRandomImage works by including a completion handler so that at the conclusion of it running the request it returns an array of [UsplashPhoto] as images. In the function call closure you can see that unsplashPhotos is assigned the retuned photos. Have a look at the code and see if you can understand it.

Completion handlers are difficult to understand and it has taken me years to get used to them. Some people understand them straight away but I could never find an explanation that made sense to me so I just kept trying until they gradually made sense.

I also added a function getDailyImage to get the daily image as a Published property dailyImageData to be used to update core Data so that each of the images reflected the daily image. Again this has a completion handler and in the function call closure we assign the imageData to dailyImageData and set imagesRetrieved = true

To do that I created a function in DailyAphorismView to updateDailyImages() which is triggered by .onChange monitoring imagesRetrieved in the UnsplashModel.

I hope you are still with me at this point.

In updateDailyImages() I looped though the aphorism records and assigned the image property with the dailyImageData. Unfortunately I could not get that to work properly and only the last record was assigned the dailyImage and I can’t figure out why. It makes no sense at all but I am no expert in Core Data either.

Updated Project:

Cheers
Chris

1 Like

I realised what I needed to do so that each image in Core Data was updated. I had to create another fetchRequest in DailyAphorismView to request all records and assigned that to a variable aphorisms and looped though them in the function updateDailyImages. I now works.

Was it your intention to set all records to the same image?

Code for DailyAphorismView:

struct DailyAphorismView: View {
    @EnvironmentObject var unsplashModel: UnsplashModel
    let managedObjectContext = PersistenceController.shared.container.viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(key: "date", ascending: true)],
        predicate: NSPredicate(
            format: "date >= %@ AND date <= %@",
            argumentArray: [
                Calendar.current.startOfDay(for: Date()),
                Calendar.current.date(byAdding: .minute, value: 1439, to: Calendar.current.startOfDay(for: Date()))!
            ]
        )
    ) var aphorism: FetchedResults<AphorismEntity>

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
    ) var aphorisms: FetchedResults<AphorismEntity>
    
    
    // MARK: - ELEMENTS
    var coverImage: some View {
        Image(uiImage: UIImage(data: aphorism.first?.image ?? Data()) ?? UIImage())
            .resizable()
            .scaledToFill()
    }
    
    var titleView: some View {
        Text(aphorism.first!.title)
            .font(.largeTitle)
            .multilineTextAlignment(.center)
            .bold()
            .minimumScaleFactor(0.5)
    }
    
    var contentsBox: some View {
        
        ScrollView {
            VStack (alignment: .leading, spacing: 10) {
                Text(aphorism.first!.summary)
                    .multilineTextAlignment(.leading)
                    .padding(.bottom, 20)
                
                if let username = aphorism.first?.imageUserName {
                    HStack(spacing: 0) {
                        Text("Снимка: ")
                        
                        Link(
                            destination: URL(string: "https://unsplash.com/@\(username)?utm_source=your_app_name&utm_medium=referral")!,
                            label: {
                                Text((aphorism.first?.imageUser)!)
                                .underline()
                        })
                        
                        Text(" от ")
                        
                        Link(
                            destination: URL(string: "https://unsplash.com/")!,
                            label: {
                                Text("Unsplash")
                                    .underline()
                            })
                        
                        Spacer()
                    }
                    .font(.caption)
                    .italic()
                    .foregroundColor(.gray)
                    .padding(.all, 2)
                }

            }
            .font(.subheadline)
        }
    }
    
    var buttonRow: some View {
        HStack {
            ShareLink(
                item: aphorism.first!.title,
//                subject: Text(aphorism[0].title),
                message: Text(aphorism.first!.summary)) {
                    Label("Сподели", systemImage: "square.and.arrow.up")
            }
            .padding(10)
            .background(RoundedRectangle(cornerRadius: 10).fill(Color("whitesand")))
            .tint(Color(.black))
            
            Spacer()
            
            Link(destination: URL(string: aphorism.first!.link)!) {
                Image(systemName: "book")
                Text("Прочети още")
            }
            .padding(10)
            .background(RoundedRectangle(cornerRadius: 10).fill(Color("vpurple")))
            .tint(Color(.white))
            
        }
        .frame(height: 60)
    }
    
    var cardView: some View {
        RoundedRectangle(cornerRadius: 25)
            .fill(.white)
            .overlay{
                VStack{
                    titleView
                        .padding(.top, 30)
                    contentsBox
                    Spacer()
                    buttonRow
                }
                .padding(.horizontal, 20)
            }
    }
    
    // MARK: - BODY
    var body: some View {
        ZStack{
            GeometryReader{ proxy in
                coverImage
                    .ignoresSafeArea()
                    .frame(height: proxy.size.height*0.30)
            }
            
            GeometryReader{ proxy in
                VStack(spacing: 0){
                    Spacer()
                    cardView
                        .frame(height: proxy.size.height * 0.8)
                }
            }
        }
        .onChange(of: unsplashModel.imagesRetrieved) { _ in
            if unsplashModel.imagesRetrieved {
                updateDailyImages()
            }
        }
    }

    func updateDailyImages() {
        print("OnChange triggered, updating Core Data images")

        let imageData = unsplashModel.dailyImageData

        for record in aphorisms {
            record.image = UIImage(data: imageData!)?.jpegData(compressionQuality: 0.8)
        }

        if managedObjectContext.hasChanges {
            do {
                try managedObjectContext.save()
            } catch {
                print(error.localizedDescription)
            }
        }
    }
}

struct DailyAphorismView_Previews: PreviewProvider {
    static var previews: some View {
        DailyAphorismView()
    }
}
1 Like

Thank you ever so much, @Chris_Parker! I never expected anyone to spend so much time trying to fix my code.

I don’t want to look a gift horse in the mounth, but what I was really striving for is for each record (aphorism) to have a different image, said image to be stored in CoreData, so that the app can work, more or less, offline after the first run. And also, so that the user could go back through previous aphorisms of the day and still see the same image.

Do you think you could spare some more time and help me accomplish this?

EDIT: The Unsplash user/username don’t seem to be loading, am I right? I think these are a must for copyright reasons.

Hi Borislav,

You are welcome. I enjoy a challenge anyway. It keeps the brain on this old chap in good order.

In fact I just got that working.

The fix was to change the updateDailyImages function to this:

    func updateDailyImages() {
        let imageData = unsplashModel.dailyImageData
        let photo = unsplashModel.unsplashPhotos.first
        
        for record in aphorisms {
            record.image = UIImage(data: imageData!)?.jpegData(compressionQuality: 0.8)
            record.imageUser = photo?.user.name
            record.imageUserName = photo?.user.username
        }

        if managedObjectContext.hasChanges {
            do {
                try managedObjectContext.save()
            } catch {
                print(error.localizedDescription)
            }
        }
    }

Yes, you should be able to do that though I am not quite sure how you are going to manage all the images over time. How many records you are planning on storing in Core Data before you start to cull the older records? How do you add new aphorisms?

Given that you are also storing image data, which will be increasing the volume of Core Data significantly, that may not be the best way to store image data.

What might be better is to save the image data to a file in the App Documents folder and then store a reference to it in Core Data. The file name can be a UUID string, which is always unique, and then store that string in Core Data as a simple field like imageUrl of type String. Retrieving the image data from the Documents folder will be fast and you wont end up with a bloated CoreData file.

When the user initially loads the App you currently have it configured to load 8 aphorism records so I’m guessing that you are intending to get 8 different images from Unsplash for each of those records. It should be easy enough to get those images since you can set the count to 8 rather than 1.

Does that sound like it should work?

@d3f7

Hey Borislav,

Good news.

I have the project downloading the required number of images and loading them into Core Data and the number of images downloaded from Unsplash match the number of records in Core Data.

This was a fun challenge I must say.

Attached is a link to the updated project:

You will have to change the signing to your Bundle identifier and add your own token key.

Essentially what I did was change the way the UnsplashModel worked as far as retrieving records from Unsplash and then downloading the image data for each record.

getRandomImage now includes an imageCount parameter which is provided via the loadData function which in turn is called from DailyAphorismView via the .onAppear modifier.

In DailyAphorismView the .onAppear modifier checks to see if imagesLoaded is false meaning that if the App has not already been installed then there are no images populating CoreData (at least no random images). in that case we get the unsplash Model to loadData with the number of images matching the aphorisms count and when that is done we set imagesLoaded to true. The imagesLoaded boolean is an SwiftUI @AppStorage object which is the same as UserDefaults except much easier to use.

Back in UnsplashModel, after the image details have been added to unsplashPhotos array we loop though that array in loadData and retrieve the imageData for each image and append that to the dailyImageData array (probably needs to be renamed).

Back in DailyAphorismView the .onChange modifier is monitoring that dailyImageData array and if the element count matches the number of records in aphorisms then we can load the imageData into Core Data via the updateDailyImages function.

Whew. I hope all that makes sense to you

Cheers
Chris

1 Like

You, sir, are as genius, as you are kind! Thank you very much! Tell me what you drink and where to send you a bottle. :slight_smile:

My pleasure. I enjoyed the challenge. My reward is giving back to the community and a nod to all of the people who were instrumental in helping me to learn how to develop App’s for iOS and of course that includes Chris Ching who kindly offered me the opportunity to be a moderator on this site.

I think the process of loading the json and then populating the records with images can be done a little more elegantly and achieved outside of the DailyAphorismView but I will have to think about it for a while on how to do that. It’s much nicer to keep data related work external to a View.

That said, the important thing at the initial launch of the App is to have something for the user to experience so as it is that is certainly achieved. It only takes a couple of seconds after the initial launch for the data to be updated with the random images so most users probably would not really notice.

Good luck with the further development of the App and if there is any other challenges you have then let me know.

Cheers
Chris

1 Like