KVO publisher printing recursively in debugger - swift

Why does the following LoginDateMonitorView result in hundreds of messages in the debugger?
2022-10-13 06:31:03.959864+0100 Test[4412:126282] [SwiftUI] Publishing changes from within view updates is not allowed, this will cause undefined behavior.
protocol PropertyObserver: ObservableObject {
associatedtype T: NSObject
associatedtype Value
var object: T { get }
var property: KeyPath<T, Value> { get }
}
extension PropertyObserver {
var objectWillChange: AnyPublisher<Value, Never> {
object.publisher(for: property).eraseToAnyPublisher()
}
}
final class User: NSObject {
#objc dynamic var lastLogin: Date = Date(timeIntervalSince1970: 9)
}
final class UserLoginMonitor: ObservableObject {
let user: User
init(_ user: User) {
self.user = user
}
}
extension UserLoginMonitor: PropertyObserver {
var object: User {
user
}
var property: KeyPath<User, Date> {
\.lastLogin
}
}
struct LoginDateMonitorView: View {
#StateObject private var monitor: UserLoginMonitor
init(user: User) {
_monitor = StateObject(wrappedValue: UserLoginMonitor(user))
}
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}

Related

SwiftUI toggles not changing when clicked on

I'm trying to build a simple SwiftUI view that displays a number of toggles. While I can get everything to display ok, I cannot get the toggles to flip. Here's is a simplified code example:
import SwiftUI
class StoreableParam: Identifiable {
let name: String
var id: String { name }
#State var isEnabled: Bool
let toggleAction: ((Bool) -> Void)?
init(name: String, isEnabled: Bool, toggleAction: ((Bool) -> Void)? = nil) {
self.name = name
self.isEnabled = isEnabled
self.toggleAction = toggleAction
}
}
class StoreableParamViewModel: ObservableObject {
#Published var storeParams: [StoreableParam] = []
init() {
let storedParam = StoreableParam(name: "Stored Param", isEnabled: false) { value in
print("Value changed")
}
storeParams.append(storedParam)
}
}
public struct UBIStoreDebugView: View {
#StateObject var viewModel: StoreableParamViewModel
public var body: some View {
VStack {
List {
ForEach(viewModel.storeParams, id: \.id) { storeParam in
Toggle(storeParam.name, isOn: storeParam.$isEnabled)
.onChange(of: storeParam.isEnabled) {
storeParam.toggleAction?($0)
}
}
}
}.navigationBarTitle("Toggle Example")
}
}
As mentioned in the comments, there are a couple of things going on:
#State is only for use in a View
Your model should be a struct
Then, you can get a Binding using the ForEach element binding syntax:
struct StoreableParam: Identifiable {
let name: String
var id: String { name }
var isEnabled: Bool
let toggleAction: ((Bool) -> Void)?
}
class StoreableParamViewModel: ObservableObject {
#Published var storeParams: [StoreableParam] = []
init() {
let storedParam = StoreableParam(name: "Stored Param", isEnabled: false) { value in
print("Value changed")
}
storeParams.append(storedParam)
}
}
public struct UBIStoreDebugView: View {
#StateObject var viewModel: StoreableParamViewModel
public var body: some View {
VStack {
List {
ForEach($viewModel.storeParams, id: \.id) { $storeParam in
Toggle(storeParam.name, isOn: $storeParam.isEnabled)
.onChange(of: storeParam.isEnabled) {
storeParam.toggleAction?($0)
}
}
}
}
}
}

NavigationLink In NavigationStack is disabled

I wrote test code for NavigationStack. The behavior of the code is a two-step transition(ContentView -> SubsubTestView -> DetailView).
But I got an error when I have selected a name in SubsubTestView.
A NavigationLink is presenting a value of type “User” but there is no matching navigationDestination declaration visible from the location of the link. The link cannot be activated.
Is there anything wrong with the wrong part of the code?
ContentView.swift
import SwiftUI
class EnvObj: ObservableObject {
#Published var users = [User(name: "a"), User(name: "b"), User(name: "c")]
}
struct User: Hashable, Identifiable, Equatable {
var id = UUID()
var name = ""
static func == (lhs: User, rhs: User) -> Bool{
return lhs.id == rhs.id
}
}
struct ContentView: View {
#EnvironmentObject var envObj: EnvObj
#State var moveToSubsub = false
var body: some View {
NavigationStack {
Button("To subsub") {
moveToSubsub = true
}
.navigationDestination(isPresented: $moveToSubsub) {
SubsubTestView(vm: VM(envObj: envObj))
}
}
}
}
struct SubsubTestView: View {
#EnvironmentObject var envObj: EnvObj
#StateObject var vm: VM
var body: some View {
VStack {
List(self.vm.envObj.users) { user in
NavigationLink(value: user) {
Text(user.name)
}
}
.navigationDestination(for: User.self) { user in
DetailView(vm: VMD(envObj: envObj, selectedUser: user))
}
}
}
}
class VM: ObservableObject {
var envObj: EnvObj = .init()
init(envObj: EnvObj) {
self.envObj = envObj
}
}
struct DetailView: View {
#StateObject var vm: VMD
var body: some View {
VStack {
TextField("Name: ", text: (self.$vm.selectedUser ?? User()).name)
Text(self.vm.selectedUser?.name ?? User().name)
Button("submit", action: self.vm.submit)
}
}
}
class VMD: ObservableObject {
var envObj: EnvObj = .init()
#Published var selectedUser: User?
init(envObj: EnvObj, selectedUser: User? = nil) {
self.envObj = envObj
self.selectedUser = selectedUser
}
private(set) lazy var submit = {
if let user = self.selectedUser {
self.update(user: user)
}
}
func update(user: User) {
self.envObj.users = self.envObj.users.map {
return $0 == user ? user : $0
}
}
}
func ??<T>(binding: Binding<T?>, fallback: T) -> Binding<T> {
return Binding(get: {
binding.wrappedValue ?? fallback
}, set: {
binding.wrappedValue = $0
})
}
Thanks,

