skip to Main Content

I’m currently learning SwiftUI and want to develop my own app. I have designed a LoginView and a LoginHandler that should take care of all the logic behind a login. When the user enters the wrong username/password, an Alert should appear on the screen. I solved this with the state variable loginError. But now comes the tricky part, as i want to pass a binding of this variable to my login function in the LoginHandler. Take a look at the following code:


import SwiftUI

struct LoginView: View
{
    @EnvironmentObject var loginHandler: LoginHandler
    
    @State private var username: String = ""
    @State private var password: String = ""
    @State private var loginError: Bool = false
    
    ...
    
    private func login()
    {
        loginHandler.login(username: username, password: password, error: $loginError)
    }
}

I am now trying to change the value of error inside my login function:

import Foundation
import SwiftUI

class LoginHandler: ObservableObject
{
    public func login(username: String, password: String, error: Binding<Bool>)
    {
        error = true
    }
}

But I’m getting the error

Cannot assign to value: ‘error’ is a ‘let’ constant

which makes sense I think because you can’t edit the parameters in swift. I have also tried _error = true because I once saw the underscore in combination with a binding, but this doesn’t worked either.

But then I came up with a working solution: error.wrappedValue = true. My only problem with that is the following statement from Apples Developer Documentation:

This property provides primary access to the value’s data. However, you don’t access wrappedValue directly. Instead, you use the property variable created with the @Binding attribute.

Although I’m super happy that it works, I wonder if there is any better way to solve this situation?

Update 20.3.21: New edge case

In the comment section I mentioned a case where you don’t know how many times your function will be used. I will now provide a little code example:

Imagine a list of downloadable files (DownloadView) that you will get from your backend:

import SwiftUI

struct DownloadView: View
{
    @EnvironmentObject var downloadHandler: DownloadHandler
    
    var body: some View
    {
        VStack
        {
            ForEach(downloadHandler.getAllDownloadableFiles())
            {
                file in DownloadItemView(file: file)
            }
        }
    }
}

Every downloadable file has a name, a small description and its own download button:

import SwiftUI

struct DownloadItemView: View
{
    @EnvironmentObject var downloadHandler: DownloadHandler
    
    @State private var downloadProgress: Double = -1
    
    var file: File
    
    var body: some View
    {
        HStack
        {
            VStack
            {
                Text(file.name)
                Text(file.description)
            }
            
            Spacer()
            
            if downloadProgress < 0
            {
                // User can start Download
                Button(action: {
                    downloadFile()
                })
                {
                    Text("Download")
                }
            }
            else
            {
                // User sees download progress
                ProgressView(value: $downloadProgress)
            }
        }
    }
    
    func downloadFile()
    {
        downloadHandler.downloadFile(file: file, progress: $downloadProgress)
    }
}

And now finally the ‘DownloadHandler’:

import Foundation
import SwiftUI

class DownloadHandler: ObservableObject
{
    public func downloadFile(file: File, progress: Binding<Double>)
    {
        // Example for changing the value
        progress = 0.5
    }
}

2

Answers


  1. You can update parameters of a function as well, here is an example, this not using Binding or State, it is inout!

    I am now trying to change the value of error inside my login function:

    Cannot assign to value: ‘error’ is a ‘let’ constant

    So with this method or example you can!


    struct ContentView: View {
        
        @State private var value: String = "Hello World!"
        
        var body: some View {
            
            Text(value)
                .padding()
            
            Button("update") {
                
                testFuction(value: &value)
            }
            
        }
    }
    
    
    func testFuction(value: inout String) {
        
        value += " updated!"
    }
    
    Login or Signup to reply.
  2. I see what you’re trying to do, but it will cause problems later on down the line because you’re dealing with State here. Now one solution would be:

    • You could just abstract the error to the class, but then you would have the username and password in one spot and the error in another.

    The ideal solution then is to abstract it all away in the same spot. Take away all of the properties from your view and have it like this:

       import SwiftUI
    
    struct LoginView: View
    {
        @EnvironmentObject var loginHandler: LoginHandler
        
    
       
       // login() <-- Call this when needed
        ...
        
        
    }
    

    Then in your class:

        import Foundation
        import SwiftUI
        
      @Published error: Bool = false
       var username = ""
       var password = ""
    
        class LoginHandler: ObservableObject
        {
            
           
    
           public func login()    {
               //If you can't login then throw your error here
               self.error = true
       }
        }
    

    The only left for you to do is to update the username and password` and you can do that with this for example

    TextField("username", text: $loginHandler.username)
    TextField("username", text: $loginHandler.password)
    

    Edit: Adding an update for the edge case:

      import SwiftUI
    
    struct ContentView: View {
        var body: some View {
            LazyVGrid(columns: gridModel) {
            
                ForEach(0..<20) { x in
                    CustomView(id: x)
                }
            }
        }
        let gridModel = [GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10),
                         GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10),
                         GridItem(.adaptive(minimum: 100, maximum: 100), spacing: 10)
        ]
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
    struct CustomView: View {
        @State private var downloaded = false
        @State private var progress = 0
        @ObservedObject private var viewModel = StateManager()
        let id: Int
        
    
        
        var body: some View {
            showAppropriateView()
        }
        @ViewBuilder private func showAppropriateView() -> some View {
            if viewModel.downloadStates[id] == true {
                VStack {
                    Circle()
                        .fill(Color.blue)
                        .frame(width: 50, height: 50)
                    
                    
                }
            } else {
                Button("Download") {
                    downloaded = true
                    viewModel.saveState(of: id, downloaded)
                }
            }
        }
    }
    
    final class StateManager: ObservableObject {
        @Published var downloadStates: [Int : Bool] = [:] {
            didSet { print(downloadStates)}
        }
        
        func saveState(of id: Int,_ downloaded: Bool) {
            downloadStates[id] = downloaded
        }
    }
    

    I didn’t add the progress to it because I’m short on time but I think this conveys the idea. You can always abstract away the individual identity needed by other views.

    Either way, let me know if this was helpful.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search