SwiftUI Error Regarding Publishing Changes from Background Thread - swiftui

I am not quite sure how to fix an error that appears three times in my View Model:
import SwiftUI
class GHViewModel: ObservableObject {
let columns: [GridItem] = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
#Published var followers: [Follower] = []
#Published var page = 1
#Published var user: User = PlaceholderData.user
#Published var networkError: GFError = .invalidURL
#Published var shouldShowNetworkError = false
#Published var shouldShowFollowerDetails = false
func getFollowers(userID: String) {
NetworkManager.shared.getFollowers(for: userID, page: self.page) { result in
switch result {
case .success(let followers):
if !followers.isEmpty {
self.followers += followers
print("**** GHViewModel -> FollowersScreen has retrieved the followers. I see \(self.followers.count) followers for this user.")
}
case .failure(let error):
self.shouldShowNetworkError = true
self.networkError = error
}
}
}
func getFollowerDetails(userID: String) {
NetworkManager.shared.getUserInfo(for: userID) { result in
switch result {
case .success(let ghUser):
self.user = ghUser
self.shouldShowFollowerDetails = true
case .failure(let error):
self.shouldShowNetworkError = true
self.networkError = error
}
}
}
}
The 3 warnings are happening at:
#Published var followers: [Follower] = []
and..
case .success(let ghUser):
self.user = ghUser
self.shouldShowFollowerDetails = true
What is this error actually saying? I suppose it's suggesting that I should be using
DispatchQueue.main.async { // code here }
around the code that's generating the warnings?
Please advise?

Yes, it is telling your to use DispatchQueue.main.async. Remember, network calls are asynchoronous. You don't know when you are getting a response, so they are automatically performed on a background thread, but you always need to update your data that is being displayed on your main thread. Therefore, every time you hit .success, you need to update the variables on the main thread by enclosing the updates in DispatchQueue.main.async.
case .success(let followers):
if !followers.isEmpty {
DispatchQueue.main.async {
self.followers += followers
print("**** GHViewModel -> FollowersScreen has retrieved the followers. I see \(self.followers.count) followers for this user.")
}
}
and
case .success(let ghUser):
DispatchQueue.main.async {
self.user = ghUser
self.shouldShowFollowerDetails = true
}
I suspect the #Published error will go away as I suspect that is thrown due to the other issues.

Related

URLSession HTTP Error not updating back in view unless the action is initiated again

I have a view with a button that calls an API, the API either returns an HTTP code 200 or 400 based on a particular scenario.
The button works just fine and everything works smoothly if code 200 is returned, however if code 400 is returned, the view is not updated that the user have to click on the button once again to get the updated message.
I added the http code property as a published variable in the VM's class and the http is an observable, but it doesn't get updated in the view on the first API call, I'm not sure what I'm missing.
I made a lot of changes to the shared code just to help in demonstrating the actual problem.
Update: Also I think another part of the problem, is that the url function returns the value before the url session returns the data, I don't know why this is happening, that when I execute it a second time it uses the values from the previous execution.
HTTPError Class
class HTTPError : Codable, ObservableObject {
var statusCode: Int?
var message: [String]?
var error: String?
init(statusCode: Int? = nil, message: [String]? = [], error: String? = nil){
self.statusCode = statusCode
self.message = message ?? []
self.error = error
}
convenience required init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.statusCode = try container.decodeIfPresent(Int.self, forKey: .statusCode)
do {
self.message = try container.decodeIfPresent([String].self, forKey: .message)
} catch {
guard let value = try container.decodeIfPresent(String.self, forKey:
.message) else {return}
self.message = []
self.message?.append(value)
}
self.error = try container.decodeIfPresent(String.self, forKey: .error)
}
VM Class
class VM: ObservableObject {
#Published var isLoading = true
#Published var httpError = HTTPError()
func checkDriverIn(_ record: DriverQRParam) async -> (Bool) {
...
var request = URLRequest(url: url)
...
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
...
let task = URLSession.shared.dataTask(with: request) { (data, response,
error) in
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
self.httpError = try! JSONDecoder().decode(HTTPError.self, from: data)
//gets updated just fine in this class//
}
task.resume()
}catch {
print("Couldn't encode data \(String(describing: error))")
}
if httpError.statusCode != nil && httpError.statusCode == 400 {
return (false)
} else {
return (true)
}
}
View.Swift
struct xyz: View {
#State private var VM = VM()
Button("click") {
Task {
await VM.checkDriverIn(driverParam)
}
}
}

Return from initializer without initializing all stored properties - Contacts Store

Code from tutorial, want to access contacts from my app, and using this as base
Need to solve this error to move forward and do not know what additional items needs initialization. Any help very appreciated.
import Contacts
final class ContactsViewModel: ObservableObject {
#Published
var contact: [Contact?]
#Published
var contacts: [Contact?] = []
#Published
var permissionsError: PermissionsError? = .none
init( contact: Contact, contacts: [Contact] , permissionsError: PermissionsError) {
permissions()
}. **<-- Error occurs here -**
func openSettings() {
permissionsError = .none
guard let settingsURL = URL(string: UIApplication.openSettingsURLString) else {return}
if UIApplication.shared.canOpenURL(settingsURL) { UIApplication.shared.open(settingsURL)}
}
func getContacts() {
Contact.fetchAll { [weak self] result in
guard let self = self else { return }
switch result {
case .success(let fetchedContacts):
DispatchQueue.main.async {
self.contacts = fetchedContacts.sorted(by: { $0.lastName < $1.lastName })
}
case .failure(let error):
self.permissionsError = .fetchError(error)
}
}
}
func permissions() {
switch CNContactStore.authorizationStatus(for: .contacts) {
case.authorized:
getContacts()
case .notDetermined, .restricted, .denied :
CNContactStore().requestAccess(for: .contacts) { [weak self] granted, error in
switch granted {
case true: self?.getContacts()
case false:
DispatchQueue.main.async {
self?.permissionsError = .userError
}
}
}

#Published Array is not updating

i'm currently struggling to fetch any changes from an published variable in SwiftUI. Most of the code is created after this tutorial on YouTube.
It's basically an app, that fetches cryptos from a firebase database. To avoid high server costs I want to update any changes of the coins to the database but not have an observer to lower the download rate.
What's the bug?
When I'm adding a coin to my favorites, it sends the data correctly to the database and updates the UI. However when I try to filter the coins the Coin-array switches back to it's previous state. I also added a breakpoint on the CoinCellViewModel(coin: coin)-Line but it only gets executed when I change the filterBy. Here's a little visualisation of the bug:
Repository
class CoinsRepository: ObservableObject {
#Published var coins = [Coin]()
var ref: DatabaseReference!
init() {
self.ref = Database.database().reference()
loadDatabase(ref)
}
func loadDatabase(_ ref: DatabaseReference) {
ref.child("coins").observeSingleEvent(of: .value) { snapshot in
guard let dictionaries = snapshot.value as? [String: Any] else { return }
var coinNames: [String] = []
self.coins = dictionaries.compactMap({ (key: String, value: Any) in
guard let dic = value as? [String: Any] else { return nil }
coinNames.append(dic["name"] as? String ?? "")
return Coin(dic)
})
}
}
func updateFavorite(_ coin: Coin, state: Bool) {
let path = ref.child("coins/\(coin.name)")
var flag = false
path.updateChildValues(["favorite": state]) { err, ref in
if let err = err {
print("ERROR: \(err.localizedDescription)")
} else {
var i = 0
var newCoinArray = self.coins
for coinA in newCoinArray {
if coinA.name == coin.name {
newCoinArray[i].favorite = state
}
i += 1
}
// I guess here's the error
DispatchQueue.main.async {
self.objectWillChange.send()
self.coins = newCoinArray
}
}
}
}
}
ViewModel
class CoinListViewModel: ObservableObject {
#Published var coinRepository = CoinsRepository()
#Published var coinCellViewModels = [CoinCellViewModel]()
#Published var filterBy: [Bool] = UserDefaults.standard.array(forKey: "filter") as? [Bool] ?? [false, false, false]
#Published var fbPrice: Double = 0.00
#Published var searchText: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$searchText
.combineLatest(coinRepository.$coins, $fbPrice, $filterBy)
.map(filter)
.sink { coins in
self.coinCellViewModels = coins.map { coin in
CoinCellViewModel(coin: coin)
}
}
.store(in: &cancellables)
}
...
}
updateFavorite(_ coin: Coin, state: Bool) get's called in the CoinCellViewModel() but I guess the code isn't necessary here...
I'm fairly new to the Combine topic and not quite getting all the new methods, so any help is appreciated!

StoreKit problem while restoring In-App Purchase

I tried to find a tutorial about IAP, and I found : This one
But when I call function to restore, nothing happens. OnTap button I call : store.restorePurchase()
extension Store {
func product(for identifier: String) -> SKProduct? { ...}
func purchaseProduct (_ product: SKProduct){ ...}
func restorePurchases() {
SKPaymentQueue.default().restoreCompletedTransactions()
print("restore")
}
}
restore is print in my console. It is the only thing that is printed in console. But in the code above, you can see .restored:that should print "UserIsPremium"
extension Store : SKPaymentTransactionObserver {
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions : [SKPaymentTransaction]){
for transaction in transactions {
var shouldFinishTransaction = false
switch transaction.transactionState{
case .purchased, .restored:
completedPurchases.append(transaction.payment.productIdentifier)
print("UserIsPremium")
shouldFinishTransaction = true
case .failed:
print("NotPremium")
shouldFinishTransaction = true
case .deferred, .purchasing:
print("...")
break
#unknown default:
print("unknown")
break
}
if shouldFinishTransaction {
print("shouldfinish")
SKPaymentQueue.default().finishTransaction(transaction)
DispatchQueue.main.async {
self.purchaseCompletionHandler?(transaction)
self.purchaseCompletionHandler = nil
}
}
}
}
}
My class Store :
import StoreKit
typealias FetchCompletionHandler = (([SKProduct]) -> Void)
typealias PurchaseCompletionHandler = ((SKPaymentTransaction?) -> Void)
class Store: NSObject, ObservableObject {
#Published var allFullVersion = [FullVersion]()
private let allProductsIdentifiers = Set ([
"ClemPapplis.OrientationEPS.versionFull"
])
private var completedPurchases = [String]() {
didSet {
DispatchQueue.main.async{ [weak self] in
guard let self = self else { return }
for index in self.allFullVersion.indices {
if self.completedPurchases.contains(self.allFullVersion[index].id){
print("completedPurchases")
UserDefaults.standard.setValue(true, forKey: "Premium")
}
self.allFullVersion[index].isLocked =
!self.completedPurchases.contains(self.allFullVersion[index].id)
}
}
}
}
I use sandbox testers.
If I buy item, no problem, datas are print in console.
Did I forget something ?
It's been a few years since I used StoreKit, but I remember having an issue with restore purchases also (and yes, #ElTomato is correct, use a separate case - I'm surprised your code actually builds). I found that the following worked:
public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
for transaction:AnyObject in transactions {
if let trans = transaction as? SKPaymentTransaction {
switch trans.transactionState {
case .purchased:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
delegate.purchaseComplete()
case .failed:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
case .restored:
SKPaymentQueue.default().finishTransaction(transaction as! SKPaymentTransaction)
default:
break
}}}
}
func restorePurchases() {
SKPaymentQueue.default().add(self)
SKPaymentQueue.default().restoreCompletedTransactions()
}
public func paymentQueueRestoreCompletedTransactionsFinished(_ queue: SKPaymentQueue) {
for _ in queue.transactions {
alreadyPurchased = true
delegate.purchasesRestored()
}
showAlert("Purchases have been restored.")
}
Don't worry about showAlert, that's just an extension I have for UIViewController. But what I'm not seeing in your code is paymentQueueRestoreCompletedTransactionsFinished(_ queue:).
I followed the same tutorial i think you did not select the select the none in here
configuration
to restore in your simulator you should select none when you select none it tries to connect apple developers in app purchase configuration. give it a try and let me know.

put observedObject in List

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