0

I created a custom class called Weather and declared an array of Weather objects.

import Foundation

class Weather {
    var cityName:String
    var temperature:Double
    var temperatureMax:Double
    var temperatureMin:Double

    init(cityName: String, temperature: Double, temperatureMax: Double, temperatureMin: Double) {

        self.cityName = cityName
        self.temperature = temperature
        self.temperatureMax = temperatureMax
        self.temperatureMin = temperatureMin

    }
}

import UIKit
import SwiftyJSON

class ViewController: UIViewController {
    @IBOutlet weak var myLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        var weatherArrays = [Weather]()
        findLocation(zipCode: "11210", weatherArrays: weatherArrays)
        print(weatherArrays[0].cityName)
    }

    func findLocation(zipCode: String, weatherArrays: [Weather])
    {
        let zip = zipCode
        let appID = "245360e32e91a426865d3ab8daab5bf3"
        let urlString = "http://api.openweathermap.org/data/2.5/weather?zip=\(zip)&appid=\(appID)&units=imperial"
        let request = URLRequest(url: URL(string: urlString)!)
        URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
            do
            {
                let json = try JSONSerialization.jsonObject(with: data!) as! NSDictionary
                let main = json["main"] as! [String:Any]
                let temp = main["temp"]! as! Double
                let name = json["name"]! as! String
                let tempMax = main["temp_max"]! as! Double
                let tempMin = main["temp_min"]! as! Double
                weatherArrays.append(Weather(cityName: name, temperature: temp, temperatureMax: tempMax, temperatureMin: tempMin))
            }
            catch
            {
                print("Error")
            }

            }.resume()
    }

}

I pass the array into a function and I append the values to the weatherArrays parameter. However, when I compile I get the error, "Cannot use mutating member on immutable value: 'weatherArrays' is a 'let' constant."

The Weather class was originally a struct but I got this same error and I read up and found that struct values cannot be edited in a function because it is pass by value. I changed the struct to a class and I am still getting this same error? Why is it saying "'weatherArrays' is a 'let' constant" when I declared weatherArrays as a var?

rmaddy
  • 314,917
  • 42
  • 532
  • 579
bhl1994
  • 1
  • 2
  • 2
    Don't try to modify the passed in array. Return a new array using a completion handler. See https://stackoverflow.com/questions/25203556/returning-data-from-async-call-in-swift-function?r=SearchResults&s=1|69.4224 for details on using the completion handler. – rmaddy Aug 07 '19 at 05:07
  • If you have defined anything globally(like weatherArrays), you are not required to pass that as parameter – Vikky Aug 07 '19 at 05:32

3 Answers3

2

Here is a better Approach for your code

import UIKit
import SwiftyJSON

struct Weather {
    let cityName:String
    let temperature:Double
    let temperatureMax:Double
    let temperatureMin:Double
}
class ViewController: UIViewController {

    @IBOutlet weak var myLabel: UILabel!

    var array = [Weather]()
    let appID = "245360e32e91a426865d3ab8daab5bf3"

    override func viewDidLoad() {
        super.viewDidLoad()

        findLocation(zipCode: "11210"){ array in
            guard let array = array else {
                print("Error")
                return
            }
            print(array.first?.cityName ?? "no city name found")
        }

    }

    func buildUrl(queryItems: [URLQueryItem]) -> URL?{
        var components = URLComponents()
        components.scheme = "http"
        components.host = "api.openweathermap.org"
        components.path = "/data/2.5/weather"
        components.queryItems = queryItems
        return components.url
    }

    func findLocation(zipCode: String, completionHandler: @escaping (_ array: [Weather]?) -> ()){

        guard let url = buildUrl(queryItems: [URLQueryItem(name: "zip", value: zipCode), URLQueryItem(name: "appID", value: appID), URLQueryItem(name: "units", value: "imperial")]) else {
            print("Error in building url")
            return
        }

        let request = URLRequest(url: url)
        URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
            guard let data = data else {
                print(error?.localizedDescription ?? "")
                completionHandler(nil)
                return
            }
            do{
                var array = [Weather]()
                let json = try JSON(data: data)
                if json["cod"].intValue == 200{
                    let main = json["main"]
                    let temp = main["temp"].doubleValue
                    let name = json["name"].stringValue
                    let tempMax = main["temp_max"].doubleValue
                    let tempMin = main["temp_min"].doubleValue
                    array.append(Weather(cityName: name, temperature: temp, temperatureMax: tempMax, temperatureMin: tempMin))
                    completionHandler(array)
                }else{
                    completionHandler(nil)
                }

            } catch let error{
                print(error)
            }
            }.resume()
    }

}

Use struct for models, we don't have to write init method

Use let instead of var when you know that the data is not going to be changed

Do not use NSDictionary.

You've written print statement right after calling the function where as the array would've been filled only after the call to server is completed. so I have used completion handler

