#ObservedObject not updating after successful network call - swift

I've hit a brick wall in my widget extension. I'm using AlamoFire and ObjectMapper to match the networking we have in the main app. I can tell that my AlamoFire network call is getting triggered and that I'm getting results back, and in the correct, expected format. However, saving the response of that network call to a #Published var doesn't seem to be working. My view and models/structs are below:
struct WidgetEntryView: View {
var entry: ResourceCategoryEntry
#ObservedObject var viewModel = WidgetResourcesView(widgetSize: .medium)
var body: some View {
if UserDefaults.forAppGroup.object(forKey: "sessionToken") as? String == nil {
PleaseLogIn()
} else if viewModel.mediumResources.count < 1 {
ErrorScreen()
} else {
MediumResourcesView(resources: viewModel.mediumResources)
}
}
}
class WidgetResourcesView: ObservableObject {
#Published var resourceGroups: [WidgetResouceGroup] = [WidgetResouceGroup]()
var widgetSize: WidgetSize = .small
var selectedCategory: String?
init(widgetSize: WidgetSize) {
self.widgetSize = widgetSize
self.selectedCategory = UserDefaults.forAppGroup.string(forKey: ResourceCategoryEntry.userDefaultKey)
getResources()
}
func getResources() {
WidgetNetworkService.getResources(widgetSize: self.widgetSize.rawValue, selectedCategory: self.selectedCategory) { resourceGroups in
DispatchQueue.main.async {
self.resourceGroups = resourceGroups
}
} failure: { _ in
print("Error Received")
}
}
var mediumResources: [WidgetResource] {
var resources = [WidgetResource]()
if let featuredResourceGroup = resourceGroups.featuredResourceGroup {
for resource in featuredResourceGroup.resources { resources.append(resource) }
}
if let nonFeaturedResourceGroup = resourceGroups.nonFeaturedResourceGroup {
for resource in nonFeaturedResourceGroup.resources { resources.append(resource) }
}
return resources
}
}
class WidgetResouceGroup: NSObject, Mappable, Identifiable {
var id = UUID()
var widgetCategory: WidgetCategory = .featured
var resources = [WidgetResource]()
required init?(map: Map) {}
func mapping(map: Map) {
id <- map["section"]
widgetCategory <- map["section"]
resources <- map["resources"]
}
}
typealias WidgetResourceGroupCollection = [WidgetResouceGroup]
extension WidgetResourceGroupCollection {
var featuredResourceGroup: WidgetResouceGroup? {
return first(where: {$0.widgetCategory == .featured})
}
var nonFeaturedResourceGroup: WidgetResouceGroup? {
return first(where: {$0.widgetCategory != .featured})
}
}
class WidgetResource: NSObject, Mappable, Identifiable {
enum ResourceType: String {
case text = "text"
case audio = "audio"
case video = "video"
}
var id = 0
var title = ""
var imageInfo: WidgetImageInfo?
var resourceType: ResourceType = .text
required init?(map: Map) {}
func mapping(map: Map) {
id <- map["object_id"]
title <- map["title"]
imageInfo <- map["image_info"]
resourceType <- map["content_type"]
}
}

You can use the objectWillChange - Property in your observable object to specifiy when the observable object should be refreshed.
Apple Dev Doku
Example by Paul Hudson

WidgetEntryView instantiates WidgetResourcesView using the ObservedObject wrapper. This causes a new instance of WidgetResourcesView to be instantiated again on every refresh. Try switching that to StateObject, and the original object will be kept in memory between view updates. I believe this is the only change needed, but I’m away so can’t test it!

Related

Swift: Error converting type 'Binding<Subject>' when passing Observed object's property to child view

