WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Need guidance and/or best practice

Forums > SwiftUI

Greetings,

I already have the following: an Entity that have these attributes and a form that feeds information to that Entity. Beside the IP, that is computed based on the host name, everything is free text as a Textfield. That works well, and when added looks like that.

What I would like to do is change the port number to a Picker and that Picker would let the user chose from a list of ports, that the user would have created beforehand. I want this list of ports to be saved in the cloud as well, like the current Entity I have.

The list of port should be unique, as in the port number is only available once in the list (no duplicates), while the hostname can have multiple instances, each using a different port (I might work later on allowing to select multiple port at once for the same hostname, but not right now).

What would be the best practice to do this?

Do i need another Entity? Do I need to update the current Entity while creating a new one? If i have 2 Entities, do they need any kind of relationship?

Than you :)!

   

This is a great question! But please note, this is not a SwiftUI question. This is a data modeling question. @twostraws and HackingWithSwift.com excel at teaching Swift concepts, iOS programming, interspersed with fantastic interface design. However, data modeling is a skill taught in separate courses.

It’s an important skill for developers to have, so it’s great to see you dig in. You’re asking the right questions.

First: Step away from the keyboard, and grab some paper and pencils. Stop thinking about HOW you display the data on the iPhone. Instead, think of the data you want to collect and store. Focus on the data.

Entity

Please don’t name anything in your model Entity. This is like naming your dog, Dog. Or naming every plate on your menu “Pasta”. Give it a descriptive name! In your app, the labels say Host. This seems like a nice descriptive name.

Design your Model

Host Model

You have already defined this model. It will hold name, IP, port, and group(?).

Port Model

Next design the Port model. What data do you want to collect about a port? Obviously, you want to know its number. Is this always in integer? What are the rules about this number? Must be greater than zero, but less than 25,000 ?

What does port 443 represent? Maybe you want a description of this data too? Port 443 is typically used for secure HTTPS data, while Port 25 is associated with SMTP email protocol. Focus on the data you want to collect.

Relationships

If you draw a line between your Host entity and your Port entity, you’re establishing a relationship. But you need to decide what the rules are. Can a Host have more than one port? Can the host have two ports with the same port number?

Can a port, say 443, be connected to the host named google.com AND to the host named hackingWithSwift.com ? In this case, Port number is NOT unique in the Port model. The combination of Port number and IP address is unique. In data modeling, this points out you may need a third entity table named PortTypes.

PortTypes

PortTypes might be a table you maintain that contains all possible port IDs and their descriptions. This could be the list your user updates and modifies. Then your app uses the values in the data table as a pick list when creating a new Host object. When your user picks the SMTP Port 25 and associates it with the google.com Host object, your model will create a new Port object where the object’s host is google.com, and the port is 25.

Your rules will stipulate that for each Host, your user can only select a port once from the list.

This is the fun of data modeling. Hope this helped.

2      

Hi @Obelix,

Thank you very much for the detailed answer :-)

This is like naming your dog, Dog.

They did it in The Walking Dead :D

'group' is an optional attribute, in case users would want to group them, by environment like Prod, PreProd, etc... or by type, like Search Engines, Social, Coding, etc...

I was already thinking of having a name for the port, so it's more user-friendly and that is why I was thinking at least a second Entity would be needed, so it can store more than 1 attribute. And ports range from 0 to 65535, with 0 being reserved to TCP, which is what I'm using here.

Can a Host have more than one port?

Yes

Can the host have two ports with the same port number?

No

Can a port, say 443, be connected to the host named google.com AND to the host named hackingWithSwift.com ?

Yes

In this case, Port number is NOT unique in the Port model. The combination of Port number and IP address is unique.

That is correct!

In data modeling, this points out you may need a third entity table named PortTypes.

This is probably what I need, but also where you lost me :)

Are you saying this 3rd table would have a relationship with the Port Entity, for port creation and a relationship with Host Entity, for creating the Picker for the user to select from? And no relationship whatsoever between Host and Port Entities?

If that is the case, what kind of attributes would be linked together? I'm not really sure I get how they are actually linked once the relationship is created in XCode. I get the idea, but putting it in practice is more challenging for sure :)

Thank you

   

Save 50% in my Black Friday sale.

