NEW: Subscribe to Hacking with Swift+ and accelerate your learning! >>

Day 98, Project 30, Challenge 3

Forums > 100 Days of Swift

Hi guys,

Been fighting with this for the whole day and totally ran out of ideas. The task is:

For a tougher challenge, take the image generation code out of cellForRowAt: generate all images when the app first launches, and use those smaller versions instead. For bonus points, combine the getDocumentsDirectory() method I introduced in project 10 so that you save the resulting cache to make sure it never happens again.

So, totally confused with what I was supposed to do, I took a look at a different user's code, which is:

if itemsSmall.isEmpty {
            for imageIndex in 0...items.count - 1 {

                let currentImage = items[imageIndex]
                let imageRootName = currentImage.replacingOccurrences(of: "Large", with: "Thumb")

                guard let path = Bundle.main.path(forResource: imageRootName, ofType: nil) else { return }
                guard let original = UIImage(contentsOfFile: path) else {return}

                let imageName = UUID().uuidString
                let pathDD = getDocumentsDirectory().appendingPathComponent(imageName)
                if let jpegData = original.jpegData(compressionQuality: 0.8) {
                    try? jpegData.write(to: pathDD)
                    itemsSmall.append(pathDD.path)
                }
            }
        }

...and I have absolutely NO idea why it is what it is. I assume that the main aim of this is a performance boost for the app. But only saving the data, without reading it at the next app launch is pointless. How can we read the data from the documents directory file? Each of the string is UUID string, so I don't even know what I should be looking for.

I looked at the project 10 again and I have a weak assumption that I should read the UUID from some array consisting of the saved paths, but here is where I'm out of ideas. As always, any help will be greatly appreciated :)

   

The idea of this challenge is to prevent delays in showing/scrolling the table view due to the process of reading the image files. So, he asks you to read all the images upon app launch, so that they're all already in memory when you show the table view.

All you need to do is read and images and put them in an array of images: var images = [UIImage]()

On the cellForRowAt: method, you just need to get the corresponding image from the array.

Hope that cleared it up.

   

Hi, so the first task of this challenge is to not do the image resizing in cellForRow method because this is wasteful as just scrolling around causes this method to do the work over and over again.

So you need to resize all the images when app launches, store them in dictionary to access them by filename quickly and that is basic solution done.

Or you can go for the second part and also save the resized images to the documents folder. Then when the app launches next time you can just read from disk inside these smaller ones and display them. If you transform the images names in predictable fashion for example with preffix small_ then you can access them using the original filenames modified to include this preffix.

   

Hacking with Swift is sponsored by NSSpain

SPONSORED Announcing NSSpain 2020: Remote Edition! An online, continuous conference for iOS developers. We’ll start on Thursday and finish on Friday, with talks, activities, and lots of fun for 36 hours, non-stop. Sound good? Join us!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Hi guys,

Thanks for the explanation. However, I am still clueless about how to READ the saved data from the DOCUMENTS DIRECTORY (sorry for the caps, didn't mean to be aggressive, just underlining the issue :D).

In the method I showed you, each of the images is saved as some UUID string. How can I read those on the launch of the app?

   

I've something similar in my project. You have to adjust it to your needs, but it should get you started ...

    //Returns the documents directory for the user
    private static func getDocumentsDirectoryLocal() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let documentsDirectory = paths[0]
        return documentsDirectory
    }

    func getProjectImage(project: Project) -> UIImage {
        if let imageURL = project.iconImageURL {
            let path = getDocumentsDirectoryLocal.appendingPathComponent(project.identifier.uuidString, isDirectory: true).appendingPathComponent(imageURL)
            return UIImage(contentsOfFile: path.path) ?? ProjectImages.projectDefaultImage
        } else {
            return ProjectImages.projectDefaultImage
        }
    }

   

The base documents URL is still the same for the app installation. So if you get the URL, append filename (like small_picture1.jpg) and save it, you will find it at them same URL when the app launches next time and you can load it into UIImage with the initializer that takes the URL parameter.

   

Hi guys, thank you so much for your replies, it means a lot to me.

I tried my best and I managed to save all images URLs into an itemsSmall array. I also made the array load when the app launches, but to my not-so-much-surprise, the interface doesn't load the images. If you have any spare time and would like to help me, I would EXTREMELY appreciate looking at some lines below, it's the whole view controller code:

import UIKit

