LAST CHANCE: Save 50% on all my Swift books and bundles! >>

SOLVED: Code review Day 77-78: Questions on MapKit, Decodable and Sorting

Forums > 100 Days of SwiftUI

Hi guys,

I'm struggling a lot with challenge on day 77,

1: Why the array is not sorted? In the struct I added the function < but it doesn't work

2: The Data that I save on disk is not going to appear when I restart the app. Why?

3: I can't assign a MapMarker using the coordinate in the struct because it gives me errors.

Below the complete code:

import CoreLocation
import SwiftUI

struct ContentView: View {
    @State private var image: UIImage?
    @State private var showingImagePicker = false
    @State private var showingAlert = false
    @State private var faces = [Face]().sorted()
    @State private var name = ""

    let locationFetcher = LocationFetcher()

    var body: some View {
        NavigationView {
            List(faces) { face in
                NavigationLink {
                    DetailView(face: face)
                } label: {
                    HStack {
                        Image(uiImage: face.image ?? UIImage(systemName: "person")!)
                            .resizable()
                            .frame(width: 100, height: 100)
                            .clipShape(Circle())
                            .overlay(Circle().strokeBorder(.yellow, lineWidth: 3))

                        Spacer()

                        Text(face.name)
                            .font(.title2)
                            .padding(.trailing)
                    }
                }
            }
            .navigationTitle("Facename")
            .sheet(isPresented: $showingImagePicker) {
                ImagePicker(image: $image)
            }
            .toolbar {
                Button() {
                    showingImagePicker = true
                } label: {
                    Image(systemName: "plus")
                }
            }
            .onChange(of: image) { _ in
                showingAlert = true
            }
            .alert("Person's name", isPresented: $showingAlert) {
                TextField("Enter name", text: $name)
                Button("Save") {
                    self.locationFetcher.start()
                    addFace()
                    name = ""
                }
            }
        }
    }

    let savePath = FileManager.documentsDirectory.appendingPathComponent("faces")

    init() {
        do {
            let data = try Data(contentsOf: savePath)
            faces = try JSONDecoder().decode([Face].self, from: data)
        } catch {
            faces = []
            print(error)
        }
    }

    func addFace() {
        if let location = self.locationFetcher.lastKnownLocation {
            let newFace = Face(id: UUID(), name: name, image: image, latitude: location.latitude, longitude: location.longitude)
            faces.append(newFace)
            save()
        } else {
            print("Unable to see location.")
        }
    }

    func save() {
        do {
            let data = try JSONEncoder().encode(faces)
            try data.write(to: savePath, options: [NSData.WritingOptions.atomic, .completeFileProtection])
        } catch {
            print("Unable to save data")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
import MapKit
import SwiftUI

struct DetailView: View {

    @State private var mapRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 50, longitude: 0), span: MKCoordinateSpan(latitudeDelta: 25, longitudeDelta: 25))
    @State var face: Face

    var body: some View {
            VStack {
                Image(uiImage: face.image ?? UIImage(systemName: "person")!)
                    .resizable()
                    .scaledToFit()
                    .padding()

                Map(coordinateRegion: $mapRegion) 

            }
            .navigationTitle("\(face.name)")
            .navigationBarTitleDisplayMode(.inline)
    }
}

struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        DetailView(face: Face.example)
    }
}
import CoreLocation
import SwiftUI

struct Face: Identifiable, Codable, Equatable, Comparable {
    var id: UUID
    var name: String
    var image: UIImage?
    let latitude: Double
    let longitude: Double

    var coordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    }

    static let example = Face(id: UUID(), name: "Giovanni", image: UIImage(systemName: "person"), latitude: 50.50, longitude: 50.50)

    enum CodingKeys: CodingKey {
        case id
        case name
        case image
        case latitude
        case longitude
    }

    init(id: UUID, name: String, image: UIImage?, latitude: Double, longitude: Double) {
        self.id = id
        self.name = name
        self.image = image
        self.latitude = latitude
        self.longitude = longitude
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.id = try container.decode(UUID.self, forKey: .id)
        self.name = try container.decode(String.self, forKey: .name)
        let imageData = try container.decode(Data.self, forKey: .image)
        self.image = UIImage(data: imageData)
        self.latitude = try container.decode(Double.self, forKey: .latitude)
        self.longitude = try container.decode(Double.self, forKey: .longitude)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .name)
        try container.encode(name, forKey: .name)
        let imageData = image?.jpegData(compressionQuality: 0.8)
        try container.encode(imageData, forKey: .image)
        try container.encode(latitude, forKey: .latitude)
        try container.encode(longitude, forKey: .longitude)
    }

    static func ==(lhs: Face, rhs: Face) -> Bool {
        lhs.id == rhs.id
    }

    static func <(lhs: Face, rhs: Face) -> Bool {
        lhs.name < rhs.name
    }
}