SAVE 50% To celebrate WWDC22, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

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

Data Model


This is just a starting point. This may not be the best way to model your solution.

To start, notice the only data you want to directly collect about a Host is its name and ipAddress. However, each host may have zero or many related Ports. For example a host may have both HTTPS, and SMTP ports. In my models I like to use the term "myPorts". This tells me that ONE host object contains a SET that contains that object's Ports. (The object can say these are "my ports".)

But you don't want to store those inside the Host object. Instead, with Core Data, you create a relationship to the Port object as a one to many relationship. You can set up rules in your code telling Core Data that each Host can only have ONE SMTP port. That is, you cannot store more than one Port object with the same port number to any Host. This prevents you from having 2 or more 443 ports, for example.

Also notice, it's not a good practice to store the Port's definition (or purpose) in the Port object. This would be redundant. If you had one thousand Hosts, and each Host had one port 443, you'd carry 1,000 strings holding the text "Secure HTTP". This is redundant, and could also cause problems. For example, your data reports could get out of whack if someone accidentally updates 600 of those descriptions with "Chat Protocol".

Instead, you have a third relationship, PortDescription. This holds ONE record for each port number along with its description. You can set up constraints on this third entity so that you can never duplicate a port number. You can't have one record with port 443 and call it "Secure HTTP" and another record with number 443 and call it "Chat Protocol".

This model allows you to have as many Hosts as you want, each host having as many Ports as necessary. If you have a port number and want to know its description, you reach out to the related PortDescription table for it.

Maintenance


This model probably points out that you need a maintenance screen to add, update, and delete Port Descriptions. You can prepopulate your application with the most common ports and their descriptions. However, you may want to add others in the future and allow your users to add them. This is a separate screen from updating your hosts and their ports. Probably this belongs in a maintenance or preference section in your application. (Think gear icon.)

Cascading Deletes


Now what!?
What happens to your data stores if you delete a record in the PortDescription table? If you have 400 records in the Port data store for port 777 and it's related to ONE record in the PortDescription table, you can always get the description for any of those 400 records. But what if you DELETE the one PortDescription record? Now you have 400 records that point to nothing. You cannot get a description for those 400 records. Core Data has you covered. But you have to think of your business rules. In one case if you delete the one PortDescription record, it might make sense to turn around and delete the 400 related records. In another case, you may want Core Data to prevent you from deleting the record until you first remove the 400 related records. Again, you have to think throught your business requirements.

Port Pick List


This model also allows you to easily select ALL the PortDescriptions in your core data store, transfer them to an internal array structure, then use this array as a pick list when adding Port records to a Host object. If you user wants to add ports to a selected Host, it could easily filter the array of ALL port descriptions and REMOVE the ones already assigned to the Host. This would help prevent your users from accidentally picking a port twice.

This is YOUR application, however! You must determine the business rules, figure out the relationships, and build the interfaces to help your users be most productive and to meet their business requirements.

Homework


Report back! We want to know your designs and business rules!

Phew! Class dismissed!

   

A lot of what you say makes sense and aligns with what I have in mind, for instance being able to rename a port description and prevent a host to get added twice with the same port. Not that it would break anything, but when you look for results later on, having duplicates just does not look good. Also i already know that i will not allow a port to be deleted if it's in use by a host :)

Before implementing it in my application, I will practice and experiment with a blank app just to get a better grip on the relationships and building the interface around it. It will probably be a lot of trial and errors, better not do that right in my app :)

If I get it right from your Dat Model above, I should start with that as Relationships: Host -> Port named myPorts and having Inverse defined and this one has a Type One-To-Many PortDescription -> Port named my Ports as well (but different from the previous one) and having reverse defined and this one has a Type One-To-Many Port -> Host named myHost and reverse defined and being Type One-To-One Port -> PortDescription named myDescription and reverse defined and being Type One-To-One

Would that be a correct understanding?

One more question: I've see a lot of video explaining Relationships and in most cases, even here on Hakcing with Swift, the Codegen is set to Manual/None instead of the default Class Definition. I'm not sure I really get the difference here and if I could just use the default or if I need to go the Manual way, if you have something simple example/explanation?

Thank you

   

So I've made some progress, not sure if in the right direction or not, we will see :)

