How to pass data from an observed class to another? - ios

This post is related to this post that I made. While there is no initialization error anymore now, it seems that there's one problem here: when you change the username in the textfield, the url and payment detail will not get updated still? Any idea how to solve this?
struct passingData: View {
#ObservedObject var userData: UserData
#ObservedObject var images: ImageURL
#ObservedObject var payment: Payment
init() {
let data = UserData()
self.userData = data
self.images = ImageURL(userData: data)
self.payment = Payment(userData: data)
}
var body: some View {
VStack{
TextField("Enter userName", text: $userData.userName)
Text("url is \(images.imageURL)")
Text("Payment detail: \(payment.paymentDate)")
}
}
}
class Payment: ObservableObject{
#Published var paymentDate = ""
#ObservedObject var userData: UserData
init(userData: UserData){
self.userData = userData
loadPaymentDate()
}
func loadPaymentDate(){
self.paymentDate = "last payment date from \(userData.userName) is 12.12.22 "
}
}
class ImageURL: ObservableObject{
#Published var imageURL = ""
#ObservedObject var userData: UserData
init(userData: UserData){
self.userData = userData
loadImageURL()
}
func loadImageURL(){
self.imageURL = "123_\(userData.userName).com"
}
}
class UserData: ObservableObject{
#Published var userName = ""
}

You cannot use #ObservedObject property wrapper in class, it is designed for View only.
Here is a demo of solution for one class. Tested with Xcode 12 / iOS 14
import Combine
class ImageURL: ObservableObject{
#Published var imageURL = ""
private var userData: UserData // << reference type
private var observer: AnyCancellable?
init(userData: UserData){
self.userData = userData
// observe changes of userName via publisher explicitly
self.observer = userData.$userName.sink(receiveValue: {[weak self] _ in
self?.loadImageURL()
})
loadImageURL()
}
func loadImageURL(){
self.imageURL = "123_\(userData.userName).com"
}
}

Related

How to control published property value update in viewmodel in SwiftUI?

I have a view model class with multiple #Published properties.
class AddPassaround : ObservableObject {
#Published var name: String = ""
#Published var reversed : String = ""
#Published var password: String = ""
#Published var age: String = ""
#Published var address: String = ""
#Published var oneAnotherProperty: String = ""
init() {
}
}
Whenever any one of the #Published property is updated, I call an API. Now there is another scenario that needs to update multiple #Published properties at once programmatically. Something like this
viewModel.name = "test"
viewModel.password = "newPassword"
viewModel.oneAnotherProperty = "notUpdateAll"
Now the problem is the API is called multiple times and view is reloaded multiple times. How can I make the API to call only once in this case only. It should work normally in other cases.
SwiftUI faces the same problem you face: when you update three separate #Published properties of your ObservableObject, SwiftUI gets notified three times.
SwiftUI avoids updating the UI three times by coalescing the events. On the first notification, SwiftUI arranges to be awakened before the run loop waits for the next event. On the later notifications, SwiftUI sees that it's already arranged to be awakened and does nothing.
UIKit, AppKit, and Core Animation also coalesce display updates; this is what methods like UIView.setNeedsDisplay and CALayer.setNeedsDisplay are for.
You can use the same sort of coalescing. One way to do it is to use NotificationQueue. You can ask NotificationQueue to enqueue a notification and post it before the run loop goes to sleep, and you can ask it to coalesce queued notifications.
class AddPassaround : ObservableObject {
#Published var name: String = ""
#Published var reversed : String = ""
#Published var password: String = ""
#Published var age: String = ""
#Published var address: String = ""
#Published var oneAnotherProperty: String = ""
private var tickets: [AnyCancellable] = []
private var notificationName: Notification.Name { .init("AddPassaround call API") }
init() {
NotificationCenter.default.publisher(for: notificationName)
.sink { [weak self] _ in self?.callAPI() }
.store(in: &tickets)
objectWillChange
.sink { [weak self] _ in self?.scheduleCallAPI() }
.store(in: &tickets)
}
private func scheduleCallAPI() {
// Arrange to callAPI soon, if I haven't already arranged it.
NotificationQueue.default.enqueue(
.init(name: notificationName),
postingStyle: .whenIdle,
coalesceMask: .onName,
forModes: [.common]
)
}
private func callAPI() {
print("this is where you call the API")
}
}
If you only want a few of your properties to trigger an API call, you can give them willSet (or didSet) observers instead of subscribing to objectWillChange:
class AddPassaround : ObservableObject {
#Published var name: String = "" {
willSet { scheduleCallAPI() }
}
#Published var reversed : String = ""
#Published var password: String = "" {
willSet { scheduleCallAPI() }
}
#Published var age: String = ""
#Published var address: String = ""
#Published var oneAnotherProperty: String = ""
private var tickets: [AnyCancellable] = []
private var notificationName: Notification.Name { .init("AddPassaround call API") }
init() {
NotificationCenter.default.publisher(for: notificationName)
.sink { [weak self] _ in self?.callAPI() }
.store(in: &tickets)
}
private func scheduleCallAPI() {
// Arrange to callAPI soon, if I haven't already arranged it.
NotificationQueue.default.enqueue(
.init(name: notificationName),
postingStyle: .whenIdle,
coalesceMask: .onName,
forModes: [.common]
)
}
private func callAPI() {
print("this is where you call the API")
}
}

