SwiftUI ViewModel update - swiftui

I have a View with many Buttons and if the User tap on a Button the Viewmodel need to update the current Button with the increased value.
class ProductVM: ObservableObject {
#Published var product : Product
init(product: Product) {
self.product = product
}
public func increaseAmount() {
var myInt = Int(self.product.amount) ?? 0
myInt += 1
self.product.amount = String(myInt)
print(myInt)
print("...")
}
}
the problem is the myInt is every time just 1 and the value can't be updated.
HOW can i update the value and save it in the current Model so that the View know its increased ??!!
struct singleButtonView: View {
#ObservedObject var productVM : ProductVM
func updatePos(){
self.productVM.increaseAmount()
}
}
and i call it with
singleButtonView(productVM: ProductVM(product: product))

Nested ObservableObjects need to be updated manually. Here is an example how this could look like:
class Product: ObservableObject, Identifiable, Codable {
let id: Int
let name: String
let prize: Double
#Published var amount: Int = 0
enum CodingKeys: String, CodingKey {
case id
case name
case prize
case amount
}
init() {
self.id = 0
self.name = "name"
self.prize = 0
}
init(id: Int, name: String, prize: Double) {
self.id = id
self.name = name
self.prize = prize
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(Int.self, forKey: .id)
name = try values.decode(String.self, forKey: .name)
prize = try values.decode(Double.self, forKey: .prize)
amount = try values.decode(Int.self, forKey: .amount)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(prize, forKey: .prize)
try container.encode(amount, forKey: .amount)
}
}
class ProductVM: ObservableObject {
#Published var product: Product
var cancelable: AnyCancellable? = nil
init(product: Product) {
self.product = product
self.cancelable = product.objectWillChange.sink(receiveValue: {
self.objectWillChange.send()
})
}
public func increaseAmount() {
self.product.amount += 1
}
}
struct ContentView: View {
#ObservedObject var productVM = ProductVM(product: Product())
var body: some View {
VStack {
Button(action: {
self.productVM.increaseAmount()
}) {
Text("Add")
}
Text("\(self.productVM.product.amount)")
}
}
}
I hope this helps!
Credits

Related

Saving favorites to UserDefaults using struct id

