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

Working with queue and unexpected (I do not expect it :-) ) behavior

Forums > SwiftUI

Greetings,

I'm working on an app and one of the action it does is to attempt to connect to a remote host and get the response on a given port. It's still work in progress, but in the basics, it works, except for an unexpected behaviro, that happens only on a physical device, not in the simulator. The code below shows the entire content, and the actual work is done in the "checkPorts" function.

I'm sure my code can be optimized, since I've started only 3 weeks ago with SwiftUI, and if you notice anything, let me know, always eager to learn, but the focus shold be on the issue I experience.

So I run the app, I tap on the button "Check Ports" at the bottom of the screen and I get the outcome pretty much instantly (Success or Failure). However, if I wait a little, some of the results change from Success to Failure without me doing anything, and Iif I switch to another app or just go to the Home screen of the device, ALL results turn to Failure. Example of a Success turning into a Failure with the error code in there:

Success: https://ibb.co/5BpJK9z Failure when the app is switched: https://ibb.co/BPFvP4T

Pretty sure it has to do with how I initiate the query, but I do not know how I'm doing it wrong. I can record a video of how it happens if that helps too.

Thank you :-)

Here is the code:

//
//  ContentView.swift
//  Universal Port Checker
//
//  Created by #mezish on 3/10/22.
//

import SwiftUI
import CoreData
import Network

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Host.hostName, ascending: true)],
        animation: .default)
    private var hosts: FetchedResults<Host>

    @State private var hostNameValue: String = ""
    @State private var hostPortValue: Int32?
    @State private var hostEnvValue: String = ""