import PhotosUI
import SwiftUI

struct ImagePicker: UIViewControllerRepresentable {
    @Binding var image: UIImage?

    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        var parent: ImagePicker

        init(_ parent: ImagePicker) {
            self.parent = parent
        }

        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            picker.dismiss(animated: true)

            guard let provider = results.first?.itemProvider else { return }

            if provider.canLoadObject(ofClass: UIImage.self) {
                provider.loadObject(ofClass: UIImage.self) { image, _ in
                    self.parent.image = image as? UIImage
                }
            }
        }
    }

    func makeUIViewController(context: Context) -> PHPickerViewController {
        let configurator = PHPickerConfiguration()

        let picker = PHPickerViewController(configuration: configurator)
        picker.delegate = context.coordinator
        return picker
    }

    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {

    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

}
import Foundation

extension FileManager {
    static var documentsDirectory: URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }
}
import CoreLocation

class LocationFetcher: NSObject, CLLocationManagerDelegate {
    let manager = CLLocationManager()
    var lastKnownLocation: CLLocationCoordinate2D?

    override init() {
        super.init()
        manager.delegate = self
    }

    func start() {
        manager.requestWhenInUseAuthorization()
        manager.startUpdatingLocation()
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        lastKnownLocation = locations.first?.coordinate
    }
}

2      

You sorted an Empty array!

So you need to add .sorted() in the init.

faces = try JSONDecoder().decode([Face].self, from: data).sorted()

or

let loadedFaces = try JSONDecoder().decode([Face].self, from: data)
faces = loadedFaces.sorted()

PS you also need to add it when you add a item to array because it will not sort until you reopen app

Move this inside the init

let savePath = FileManager.documentsDirectory.appendingPathComponent("faces")

2      

Thank you for your answer, I did like you told me but it doesn't work.

When I open the app I have zero items in the list.

Probably it's an error in the encode process?

2      

I resolved the third question, can somebody help me on the other two?

Thank you guys

2      

Hi, In the encode(to) function you have

try container.encode(id, forKey: .name)

it should be forKey: .id

2      

@Hectorcrdna you're right, I changed the encode function but it doesn't work. When I reopen the app, there is no data on the screen...

2      

@andreasara-dev I have no clue as to why your approach is not working but i managed to get it to work using the following approach;

create a new Class called Faces and add the code below:

class Faces: ObservableObject, Codable {
    @Published var array = [Face]()

    enum CodingKeys: String, CodingKey {
        case array
    }

    init() { }

    required init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        array = try values.decode([Face].self, forKey: .array)

    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(array, forKey: .array)

    }
}

Once you have that create a property for the Faces Class in ContentView:

@State private var faces = Faces()

Then you're going to change faces to faces.array in a few places:

            List(faces.array.sorted()) { face in //List in ContentView                       

            faces.array = try JSONDecoder().decode([Face].self, from: data) //Init in ContentView

            faces.array.append(newFace) //addFace method

            let data = try JSONEncoder().encode(faces.array) //save method

Let me know if it works for you, hopefully it does.

Update: I just found a way of making it work using your approach, Instead of having the decode on the Init() change it to an onApear on the navigationView

.onAppear {
            do {
                let data = try Data(contentsOf: savePath)
                faces = try JSONDecoder().decode([Face].self, from: data)
            } catch let DecodingError.dataCorrupted(context) {
                print(context)
            } catch let DecodingError.keyNotFound(key, context) {
                print("Key '\(key)' not found:", context.debugDescription)
                print("codingPath:", context.codingPath)
            } catch let DecodingError.valueNotFound(value, context) {
                print("Value '\(value)' not found:", context.debugDescription)
                print("codingPath:", context.codingPath)
            } catch let DecodingError.typeMismatch(type, context)  {
                print("Type '\(type)' mismatch:", context.debugDescription)
                print("codingPath:", context.codingPath)
            } catch {
                print("error: ", error)
            }
        }

I think what's going on is that the init() runs before any property gets initialized so after you load the data it gets replaced by the empty array the property is set to, but i could be off...

2      

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until July 28th.

Click to save your free spot now

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.