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

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

Related

Thread 1: Fatal error: Index out of range In SwiftUI

i am trying to make a small Social Media app. the friends and friendrequest gets stored as User in different arrays. But when i want to loop the array it an shows which user send a request it first works but when i accept the user and he is remove from the Array i am getting this error "Thread 1: Fatal error: Index out of range" i know its because the loop wants to loop to a index which doesn't exist anymore but how do i fix it ?
struct FriendsView: View {
#EnvironmentObject var appUser: User
var body: some View {
List {
ForEach(0..<appUser.friendAnfrage.count) {
durchlauf in
SingleFriendView(user: appUser.friendAnfrage[durchlauf])
}
}
}
}
class User: ObservableObject{
#Published var username: String = ""
#Published var name: String = ""
var password: String = ""
#Published var email: String = ""
#Published var beschreibung: String = ""
#Published var profilBild: UIImage?
#Published var friends = [User]()
#Published var friendAnfrage = [User]()
#Published var anfrageGesendet = [User]()
#Published var feed = [SinglePostView]()
func addFriend(friend: User,appUser: User) {
friend.friendAnfrage.append(appUser)
appUser.anfrageGesendet.append(friend)
}
func newFriend(newFriend: User) {
friends.append(newFriend)
for i in 0..<friendAnfrage.count {
if friendAnfrage[i].username == newFriend.username {
friendAnfrage.remove(at: i)
}
}
}
func friendAnfrage(friend: User,appUser: User) {
appUser.friendAnfrage.append(friend)
}
func makePost(image: UIImage,appUser: User) {
feed.append(SinglePostView(bild: image, ersteller: appUser))
for i in 0..<friends.count {
friends[i].feed.append(SinglePostView(bild: image, ersteller: appUser))
}
}
}
ForEach with an index-based approach is dangerous in SwiftUI. Instead, make your model identifiable.
class User: ObservableObject, Identifiable {
var id = UUID()
//...
Then, change your loop:
ForEach(appUser.friendAnfrage) { item in
SingleFriendView(user: item)
}
Unrelated to this exact issue, but generally SwiftUI does better with using a struct for a model instead of a class. If a User in friends is updated with your current code, because it's a nested ObservableObject, your View will not get automatically updated.
User should be a struct and ForEach isn't a traditional loop, it's a View that must be supplied identifiable data, e.g.
struct FriendsView: View {
#EnvironmentObject var model: Model
var body: some View {
List {
ForEach($model.users) { $user in
SingleFriendView(user: $user)
}
}
}
}
struct User: Identifiable{
let id = UUID()
var username: String = ""
var friends: [UUID] = []
}
class Model: ObservableObject {
#Published var users: [User] = []
}

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 data from an observed class to another?

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

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
...

Subscribing to changes to #Published

I am trying to bind the value of query to a search box sitting in a SwiftUI view.
class DataSet: ObservedObject {
...
#Published var query: String = ""
init() {
let sub = AnySubscriber<String, Never>(
receiveSubscription: nil,
receiveValue: { query in
print(query)
return .unlimited
})
self.$query.subscribe(sub)
}
...
}
When the user changes the value of the query I'd like to filter some other property in my ObservedObject. Yet I cannot find anywhere in the documentation how do I subscribe to changes to query property.
I would use the following approach
class DataSet: ObservableObject {
#Published var query: String = ""
private var subscribers = Set<AnyCancellable>()
init() {
self.$query
.sink { newQuery in
// do something here with newQuery
}
.store(in: &subscribers)
}
}

Resources