Home > Net >  How to set up SwiftUI app to pass inputted value as parameter into URL string
How to set up SwiftUI app to pass inputted value as parameter into URL string

Time:11-11

I am attempting to build a basic SwiftUI weather app. The app allows the user to search weather by city name, using the OpenWeatherMap API. I configured the inputted city name from the text field to be injected into name: "" in WeatherModel, inside the fetchWeather() function in the viewModel. I then configured the OpenWeatherMap URL string to take in searchedCity.name as a parameter (see viewModel below). This setup seems to work fine, as I am able to search for weather by city name. However, I want to seek feedback as to whether or not the practice of passing searchCity.name directly into the URL (in the viewModel) is correct. In regards to:

let searchedCity = WeatherModel(...

... I am not sure what to do with the CurrentWeather and WeatherInfo inside that instance of WeatherModel. Since I'm only using "searchedCity" to pass the name of the city into the URL, how should "CurrentWeather.init(temp: 123.00)" and "weather: [WeatherInfo.init(description: "")]" be set? Is it correct to implement values for temp and description, such as 123 and ""?

Here is my full code below:

ContentView

import SwiftUI

struct ContentView: View {
    
    // Whenever something in the viewmodel changes, the content view will know to update the UI related elements
    @StateObject var viewModel = WeatherViewModel()
    // @State private var textField = ""
        
    var body: some View {
        NavigationView {

            VStack {
                TextField("Enter City Name", text: $viewModel.enterCityName).textFieldStyle(.roundedBorder)
                
                Button(action: {
                    viewModel.fetchWeather()
                    viewModel.enterCityName = ""
                }, label: {
                    Text("Search")
                        .padding(10)
                        .background(Color.green)
                        .foregroundColor(Color.white)
                        .cornerRadius(10)
                })
                
                Text(viewModel.title)
                    .font(.system(size: 32))
                Text(viewModel.temp)
                    .font(.system(size: 44))
                Text(viewModel.descriptionText)
                    .font(.system(size: 24))

                Spacer()
            }
            .navigationTitle("Weather MVVM")
        }.padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Model

import Foundation

// Data, Model should mirror the JSON layout

//Codable is the property needed to convert JSON into a struct
struct WeatherModel: Codable {
    let name: String
    let main: CurrentWeather
    let weather: [WeatherInfo]
}

struct CurrentWeather: Codable {
    let temp: Float
}

struct WeatherInfo: Codable {
    let description: String
}

ViewModel

import Foundation

class WeatherViewModel: ObservableObject {
    //everytime these properties are updated, any view holding onto an instance of this viewModel will go ahead and updated the respective UI
    
    @Published var title: String = "-"
    @Published var temp: String = "-"
    @Published var descriptionText: String = "-"
    @Published var enterCityName: String = ""
    
    init() {
        fetchWeather()
    }
    
    func fetchWeather() {
        let searchedCity = WeatherModel(name: enterCityName, main: CurrentWeather.init(temp: 123.00), weather: [WeatherInfo.init(description: "")])

        guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(searchedCity.name)&units=imperial&appid=<myAPIKey>") else {
            return
        }
        
        let task = URLSession.shared.dataTask(with: url) { data, _, error in
            // get data
            guard let data = data, error == nil else {
                return
            }
            
            //convert data to model
            do {
                let model = try JSONDecoder().decode(WeatherModel.self, from: data)
                
                DispatchQueue.main.async {
                    self.title = model.name
                    self.temp = "\(model.main.temp)"
                    self.descriptionText = model.weather.first?.description ?? "No Description"
                }
            }
            catch {
                print(error)
            }
        }
        task.resume()
    }
}

CodePudding user response:

There are many ways to do what you ask, the following code is just one approach. Since you only need the city name to get the result, just use only that in the url string. Also using your WeatherModel in the WeatherViewModel avoids duplicating the data into various intermediate variables.

PS: do not post your secret appid key in your url.

import Foundation
import SwiftUI

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    @StateObject var viewModel = WeatherViewModel()
    @State private var cityName = ""  // <-- use this to get the city name
    
    var body: some View {
        NavigationView {
            VStack {
                TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
                
                Button(action: {
                    viewModel.fetchWeather(for: cityName) // <-- let the model fetch the results
                    cityName = ""
                }, label: {
                    Text("Search")
                        .padding(10)
                        .background(Color.green)
                        .foregroundColor(Color.white)
                        .cornerRadius(10)
                })
                // --- display the results ---
                Text(viewModel.cityWeather.name).font(.system(size: 32))
                Text("\(viewModel.cityWeather.main.temp)").font(.system(size: 44))
                Text(viewModel.cityWeather.firstWeatherInfo()).font(.system(size: 24))
                
                Spacer()
            }
            .navigationTitle("Weather MVVM")
        }.navigationViewStyle(.stack)
    }
}

class WeatherViewModel: ObservableObject {
    // use your WeatherModel that you get from the fetch results
    @Published var cityWeather: WeatherModel = WeatherModel()
    
    func fetchWeather(for cityName: String) {
        guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOURKEY") else { return }
        
        let task = URLSession.shared.dataTask(with: url) { data, _, error in
            guard let data = data, error == nil else { return }
            do {
                let model = try JSONDecoder().decode(WeatherModel.self, from: data)
                DispatchQueue.main.async {
                    self.cityWeather = model
                }
            }
            catch {
                print(error) // <-- need to deal with errors here
            }
        }
        task.resume()
    }
    
}

struct WeatherModel: Codable {
    var name: String = ""
    var main: CurrentWeather = CurrentWeather()
    var weather: [WeatherInfo] = []
    
    func firstWeatherInfo() -> String {
        return weather.count > 0 ? weather[0].description : ""
    }
}

struct CurrentWeather: Codable {
    var temp: Float = 0.0
}

struct WeatherInfo: Codable {
    var description: String = ""
}

CodePudding user response:

I want to seek feedback as to whether or not the practice of passing searchCity.name directly into the URL (in the viewModel) is correct.e

You should alway avoid to pass fake values to an object/class like Int(123). Instead you should use nullable structures or classes.

I don't see the need of create a whole WeatherModel instance just to read one property from it, one property that you already have in a viewmodel's enterCityName property. Just use the viewmodel's enterCityName property instead.

  • Related