SwiftUI MVVM simple concept confusion - advice needed

I'm struggling with some basic MVVM concepts in SwiftUI. I appreciate this is probably a simple question but my brain is frazzled I can't figure it out.
Here's my models/views/viewmodels etc.
import Foundation
struct Challenges {
var all: [Challenge]
init() {
all = []
}
}
struct Challenge: Identifiable, Codable, Hashable {
private(set) var id = UUID()
private(set) var name: String
private(set) var description: String
private(set) var gpxFile: String
private(set) var travelledDistanceMetres: Double = 0
init(name: String, description: String, gpxFile: String) {
self.name = name
self.description = description
self.gpxFile = gpxFile
}
mutating func addDistance(_ distance: Double) {
travelledDistanceMetres += distance
}
}
import SwiftUI
#main
struct ActivityChallengesApp: App {
var body: some Scene {
WindowGroup {
ChallengesView()
.environmentObject(ChallengesViewModel())
}
}
}
import SwiftUI
class ChallengesViewModel: ObservableObject {
#Published var challenges: Challenges
init() {
challenges = Challenges()
challenges.all = DefaultChallenges.ALL
}
func addDistance(_ distance: Double, to challenge: Challenge) {
challenges.all[challenge].addDistance(distance)
}
}
import SwiftUI
struct ChallengesView: View {
#EnvironmentObject var challengesViewModel: ChallengesViewModel
var body: some View {
NavigationView {
List {
ForEach(challengesViewModel.challenges.all) { challenge in
NavigationLink {
ChallengeView(challenge)
.environmentObject(challengesViewModel)
} label: {
VStack(alignment: .leading) {
Text(challenge.name)
Text("\(challenge.travelledDistanceMetres)")
}
}
}
}
.navigationTitle("Challenges")
}
}
}
import SwiftUI
struct ChallengeView: View {
var challenge: Challenge
#EnvironmentObject var challengesViewModel: ChallengesViewModel
init(_ challenge: Challenge) {
self.challenge = challenge
}
var body: some View {
VStack(alignment: .leading) {
Text(challenge.name)
Text("\(challenge.travelledDistanceMetres)")
}
.onTapGesture {
handleTap()
}
}
func handleTap() {
challengesViewModel.addDistance(40, to: challenge)
}
}
I understand the concepts but I'm confused as to what the ViewModel should be.
I feel like this is overkill, i.e. sending a model object to the view and the view model as an environment object. With this set up, I call the addDistance() function in the view model from within the view to make changes to the model.
ChallengeView(challenge)
.environmentObject(challengesViewModel)
Is it better to have a view model for the collection or one view model per model object?
This is the simplest version I could come up with.
I don't really understand the need for the challenges.all ? So I took it out.
I have
a struct for the single challenge
an observable class which is publishing the challenges array
instantiate this once with #StateObject and pass it down as you did
btw: You don't need explicit initializers for structs
this is it:
#main
struct ActivityChallengesApp: App {
// here you create your model once
#StateObject var challenges = ChallengesModel()
var body: some Scene {
WindowGroup {
ChallengesView()
.environmentObject(challenges)
}
}
}
struct Challenge: Identifiable, Codable, Hashable {
var id = UUID()
var name: String
var description: String
var gpxFile: String
var travelledDistanceMetres: Double = 0
mutating func addDistance(_ distance: Double) {
travelledDistanceMetres += distance
}
}
class ChallengesModel: ObservableObject {
#Published var challenges: [Challenge]
init() {
// Test data
challenges = [
Challenge(name: "Challenge One", description: "?", gpxFile: ""),
Challenge(name: "Challenge Two", description: "?", gpxFile: ""),
Challenge(name: "Last Challenge", description: "?", gpxFile: "")
]
}
func addDistance(_ distance: Double, to challenge: Challenge) {
// find the challenge and update it
if let i = challenges.firstIndex(where: {$0.id == challenge.id}) {
challenges[i].addDistance(distance)
}
}
}
struct ChallengesView: View {
#EnvironmentObject var challengesModel: ChallengesModel
var body: some View {
NavigationView {
List {
ForEach(challengesModel.challenges) { challenge in
NavigationLink {
ChallengeView(challenge: challenge)
.environmentObject(challengesModel)
} label: {
VStack(alignment: .leading) {
Text(challenge.name)
Text("\(challenge.travelledDistanceMetres)")
}
}
}
}
.navigationTitle("Challenges")
}
}
}
struct ChallengeView: View {
var challenge: Challenge
#EnvironmentObject var challengesModel: ChallengesModel
var body: some View {
VStack(alignment: .leading) {
Text(challenge.name)
Text("\(challenge.travelledDistanceMetres)")
}
.onTapGesture {
handleTap()
}
}
func handleTap() {
challengesModel.addDistance(40, to: challenge)
}
}

