Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

'Modifying state during view update, this will cause undefined behavior.' error when typing on a textfield (SwiftUI)

Tags:

state

swiftui

I have two textfields, assigned to:

@State  private var emailAddress: String = ""
@State  private var password: String = ""

Now whenever I am typing on it, the app seems to get stuck and gives me this error:

'Modifying state during view update, this will cause undefined behavior.'

I have a StartView():

class UserSettings: ObservableObject {

var didChange = PassthroughSubject<Void, Never>()

@Published var loggedIn : Bool = false {
     didSet {
         didChange.send(())
     }
 }
}

struct StartView: View {
@EnvironmentObject var settings: UserSettings

var body: some View {
    if settings.loggedIn {
        return AnyView(TabbarView())
    }else {
        return AnyView(ContentView())
    }
}

}

I have created a ObservableObject class of UserSettings that has loggedIn bool value. When the user taps on 'Log In' button in LogInView(), this bool value becomes true and a new view appears (TabbarView())

This is LogInView():

struct LogInView: View {
@EnvironmentObject var settings: UserSettings

@State  private var emailAddress: String = ""
@State  private var password: String = ""

var body: some View {
GeometryReader { geometry in
        VStack (alignment: .center){
            HStack {
                Image("2")
                .resizable()
                .frame(width: 20, height: 20)
                Text("Social App")
                    .font(.system(size: 12))

            }.padding(.top, 30)
                .padding(.bottom, 10)

            Text("Log In to Your Account")
                .font(.title)
                .font(.system(size: 14, weight: .bold, design: Font.Design.default))
                .padding(.bottom, 50)

            TextField("Email", text: self.$emailAddress)
                .frame(width: geometry.size.width - 45, height: 50)
                .textContentType(.emailAddress)
                .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
                .accentColor(.red)
                .background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
                .cornerRadius(5)


            TextField("Password", text: self.$password)
                .frame(width: geometry.size.width - 45, height: 50)
                .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
                .foregroundColor(.gray)
                .background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
                .textContentType(.password)
                .cornerRadius(5)

             Button(action: {
                self.settings.loggedIn = true
             }) {
                    HStack {
                        Text("Log In")
                    }
                        .padding()
                        .frame(width: geometry.size.width - 40, height: 40)
                        .foregroundColor(Color.white)
                        .background(Color.blue)
                        .cornerRadius(5)
                }
                 .padding(.bottom, 40)

            Divider()

            Button(action: {
                    print("Take to forget password VC")
                    }) {
                    Text("Forgot your password?")
            }

            Spacer()

            }
            .padding(.bottom, 90)
        }
}
}

I know this error appears if I am updating the view while state is being modified (when typing in textfield). But I am not updating the view anywhere in the Log In screen. Then why this error occurs. Help will be appreciated!

like image 359
Osama Naeem Avatar asked Aug 03 '19 18:08

Osama Naeem


2 Answers

This works for me, you don't even need to import Combine! When you use @Published, SwiftUI will automatically synthesize the objectWillChange subject, and will call send whenever the property is mutated. You can still call .send() manually if you need to, but in most cases you won't.

class UserSettings: ObservableObject {
    @Published var loggedIn : Bool = false
}

Excerpt from beta 5 release notes:

You can manually conform to ObservableObject by defining an objectWillChange publisher that emits before the object changes. However, by default, ObservableObject automatically synthesizes objectWillChange and emits before any @Published properties change.

This is the full code that is working fine for me (both iPhone Xr and real device, iPad 6th Gen):

window.rootViewController = UIHostingController(rootView: ContentView().environmentObject(UserSettings()))
import SwiftUI

struct ContentView: View {
    var body: some View {
        StartView()
    }
}

class UserSettings: ObservableObject {
    @Published var loggedIn : Bool = false
}

struct StartView: View {
    @EnvironmentObject var settings: UserSettings

    var body: some View {
        if settings.loggedIn {
            return AnyView(Text("LOGGED IN"))
        } else {
            return AnyView(LogInView())
        }
    }
}

struct LogInView: View {
    @EnvironmentObject var settings: UserSettings


    @State  private var emailAddress: String = ""
    @State  private var password: String = ""

    var body: some View {
        GeometryReader { geometry in
            VStack (alignment: .center){
                HStack {
                    Image(systemName: "2.circle.fill")
                        .resizable()
                        .frame(width: 20, height: 20)
                    Text("Social App")
                        .font(.system(size: 12))

                }.padding(.top, 30)
                    .padding(.bottom, 10)

                Text("Log In to Your Account")
                    .font(.title)
                    .font(.system(size: 14, weight: .bold, design: Font.Design.default))
                    .padding(.bottom, 50)

                TextField("Email", text: self.$emailAddress)
                    .frame(width: geometry.size.width - 45, height: 50)
                    .textContentType(.emailAddress)
                    .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
                    .accentColor(.red)
                    .background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
                    .cornerRadius(5)


                TextField("Password", text: self.$password)
                    .frame(width: geometry.size.width - 45, height: 50)
                    .padding(EdgeInsets(top: 0, leading: 5, bottom: 0, trailing: 0))
                    .foregroundColor(.gray)
                    .background(Color(red: 242 / 255, green: 242 / 255, blue: 242 / 255))
                    .textContentType(.password)
                    .cornerRadius(5)

                Button(action: {
                    self.settings.loggedIn = true
                }) {
                    HStack {
                        Text("Log In")
                    }
                    .padding()
                    .frame(width: geometry.size.width - 40, height: 40)
                    .foregroundColor(Color.white)
                    .background(Color.blue)
                    .cornerRadius(5)
                }
                .padding(.bottom, 40)

                Divider()

                Button(action: {
                    print("Take to forget password VC")
                }) {
                    Text("Forgot your password?")
                }

                Spacer()

            }
            .padding(.bottom, 90)
        }
    }
}
like image 188
kontiki Avatar answered Dec 16 '22 10:12

kontiki


I guess this is a bug. This message you got is also happening on this simple view which filters out list entries by user input. Just typing fast in the text field causes this issue. If you enter the first character into the text field, the UI stuck for some time.

struct ContentView: View {
    @State private var list: [String] = (0..<500).map { "Text \($0)" }
    @State private var searchText: String = ""

    var filteredList: [String] {
        guard !searchText.isEmpty else { return list }
        return list.filter({ $0.contains(self.searchText) })
    }

    var body: some View {
        VStack {
            TextField("Search", text: $searchText)
            List(filteredList, id: \String.self) { t in Text(t) }
        }
        .padding()
    }
}

A workaround is to move the @State variables into a model. So this seems to be an issue with @State:

class Model: ObservableObject {
    @Published var list: [String] = (0..<500).map { "Text \($0)" }
    @Published var searchText: String = ""

    var filteredList: [String] {
        guard !searchText.isEmpty else { return list }
        return list.filter({ $0.contains(self.searchText) })
    }
}

struct ContentView: View {
    @ObservedObject var model: Model

    var body: some View {
        VStack {
            TextField("Search", text: $model.searchText)
            List(model.filteredList, id: \String.self) { t in Text(t) }
        }
        .padding()
    }
}
like image 44
Patrick Sturm Avatar answered Dec 16 '22 12:12

Patrick Sturm