I've set up the 3 Entities like you suggested and their relationships too, which now looks like that https://ibb.co/7zHYSfx

I've set up the basic code and it works until I tried to add an entry within the relationship and then I get this error:

2022-03-23 00:54:32.661840-0500 ExperimentWithRelationships[92944:4844176] [error] warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'HostEntity' so +entity is unable to disambiguate. CoreData: warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'HostEntity' so +entity is unable to disambiguate. 2022-03-23 00:54:32.662057-0500 ExperimentWithRelationships[92944:4844176] [error] warning: 'HostEntity' (0x600001b0c210) from NSManagedObjectModel (0x600000f30280) claims 'HostEntity'. CoreData: warning: 'HostEntity' (0x600001b0c210) from NSManagedObjectModel (0x600000f30280) claims 'HostEntity'. 2022-03-23 00:54:32.662204-0500 ExperimentWithRelationships[92944:4844176] [error] warning: 'HostEntity' (0x600001b08000) from NSManagedObjectModel (0x600000f0d180) claims 'HostEntity'. CoreData: warning: 'HostEntity' (0x600001b08000) from NSManagedObjectModel (0x600000f0d180) claims 'HostEntity'. 2022-03-23 00:54:32.662338-0500 ExperimentWithRelationships[92944:4844176] [error] error: +[HostEntity entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass CoreData: error: +[HostEntity entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass (lldb)

On that line https://ibb.co/zQ7kpmG

Here is the full code:

//
//  CoreDataManager.swift
//  ExperimentWithRelationships
//
//  Created by mezish on 3/22/22.
//

import Foundation
import CoreData

class CoreDataManager {

    static let instance = CoreDataManager()
    let container: NSPersistentCloudKitContainer
    let context: NSManagedObjectContext

    init() {
        container = NSPersistentCloudKitContainer(name: "ExperimentWithRelationships")
        container.loadPersistentStores { (description, error) in
            if let error = error {
                print("Error loading Core Data. \(error)")
            }
        }
        context = container.viewContext
    } // end init

    func save() {
        do {
            try context.save()
        } catch let error {
            print("Error saving Core Data. \(error.localizedDescription)")
        }
    } // end fund
}

class CoreDataViewModel: ObservableObject {

    let manager = CoreDataManager.instance
    @Published var hostToPort: [HostToPortEntity] = []
    @Published var hosts: [HostEntity] = []
    @Published var ports: [PortEntity] = []

    init() {
        getHosts()
        getHostToPorts()
        getPorts()
    } // end init

    func getHostToPorts() {
        let request = NSFetchRequest<HostToPortEntity>(entityName: "HostToPortEntity")

        do {
            hostToPort = try manager.context.fetch(request)
        } catch let error {
            print("Error fetching. \(error.localizedDescription)")
        }
    } // end func

    func getHosts() {
        let request = NSFetchRequest<HostEntity>(entityName: "HostEntity")

        do {
            hosts = try manager.context.fetch(request)
        } catch let error {
            print("Error fetching. \(error.localizedDescription)")
        }
    } // end func

    func getPorts() {
        let request = NSFetchRequest<PortEntity>(entityName: "PortEntity")

        do {
            ports = try manager.context.fetch(request)
        } catch let error {
            print("Error fetching. \(error.localizedDescription)")
        }
    } // end func

    func addPort() {
        let portsToAdd = [443, 80, 25]
        let portDescriptionsToAdd = ["ssl", "http", "smtp"]

        for (index, _) in portsToAdd.enumerated() {
            let newPort = PortEntity(context: manager.context)
            newPort.portDescription = "\(portDescriptionsToAdd[index])"
            newPort.portNumber = Int32(portsToAdd[index])
            newPort.addToMyPorts([Int32(portsToAdd[index])])
        }
        save()
    } // end func

    func addHost() {
        let hostsToAdd = ["www.google.com", "www.yahoo.com", "www.github.com"]
        let portsToAdd = [443, 80, 25]
        let groupsToAdd = ["Search", "Search", "Coding"]

        for (index, host) in hostsToAdd.enumerated() {
            let newHost = HostEntity(context: manager.context)
            newHost.hostName = "\(host)"
            newHost.hostPort = Int32(portsToAdd[index])
            newHost.hostEnvironment = "\(groupsToAdd[index])"
            newHost.addToMyPorts([Int32(portsToAdd[index])])
        }
        save()
    } // end func

    func deleteAllRecords(entity: String) {
        // Specify a batch to delete with a fetch request
        let fetchRequest: NSFetchRequest<NSFetchRequestResult>
        fetchRequest = NSFetchRequest(entityName: entity)

        // Create a batch delete request for the
        // fetch request
        let deleteRequest = NSBatchDeleteRequest(
            fetchRequest: fetchRequest
        )

        // Specify the result of the NSBatchDeleteRequest
        // should be the NSManagedObject IDs for the
        // deleted objects
        deleteRequest.resultType = .resultTypeObjectIDs

        // Perform the batch delete
        do {

            let batchDelete = try manager.context.execute(deleteRequest)
                as? NSBatchDeleteResult

            guard let deleteResult = batchDelete?.result
            as? [NSManagedObjectID]
            else { return }

            let deletedObjects: [AnyHashable: Any] = [
                NSDeletedObjectsKey: deleteResult
            ]

            // Merge the delete changes into the managed
            // object context
            hosts.removeAll()
            ports.removeAll()
            hostToPort.removeAll()
            DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
                NSManagedObjectContext.mergeChanges(
                    fromRemoteContextSave: deletedObjects,
                    into: [self.manager.context]
                )
                self.getHosts()
                self.getHostToPorts()
                self.getPorts()
            }
        } catch {
            // handle error
            print(error)
        }
    } // end func

    func save() {
        hosts.removeAll()
        ports.removeAll()
        hostToPort.removeAll()

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.manager.save()
            self.getHosts()
            self.getHostToPorts()
            self.getPorts()
        }
    } // end func

}