I'm trying to save the users favorite cities in UserDefaults. Found this solution saving the struct ID - builds and runs but does not appear to be saving: On app relaunch, the previously tapped Button is reset.
I'm pretty sure I'm missing something…
Here's my data struct and class:
struct City: Codable {
var id = UUID().uuidString
var name: String
}
class Favorites: ObservableObject {
private var cities: Set<String>
let defaults = UserDefaults.standard
var items: [City] = [
City(name: "London"),
City(name: "Paris"),
City(name: "Berlin")
]
init() {
let decoder = PropertyListDecoder()
if let data = defaults.data(forKey: "Favorites") {
let cityData = try? decoder.decode(Set<String>.self, from: data)
self.cities = cityData ?? []
return
} else {
self.cities = []
}
}
func getTaskIds() -> Set<String> {
return self.cities
}
func contains(_ city: City) -> Bool {
cities.contains(city.id)
}
func add(_ city: City) {
objectWillChange.send()
cities.contains(city.id)
save()
}
func remove(_ city: City) {
objectWillChange.send()
cities.remove(city.id)
save()
}
func save() {
let encoder = PropertyListEncoder()
if let encoded = try? encoder.encode(tasks) {
defaults.setValue(encoded, forKey: "Favorites")
}
}
}
and here's the TestDataView
struct TestData: View {
#StateObject var favorites = Favorites()
var body: some View {
ForEach(self.favorites.items, id: \.id) { item in
VStack {
Text(item.title)
Button(action: {
if self.favorites.contains(item) {
self.favorites.remove(item)
} else {
self.favorites.add(item)
}
}) {
HStack {
Image(systemName: self.favorites.contains(item) ? "heart.fill" : "heart")
.foregroundColor(self.favorites.contains(item) ? .red : .white)
}
}
}
}
}
}
There were a few issues, which I'll address below. Here's the working code:
struct ContentView: View {
#StateObject var favorites = Favorites()
var body: some View {
VStack(spacing: 10) {
ForEach(Array(self.favorites.cities), id: \.id) { item in
VStack {
Text(item.name)
Button(action: {
if self.favorites.contains(item) {
self.favorites.remove(item)
} else {
self.favorites.add(item)
}
}) {
HStack {
Image(systemName: self.favorites.contains(item) ? "heart.fill" : "heart")
.foregroundColor(self.favorites.contains(item) ? .red : .black)
}
}
}
}
}
}
}
struct City: Codable, Hashable {
var id = UUID().uuidString
var name: String
}
class Favorites: ObservableObject {
#Published var cities: Set<City> = []
#Published var favorites: Set<String> = []
let defaults = UserDefaults.standard
var initialItems: [City] = [
City(name: "London"),
City(name: "Paris"),
City(name: "Berlin")
]
init() {
let decoder = PropertyListDecoder()
if let data = defaults.data(forKey: "Cities") {
cities = (try? decoder.decode(Set<City>.self, from: data)) ?? Set(initialItems)
} else {
cities = Set(initialItems)
}
self.favorites = Set(defaults.array(forKey: "Favorites") as? [String] ?? [])
}
func getTaskIds() -> Set<String> {
return self.favorites
}
func contains(_ city: City) -> Bool {
favorites.contains(city.id)
}
func add(_ city: City) {
favorites.insert(city.id)
save()
}
func remove(_ city: City) {
favorites.remove(city.id)
save()
}
func save() {
let encoder = PropertyListEncoder()
if let encoded = try? encoder.encode(self.cities) {
self.defaults.set(encoded, forKey: "Cities")
}
self.defaults.set(Array(self.favorites), forKey: "Favorites")
defaults.synchronize()
}
}
Issues with the original:
The biggest issue was that items was getting recreated on each new launch and City has an id that is assigned a UUID on creation. This guaranteed that every new launch, each batch of cities would have different UUIDs, so a saving situation would never work.
There were some general typos and references to properties that didn't actually exist.
What I did:
Made cities and favorites both #Published properties so that you don't have to call objectWillChange.send by hand
On init, load both the cities and the favorites. That way, the cities, once initially created, will always have the same UUIDs, since they're getting loaded from a saved state
On save, I save both Sets -- the favorites and the cities
In the original ForEach, I iterate through all of the cities and then only mark the ones that are part of favorites
Important note: While testing this, I discovered that at least on Xcode 12.3 / iOS 14.3, syncing to UserDefaults is slow, even when using the now-unrecommended synchronize method. I kept wondering why my changes weren't reflected when I killed and then re-opened the app. Eventually figured out that everything works if I give it about 10-15 seconds to sync to UserDefaults before killing the app and then opening it again.

SwiftUI: How to publish a variable in a class member object (another instance of a class) and update UI in View