I want to load data from an API, then pass that data to several child views.
Here's a minimal example with one child view (DetailsView). I am getting this error:
Cannot convert value of type 'Binding<Subject>' to expected argument type 'BusinessDetails'
import Foundation
import SwiftUI
import Alamofire
struct BusinessView: View {
var shop: Business
class Observer : ObservableObject{
#Published public var shop = BusinessDetails()
#Published public var loading = false
init(){ shop = await getDetails(id: shop.id) }
func getDetails(id: String) async -> (BusinessDetails) {
let params = [
id: id
]
self.loading = true
self.shop = try await AF.request("https://api.com/details", parameters: params).serializingDecodable(BusinessDetails.self).value
self.loading = false
return self.shop
}
}
#StateObject var observed = Observer()
var body: some View {
if !observed.loading {
TabView {
DetailsView(shop: $observed.shop)
.tabItem {
Label("Details", systemImage: "")
}
}
}
}
}
This has worked before when the Observed object's property wasn't an object itself (like how the loading property doesn't cause an error).
When using async/await you should use the .task modifier and remove the object. The task will be started when the view appears, cancelled when it disappears and restarted when the id changes. This saves you a lot of effort trying to link async task lifecycle to object lifecycle. e.g.
struct BusinessView: View {
let shop: Business
#State var shopDetails = BusinessDetails()
#State var loading = false
var body: some View {
if loading {
Text("Loading")
}
else {
TabView {
DetailsView(shop: shopDetails)
.tabItem {
Label("Details", systemImage: "")
}
}
}
.task(id: shop.id) {
loading = true
shopDetails = await Self.getDetails(id: shop.id) // usually we have a try catch around this so we can show an error message
loading = false
}
}
// you can move this func somewhere else if you like
static func getDetails(id: String) async -> BusinessDetails{
let params = [
id: id
]
let result = try await AF.request("https://api.com/details", parameters: params).serializingDecodable(BusinessDetails.self).value
return result
}
}
}

UserDefaults keypath publisher doesn't fire beyond first value

Given a obj-c keypath
#objc dynamic var someProp: String { string(forKey: "someProp") }
A regular publisher:
private let sub = UserDefaults.standard.publisher(for: \.someProp).sink { print($0) }
This publishes only works for the first value (e.g. the current value).
However observing the sub publisher from SwiftUI works fine:
.onReceive(pub) { value in
print("received", value)
}
This publishes any subsequent updates.
Any ideas why the former doesn't work?
Edit: Here is a minimal reproducible example:
public extension UserDefaults {
#objc dynamic var value1: Int {
integer(forKey: "string1")
}
}
struct ContentView: View {
#StateObject var vm = ViewModel()
private let pub = UserDefaults.standard.publisher(for: \.value1)
var body: some View {
Button("Add") {
var value = UserDefaults.standard.value(forKey: "value1") as? Int ?? 0
value += 1
debugPrint("SET", value)
UserDefaults.standard.set(value, forKey: "value1")
}
.onReceive(pub) { value in
debugPrint("UI", value)
}
}
class ViewModel: ObservableObject {
private let sub = UserDefaults.standard.publisher(for: \.value1).sink {
debugPrint("SUB", $0)
}
}
}
The error here is how you access and assign your values in the Button action. You are setting the values for the key value1. But the publisher observes the key string1 with the dynamic var named value1.
TLDR: You confused the dynamic var with your key
I would recommend you ommit the access via .value(forKey: "") and use only your dynamic var.
public extension UserDefaults {
#objc dynamic var value1: Int {
// add getter and setter
get{
integer(forKey: "string1")
}
set{
set(newValue, forKey: "string1")
}
}
}
struct ContentView: View {
#StateObject var vm = ViewModel()
private let pub = UserDefaults.standard.publisher(for: \.value1)
var body: some View {
Button("Add") {
//here
UserDefaults.standard.value1 += 1
debugPrint("SET", UserDefaults.standard.value1)
}
.onReceive(pub) { value in
debugPrint("UI", value)
}
}
class ViewModel: ObservableObject {
private let sub = UserDefaults.standard.publisher(for: \.value1).sink {
debugPrint("SUB", $0)
}
}
}
Prints:
"SUB" 0
"UI" 0
"SET" 1
"SUB" 1
"UI" 1
"SET" 2
"SUB" 2
"UI" 2
"SET" 3
"SUB" 3
"UI" 3