I saw you have installed SwiftyJSON but you weren't actually using the benefits of it. Look at the parsing section.

About the error you were getting is because Swift is pass by value i.e. when you are passing array object you were actually passing a copy of it not the actual array. if you want the same array to be modified, you need to us inout. A great tutorial can be found for that

Edit: As suggested by @rmaddy to make the code much safer by returning a new array. Please see his comment for more information.

Sahil Manchanda
  • 9,812
  • 4
  • 39
  • 89
  • 1
    Actually, a better approach would be to create and return (via the completion handler) a new array containing just the new entries. Then let the caller do whatever is needed with the new values. Which in this case might be to add them to an existing array. It makes the code more modular. It also makes the code safer because right now your code is modifying the array in the background and some other code may be trying to use the array on the main queue. This can cause a crash. – rmaddy Aug 07 '19 at 05:44
  • 1
    One other suggestion for your update. Have the completion handler provide an optional array. Then you can return `nil` when there is an error. Then the caller can tell the difference between "no data" and an "error". – rmaddy Aug 07 '19 at 05:53
  • @SahilManchanda One another suggestion. Use `URLComponents` instead of hard coding url. `var components = URLComponents() components.scheme = "http" components.host = "api.openweathermap.org" components.path = "/data/2.5/weather" components.queryItems = [URLQueryItem(name: "zip", value: zipCode), URLQueryItem(name: "appID", value: appID), URLQueryItem(name: "units", value: "imperial")] guard let url = components.url else { return }` – RajeshKumar R Aug 07 '19 at 06:00
0

Because when you pass a parameter in the function it always passes let type. Type following code maybe it's help

import Foundation

class Weather {
    var cityName:String
    var temperature:Double
    var temperatureMax:Double
    var temperatureMin:Double

    init(cityName: String, temperature: Double, temperatureMax: Double, temperatureMin: Double) {

        self.cityName = cityName
        self.temperature = temperature
        self.temperatureMax = temperatureMax
        self.temperatureMin = temperatureMin

    }
}

import UIKit
import SwiftyJSON

class ViewController: UIViewController {

    var weatherArrays = [Weather]()
    @IBOutlet weak var myLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        findLocation(zipCode: "11210")

    }

    func findLocation(zipCode: String)
    {
        let zip = zipCode
        let appID = "245360e32e91a426865d3ab8daab5bf3"
        let urlString = "http://api.openweathermap.org/data/2.5/weather?zip=\(zip)&appid=\(appID)&units=imperial"
        let request = URLRequest(url: URL(string: urlString)!)
        URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
            do
            {
                let json = try JSONSerialization.jsonObject(with: data!) as! NSDictionary
                let main = json["main"] as! [String:Any]
                let temp = main["temp"]! as! Double
                let name = json["name"]! as! String
                let tempMax = main["temp_max"]! as! Double
                let tempMin = main["temp_min"]! as! Double
                self.weatherArrays.append(Weather(cityName: name, temperature: temp, temperatureMax: tempMax, temperatureMin: tempMin))
            }
            catch
            {
                print("Error")
            }

            }.resume()
    }

}
Ashish
  • 706
  • 8
  • 22
0

First of all as rmaddy said in comment you should not do this.But if you are still interested to know how to make passed parameter mutable, below is an example.

// function should accept parameter as inout
func test(arr: inout [String]){
arr.append("item3")
print(arr) // ["item1", "item2", "item3"]
}
// you should pass mutable array
var item = ["item1", "item2"]
test(arr: &item)

Note from Swift doc

Function parameters are constants by default. Trying to change the value of a function parameter from within the body of that function results in a compile-time error. This means that you can’t change the value of a parameter by mistake. If you want a function to modify a parameter’s value, and you want those changes to persist after the function call has ended, define that parameter as an in-out parameter instead.

For more info refer to this link. https://docs.swift.org/swift-book/LanguageGuide/Functions.html

Vikky
  • 914
  • 1
  • 6
  • 16
  • This is not a good solution due to the async nature of the call in the question. – rmaddy Aug 07 '19 at 05:26
  • @rmaddy You have already provided solution in OP's comment, I was just trying to explain his one more concern about how to make passed argument mutable – Vikky Aug 07 '19 at 05:30
  • @Vikky But OP's `findLocation` contains an async API call. So even if he uses your solution `print(weatherArrays[0].cityName)` will crash – RajeshKumar R Aug 07 '19 at 05:31
  • @RajeshKumarR I think you commented in wrong place, this isn't my solution – Vikky Aug 07 '19 at 05:34
  • @Vikky Your solution would work for normal methods which doesn't contain any async API calls. But in this question it would crash – RajeshKumar R Aug 07 '19 at 05:37
  • @RajeshKumarR Yes until you don't get response you should not print. I was just explaining how to make passed parameter mutable, and I started with statement "Do not do this". – Vikky Aug 07 '19 at 05:41