class SelectionViewController: UITableViewController {
    var items = [String]() // this is the array that will store the filenames to load
    var itemsSmall = [String]() // this is the array that will store the filenames of the smaller, saved images.
    var dirty = false
override func viewDidLoad() {
    super.viewDidLoad()

    let defaults = UserDefaults.standard
    if let savedItemsSmall = defaults.object(forKey: "itemsSmall") as? [String] {
        itemsSmall = savedItemsSmall
        print("Small items loaded successfully.")
    }

    title = "Reactionist"

    tableView.rowHeight = 90
    tableView.separatorStyle = .none
    // When we request a cell, we'll get one back reused automatically:
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")

    // load all the JPEGs into our array
    let fm = FileManager.default
    let path = Bundle.main.resourcePath
    if let path = path {
        if let tempItems = try? fm.contentsOfDirectory(atPath: path) {
            for item in tempItems {
                if item.range(of: "Large") != nil {
                    items.append(item)
                }
            }
        }
    }

    if itemsSmall.isEmpty {
        print("itemsSmall was empty")
        for imageIndex in 0...items.count - 1 {

            let currentImage = items[imageIndex]
            let imageRootName = currentImage.replacingOccurrences(of: "Large", with: "Thumb")

            guard let path = Bundle.main.path(forResource: imageRootName, ofType: nil) else { return }
            guard let original = UIImage(contentsOfFile: path) else { return }

            let imageName = UUID().uuidString
            let pathDD = getDocumentsDirectory().appendingPathComponent(imageName)
            if let jpegData = original.jpegData(compressionQuality: 0.8) {
                try? jpegData.write(to: pathDD)
                itemsSmall.append(pathDD.path)
                print("Small items saved successfully.")

                let defaults = UserDefaults.standard
                defaults.set(itemsSmall, forKey: "itemsSmall")
            }
        }
    }
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    if dirty {
        // we've been marked as needing a counter reload, so reload the whole table
        tableView.reloadData()
    }
}

// MARK: - Table view data source

override func numberOfSections(in tableView: UITableView) -> Int {
    // Return the number of sections.
    return 1
}

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // Return the number of rows in the section.
    return items.count * 10
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)

    // find the image for this cell, and load its thumbnail
    let currentImage = itemsSmall[indexPath.row % items.count]

    let renderRect = CGRect(origin: .zero, size: CGSize(width: 90, height: 90))
    let renderer = UIGraphicsImageRenderer(size: renderRect.size)

    let rounded = renderer.image { ctx in
        ctx.cgContext.addEllipse(in: renderRect)
        ctx.cgContext.clip()
            if let original = UIImage(contentsOfFile: itemsSmall[indexPath.row % items.count]) {
                original.draw(in: renderRect)
            }
    }

    cell.imageView?.image = rounded

    // give the images a nice shadow to make them look a bit more dramatic
    cell.imageView?.layer.shadowColor = UIColor.black.cgColor
    cell.imageView?.layer.shadowOpacity = 1
    cell.imageView?.layer.shadowRadius = 10
    cell.imageView?.layer.shadowOffset = CGSize.zero
    cell.imageView?.layer.shadowPath = UIBezierPath(ovalIn: renderRect).cgPath

    // each image stores how often it's been tapped
    let defaults = UserDefaults.standard
    cell.textLabel?.text = "\(defaults.integer(forKey: currentImage))"

    return cell
}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let vc = ImageViewController()
    vc.image = items[indexPath.row % items.count]
    vc.owner = self

    // mark us as not needing a counter reload when we return
    dirty = false

    navigationController?.pushViewController(vc, animated: true)
}

func getDocumentsDirectory() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    return paths[0]
}

}

   

That's a lot of code for us to dissect. Try to go at it small chuncks at a time and debug (at the least with print()). For starters

guard let path = Bundle.main.path(forResource: imageRootName, ofType: nil) else { return }
            guard let original = UIImage(contentsOfFile: path) else { return }

Are these getting set properly instead of returning early ?

   

Yes, everything checks out, there are no errors or any pre-mature returns on the way.

HOWEVER, I noticed that when I printed out the file URL of the first image saved, which looked like this:

/Users/macbook/Library/Developer/CoreSimulator/Devices/1F615CE7-A66D-458D-B0DF-0AB763505B4A/data/Containers/Data/Application/5C9B0CED-37DC-4DCB-A20C-69CC49A54EC5/Documents/F24175AF-F2DE-4847-8D7B-359FF20DDDB1

I tried to enter the URL into Finder so I could check if the image had been indeed created, but Finder is not able to access the provided URL, as if it didn't exist. What can the possible reason for it?

   

if the URL is correct you should see the documents directory of your simulator when pasting this int finder

/Users/macbook/Library/Developer/CoreSimulator/Devices/1F615CE7-A66D-458D-B0DF-0AB763505B4A/data/Containers/Data/Application/5C9B0CED-37DC-4DCB-A20C-69CC49A54EC5/Documents/

If that doesn't work at all, you didn't get the right URL for the documents directory. If you see an empty directory, there's something going wrong with the filename.

try doing a simple test with loading just one image from 1 url in viewDidLoad. If you get that to work, you should be on your way ...

   

Hacking with Swift is sponsored by NSSpain

SPONSORED Announcing NSSpain 2020: Remote Edition! An online, continuous conference for iOS developers. We’ll start on Thursday and finish on Friday, with talks, activities, and lots of fun for 36 hours, non-stop. Sound good? Join us!

Find out more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

Not logged in

Log in
 

Link copied to your pasteboard.