Share Data Binding Between Class and Struct View

For example, when I get data from API success or unsuccess I would like to show Alert but Alert there need isPresented type is Binding that's why I want to use concept Share Data as Binding between class and View is possible or have another way to do please help thanks advance.
#Published properties work well for sharing data between ObservableObjects and Views.
For example:
class ViewModel : ObservableObject {
struct APIError : Identifiable {
var id = UUID()
var message : String
}
#Published var error : APIError?
func apiCall() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.error = APIError(message: "Error message")
}
}
}
struct ContentView : View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Text("Hello, world!")
.alert(item: $viewModel.error) { item in
Alert(title: Text(item.message))
}
}
.onAppear {
viewModel.apiCall()
}
}
}
You could also do this with a custom Binding, but it's a little messier. The above example would definitely be my go-to.
class ViewModel : ObservableObject {
#Published var errorMessage : String?
var alertBinding : Binding<Bool> {
.init {
self.errorMessage != nil
} set: { newValue in
if !newValue { self.errorMessage = nil }
}
}
func apiCall() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.errorMessage = "Error!"
}
}
}
struct ContentView : View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
Text("Hello, world!")
.alert(isPresented: viewModel.alertBinding) {
Alert(title: Text(viewModel.errorMessage ?? "(unknown)"))
}
}
.onAppear {
viewModel.apiCall()
}
}
}

Update a row in a list (SwiftUI)

I'm an early bird in programming so I know this question can be ridiculous from the point of view of an expert but I'm stuck in this situation from several days.
I would like to update a row by using a button "Edit" (pencil) after having used another button to store the item with a TextField.
Here's the code:
class Food: Hashable, Codable, Equatable {
var id : UUID = UUID()
var name : String
init(name: String) {
self.name = name
}
static func == (lhs: Food, rhs: Food) -> Bool {
return lhs.name == rhs.name
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
}
}
class Manager: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#Published var shoppingChart: [Food] = []
init() {
let milk = Food(name: "Milk")
let coffee = Food(name: "Coffee")
shoppingChart.append(milk)
shoppingChart.append(coffee)
}
func newFood(name: String) {
let food = Food(name: name)
shoppingChart.insert(food, at: 0)
}
}
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
var body: some View {
VStack {
List {
ForEach(self.dm.shoppingChart, id:\.self) { food in
HStack {
Text(food.name)
Image(systemName: "pencil")
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}
struct Add: View {
#ObservedObject var dm : Manager
#State var fieldtext : String = ""
#Binding var isAddFoodOpened : Bool
var body: some View {
VStack {
TextField("Write a food", text: $fieldtext)
buttonSave
}
}
var buttonSave : some View {
Button(action: {
self.dm.newFood(name: self.fieldtext)
self.isAddFoodOpened = false
}) {
Text("Save")
}
}
}
The #ObservedObject var dm : Manager object is never initialized.
Try initialized dm in ContentView like this:
#ObservedObject var dm = Manager()
Ok, so if I understand correctly you want to update/edit a row by using a button "Edit".
This will do it:
struct ContentView: View {
#ObservedObject var dm : Manager
#State var isAddFoodOpened = false
#State var isEditOpened = false
#State var fieldtext : String = ""
var body: some View {
VStack {
List {
ForEach(0..<self.dm.shoppingChart.count, id:\.self) { i in
HStack {
Text(self.dm.shoppingChart[i].name)
Button(action: { self.isEditOpened.toggle() }) {
Image(systemName: "pencil")
}.sheet(isPresented: self.$isEditOpened) {
TextField(self.dm.shoppingChart[i].name, text: self.$fieldtext, onEditingChanged: { _ in
self.dm.shoppingChart[i].name = self.fieldtext
})
}
}
}
}
self.buttonAdd
}
}
var buttonAdd: some View {
Button(action: {
self.isAddFoodOpened.toggle()
}) {
Text("Add")
}
.sheet(isPresented: $isAddFoodOpened) {
Add(dm: self.dm, fieldtext: "", isAddFoodOpened: self.$isAddFoodOpened)
}
}
}