SwiftUI why doesn't my view update once this object property changes?

#ObservedObject var vm: PlayerViewModel
var body : some View {
Button("Test"){
Task{
await vm.getPlayer(username: username)
switch vm.state{
case .success(var player):
let docRef = vm.db.collection("players").document(player.displayname)
docRef.getDocument {document, error in
if let document = document, document.exists {
print("document exists")
player.todayFinalKills = document["todayFinalKills"] as? Int
print(player.todayFinalKills ?? 0)
}
}
}
}
So I just have some firestore database here(not really relevant to the question I don't think) I'm calling some function, I check the state of some enum and then check if the document exists, if it exists I change one property in the associated data of the enum. This is what this prints:
So it does seem that the property is getting updated in the player object for sure but then I have another view with a List
struct DailyView: View {
#ObservedObject var vm: PlayerViewModel
var body: some View {
List{
Text("\(player.todayFinalKills ?? 0)")
}
}
}
But this view always has a zero, implying that this still thinks todayFinalKills is nil? If it literally prints it out as 8291 why is this happening? I was thinking maybe if I switch on a enum like this and extract the associated data maybe it creates a copy but I wasn't able to find much information about this online. Can anyone help me figure out why this is happening? I am passing the same ViewModel into all these views.
Edit: PlayerViewModel.swift
import FirebaseFirestore
import Foundation
#MainActor
class PlayerViewModel: ObservableObject{
enum PlayerState {
case na
case loading
case success(data: Player)
case failed(error : Error)
}
var db = Firestore.firestore()
#Published private(set) var state: PlayerState = .na
private let service: PlayerService
init(service: PlayerService)
{
self.service = service
}
func getPlayer(username: String) async {
state = .loading
do{
let uuid = try await service.fetchUUID(username: username)
let player = try await service.fetchPlayer(uuid: uuid)
state = .success(data: player)
}
catch{
print(error)
state = .failed(error: error)
}
}
}
DailyView.swift
import SwiftUI
struct DailyView: View {
#ObservedObject var vm: PlayerViewModel
var body: some View {
switch vm.state{
case .success(let player):
List{
Text("\(player.todayFinalKills ?? 0)")
}
default:
EmptyView()
}
}
}
struct DailyView_Previews: PreviewProvider {
static var previews: some View {
DailyView(vm: PlayerViewModel(service: PlayerService()))
}
}
LoginView.swift
struct LoginView: View {
#ObservedObject var vm : PlayerViewModel
#State private var username = ""
#State private var profileLoaded = false
private var searchAllowed : Bool{
if(username.count>2)
{
return false
}
return true
}
var body: some View {
NavigationView{
ZStack{
VStack{
Button{
Task{
await vm.getPlayer(username: username)
switch vm.state{
case .success(var player):
let docRef = vm.db.collection("players").document(player.displayname)
docRef.getDocument {document, error in
if let document = document, document.exists {
player.todayFinalKills = document["todayFinalKills"] as? Int
print(player.todayFinalKills ?? 0)
}
else{
let stats : [String : Int] = [
"todayFinalKills": player.stats.Bedwars?.final_kills_bedwars ?? 0,
"todayFinalDeaths" : player.stats.Bedwars?.final_deaths_bedwars ?? 0,
"todayBedwarsWins" : player.stats.Bedwars?.wins_bedwars ?? 0,
"todayBedwarsLosses" : player.stats.Bedwars?.losses_bedwars ?? 0,
"todayTntRunLosses" : player.stats.TNTGames?.deaths_tntrun ?? 0,
"todayTntRunWins" : player.achievements.tntgames_tnt_run_wins ?? 0,
]
vm.db.newPlayerDocument(data: stats, username: player.displayname, uuid: player.uuid)
}
}
profileLoaded = true
default:
print("default")
}
}
} label:
{
ZStack{
RoundedRectangle(cornerRadius: 5)
.fill(.gray)
.frame(width: 200, height: 50)
Text("Search")
.foregroundColor(.white)
}
}
Model.swift
struct Player : Codable, Hashable, Equatable{
static func == (lhs: Player, rhs: Player) -> Bool {
lhs.uuid == rhs.uuid
}
// so a player contains many objects, like stats, achievements, etc
var stats: Stats
var achievements: Achievements
var displayname: String
var uuid : String
var networkExp: Int
var knownAliases : [String]
var karma: Int
var newPackageRank : String? //this could be MVP_PLUS
var rankPlusColor : String? //this could be LIGHT_PURPLE
//daily statistics for the player.
**var todayFinalKills : Int?**
var todayBedwarsLosses : Int?
var todayBedwarsWins: Int?
var todayFinalDeaths: Int?
var todayTntRunWins: Int?
var TodayTntRunLosses: Int?
//

Views dependant on UserDefaults not updating on change

So I have a class that records the state of a toggle and a selection of a picker into UserDefaults
import Foundation
import Combine
class UserSettings: ObservableObject {
#Published var shouldSort: Bool {
didSet {
UserDefaults.standard.set(shouldSort, forKey: "shouldSort")
}
}
#Published var sortKey: String {
didSet {
UserDefaults.standard.set(sortKey, forKey: "sortKey")
}
}
public var sortKeys = ["alphabetical", "length", "newest", "oldest"]
init() {
self.shouldSort = UserDefaults.standard.object(forKey: "shouldSort") as? Bool ?? true
self.sortKey = UserDefaults.standard.object(forKey: "sortKey") as? String ?? "Name"
}
}
On my settings page I use the following code
#ObservedObject var userSettings = UserSettings()
...
Toggle(isOn: $userSettings.shouldSort, label: {Text("Sort Books")})
Picker(selection: $userSettings.sortKey, label: Text("Sort By"), content: {
ForEach(userSettings.sortKeys, id: \.self){ key in
Text(key)
}
})
This code changes the value just fine because if I close and open the app, the views update based on the data. I am reading the data with
#State var sorted = UserDefaults.standard.bool(forKey: "shouldSort")
#State var sort = UserDefaults.standard.string(forKey: "sortKey")
in my content view. (shouldSort calls a function to sort if true and sortKey determines how the data is sorted)
Am I reading the data wrong with the #State variable (can #State even detect changes in state of UserDefaults)?
Forget all what you learnt about UserDefaults in UIKit and say Hello to AppStorage in SwiftUI, use this Codes:
#AppStorage("shouldSort") var sorted: Bool = false
#AppStorage("sortKey") var sort: String = ""

How to pass observed properties to other classes?

I created an instance of UserData so that other classes can observe this instance and show the necessary information by using the username. What I am trying to do here is, when a user is logged in, different classes with user related stored properties will be updated ( i.e. by calling the api) from time to time according to the user activity in the app.
However, it shows the error 'Cannot use instance member 'userData' within property initializer; property initializers run before 'self' is available'. Any ideas how to solve this?
I am not sure how to pass the data from a single ObservedObject to another.
struct passingData: View {
#ObservedObject var userData = UserData()
#ObservedObject var images = ImageURL(userData: userData)
#ObservedObject var payment = Payment(userData: userData)
var body: some View {
VStack{
TextField("Enter userName", text: $userData.userName)
Text("url is \(images.imageURL)")
Text("Payment detail: \(payment.paymentDate)")
}
}
}
class Payment: ObservableObject{
#Published var paymentDate = ""
#ObservedObject var userData: UserData
init(userData: UserData){
self.userData = userData
loadPaymentDate()
}
func loadPaymentDate(){
self.paymentDate = "last payment date from \(userData.userName) is someDate "
}
}
class ImageURL: ObservableObject{
#Published var imageURL = ""
#ObservedObject var userData: UserData
init(userData: UserData){
self.userData = userData
loadImageURL()
}
func loadImageURL(){
self.imageURL = "123_\(userData.userName).com"
}
}
class UserData: ObservableObject{
#Published var userName = ""
}
Here is possible solution:
struct passingData: View {
#ObservedObject var userData: UserData
#ObservedObject var images: ImageURL
#ObservedObject var payment: Payment
init() {
let data = UserData()
self.userData = data
self.images = ImageURL(userData: data)
self.payment = Payment(userData: data)
}
// ... other code

SwiftUI: How can I catch changing value from observed object when I execute function

I have a problem with observed object in SwiftUI.
I can see changing values of observed object on the View struct.
However in class or function, even if I change text value of TextField(which is observable object) but "self.codeTwo.text still did not have changed.
here's my code sample (this is my ObservableObject)
class settingCodeTwo: ObservableObject {
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: settingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: settingCodeTwo.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
and the main problem is... "self.codeTwo.text" never changed!
class NetworkManager: ObservableObject {
#ObservedObject var codeTwo = settingCodeTwo()
#Published var posts = [Post]()
func fetchData() {
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "\(self.codeTwo.text)" //This one I want to use observable object
urlComponents.path = "/mob_json/mob_json.aspx"
urlComponents.queryItems = [
URLQueryItem(name: "nm_sp", value: "UP_MOB_CHECK_LOGIN"),
URLQueryItem(name: "param", value: "1000|1000|\(Gpass.hahaha)")
]
if let url = urlComponents.url {
print(url)
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.Table
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
and this is view, I can catch change of the value in this one
import SwiftUI
import Combine
struct SettingView: View {
#ObservedObject var codeTwo = settingCodeTwo()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: $codeTwo.text).textFieldStyle(BottomLineTextFieldStyle()).foregroundColor(.blue)
Text(codeTwo.text)
}
}
}
}
Help me please.
Non-SwiftUI Code
Use ObservedObject only for SwiftUI, your function / other non-SwiftUI code will not react to the changes.
Use a subscriber like Sink to observe changes to any publisher. (Every #Published variable has a publisher as a wrapped value, you can use it by prefixing with $ sign.
Reason for SwiftUI View not reacting to class property changes:
struct is a value type so when any of it's properties change then the value of the struct has changed
class is a reference type, when any of it's properties change, the underlying class instance is still the same.
If you assign a new class instance then you will notice that the view reacts to the change.
Approach:
Use a separate view and that accepts codeTwoText as #Binding that way when the codeTwoText changes the view would update to reflect the new value.
You can keep the model as a class so no changes there.
Example
class Model : ObservableObject {
#Published var name : String //Ensure the property is `Published`.
init(name: String) {
self.name = name
}
}
struct NameView : View {
#Binding var name : String
var body: some View {
return Text(name)
}
}
struct ContentView: View {
#ObservedObject var model : Model
var body: some View {
VStack {
Text("Hello, World!")
NameView(name: $model.name) //Passing the Binding to name
}
}
}
Testing
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let model = Model(name: "aaa")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
model.name = "bbb"
}
return ContentView(model: model)
}
}
It is used two different instances of SettingCodeTwo - one in NetworkNamager another in SettingsView, so they are not synchronised if created at same time.
Here is an approach to keep those two instances self-synchronised (it is possible because they use same storage - UserDefaults)
Tested with Xcode 11.4 / iOS 13.4
Modified code below (see also important comments inline)
extension UserDefaults {
#objc dynamic var textKey2: String { // helper keypath
return string(forKey: "textKey2") ?? ""
}
}
class SettingCodeTwo: ObservableObject { // use capitalised name for class !!!
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: SettingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
private var observer: NSKeyValueObservation!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: SettingCodeTwo.userDefaultTextKey)
}
observer = UserDefaults.standard.observe(\.textKey2, options: [.new]) { _, value in
if let newValue = value.newValue, self.text != newValue { // << avoid cycling on changed self
self.text = newValue
}
}
}
}
class NetworkManager: ObservableObject {
var codeTwo = SettingCodeTwo() // no #ObservedObject needed here
...

Resources