skip to Main Content

I need to fetch data before view loads and display the data in a button text.

Locations.swift

class GetLocations :ObservableObject{

 @Published var arrLocations = NSArray()
 
   func getLocNames(Action:String, Id: String, completion: @escaping (NSArray) -> Void){

       //fetch data from server
     let session = URLSession.shared
        session.dataTask(with: request) { (data, response, error)  
    in 
if let response = response {
                    print(response)
                }
 if let data = data {
                    let parseResult: NSDictionary!
                    do {
                        parseResult = try JSONSerialization.jsonObject(with: data, options: []) as? NSDictionary
self.arrLocations = (parseResult.value(forKey: "InfoList"))as! NSArray 
                    }catch {
                       print(error)
                   }
                }
            }.resume()
           
           return self.arrLocations
    }
} 

Now I called the above function in another swiftUI file

GetLocationData.swift
    import SwiftUI
    struct GetLocationData: View {
     @State var arrloc = NSArray()
     init() {
                let g = GetLocations()
                g.getLocNames(Action: "US", Id: "ba6fcd92-3d1e-4fb6-b135-0c47ea1815cd"){locationsarray in
                    self.arrloc = locationsarray
       }
}
    var body: some View {
      VStack(spacing: 20){
        Button(action: {},
         label: {
                 Text("Select Menu"). //Here I need to assign the locationsarray I got from server and assign the valueof key to it (In obj-C we have [array objectAtIndex:0]objectForKey:@"Name"]].
 I couldnt find the equivalent of it in swift

                .frame(minWidth: 0, maxWidth: 500)
                .padding()
                .background(Color.clear)
                .foregroundColor(Color.black)
                .font(.custom("Open Sans", size: 18))
                .overlay(
                          RoundedRectangle(cornerRadius: 10)
                           .stroke(Color.gray, lineWidth: 2)
                                )

        })
      }
    }

Error in init() : Escaping closure captures mutating 'self' parameter
How can I get it solved and get data before the view loads and assign the data to button text ?

2

Answers


  1. Let’s simplify the problem. You are trying to run an asynchronous function in the init, setting the result of that to a property. The problem is that since GetLocationData hasn’t been instantiated yet, there is no instance of self to use.

    Here is a simplified example of the problem:

    struct ContentView: View {
        @State private var completed = false
    
        init() {
            someAsyncronousTask { newCompleted in // <- Error here
                completed = newCompleted
            }
        }
    
        var body: some View {
            Text("Completed: (completed ? "yes" : "no")")
        }
    
        private func someAsyncronousTask(completion: @escaping (Bool) -> Void) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                // Waited 3 seconds to simulate something like URLSession request
                completion(true)
            }
        }
    }
    

    This gives us the error, like you had:

    Escaping closure captures mutating ‘self’ parameter

    To fix this, just run this code in the onAppear of the view.

    Fixed example code:

    struct ContentView: View {
        @State private var completed = false
    
        var body: some View {
            Text("Completed: (completed ? "yes" : "no")")
                .onAppear {
                    someAsyncronousTask { newCompleted in
                        completed = newCompleted
                    }
                }
        }
    
        private func someAsyncronousTask(completion: @escaping (Bool) -> Void) {
            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                // Waited 3 seconds to simulate something like URLSession request
                completion(true)
            }
        }
    }
    

    This obviously needs to be converted into your code, but I hope this gives you the idea because of the simplified code.

    The end result should look something like this:

    struct GetLocationData: View {
        @State var arrloc = NSArray()
    
        var body: some View {
            VStack(spacing: 20){
                Button(action: {},
                       label: {
                    Text("Select Menu"). //Here I need to assign the locationsarray I got from server and assign the valueof key to it (In obj-C we have [array objectAtIndex:0]objectForKey:@"Name"]].
                    I couldnt find the equivalent of it in swift
    
                        .frame(minWidth: 0, maxWidth: 500)
                        .padding()
                        .background(Color.clear)
                        .foregroundColor(Color.black)
                        .font(.custom("Open Sans", size: 18))
                        .overlay(
                            RoundedRectangle(cornerRadius: 10)
                                .stroke(Color.gray, lineWidth: 2)
                        )
    
                })
            }
            .onAppear {
                let g = GetLocations()
                g.getLocNames(Action: "US", Id: "ba6fcd92-3d1e-4fb6-b135-0c47ea1815cd"){locationsarray in
                    self.arrloc = locationsarray
                }
            }
        }
    }
    

    Note: you should use Array rather than NSArray.

    Login or Signup to reply.
  2. The first thing you need to clear up is the separation between your model and your view.

    GetLocations is your model class. It is an ObservableObject and it has an @Published array of your location names. There is no need for the @State array. You should refer to an instance of your model object directly.

    Since the model object will update its own arrLocations property, there is also no need to pass a completion handler to getLocNames – This is the cause of your error. You are trying to mutate self – your view struct – in a closure from its own initialiser. This is not possible as structs are immutable.

    While we are looking at your model object, let’s get rid of the use of NSArray (You should always use properly typed arrays in Swift) and the old-style JSONSerialization and use Codable. I assume that the array you are after is an array of some struct, that has at least a Name property which is a string – If you want other properties you need to add those to the struct. We’ll also add codingKeys to map the upper-case property names to camel case.

    struct LocationResponse {
        let infoList: [LocationInfo]
    
        enum CodingKeys: String, CodingKey {
            case infoList = "InfoList"
        }
    }
    
    struct LocationInfo {
        let name: String
    
        enum CodingKeys: String, CodingKey {
            case name = "Name"
        }
    }
    
    class GetLocations: ObservableObject{
    
     @Published var arrLocations = [String]()
     
       func getLocNames(Action:String, Id: String {
    
           //fetch data from server
           let session = URLSession.shared
           session.dataTask(with: request) { (data, response, error) in 
               if let response = response {
                        print(response)
               }
    
               if let data = data {
                   do {
                       let locationResponse = try JSONDecoder().decode(LocationResponse.self, from: data)
                       DispatchQueue.main.async {
                           self.arrLocations = locationResponse.infoList
                       }
                   } catch {
                       print("JSON decoding error: (error)")
                   }
               }
            }.resume()
        }
    } 
    

    Now, you need to pass an instance of your model object to your view. I suggest using the environment. Add the instance to your environment by using the .environmentObject view modifier where you create your GetLocationData view

    GetLocationData().environmentObject(GetLocations())
    

    Then you can use it in your content view. Rather than using init, I suggest you use .onAppear to fetch the locations.

    I couldn’t quite follow what you wanted to do with the buttons, so I have replaced that code with a simple list of the location names – You should be able to work from this

    struct GetLocationData: View {
        @EnvironmentObject: getLocations: GetLocations
          
        var body: some View {
          VStack(spacing: 20){
              ForEach(getLocations.infoList, id: .self) { location in
                  Text(location.name)
              }
          }.onAppear {
              self.getLocations.getLocNames(Action: "US", Id: "ba6fcd92-3d1e-4fb6-b135-0c47ea1815cd")
          }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search