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)
}
}