I have a class PlayAudio to read an audio file and play. In PlayAudio, I have #objc updateUI function to add to CADisplayLink. I have another class Updater where I initialize and control isPaused of CADisplayLink. I've instantiated #Published var playAudio: PlayAudio so I can call it from View as updater.playAudio. My question is, although I can print playAudio.positionSliderValue real time in active CADisplayLink, playAudio.positionSliderValue does not update the UI in View. How can I achieve it? I want to activate and deActivate CADisplayLink from a separate class to maintain weak ownership (If I'm not mistaken...).
When #State var volume is updated, volume slider also updates, so I think I'm successfully updating the value itself, but I can't figure it out that update to trigger updates in UI. Any thoughts or suggestions are appreciated. Thanks.
import SwiftUI
import AVKit
struct ContentView: View {
#ObservedObject var updater = Updater()
#State var volume = 0.0
var body: some View {
Text("\(volume)")
VStack {
Slider(value:
// in order to get continuous value changes, I do this instead of $updater.playAudio.volumeSliderValue
Binding(get: {
updater.playAudio.volumeSliderValue
}, set: { (newValue) in
updater.playAudio.volumeSliderValue = newValue
updater.playAudio.setVolume()
volume = newValue
})
, in: 0...1)
Button(action: {
updater.playAudio.play()
// activate CADisplayLink
updater.activate()
// run CADisplayLink
updater.updater?.isPaused = false
}, label: {
Text("Play File")
})
Slider(value:
// in order to get continuous value changes, I do this instead of $playAudio.positionSliderValue
Binding(get: {
updater.playAudio.positionSliderValue
}, set: { (newValue) in
updater.playAudio.positionSliderValue = newValue
updater.playAudio.seek()
})
, in: 0.0...updater.playAudio.positionSliderTotal) { _ in
updater.playAudio.seek()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class Updater: ObservableObject {
var updater: CADisplayLink?
#Published var playAudio: PlayAudio
init(){
self.playAudio = PlayAudio()
self.updater = CADisplayLink(target: playAudio, selector: #selector(playAudio.updateUI))
}
func activate() {
self.updater?.add(to: .main, forMode: .default)
}
func deActivate() {
self.updater?.invalidate()
}
}
class PlayAudio: ObservableObject {
var sampleRate = Double()
var totalFrame = AVAudioFramePosition()
var startTime = AVAudioTime()
var newFramePosition = AVAudioFramePosition()
let url = Bundle.main.urls(forResourcesWithExtension: "mp4", subdirectory: nil)?.first
var audioFile = AVAudioFile()
var engine = AVAudioEngine()
var avAudioPlayerNode = AVAudioPlayerNode()
#Published var volumeSliderValue: Double = 0.7
#Published var positionSliderTotal: Double = 0.0
#Published var positionSliderValue: Double = 0.0
#objc func updateUI() {
positionSliderValue = Double(currentFrame)
// this prints ok, but I want it to update the UI in the View
print(positionSliderValue)
}
init () {
readFile()
schedulePlayer()
getTotalFrameDouble()
}
var currentFrame: AVAudioFramePosition {
guard let lastRenderTime = avAudioPlayerNode.lastRenderTime,
let playerTime = avAudioPlayerNode.playerTime(forNodeTime: lastRenderTime)
else {
return 0
}
return playerTime.sampleTime + newFramePosition
}
func getTotalFrameDouble() {
positionSliderTotal = Double(totalFrame)
print(positionSliderValue)
}
func readFile() {
guard let url = url else {
return
}
do {
self.audioFile = try AVAudioFile(forReading: url)
} catch let error {
print(error)
}
self.sampleRate = audioFile.processingFormat.sampleRate
self.totalFrame = audioFile.length
}
func setupEngine() {
engine.attach(avAudioPlayerNode)
engine.connect(avAudioPlayerNode, to: engine.mainMixerNode, format: audioFile.processingFormat)
engine.prepare()
do {
try engine.start()
} catch let error {
print(error)
}
}
func schedulePlayer() {
newFramePosition = 0
engine.reset()
setupEngine()
avAudioPlayerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
}
func play() {
let outputFormat = avAudioPlayerNode.outputFormat(forBus: AVAudioNodeBus(0))
let lastRenderTime = avAudioPlayerNode.lastRenderTime?.sampleTime ?? 0
// need to convert from AVAudioFramePosition to AVAudioTime
startTime = AVAudioTime(sampleTime: AVAudioFramePosition(Double(lastRenderTime)), atRate: Double(outputFormat.sampleRate))
avAudioPlayerNode.play(at: startTime)
}
func seek() {
// player time (needs to be converted to player node time
newFramePosition = AVAudioFramePosition(positionSliderValue)
let framesToPlay = totalFrame - newFramePosition
avAudioPlayerNode.stop()
if framesToPlay > 100 {
avAudioPlayerNode.scheduleSegment(audioFile, startingFrame: newFramePosition, frameCount: AVAudioFrameCount(framesToPlay), at: nil, completionHandler: nil)
}
play()
}
func setVolume() {
avAudioPlayerNode.volume = Float(volumeSliderValue)
}
}

SwiftUI can not update class data updates

I came across a situation that you use class data as your data source, and display them in a swiftUI list view, when you update your data source, the swiftUI list view won't be updated, what can we do to make the class data updates interactive with swiftUI?
see code blow:
I define the environment object :
import Foundation
import Combine
class DataSource: ObservableObject {
public static let shared = DataSource()
#Published var datalist: [RowData] = []
func fetch() -> Void {
for n in 1...50 {
let data = RowData(title: "Index:\(n)", count: 0)
datalist.insert(data, at: 0)
}
}
func update() {
for data in datalist {
data.count = data.count+1
print("\(data.title) update count to :\(data.count)")
data.objectWillChange.send()
}
self.objectWillChange.send()
}
}
to display each data in a Row View:
import SwiftUI
struct RowView: View {
#State var data: RowData
var body: some View {
HStack{
Text(data.title)
Spacer()
Text("\(data.count)")
}.padding()
}
}
struct RowView_Previews: PreviewProvider {
static var previews: some View {
RowView(data: RowData(title: "text", count: 1))
}
}
class RowData: ObservableObject {
var title: String = ""
var count: Int = 0
init(title: String, count: Int) {
self.title = title
self.count = count
}
}
in content view, display the data in a list view, I would like to refresh all the view updates when click update button. the button triggers the update methods to update the class data value from data source.
struct ContentView: View {
#EnvironmentObject var data: DataSource
#State var shouldUpdate:Bool = false
#State var localData:[RowData] = []
var body: some View {
VStack {
Button(action: {
// your action here
self.data.update()
self.shouldUpdate.toggle()
self.localData.removeAll()
self.localData = self.data.datalist
}) {
Text("update")
}
List {
ForEach(0..<self.localData.count, id:\.self) { index in
RowView(data: self.localData[index])
}
}
}
}
}
Well... I don't see the reason to have localData, but, anyway, here is modified code that works.
Tested with Xcode 12 / iOS 14
class DataSource: ObservableObject {
public static let shared = DataSource()
#Published var datalist: [RowData] = []
func fetch() -> Void {
for n in 1...50 {
let data = RowData(title: "Index:\(n)", count: 0)
datalist.insert(data, at: 0)
}
}
func update() {
for data in datalist {
data.count = data.count+1
print("\(data.title) update count to :\(data.count)")
}
self.objectWillChange.send()
}
}
struct RowView: View {
#ObservedObject var data: RowData
var body: some View {
HStack{
Text(data.title)
Spacer()
Text("\(data.count)")
}.padding()
}
}
class RowData: ObservableObject {
#Published var title: String = ""
#Published var count: Int = 0
init(title: String, count: Int) {
self.title = title
self.count = count
}
}
struct ContentView: View {
#EnvironmentObject var data: DataSource
#State var localData:[RowData] = []
var body: some View {
VStack {
Button(action: {
// your action here
self.data.update()
self.localData = self.data.datalist
}) {
Text("update")
}
List {
ForEach(0..<self.localData.count, id:\.self) { index in
RowView(data: self.localData[index])
}
}
}
.onAppear {
self.data.fetch()
self.localData = self.data.datalist
}
}
}

SwiftUI Picker desn't bind with ObservedObject

I'm trying to fill up a Picker with data fetched asynchronously from external API.
This is my model:
struct AppModel: Identifiable {
var id = UUID()
var appId: String
var appBundleId : String
var appName: String
var appSKU: String
}
The class that fetches data and publish is:
class AppViewModel: ObservableObject {
private var appStoreProvider: AppProvider? = AppProvider()
#Published private(set) var listOfApps: [AppModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
self.loading = true
appStoreProvider?.dataProviderAppList { [weak self] (appList: [AppModel]) in
guard let self = self else {return}
DispatchQueue.main.async() {
self.listOfApps = appList
self.loading = false
}
}
}
init() {
fetchAppList()
}
}
The View is:
struct AppView: View {
#ObservedObject var appViewModel: AppViewModel = AppViewModel()
#State private var selectedApp = 0
var body: some View {
ActivityIndicatorView(isShowing: self.appViewModel.loading) {
VStack{
// The Picker doesn't bind with appViewModel
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName).tag(app.appName)
}
}
// The List correctly binds with appViewModel
List {
ForEach(self.appViewModel.listOfApps){ app in
Text(app.appName.capitalized)
}
}
}
}
}
}
While the List view binds with the observed object appViewModel, the Picker doesn't behave in the same way. I can't realize why. Any help ?
I filed bug report, FB7670992. Apple responded yesterday, suggesting that I confirm this behavior in iOS 14, beta 1. It appears to now have been resolved.
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
Picker("", selection: $viewModel.wheelPickerValue) {
ForEach(viewModel.objects) { object in
Text(object.string)
}
}
.pickerStyle(WheelPickerStyle())
.labelsHidden()
}
}
Where
struct Object: Identifiable {
let id = UUID().uuidString
let string: String
}
class ViewModel: ObservableObject {
private var counter = 0
#Published private(set) var objects: [Object] = []
#Published var segmentedPickerValue: String = ""
#Published var wheelPickerValue: String = ""
fileprivate func nextSetOfValues() {
let newCounter = counter + 3
objects = (counter..<newCounter).map { value in Object(string: "\(value)") }
let id = objects.first?.id ?? ""
segmentedPickerValue = id
wheelPickerValue = id
counter = newCounter
}
init() {
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
guard let self = self else { timer.invalidate(); return }
self.nextSetOfValues()
}
timer.fire()
}
}
Results in:
I can't put this into your code because it is incomplete but here is a sample.
Pickers aren't meant to be dynamic. They have to be completely reloaded.
class DynamicPickerViewModel: ObservableObject {
#Published private(set) var listOfApps: [YourModel] = []
#Published private(set) var loading = false
fileprivate func fetchAppList() {
loading = true
DispatchQueue.main.async() {
self.listOfApps.append(YourModel.addSample())
self.loading = false
}
}
init() {
fetchAppList()
}
}
struct DynamicPicker: View {
#ObservedObject var vm = DynamicPickerViewModel()
#State private var selectedApp = ""
var body: some View {
VStack{
//Use your loading var to reload the picker when it is done
if !vm.loading{
//Picker is not meant to be dynamic, it needs to be completly reloaded
Picker(selection: self.$selectedApp, label: Text("")) {
ForEach(self.vm.listOfApps){ app in
Text(app.name!).tag(app.name!)
}
}
}//else - needs a view while the list is being loaded/loading = true
List {
ForEach(self.vm.listOfApps){ app in
Text(app.name!.capitalized)
}
}
Button(action: {
self.vm.fetchAppList()
}, label: {Text("fetch")})
}
}
}
struct DynamicPicker_Previews: PreviewProvider {
static var previews: some View {
DynamicPicker()
}
}