how can i merge these SwiftUI async tasks in one task to get a correct result from function when changing one of the parameter of that func

I want to update and use container members in picker , textfield and get price function.
struct PropertyContainer: Equatable {
static func == (lhs: PropertyContainer, rhs: PropertyContainer) -> Bool {
return lhs.fromQty == rhs.fromQty && lhs.fromCurrency == rhs.fromCurrency && lhs.toCurrency == rhs.toCurrency && lhs.toQty == rhs.toQty
}
#State var toQty: Double = 1.0
#State var toCurrency: String = "ETH"
#State var fromQty: Double = 1.0
#State var fromCurrency: String = "BTC"
}
struct ContentView: View {
#State private var propertyContainer = PropertyContainer()
using memebers of container in text field and picker
so I need updates for getprice func.
.task(id: propertyContainer) {
do {
price = try await listMaker.getPrice(fromCurrency: propertyContainer.fromCurrency, fromQty: propertyContainer.fromQty, toCurrency: propertyContainer.toCurrency, toQty: propertyContainer.toQty, type: "Fixed")
print(propertyContainer.fromCurrency)
} catch {
print("Error")
}
}
As all of your function calls are the same you could do it with a bit of refactoring:
Create a struct that contains all the properties you want to react to (Note: as you did not provide the properties e.g. toQty... this will be a general example):
struct PropertyContainer: Equatable{ //important to conform to Equatable
var property1: Bool = true
var property2: Bool = true
// and so on....
}
Create a #State var that holds your properties in your View:
#State private var propertyContainer = PropertyContainer()
Create your task as follows:
.task(id: propertyContainer) {
do {
price =
try await listMaker.getPrice(fromCurrency: fromCurrency, fromQty: fromQty, toCurrency: toCurrency, toQty: toQty, type: "Fixed")
} catch {
print("Error")
}
}
Now access (read and write) your properties only through the container propertyContainer e.g.:
Button {
propertyContainer.property1.toggle()
} label: {
Text("label")
}
When you change a property in your container the whole struct changes and your task gets executed.
Edit:
Example for Picker:
Picker("", selection: $propertyContainer.selected) {//beware the $ for the binding
ForEach(selectable) { selection in
Text(selection)
}
}

SwiftUI+Combine - Dynamicaly subscribing to a dict of publishers

In my project i hold a large dict of items that are updated via grpc stream. Inside the app there are several places i am rendering these items to UI and i would like to propagate the realtime updates.
Simplified code:
struct Item: Identifiable {
var id:String = UUID().uuidString
var name:String
var someKey:String
init(name:String){
self.name=name
}
}
class DataRepository {
public var serverSymbols: [String: CurrentValueSubject<Item, Never>] = [:]
// method that populates the dict
func getServerSymbols(serverID:Int){
someService.fetchServerSymbols(serverID: serverID){ response in
response.data.forEach { (name,sym) in
self.serverSymbols[name] = CurrentValueSubject(Item(sym))
}
}
}
// background stream that updates the values
func serverStream(symbols:[String] = []){
someService.initStream(){ update in
DispatchQueue.main.async {
self.serverSymbols[data.id]?.value.someKey = data.someKey
}
}
}
}
ViewModel:
class SampleViewModel: ObservableObject {
#Injected var repo:DataRepository // injection via Resolver
// hardcoded value here for simplicity (otherwise dynamically added/removed by user)
#Published private(set) var favorites:[String] = ["item1","item2"]
func getItem(item:String) -> Item {
guard let item = repo.serverSymbols[item] else { return Item(name:"N/A")}
return ItemPublisher(item: item).data
}
}
class ItemPublisher: ObservableObject {
#Published var data:Item = Item(name:"")
private var cancellables = Set<AnyCancellable>()
init(item:CurrentValueSubject<Item, Never>){
item
.receive(on: DispatchQueue.main)
.assignNoRetain(to: \.data, on: self)
.store(in: &cancellables)
}
}
Main View with subviews:
struct FavoritesView: View {
#ObservedObject var viewModel: QuotesViewModel = Resolver.resolve()
var body: some View {
VStack {
ForEach(viewModel.favorites, id: \.self) { item in
FavoriteCardView(item: viewModel.getItem(item: item))
}
}
}
}
struct FavoriteCardView: View {
var item:Item
var body: some View {
VStack {
Text(item.name)
Text(item.someKey) // dynamic value that should receive the updates
}
}
}
I must've clearly missed something or it's a completely wrong approach, however my Item cards do not receive any updates (i verified the backend stream is active and serverSymbols dict is getting updated). Any advice would be appreciated!
I've realised i've made a mistake - in order to receive the updates i need to pass down the ItemPublisher itself. (i was incorrectly returning ItemPublisher.data from my viewModel's method)
I've refactored the code and make the ItemPublisher provide the data directly from my repository using the item key, so now each card is subscribing individualy using the publisher.
Final working code now:
class SampleViewModel: ObservableObject {
// hardcoded value here for simplicity (otherwise dynamically added/removed by user)
#Published private(set) var favorites:[String] = ["item1","item2"]
}
MainView and CardView:
struct FavoritesView: View {
#ObservedObject var viewModel: QuotesViewModel = Resolver.resolve()
var body: some View {
VStack {
ForEach(viewModel.favorites, id: \.self) { item in
FavoriteCardView(item)
}
}
}
}
struct FavoriteCardView: View {
var itemName:String
#ObservedObject var item:ItemPublisher
init(_ itemName:String){
self.itemName = itemName
self.item = ItemPublisher(item:item)
}
var body: some View {
let itemData = item.data
VStack {
Text(itemData.name)
Text(itemData.someKey)
}
}
}
and lastly, modified ItemPublisher:
class ItemPublisher: ObservableObject {
#Injected var repo:DataRepository
#Published var data:Item = Item(name:"")
private var cancellables = Set<AnyCancellable>()
init(item:String){
self.data = Item(name:item)
if let item = repo.serverSymbols[item] {
self.data = item.value
item.receive(on: DispatchQueue.main)
.assignNoRetain(to: \.data, on: self)
.store(in: &cancellables)
}
}
}

How to create my game architecture in a protocol oriented way?

I haven't found any good ways to design a protocol oriented item architecture for games.
Heres the first version with Structs:
protocol Usable {
func useItem()
}
protocol Item {
var name: String { get }
var amount: Int { get }
var image: String { get }
}
struct Sword: Item, Usable {
var name = ""
var amount = 0
var image = ""
func useItem() {
}
}
struct Shield: Item, Usable {
var name = ""
var amount = 0
var image = ""
func useItem() {
}
}
The problem with this is I have to copy paste the variables which are A LOT of code across items.
Heres the second version with Classes:
protocol Usable {
func useItem()
}
class BaseItem {
var name = ""
var amount = 0
var image = ""
}
class SwordClass: BaseItem, Usable {
func useItem() {
}
}
This looks pretty good, but the problem is these are reference types and I would prefer them to be value types.
What is the right way to solve this problem?
You should create a generic struct which conforms to your protocols and which requires initialisation with default values and a 'use' closure:
protocol Usable {
func useItem()
}
protocol Item {
var name: String { get }
var amount: Int { get }
var image: String { get }
}
struct UsableItem: Item, Usable {
var name = ""
var amount = 0
var image = ""
let use: (Void -> Void)
init(name: String, image: String, use: (Void -> Void)) {
self.name = name
self.image = image
self.use = use
}
func useItem() {
self.use()
}
}
Then your JSON processing would create instances with the appropriate logic:
var sword = UsableItem(name: "sword", image: "sword") {
print("cut")
}