//    @State private var hostID = UUID()

    @State private var showingAddHost = false
    @State private var showingDeleteAllAlert = false
    @State private var showingInfoView = false
    @State var hasRecords:Bool = true

    let statusDefaultColor = Color("StatusDefaultColor")

    var body: some View {
        NavigationView {
            List {
                ForEach(hosts) { host in
                            NavigationLink(destination: DetailedHostView(host: host)) {
                                VStack {
                                    HStack {
                                        Text(host.hostName!)
                                            .multilineTextAlignment(.leading)
                                            .lineLimit(1)
                                            .truncationMode(.tail)
                                    }  // end HStack
                                    HStack {
                                        Image(systemName: "network")
                                            .shadow(color: statusDefaultColor, radius: 10)
                                            .shadow(color: .blue, radius: 5)
                                        Spacer()
                                        Text(String(host.hostEnvironment!))
                                            .multilineTextAlignment(.trailing)
                                        Spacer()
                                        if host.hostStatus == "Success" {
                                            Image(systemName: "checkmark.circle.fill")
                                                .foregroundColor(.green)
                                                .shadow(color: .green, radius: 5)
                                        } else if host.hostStatus == "Unchecked" {
                                            Image(systemName: "questionmark.circle.fill")
                                                .foregroundColor(.yellow)
                                                .shadow(color: .yellow, radius: 5)
                                        } else if host.hostStatus == "Checking" {
                                            GifImage("checking")
                                                .frame(width: 17, height: 17)
                                                .shadow(color: .green, radius: 5)
                                        } else if host.hostStatus == "Failure" {
                                            Image(systemName: "x.circle.fill")
                                                    .foregroundColor(.red)
                                                    .shadow(color: .red, radius: 5)
                                        }
                                    }  // end HStack
                                    HStack {
                                        Text(String(host.hostPort))
                                            .multilineTextAlignment(.trailing)
                                    }  // end HStack
                                } // end VStack
                            } // end DetailedView call
                    } // end ForEach
                    .onDelete(perform: deleteItems)
            } // end List
            .sheet(isPresented: $showingAddHost, onDismiss: {
                if getCoreDataRecordCount() == 0 {
                    hasRecords = false
                } else {
                    hasRecords = true
                }
            }, content: {
                AddHostView()
            })
            .navigationTitle("Hosts")
            .onAppear {
                if getCoreDataRecordCount() == 0 {
                    hasRecords = false
                }
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    EditButton().disabled(hasRecords == false)
                }
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("Delete All") {
                                showingDeleteAllAlert = true
                    }
                    .alert(isPresented:$showingDeleteAllAlert) {
                        Alert(
                            title: Text("Are you sure you want to delete all the hosts?"),
                            message: Text("There is no hidden undo button!"),
                            primaryButton: .destructive(Text("YES I'm sure!")) {
                                    deleteAllRecords()
                                },
                            secondaryButton: .cancel(Text("NO, I want to keep the hosts"))
                        )
                    }
                    .disabled(hasRecords == false)
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        showingAddHost = true
                    } label: {
                        Text("Add Host")
                    }
                }
                ToolbarItem(placement: .bottomBar) {
                    Button() {
                        showingInfoView = true
                    } label: {
                        Image(systemName: "info.circle")
                    }
                }
                ToolbarItem(placement: .bottomBar) {
                            Spacer()
                }
                ToolbarItem(placement: .bottomBar) {
                    Button("Check Ports") {
                        checkPorts()
                    }
                    .disabled(hasRecords == false)
                }
                ToolbarItem(placement: .bottomBar) {
                            Spacer()
                }
                ToolbarItem(placement: .bottomBar) {
                    Image(systemName: "gear")
                        .opacity(0)
                        .foregroundColor(.blue)
                }
            } // end toolbar
        } // end NavigationView
        .sheet(isPresented: $showingInfoView, content: {
            InformationView()
        })
    } // end some View

    private func checkPorts() {
        for host in hosts {

            host.hostStatus = "Checking"
            host.hostStatusDetails = ""

            let nwHost = NWEndpoint.Host(host.hostName!)
            let nwPort = NWEndpoint.Port(String(host.hostPort))

            let connection = NWConnection(host: nwHost, port: nwPort!, using: .tcp)
            connection.stateUpdateHandler = { (newState) in
                switch(newState) {
                    case .ready:
                    print("Handle connection established")
                    var nwIPAddress = connection.currentPath!.remoteEndpoint.debugDescription
                    nwIPAddress = nwIPAddress.replacingOccurrences(of: "Optional(", with: "")
                    nwIPAddress = nwIPAddress.replacingOccurrences(of: ")", with: "")
                    nwIPAddress = nwIPAddress.components(separatedBy: ":")[0]
                    host.hostIP = "\(String(describing: nwIPAddress))"
                    host.hostStatus = "Success"
                    host.hostStatusDetails = "Port \(nwPort!) for \(nwHost) (\(nwIPAddress)) current status is: Opened"
                    case .waiting(let error):
                    if error.debugDescription.contains("NoSuchRecord") {
                        host.hostStatus = "Failure"
                        host.hostStatusDetails = "The hostname specified does not exist.\n\nDouble-check the hostname and try again."
                    } else if
                        error.debugDescription.contains("Operation timed out") {
                        host.hostStatus = "Failure"
                        host.hostStatusDetails = "A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond on port \(String(describing: nwPort!))\n\nIf you believe this port should be opened, verify your firewall rules to make sure they are not blocking the traffic."
                    } else {
                        print("Waiting for network", error.debugDescription)
                        host.hostStatus = "Failure"
                        host.hostStatusDetails = "\(error.debugDescription)"
                    }
                    case .failed(let error):
                    print("Fatal connection error", error.localizedDescription)
                    host.hostStatus = "Failure"
                    host.hostStatusDetails = "Fatal connection error (\(error.debugDescription))"
                    default:
                    break
                }
            }
            connection.start(queue: .main)
        }
    } // end func

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

            do {
                try viewContext.save()
                if getCoreDataRecordCount() == 0 {
                    hasRecords = false
                } else {
                    hasRecords = true
                }
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    } // end func

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

        // 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 viewContext.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
            NSManagedObjectContext.mergeChanges(
                fromRemoteContextSave: deletedObjects,
                into: [viewContext]
            )
            hasRecords = false
        } catch {
            // handle error
            print(error)
        }
    } // end func

    func getCoreDataRecordCount() -> Int{
        var countOfItems: Int = 0
        let context = PersistenceController.shared.container.viewContext

        let itemFetchRequest: NSFetchRequest<Host> = Host.fetchRequest()

        do {
            countOfItems = try context.count(for: itemFetchRequest)
            //print (countOfItems)
        }
        catch {
            print (error)
        }
        return countOfItems
    } // end func
}

//private let itemFormatter: DateFormatter = {
//    let formatter = DateFormatter()
//    formatter.dateStyle = .short
//    formatter.timeStyle = .medium
//    return formatter
//}()

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

   

I've resolved the issue without really understanding what is wrong though.

I've updated the code to "ignore" the error like this:

case .failed(let error):
                    if host.hostStatus == "Success" {
                        // do nothing, just catch the false positive and don't reset the status
                    } else {
                        print("Fatal connection error", error.localizedDescription)
                        host.hostStatus = "Failure"
                        host.hostStatusDetails = "Fatal connection error (\(error.debugDescription))"
                    }

But eventually, I'm still interested in understanding how to do this better and probably change the way I handle the queue, if someone has any idea, I'm listening :-)

   

Spoke too soon. Was working with the code I just posted earlier today, but looks like it's failing again now :-(

   

Hacking with Swift is sponsored by Emerge

SPONSORED Why are Swift reference types bad for app startup time, and what’s the performance cost of protocol conformances? That’s just a couple of the topics you can learn about on the Emerge blog — written by the app performance experts behind Emerge’s advanced app optimization and monitoring tools, based on their experience of working at companies like Apple, Airbnb, Snap, and Spotify.

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

You are not logged in

Log in or create account
 

Link copied to your pasteboard.