SwiftUI Update Model with Button and Increase amount

i want to increase a product of its tapped count. so if the user press a button the Model needs to be updated and the amount += 1
I try the following code and the model does not update the amount.
struct GridCell: View {
#EnvironmentObject var order: Order
class ViewModel: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var product : Product {
willSet {
self.objectWillChange.send()
}
}
init(product: Product) {
self.product = product
}
public func increaseAmount() {
self.product.amount += 1
self.objectWillChange.send()
print("\(self.product.amount) / \(self.product.id)")
}
}
func updatePos(){
self.viewModel.increaseAmount()
let itempreis = viewModel.product.preis
order.totalAmount += itempreis
print(order.totalAmount)
}
#ObservedObject var viewModel: ViewModel
var body: some View {
Button(action: {
print("TAPPED")
self.updatePos()
}) {
...
}
}
and GridCell would be
GridCell(viewModel: GridCell.ViewModel(product: $0))
and Product is
struct Product : Identifiable, Equatable, Decodable {
var id: String
var name: String
let preis: Double
var amount: Int
}
In my print statement the amount is not updating and every time I press the button the value is like 1.
What did I forget?
Try this
public func increaseCharacterStrength() {
self.product.amount += 1
}
Because Product is a struct when you were creating the variable var viewproduct = self.product you were creating a local copy of product, so viewproduct.amount += 1 just increases the amount for that local variable, not for the actual model.
I think you are not defining view model correctly. Since I do not have the structure of the product and order, I simplified the model to meet the needs of our discussion. For the example provided below, you can create the instance of GridCell (like GridCell()) and tapping the button increases the quantity by 1. Also note one can simplify further by using #Published property wrapper in the view model instead of Passthrough
class Order: ObservableObject {
#Published var totalAmount : Int = 0
}
struct Product {
var id = UUID()
var amount: Int = 0
var preis: Int = 0
}
struct GridCell: View {
#EnvironmentObject var order: Order
func updatePos() {
self.viewModel.increaseCharacterStrength()
let itempreis = self.viewModel.product.preis
order.totalAmount += itempreis
//print(order.totalAmount)
print("---- \(self.viewModel.product.amount) / \(self.viewModel.product.id)")
}
#ObservedObject var viewModel = ViewModel(product: Product())
var body: some View {
Button(action: {
print("TAPPED")
self.updatePos()
}) {
Text("Increase order")
}
}
}
class ViewModel: ObservableObject {
let objectWillChange = PassthroughSubject < Void,
Never > ()
var product: Product {
willSet {
self.objectWillChange.send()
}
}
init(product: Product) {
self.product = product
}
public func increaseCharacterStrength() {
self.product.amount += 1
self.objectWillChange.send()
}
}