I am trying to figure out how to display errors such as those found in the App Store Store class and Persistence.swift file in a single reporting module. Some of my errors within views use an environment object to tie the error production to the error reporting, but this isn’t feasible with errors in different classes.
So any pointers on handling these types of errors would be much appreciated.
Below is an example of my process to report an error within a view
// the save button has been pressed
struct saveButton: View {
#Environment(\.managedObjectContext) var viewContext
#EnvironmentObject var errorHandling: ErrorHandling
var body: some View {
// prepping data for save to core data
do {
try viewContext.save()
} catch {
// report: Unable to Save Transaction
self.errorHandling.handle(error: AppError.saveTransactionError)
}
The App Store purchase method (below) may produce two errors that I would like to display with my app. This is the type of logic that I need help display the error within a view
#MainActor
func purchase() {
Task.init {
guard let product = products.first else {
return
}
guard AppStore.canMakePayments else {
return
}
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await transaction.finish()
// save to user defaults
self.purchasedIds = transaction.productID
break
case .unverified:
// throw PurchaseError.failed
// FIX: report error (Unable to purchase verification failed)
break
}
break
case .userCancelled:
break
// asked to buy (setting on phone prevents purchases)
case .pending:
break
default:
break
}
}
catch {
print(error.localizedDescription)
// FIX: report error (Unable to Complete Purchase)
}
}
}
enum AppError: LocalizedError {
case storePurchaseError
case storeVerificationError
var errorDescription: String? {
switch self {
case .storePurchaseError:
return NSLocalizedString("Store Purchase Error", comment: "")
case .storeVerificationError:
return NSLocalizedString("Store Verification Error", comment: "")
}
}
}
Below is some code I have been using to display an alert
struct ErrorAlert: Identifiable {
var id = UUID()
var message: String
var dismissAction: (() -> Void)?
}
class ErrorHandling: ObservableObject {
#Published var currentAlert: ErrorAlert?
func handle(error: Error) {
currentAlert = ErrorAlert(message: error.localizedDescription)
}
}
struct HandleErrorsByShowingAlertViewModifier: ViewModifier {
#StateObject var errorHandling = ErrorHandling()
func body(content: Content) -> some View {
content
.environmentObject(errorHandling)
.background(
EmptyView()
.alert(item: $errorHandling.currentAlert) { currentAlert in
Alert(
title: Text("Error"),
message: Text(currentAlert.message),
dismissButton: .default(Text("Ok")) {
currentAlert.dismissAction?()
}
)
}
)
}
}
extension View {
func withErrorHandling() -> some View {
modifier(HandleErrorsByShowingAlertViewModifier())
}
}
Related
I have RestManager class which is used to fetch data from Internet
class RestManager {
func fetchData<T: Decodable>(url: URL) -> AnyPublisher<T, ErrorType> {
URLSession.shared
.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: T.self, decoder: JSONDecoder())
.mapError({ error in
if let error = error as? URLError {
switch error.code {
case .notConnectedToInternet, .timedOut, .networkConnectionLost:
return .noInternetConnection
case .cannotDecodeRawData, .cannotDecodeContentData:
return .empty
default:
return .general
}
}
return .general
})
.eraseToAnyPublisher()
}
}
In Repository class there is function getCountriesList which, using RestManager is returning AnyPublisher<[Country], ErrorType> where ErrorType represents enum with custom cases for error handling (.noInternetConnection, .general, .empty)
class Covid19RepositoryImpl: Covid19Repository {
func getCountriesList() -> AnyPublisher<[Country], ErrorType> {
let url = RestEndpoints.countriesList.endpoint()
return RestManager().fetchData(url: url)
}
}
In viewModel class, in function getAllCountries, pipeline is created for fetching and saving data in countries variable, and in .sink in completion I tried to save ErrorType (if there is any error) in specific variable called error
I tried to use like this
class CountriesViewModelImpl: CountriesViewModel {
var repository: Covid19Repository
#Published var countries: [Country] = []
#Published var error: ErrorType?
#Published var loader: Bool = true
private var cancellables: Set<AnyCancellable> = .init()
init(repository: Covid19Repository){
self.repository = repository
getAllCountries()
}
func getAllCountries() {
repository
.getCountriesList()
.receive(on: RunLoop.main)
.sink { error in
self.error = error
}
} receiveValue: { [unowned self] newCountries in
self.countries = newCountries
self.error = nil
self.loader = false
}
.store(in: &cancellables)
}
}
But it returns Cannot assign value of type 'Subscribers.Completion' to type 'ErrorType'
Is there any other way I can handle error?
sink returns an enum in the receiveCompletion closure with finished and failure cases
.sink { completion in
switch completion {
case .finished: print("finished")
case .failure(let error): self.error = error
}
} receiveValue: ...
My realm database structure looks like this example:
class Person: Object, Identifiable {
#objc dynamic var id: String = NSUUID().uuidString
#objc dynamic var name: String = ""
var dogs = RealmSwift.List<Dog>()
}
class Dog: Object, Identifiable {
#objc dynamic var id: String = NSUUID().uuidString
#objc dynamic var name: String = ""
let human = RealmSwift.LinkingObjects<Person>(fromType: Person.self, property: "dogs")
}
To simplify CRUD operations I have a database manager:
extension Realm {
public func safeWrite(_ block: (() throws -> Void)) throws {
if isInWriteTransaction {
try block()
} else {
try write(block)
}
}
}
class DatabaseManager {
private let realm: Realm
public static let sharedInstance = DatabaseManager()
private init(){
realm = try! Realm()
}
func save(_ obj: Object){
do {
try realm.safeWrite {
realm.add(obj, update: .all)
}
} catch {
NSLog("error saving object: %#", [error])
}
}
func save(_ obj: Object,_ block: () -> Void){
do {
try realm.safeWrite{
realm.add(obj, update: .all)
block()
}
} catch {
NSLog("error saving object: %#", [error])
}
}
func save(_ objs: [Object]){
do {
try realm.safeWrite {
realm.add(objs, update: .all)
}
} catch {
NSLog("error saving object: %#", [error])
}
}
func fetchData<T: Object>(type: T.Type) -> Results<T>{
let results: Results<T> = realm.objects(type)
return results
}
func delete(_ obj: Object){
do {
try realm.safeWrite {
realm.delete(obj)
}
} catch {
NSLog("error deleting object: %#", [error])
}
}
func update(_ block: #escaping () -> Void){
do {
try realm.safeWrite{
block()
}
} catch {
NSLog("error updating object: %#", [error])
}
}
}
Now I have different views where I need to access my database and display some data. For this case I've created a view model class:
class PersonViewModel: ObservableObject {
let realm = DatabaseManager.sharedInstance
#Published var persons: Results<Person> = DatabaseManager.sharedInstance.fetchData(type: Person.self).sorted(byKeyPath: "name", ascending: true)
public func fetch(){
self.persons = DatabaseManager.sharedInstance.fetchData(type: Person.self).sorted(byKeyPath: "name", ascending: true)
}
public func addPerson(person: Person){
realm.save(Person)
self.fetch() // <---- necessary to update the UI: how to auto update?
}
}
This view model class will be passed as an #EnvironmentObject to the views where I need the data. As you can see I need to fetch after each database operation all data again to have a "fresh updated" Results<Person>. I know that Results<T> are live, but it has no effect to the UI. Is there any way to auto updating all views when I do a change on the database without fetching all data manually again?
While Realm automatically updates collections as new data is deleted, added or changed, there needs to be a trigger that notifies your code of those changes.
That's a notification aka observer.
In this case there's an Results object persons populated with the fetchData function:
#Published var persons: Results<Person> = DatabaseManager.sharedInstance.fetchData(type: Person.self).sorted(byKeyPath: "name", ascending: true)
So when a person is deleted, added or changed the persons results object will be automatically updated. However to be notified of that, an observer needs to be added, and there are two components: a class level notification token and then a function to handle the event.
A notification token is defined like this (note a strong reference is needed to keep it alive)
var notificationToken: NotificationToken? = nil
and ensure it's deallocated when the view closes
deinit {
self.notificationToken?.invalidate()
}
and then the code (in it's simplist form) that will execute when changes to people occurs
self.notificationToken = self.persons?.observe { changes in
switch changes {
case .initial:
print("initial load")
//the initial data is ready - reload tableviews/update ui etc
case .update(_, _, _, _):
print("update")
//some change occurred, reload tableviews/update ui etc
case .error(let error):
fatalError("\(error)")
}
}
Here's a link that covers this in depth Collection Notifications
I'm struggling with what I thought would be a pretty simple problem to solve.
See below...
I have a class - "Connect" - This is #Observable.
Within Connect I have a property "theMessage" which is #Published.
I then have my ContentView which references the property "theMessage"
When the app is launched the view loads OK but...
When the button is the methods within Connect are triggered but the view does not reload.
I believe that the problem is in the "receive" method towards the bottom of "Connect"
Within the closure of this method I can see I'm the debug..
incomingMessage received OK
theMethod gets set OK
But the view doesn't change
Any Help Or Ideas Would Be Appreciated
import Foundation
import Network
class Connect: ObservableObject {
static let sharedInstance = Connect()
private var talking: NWConnection?
private var listening: NWListener?
#Published var theMessage = "Still No Message"
// DEFINE LISTENER
func listenUDP(port: NWEndpoint.Port) {
do {
self.listening = try NWListener(using: .udp, on: port)
self.listening?.stateUpdateHandler = {(newState) in
switch newState {
case .ready:
print("ready")
default:
break
}
}
self.listening?.newConnectionHandler = {(newConnection) in
newConnection.stateUpdateHandler = {newState in
switch newState {
case .ready:
print("new connection")
self.receive(on: newConnection)
default:
break
}
}
newConnection.start(queue: DispatchQueue(label: "new client"))
}
} catch {
print("unable to create listener")
}
self.listening?.start(queue: .main)
}// END OF FUNC - LISTEN TO UDP
// DEFINE ON RECEIVE
func receive(on connection: NWConnection) {
connection.receiveMessage { (data, context, isComplete, error) in
if let error = error {
print(error)
return
}
if let data = data, !data.isEmpty {
let incomingString = String(decoding: data, as: UTF8.self)
print("Incoming String -\(incomingString)")
DispatchQueue.main.async { [weak self] in
self?.objectWillChange.send()
self?.theMessage = incomingString
print(self?.theMessage ?? "Self Got Binned")
}
}
}
}// END OF FUNC - RECEIVE
// DEFINE TALKER
func connectToUDP(hostUDP:NWEndpoint.Host,portUDP:NWEndpoint.Port) {
self.talking = NWConnection(host: hostUDP, port: portUDP, using: .udp)
self.talking?.stateUpdateHandler = { (newState) in
switch (newState) {
case .ready:
break
default:
break
}
}
self.talking?.start(queue: .main)
}// END OF DEFINE TALKER
// SEND A MESSAGE
func sendUDP(_ content: String) {
let contentToSendUDP = content.data(using: String.Encoding.utf8)
self.talking?.send(content: contentToSendUDP, completion: NWConnection.SendCompletion.contentProcessed(({ (NWError) in
if (NWError == nil) {
// code
} else {
print("ERROR! Error when data (Type: String) sending. NWError: \n \(NWError!) ")
}
})))
}
}// END OF CLASS - CONNECT
import SwiftUI
import Network
struct ContentView: View {
#ObservedObject var connect = Connect.sharedInstance
let communication = Connect()
var body: some View {
VStack {
Text("Incoming Message - \(self.connect.theMessage)")
.padding(100)
.onAppear(){
// LISTENER
let port2U = NWEndpoint.Port.init(integerLiteral: 1984)
communication.listenUDP(port: port2U)
}
Button(action: {
let host = NWEndpoint.Host.init("localhost")
let port = NWEndpoint.Port.init("1984")
self.communication.connectToUDP(hostUDP: host, portUDP: port!)
self.communication.sendUDP("/cue/MyText/start")
}) {
Text("smoke")
}
}// END VSTACK
}// END OF BODY
}// END OF VIEW
This code in your Connect class creates a singleton instance
static let sharedInstance = Connect()
Then the View creates another instance with this
let communication = Connect()
One cannot see what the other is doing. It is like creating having 2 cars, 2 houses, 2 people.
Remove communication and replace with connect. Observe and use the Singleton.
struct ConnectView: View {
#ObservedObject var connect = Connect.sharedInstance
var body: some View {
VStack {
Text("Incoming Message - \(self.connect.theMessage)")
.padding(100)
.onAppear(){
// LISTENER
let port2U = NWEndpoint.Port.init(integerLiteral: 1984)
connect.listenUDP(port: port2U)
}
Button(action: {
let host = NWEndpoint.Host.init("localhost")
let port = NWEndpoint.Port.init("1984")
self.connect.connectToUDP(hostUDP: host, portUDP: port!)
self.connect.sendUDP("/cue/MyText/start")
}) {
Text("smoke")
}
}// END VSTACK
}// END OF BODY
}// END OF VIEW
It is good practice to make the initializer of a class private if you will have a Singleton pattern.
Add this to your Connect class to prevent this issue.
private init(){
}
I am trying to integrate the ORSSerialPort framework with Combine into SwiftUi. ORSSerialPort GitHub
ORSSerialPort implements Key-Value Observing(KVO) and a Delegate Pattern. In his example, he works with the Delegate Pattern and UIKit. I will use Combine and SwiftUI. Not the Delegate Pattern.
Because the ORSSerialPort is a KVO Object, it should be possible ORSSerialPort variables to subscribe, but her is my Problem. It will not work. I don't know why.
what i try:
#Published var isOpen: String = ""
var orsSerialPort: ORSSerialPort = ORSSerialPort(path: "/dev/cu.usbmodem146101")!
var publisher: NSObject.KeyValueObservingPublisher<ORSSerialPort, Bool>
var isOpenSub: AnyCancellable?
init(){
publisher = orsSerialPort.publisher(for: \.isOpen)
isOpenSub = publisher
.map{ (x) -> String in return "Is \(x ? "Opend":"Close" )" }
.assign(to: \.isOpen, on: self)
}
The publisher is trigger always one time, and that is by the init. If I close and open the port with my function, it will not trigger again.
func cloes(){
orsSerialPort.close()
}
func open(){
orsSerialPort.open()
}
For Debugging I try this to the the Stream. And I will see again that the Publisher will always trigger only one time. But the Port is closing and opining.
.sink(receiveCompletion: { completion in
print("subScribeIsOpen complet")
switch completion {
case .finished:
print("subScribeIsOpen complet")
case .failure(let error):
print("subScribeIsOpen fail")
print(error.localizedDescription)
}
}, receiveValue: { value in
print("subScribeIsOpen receive \(value)")
self.isOpen = value
})
My Demo Code
import Foundation
import ORSSerial
import Combine
import SwiftUI
class ORSSerialPortCombine: ObservableObject {
#Published var status: String = ""
#Published var isOpen: String = ""
var orsSerialPort: ORSSerialPort = ORSSerialPort(path: "/dev/cu.usbmodem146101")!
var publisher: NSObject.KeyValueObservingPublisher<ORSSerialPort, Bool>
var isOpenSub: AnyCancellable?
init(){
publisher = orsSerialPort.publisher(for: \.isOpen)
isOpenSub = publisher
.map{ (x) -> String in return "Is \(x ? "Opend":"Close" )" }
// .assign(to: \.isOpen, on: self)
.sink(receiveCompletion: { completion in
print("subScribeIsOpen complet")
switch completion {
case .finished:
print("subScribeIsOpen complet")
case .failure(let error):
print("subScribeIsOpen fail")
print(error.localizedDescription)
}
}, receiveValue: { value in
print("subScribeIsOpen receive \(value)")
self.isOpen = value
})
}
func cloes(){
orsSerialPort.close()
}
func open(){
orsSerialPort.open()
}
func updateStatus(){
status = "Serial port is \(orsSerialPort.isOpen ? "Opend":"Close" )"
}
}
struct ORSSerialPortCombineView: View{
#ObservedObject var model = ORSSerialPortCombine()
var body: some View{
VStack{
Text( model.isOpen )
HStack{
Button("Close") { model.cloes() }
Button("Open") { model.open() }
Button("Staus") { model.updateStatus() }
}
Text( model.status )
}
}
}
I get the data from my api and create a class for them. I can use swifyJSON to init them correctly. The problem is that when I put my observedObject in a List, it can only show correctly once. It will crashed after I changed the view. It's very strong because my other List with similar data struct can work.(this view is in a tabView) Is somebody know where my getAllNotification() should put view.onAppear() or List.onAppear()? Thanks!!
class ManagerNotification : Identifiable, ObservableObject{
#Published var id = UUID()
var notifyId : Int = 0
var requestId : Int = 0
var requestName: String = ""
var groupName : String = ""
// var imageName: String { return name }
init(jsonData:JSON) {
notifyId = jsonData["notifyId"].intValue
requestId = jsonData["requestId"].intValue
requestName = jsonData["requestName"].stringValue
groupName = jsonData["groupName"].stringValue
}
}
import SwiftUI
import SwiftyJSON
struct NotificationView: View {
var roles = ["userNotification", "managerNotification"]
#EnvironmentObject var userToken:UserToken
#State var show = false
#State private var selectedIndex = 0
#State var userNotifications : [UserNotification] = [UserNotification]()
#State var managerNotifications : [ManagerNotification] = [ManagerNotification]()
var body: some View {
VStack {
Picker(selection: $selectedIndex, label: Text(" ")) {
ForEach(0..<roles.count) { (index) in
Text(self.roles[index])
}
}
.pickerStyle(SegmentedPickerStyle())
containedView()
Spacer()
}
.onAppear(perform: getAllNotification)
}
func containedView() -> AnyView {
switch selectedIndex {
case 0:
return AnyView(
List(userNotifications) { userNotification in
UserNotificationCellView(userNotification: userNotification)
}
)
case 1:
return AnyView(
List(managerNotifications) { managernotification in
ManagerNotificationCellView(managerNotification : managernotification)
}
.onAppear(perform: getManagerNotification)
)
default:
return AnyView(Text("22").padding(40))
}
}
func getAllNotification(){
// if (self.userNotifications.count != 0){
// self.userNotifications.removeAll()
// }
// I think the crash was in here, because when i don't use removeAll().
// It works fine, but i don't want every times i change to this view. my array will be longer and
// longer
if (self.managerNotifications.count != 0){
self.managerNotifications.removeAll()
}
NetWorkController.sharedInstance.connectApiByPost(api: "/User/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.userNotifications.append(UserNotification(jsonData: notification))
}
}
}
}
}
NetWorkController.sharedInstance.connectApiByPost(api: "/Manager/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.managerNotifications.append(ManagerNotification(jsonData: notification))
}
}
}
}
}
}
func getManagerNotification(){
// if (self.managerNotifications.count != 0){
// self.managerNotifications.removeAll()
// }
print(self.managerNotifications.count)
NetWorkController.sharedInstance.connectApiByPost(api: "/Manager/email", params: ["token": "\(self.userToken.token)"])
{(jsonData) in
if let result = jsonData["msg"].string{
print("eeee: \(result)")
if(result == "you dont have any email"){
}else if(result == "success get email"){
if let searchResults = jsonData["mail"].array {
for notification in searchResults {
self.managerNotifications.append(ManagerNotification(jsonData: notification))
}
}
}
}
}
}
}
error message
Warning once only: UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window). This may cause bugs by forcing views inside the table view to load and perform layout without accurate information (e.g. table view bounds, trait collection, layout margins, safe area insets, etc), and will also cause unnecessary performance overhead due to extra layout passes. Make a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy to catch this in the debugger and see what caused this to occur, so you can avoid this action altogether if possible, or defer it until the table view has been added to a window. reason: 'attempt to delete section 0, but there are only 0 sections before the update'
I think you are confused about the role of #State and #ObservebableObject; it's not like MVC where you replace the ViewController with a SwiftUI.View as it appears you are trying to do in your example. Instead the view should be a function of either some local #State and/or an external #ObservedObject. This is closer to MVVM where your #ObservedObject is analogous to the ViewModel and the view will rebuild itself in response to changes in the #Published properties on the ObservableObject.
TLDR: move your fetching logic to an ObservableObject and use #Published to allow the view to subscribe to the results. I have an example here: https://github.com/joshuajhomann/TVMaze-SwiftUI-Navigation