And the page that makes the call to that function:

//
//  HostView.swift
//  ExperimentWithRelationships
//
//  Created by mezish on 3/21/22.
//

import SwiftUI
import CoreData

struct HostView: View {

    @StateObject var vm = CoreDataViewModel()

    var body: some View {
        NavigationView {
            List {
                ForEach(vm.hosts) { host in
                    NavigationLink {
                        Form {
                            HStack {
                                Text("Host Name")
                                Spacer()
                                Text("\(host.hostName!)")
                            }
                            HStack {
                                Text("Group")
                                Spacer()
                                Text("\(host.hostEnvironment!)")
                            }
                        } // end Form
                    } label: {
                        VStack {
                            HStack {
                            Text("\(host.hostName!)")
                                Spacer()
                            }
                            HStack {
                                Text("\(host.hostEnvironment!)")
                                Spacer()
                            }
                        }
                    }
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button() {
                        vm.deleteAllRecords(entity: "HostEntity")
                    } label: {
                        Text("Delete All")
                    }
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: vm.addHost) {
                        Label("Add Host", systemImage: "plus")
                    }
                }
            }
            Text("Select an item")
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { vm.hosts[$0] }.forEach(vm.manager.context.delete)

            do {
                try vm.manager.context.save()
            } catch {
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

struct HostView_Previews: PreviewProvider {
    static var previews: some View {
        HostView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

I do not know what I do wrong here :-(

Thank you :-)

PS: Can't seem to be able to post images like you do. Either it's a link to the image or it does show like a broken image like in this post (right-click and open image does work though).

   

@Nigel to the rescue.

Please try searching for the answer in the forums, before giving up!

See: How to post graphics in HWS Forums

   

That is the only post on the topic I could find, but it's valid only for dropbox. There is no ?dl=1 at the end of my image url, since it's coming from imgbb. Will ask in a separate topic to not pollute this one.

Still stuck with my issue with the code though, if you see something obvious I'm doing wrong.

   

I have tried multiple things tonight to no avail.

The only progress I've made is actually understanding that my actual error is this one:

Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)

It's most probably because of all the errors about that "Failed to find a unique match for an NSEntityDescription to a managed object subclass CoreData: error: +[HostEntity entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass (lldb)" but I have no clue why this happens as I do not believe I'm having multiple classes with the same name and from what I gathered, I'm only accessing it once in the init of the class.

I'll probably restart from scratch and see where this goes I guess.

   

I've created a brand new application, and really did the minimum on it, as in: Creating the Entities Setting their relationships Updating the View to display the information from 1 Entity only Updated the Save function to account for the Entity name and add an entry in the second Entity

Still no luck and same error message:

Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)

Nothing about the duplicate Entities this time, just this error.

And all I do is that line (since it's a static test to add, I just pass a plain Integer, nothing fancy):

newPort.addToMyPorts([443])

Why is this line of code throwing that error? Am I doing it wrong?

Thank you

   

Still don't know what I'm doing wrong with using the syntax (and kinda feel like I'm talking alone here), but made some progress using a different syntax.

Now it looks like that:

func addPort() {
        let portsToAdd = [443, 80, 25]
        let portDescriptionsToAdd = ["ssl", "http", "smtp"]

        for (index, _) in portsToAdd.enumerated() {
            let newPort = PortEntity(context: manager.context)
            let newHostToPort = HostToPortEntity(context: manager.context)
            newPort.portDescription = "\(portDescriptionsToAdd[index])"
            newPort.portNumber = Int32(portsToAdd[index])
            newHostToPort.myDescription = newPort
        }
        save()
    } // end func

    func addHost() {
        let hostsToAdd = ["www.google.com", "www.yahoo.com", "www.github.com"]
        let portsToAdd = [443, 80, 25]
        let groupsToAdd = ["Search", "Search", "Coding"]

        for (index, host) in hostsToAdd.enumerated() {
            let newHost = HostEntity(context: manager.context)
            let newHostToPort = HostToPortEntity(context: manager.context)

            newHost.hostName = "\(host)"
            newHost.hostPort = Int32(portsToAdd[index])
            newHost.hostEnvironment = "\(groupsToAdd[index])"
            newHostToPort.myHost = newHost
        }
        save()
    } // end func

That works well to add everything, but the issue I have now is that it's all decorrelated, as in when I add a host, it adds an entry of myHost in HostToPortEntity and if I do the same with a new port, it adds an entry of myDescription in HostToPortEntity, but then I fail to be able, on one page, to display all the information at once, since adding a new port and adding a new host creates 2 different entries in the Entity.

I'll keep looking...

   

I think I got something that works. It's not pretty in the code, but it's functional for now. Turns out, I do not need a Relationship at all, nor 3 Entitites, but just 2 of them.

One for the Host information, and one for the Service information (port number and port name). As for the Picker, I display it like that:

Picker(selection: $hostServices) {
                            ForEach(services, id: \.self) { (service: ServiceEntity) in
                                Text("\(service.serviceName!) - \(String(service.servicePort))").tag(service as ServiceEntity?)
                            }
                        } label: {
                            Text("Select a port*")
                        }
                        .onChange(of: hostServices) { newValue in
                            hostPortValue = String(newValue.servicePort)
                        }

hostServices is a reference to the Service Entity, which contains the port names and port numbers. hostPortValue is just a variable that will store the value.

From there I will save the port number in the Host Entity. And on the edit form, I use similar approach:

Picker(selection: $hostServices) {
                            ForEach(services, id: \.self) { (service: ServiceEntity) in
                                Text("\(service.serviceName!) - \(String(service.servicePort))").tag(service as ServiceEntity?)
                            }
                        } label: {
                            Text("Select a port*")
                        }
                        .onAppear {
                            if hasPortOnAppearRunOnce == false {
                                services.forEach({ ServiceEntity in
                                    if ServiceEntity.servicePort == host.hostPort {
                                        hostServices = ServiceEntity
                                        hostPortOldValue = String(host.hostPort)
                                    }
                                })
                                hasPortOnAppearRunOnce = true
                            }
                        }

And if the port has changed, again, will save that new information in the Host Entity.

Finally, if someone were to change the port number of the service, I will update the information in both the Service Entity and the Host Entity:

// update the port number in the Service Entity
                service.servicePort = Int32(servicePortNewValue)!

                // update the port number in the Host Entity
                hosts.forEach { host in
                    if host.hostPort == Int32(servicePortOldValue) {
                        //print("updating value")
                        host.hostPort = Int32(servicePortNewValue)!
                    }
                }

I can now work on tidying up the code now :-)

   

Save 50% in my Black Friday sale.

SAVE 50% To celebrate WWDC22, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

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

You are not logged in

Log in or create account
 

Link copied to your pasteboard.