如何设置 SwiftUI 应用程序以将输入值作为参数传递给 URL 字符串

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

我正在尝试构建一个基本的 SwiftUI 天气应用程序。该应用程序允许用户使用 OpenWeatherMap API 按城市名称搜索天气。我将文本字段中输入的城市名称配置为注入 name: "" in WeatherModel,在 viewModel 的 fetchWeather() 函数内。然后我将 OpenWeatherMap URL 字符串配置为将 searchedCity.name 作为参数接收(参见下面的 viewModel)。此设置似乎工作正常,因为我能够按城市名称搜索天气。但是,我想就将 searchCity.name 直接传递到 URL (在 viewModel 中)的做法是否正确征求反馈。关于:

let searchedCity = WeatherModel(...

...我不确定如何处理 WeatherModel 实例中的 CurrentWeather 和 WeatherInfo。由于我只使用“searchedCity”将城市名称传递到 URL,因此“CurrentWeather.init(temp: 123.00)”和“weather: [WeatherInfo.init(description :"")]" 可以设置吗?为 temp 和 description 实现值是否正确,例如 123 和 ""?

下面是我的完整代码:

内容视图

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()
    }
}

型号

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
}

视图模型

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()
    }
}

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

您应该始终避免将假值传递给 object/class,例如 Int(123)。 相反,您应该使用可空结构或 类.

我认为不需要创建一个完整的 WeatherModel 实例来从中读取一个 属性,一个 属性 您已经在视图模型的 enterCityName 属性。只需使用视图模型的 enterCityName 属性 即可。

有很多方法可以完成您的要求,下面的代码只是一种方法。由于您只需要城市名称即可获得结果,因此只需在 url 字符串中使用城市名称即可。此外,在 WeatherViewModel 中使用 WeatherModel 可避免将数据复制到各种中间变量中。

PS:不要 post 你的秘密 appid 密钥在你的 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 = ""
}