iosswiftxcodeswiftui

Issues Switching From UIKit to SwiftUI and Passing API Data to the View


I just converted my app to the SwiftUI framework. The front end works great, but I am having a huge issue - I can't figure out how to fetch my data from a user UITextField entry and update my view. This project has a lot of code, but I've condensed it into a few files and will share only a portion since the other portions of the app are structured in a similar manner. I would really appreciate help anyone can offer. I intend to replace many of the strings in the METARView Text labels with variables based on the decoded API call data.

Front End: ContentView()

//
//  ContentView.swift
//  Aviate
//
//  Created by Grayson Bertaina on 9/27/20.
//
import Combine
import SwiftUI
import Foundation

class Query: ObservableObject {
    var didChange = PassthroughSubject<Void, Never>()
}

struct ContentView: View {
    
    
    init() {
        UITableView.appearance().backgroundColor = .clear
    }
    
    
    @State var stationQuery: String = ""

    
    
    var body: some View {

            NavigationView{
           
                VStack {
                    Text("The App for GA Pilots")
                    Spacer()
                    
                    VStack(alignment: .leading){
                        Text("News")
                            .background(Color.blue)
                            .font(.title)
                            .padding()
                            
                        Text("Aviate now supports chart lookup functionality!")
                            .padding()
                            
                            
                        
                    }
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .padding()
                    
                    
                    
                        
                    VStack(alignment: .leading) {
                        
                        Text("Favorite Airports")
                            .font(.title)
                            .padding()
                        
                        
                        List {
                            
                            
                            Section(header: Text("Favorite Airports")) {
                            NavigationLink(destination: METARView(search: stationQuery)) {
                                Text("Airport 1")
                            }
                            }
                            
                            Section(header: Text("Nearby Stations")) {
                            NavigationLink(destination: METARView(search: stationQuery)) {
                                Text("Airport 1")
                                }
                            }
                        }
                    
                        }
                    

                    Spacer()
                    
                    
                    HStack{
                        TextField("Enter ICAO", text: $stationQuery)
                            .padding()
                            .border(Color.black)
                            
                        NavigationLink(destination: METARView(search: stationQuery)) {
                            Text("Search")
                            

                        }
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                    }
                    .padding()
                    
                    
                    
                }
                .navigationBarTitle("Aviate Home")
                
           
    }
        
    }
    
    
    
}





struct METARView: View {

    
    @State var weatherManager = WeatherManager()
       
       init(search: String) {
           self.weatherManager.fetchWeather(stationICAO: search)
       }

    var body: some View {
        
        
        
        ScrollView{
            
            METARReport(model: weatherManager.weather)
            MapView()
                .frame(height: 200)
                .edgesIgnoringSafeArea(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/)
            
            METARTitleView()
                .frame(height: 250)
               
            METARReport()
                
            Spacer()
        }
        .edgesIgnoringSafeArea(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/)
    }
    
    
}

struct TAFView: View {

    var search: String

    var body: some View {

        ScrollView{
            MapView()
                .frame(height: 200)
                .edgesIgnoringSafeArea(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/)
            METARTitleView()
                .frame(height: 250)
               
            TAFReport()
                
           
        }
        .edgesIgnoringSafeArea(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/)
    }
}


struct AirportView: View {

    var search: String

    var body: some View {

        ScrollView{
            MapView()
                .frame(height: 200)
                .edgesIgnoringSafeArea(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/)
            
               
            AirportReport()
                
           
        }
        .edgesIgnoringSafeArea(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewDevice("iPhone 11")
        
    }
}



final class DataModel: ObservableObject {
  static let shared = DataModel()
  @Published var observedString = "" {
    didSet {
      //RUNS SUCCESSFULLY
      print("Observed string changed to \(observedString)")
    }
  }
}

Here is the part of METARView() that is needed to understand where the data is going - it will present the pulled data.

METARReport()

//
//  METARReport.swift
//  Aviate
//
//  Created by Grayson Bertaina on 9/29/20.
//

import SwiftUI
import Foundation


var weatherManager = WeatherManager()



struct METARReport: View {
    

    
    var body: some View {
        ScrollView {
            VStack{
            HStack{
                Text("METAR Report")
                    .font(.title)
                    
                Spacer()
                Text("Time: " + "")
                    .font(.headline)
            }
            
            .padding()
            
            HStack{
                Text("Flight Rules")
                  
                Spacer()
                Text("VFR")
                    .padding()
                    .background(Color.green)
                 
            }
            .padding()
            .font(.headline)
            
                HStack(spacing: 20){
                    VStack(alignment: .leading){
                        Text("Visibility")
                          
                        Spacer()
                        Text("10" + " SM")
                            
                           
                    }
                    
                    
                    VStack(alignment: .leading){
                        Text("Temperature")
                          
                        Spacer()
                        Text("25" + " °C")
                         
                           
                    }
                    
                    VStack(alignment: .leading){
                        Text("Dew Point")
                          
                        Spacer()
                        Text("25" + " °C")
                           
                           
                    }
                 
            }
            .padding()
            .font(.headline)
                
                VStack(alignment: .center){
                    Text("Clouds")
                        .font(.headline)
                        .padding()
                        
                    HStack() {
                        Text("Few")
                        Spacer()
                    
                        Text("1200")
                    }
                    .padding()
                    HStack() {
                        Text("Few")
                        Spacer()
                    
                        Text("1200")
                    }
                    .padding()
                    HStack() {
                        Text("Few")
                        Spacer()
                    
                        Text("1200")
                    }
                    .padding()
                }
                .font(.headline)
            
                HStack{
                    Text("Altimeter")
                      
                    Spacer()
                    Text("29.92")
                     
                }
                .font(.headline)
                .padding()
                
                VStack{
                    Text("Wind")
                    HStack{
                        Text("5" + "kt")
                        Text("G")
                        Text("12" + "kt")
                        Text("From")
                        Text("310" + "°")
                        
                    }
                    .padding()
                    
                }
                .font(.headline)
            
                HStack {
                    Text("Special Wx")
                    Spacer()
                    Text("+RA")
                }
                .padding()
                .font(.headline)
                
                VStack {
                    Text("Remarks")
                    Spacer()
                    Text("No Remarks")
                }
                .padding()
                .font(.headline)
        }
    }
    }
}

struct METARReport_Previews: PreviewProvider {
    static var previews: some View {
        METARReport()
    }
}

Back End: WeatherStorage (Contains WeatherData, WeatherModel, and WeatherManager with API Call)

struct WeatherData: Codable {
   
    let flight_rules: String?
    let remarks: String?
    let wind_speed: WindSpeed?
    let wind_gust: WindGust?
    let wind_direction: WindDirection?
    let visibility: Visibility?
    let time: Time?
    let station: String?
    let temperature: Temperature?
    let raw: String?
    let clouds: [Clouds?]
    let wx_codes: [Wxcodes?]
}

struct Clouds: Codable {
    let type: String
    let altitude: Int
}

struct Time: Codable {
    let repr: String
}

struct WindSpeed: Codable {
    let value: Int
}

struct WindGust: Codable {
    let value: Int
}

struct WindDirection: Codable {
    let repr: String
}

struct Visibility: Codable {
    let repr: String
}

struct Remarks: Codable {
    let remarks: String
}

struct Altimeter: Codable {
    let repr: String?
    let spoken: String?
}

struct Temperature: Codable {
    let repr: String
}

struct Dewpoint: Codable {
    let repr: String
}

struct Wxcodes: Codable {
    let value: String
}

// WeatherModel

//
//  WeatherModel.swift
//  AvWx Pro
//
//  Created by Grayson Bertaina on 9/22/20.
//

import Foundation

struct WeatherModel {
    let reportingStation: String
    let windGust: Int
    let windSpeed: Int
    let windDirection: String
    let visibility: String
    let flightRules: String
    let time: String
    let remarks: String
    let temperature: String
    let dewpoint: String
    let rawMETAR: String
    let lowestCloudsType: String
    let lowestCloudsAlt: Int
    let middleCloudsType: String
    let middleCloudsAlt: Int
    let highestCloudsType: String
    let highestCloudsAlt: Int
    let firstWxCode: String
    
    var windGustString: String {
        return String(format: "%u" + "kt", windGust)
    }
    
    
    var windSpeedString: String {
        return String(format: "%u" + "kt", windSpeed)
    }
    
    
    var visUnits: String {
        return visibility + " SM"
    }
    
    var degUnits: String {
        return windDirection + "°"
    }
    
    var tempUnits: String {
        return temperature + "°C"
    }
    
    var dewUnits: String {
        return dewpoint + "°C"
    }
    
    var altToString1: String {
        return String(format: "%u" + "00 ft", lowestCloudsAlt)
    }
    
    var altToString2: String {
        return String(format: "%u" + "00 ft", middleCloudsAlt)
    }
    
    var altToString3: String {
        return String(format: "%u" + "00 ft", highestCloudsAlt)
    }
    

    
    var flightConditions: String {
        switch flightRules {
        case "VFR":
            return "green"
        case "MVFR":
            return "blue"
        case "IFR":
            return "red"
        case "LIFR":
            return "purple"
        default:
            return "gray"
        
        }
    }
}

    private func parseJSON(_ weatherData: Data) -> WeatherModel? {
        do {
            let decoder = JSONDecoder()
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)

            
            let clouds = decodedData.clouds
            let wxcodes = decodedData.wx_codes
            let reportingStationVar = decodedData.station ?? "N/A"
            let windGustValue = decodedData.wind_gust?.value ?? 0
            let windSpeedValue = decodedData.wind_speed?.value ?? 0
            let windDirectionValue = decodedData.wind_direction?.repr ?? "N/A"
            let visibilityValue = decodedData.visibility?.repr ?? "N/A"
            let flightRulesValue = decodedData.flight_rules ?? "N/A"
            let timeReportedMETAR = decodedData.time?.repr ?? "N/A"
            let remarksReportedMETAR = decodedData.remarks ?? "N/A"
            let tempMETAR = decodedData.temperature?.repr ?? "No Data"
            let rawMETARData = decodedData.raw ?? "N/A"
            let lowCloudsType = (clouds.count > 0 ? clouds[0]?.type : nil) ?? "N/A"
            let midCloudsType = (clouds.count > 1 ? clouds[1]?.type : nil) ?? "N/A"
            let highCloudsType = (clouds.count > 2 ? clouds[2]?.type : nil) ?? "N/A"
            let lowCloudsAlt = (clouds.count > 0 ? clouds[0]?.altitude : nil) ?? 0
            let midCloudsAlt = (clouds.count > 1 ? clouds[1]?.altitude : nil) ?? 0
            let highCloudsAlt = (clouds.count > 2 ? clouds[2]?.altitude : nil) ?? 0
            let firstWxCode1 = (wxcodes.count > 0 ? wxcodes[0]?.value : "N/A") ?? "N/A"
            
            let weather = WeatherModel(reportingStation: reportingStationVar, windGust: windGustValue, windSpeed: windSpeedValue, windDirection: windDirectionValue, visibility: visibilityValue, flightRules: flightRulesValue, time: timeReportedMETAR, remarks: remarksReportedMETAR, temperature: tempMETAR, dewpoint: rawMETARData, rawMETAR: rawMETARData, lowestCloudsType: lowCloudsType, lowestCloudsAlt: lowCloudsAlt, middleCloudsType: midCloudsType, middleCloudsAlt: midCloudsAlt, highestCloudsType: highCloudsType, highestCloudsAlt: highCloudsAlt, firstWxCode: firstWxCode1)
            
            return weather
            
        } catch {
            print(error)
            return nil
        }
    }

You will notice that there are some useless functions, like trying fetchWeather at the top of my ContentView. I am trying various things to get data to be pulled but don't know where to put the required:

I am new to Swift so I apologize if any part of my question is vague. I am definitely in a bit over my head! Thanks for the help and have a great day.

Add on:

//
//  WeatherData.swift
//  SwitUIAppExample
//
//  Created by mac on 30/09/2020.
//  Copyright © 2020 Kamran. All rights reserved.
//

//
//  WeatherData.swift
//  AvWx Pro
//
//  Created by Grayson Bertaina on 9/21/20.
//

import Foundation

struct WeatherData: Codable {
   
    
    let clouds: [Clouds?]
    let flight_rules: String?
    let remarks: String?
    let wind_speed: WindSpeed?
    let wind_gust: WindGust?
    let wind_direction: WindDirection?
    let visibility: Visibility?
    let time: Time?
    let station: String?
    let altimeter: Altimeter?
    let temperature: Temperature?
    let dewpoint: Dewpoint?
    let wx_codes: [Wxcodes?]
    let raw: String?
}



struct Clouds: Codable {
    let type: String
    let altitude: Int
}

struct Time: Codable {
    let repr: String
}

struct WindSpeed: Codable {
    let value: Int
}

struct WindGust: Codable {
    let value: Int
}

struct WindDirection: Codable {
    let repr: String
}

struct Visibility: Codable {
    let repr: String
}

struct Remarks: Codable {
    let remarks: String
}

struct Altimeter: Codable {
    let value: Double
}

struct Temperature: Codable {
    let repr: String
}

struct Dewpoint: Codable {
    let repr: String
}

struct Wxcodes: Codable {
    let value: String
}


// WeatherModel

//
//  WeatherModel.swift
//  AvWx Pro
//
//  Created by Grayson Bertaina on 9/22/20.
//

import Foundation

struct WeatherModel {
    
    
    let lowestCloudsType: String
    let lowestCloudsAlt: Int
    let middleCloudsType: String
    let middleCloudsAlt: Int
    let highestCloudsType: String
    let highestCloudsAlt: Int
    let reportingStation: String
    let windGust: Int
    let windSpeed: Int
    let windDirection: String
    let visibility: String
    let flightRules: String
    let time: String
    let remarks: String
    let altimeter: Double
    let temperature: String
    let dewpoint: String
    let firstWxCode: String
    let rawMETAR: String
    
    var altToString1: String {
        return String(format: "%u" + "00 ft", lowestCloudsAlt)
    }
    
    var altToString2: String {
        return String(format: "%u" + "00 ft", middleCloudsAlt)
    }
    
    var altToString3: String {
        return String(format: "%u" + "00 ft", highestCloudsAlt)
    }
    
    var windGustString: String {
        return String(format: "%u" + "kt", windGust)
    }
    
    
    var windSpeedString: String {
        return String(format: "%u" + "kt", windSpeed)
    }
    
    var altimeterString: String {
        return String(format: "%.2f" + " inHg", altimeter as CVarArg)
    }
    
    var visUnits: String {
        return visibility + " SM"
    }
    
    var degUnits: String {
        return windDirection + "°"
    }
    
    var tempUnits: String {
        return temperature + "°C"
    }
    
    var dewUnits: String {
        return dewpoint + "°C"
    }

    
    var flightConditions: String {
        switch flightRules {
        case "VFR":
            return "green"
        case "MVFR":
            return "blue"
        case "IFR":
            return "red"
        case "LIFR":
            return "purple"
        default:
            return "gray"
        
        }
    }
}



// WeatherManager

//
//  WeatherManager.swift
//  AvWx Pro
//
//  Created by Grayson Bertaina on 9/21/20.
//

import Foundation

class WeatherManager: ObservableObject {
    
    @Published var weater: WeatherModel?
    
    let weatherURL = "https://avwx.rest/api/metar/"
    

    func fetchWeather (stationICAO: String) {
        let urlString = "\(weatherURL)\(stationICAO)?token=OVi45FiTDo1LmyodShfOfoizNe5m9wyuO6Mkc95AN-c"
        performRequest(with: urlString)
    }
    
    func performRequest (with urlString: String) {
        if let url = URL(string: urlString) {
            let session = URLSession(configuration: .default)
                
            
            let task = session.dataTask(with: url) { (data, response, error) in
                if error != nil {
                    print(error)
                    return
                }
                
                if let safeData = data {
                    self.weater = self.parseJSON(safeData)
                }
            }
            
            task.resume()
            print(urlString)
            
            
            }
        }
    
   
    private func parseJSON(_ weatherData: Data) -> WeatherModel? {
        do {
            let decoder = JSONDecoder()
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)

            let clouds = decodedData.clouds
            let wxcodes = decodedData.wx_codes
            let lowCloudsType = (clouds.count > 0 ? clouds[0]?.type : nil) ?? "N/A"
            let midCloudsType = (clouds.count > 1 ? clouds[1]?.type : nil) ?? "N/A"
            let highCloudsType = (clouds.count > 2 ? clouds[2]?.type : nil) ?? "N/A"
            let lowCloudsAlt = (clouds.count > 0 ? clouds[0]?.altitude : nil) ?? 0
            let midCloudsAlt = (clouds.count > 1 ? clouds[1]?.altitude : nil) ?? 0
            let highCloudsAlt = (clouds.count > 2 ? clouds[2]?.altitude : nil) ?? 0
            let reportingStationVar = decodedData.station ?? "N/A"
            let windGustValue = decodedData.wind_gust?.value ?? 0
            let windSpeedValue = decodedData.wind_speed?.value ?? 0
            let windDirectionValue = decodedData.wind_direction?.repr ?? "N/A"
            let visibilityValue = decodedData.visibility?.repr ?? "N/A"
            let flightRulesValue = decodedData.flight_rules ?? "N/A"
            let timeReportedMETAR = decodedData.time?.repr ?? "N/A"
            let remarksReportedMETAR = decodedData.remarks ?? "N/A"
            let altimeterMETAR = decodedData.altimeter?.value ?? 0
            let tempMETAR = decodedData.temperature?.repr ?? "No Data"
            let dewMETAR = decodedData.dewpoint?.repr ?? "No Data"
            let firstWxCode1 = (wxcodes.count > 0 ? wxcodes[0]?.value : "N/A") ?? "N/A"
            let rawMETARData = decodedData.raw ?? "N/A"
            
            let weather = WeatherModel(lowestCloudsType: lowCloudsType , lowestCloudsAlt: lowCloudsAlt, middleCloudsType: midCloudsType , middleCloudsAlt: midCloudsAlt, highestCloudsType: highCloudsType , highestCloudsAlt: highCloudsAlt, reportingStation: reportingStationVar, windGust: windGustValue, windSpeed: windSpeedValue, windDirection: windDirectionValue, visibility: visibilityValue, flightRules: flightRulesValue, time: timeReportedMETAR, remarks: remarksReportedMETAR, altimeter: altimeterMETAR, temperature: tempMETAR, dewpoint: dewMETAR, firstWxCode: firstWxCode1, rawMETAR: rawMETARData)
            return weather
            
        } catch {
            print(error)
            return nil
        }
    }
    
    

}

Solution

  • You need to introduce few changes as below,

    Change WeatherManager to class, conform to ObservableObject and introduce weather model as below,

    class WeatherManager: ObservableObject {
        
        @Published var weather: WeatherModel?
        
    }
    

    Rest of the WeatherManager will be same except that you need to set the above property after parsing inside performRequest method as below,

    if let safeData = data {
        self.weather = self.parseJSON(safeData)
    }
    

    Now change METARReport by introducing var model: WeatherModel? and set all the labels same as time shown below,

    struct METARReport: View {
        
        var model: WeatherModel?
        
        var body: some View {
            ScrollView {
                VStack{
                HStack{
                    Text("METAR Report")
                        .font(.title)
                        
                    Spacer()
                    Text("Time: " + (self.model?.time ?? ""))
                        .font(.headline)
                }
    
    .....
    }
    

    Finally just update METARView as below,

    struct METARView: View {
    
        @State var weatherManager = WeatherManager()
        
        init(search: String) {
            self.weatherManager.fetchWeather(stationICAO: search)
        }
        
        var body: some View {
        
            ScrollView{
                // ...
                METARReport(model: weatherManager.weather)
                    
                Spacer()
            }
            .edgesIgnoringSafeArea(/*@START_MENU_TOKEN@*/.all/*@END_MENU_TOKEN@*/)
        }
    }
    

    Note: There were some parsing issues as well in your code so i used the below models to make it work.

    struct WeatherData: Codable {
       
        let flight_rules: String?
        let remarks: String?
        let wind_speed: WindSpeed?
        let wind_gust: WindGust?
        let wind_direction: WindDirection?
        let visibility: Visibility?
        let time: Time?
        let station: String?
        let temperature: Temperature?
        let raw: String?
    }
    
    struct Clouds: Codable {
        let type: String
        let altitude: Int
    }
    
    struct Time: Codable {
        let repr: String
    }
    
    struct WindSpeed: Codable {
        let value: Int
    }
    
    struct WindGust: Codable {
        let value: Int
    }
    
    struct WindDirection: Codable {
        let repr: String
    }
    
    struct Visibility: Codable {
        let repr: String
    }
    
    struct Remarks: Codable {
        let remarks: String
    }
    
    struct Altimeter: Codable {
        let repr: String?
        let spoken: String?
    }
    
    struct Temperature: Codable {
        let repr: String
    }
    
    struct Dewpoint: Codable {
        let repr: String
    }
    
    struct Wxcodes: Codable {
        let value: String
    }
    
    // WeatherModel
    
    //
    //  WeatherModel.swift
    //  AvWx Pro
    //
    //  Created by Grayson Bertaina on 9/22/20.
    //
    
    import Foundation
    
    struct WeatherModel {
        let reportingStation: String
        let windGust: Int
        let windSpeed: Int
        let windDirection: String
        let visibility: String
        let flightRules: String
        let time: String
        let remarks: String
        let temperature: String
        let dewpoint: String
        let rawMETAR: String
        
        var windGustString: String {
            return String(format: "%u" + "kt", windGust)
        }
        
        
        var windSpeedString: String {
            return String(format: "%u" + "kt", windSpeed)
        }
        
        
        var visUnits: String {
            return visibility + " SM"
        }
        
        var degUnits: String {
            return windDirection + "°"
        }
        
        var tempUnits: String {
            return temperature + "°C"
        }
        
        var dewUnits: String {
            return dewpoint + "°C"
        }
    
        
        var flightConditions: String {
            switch flightRules {
            case "VFR":
                return "green"
            case "MVFR":
                return "blue"
            case "IFR":
                return "red"
            case "LIFR":
                return "purple"
            default:
                return "gray"
            
            }
        }
    }
    
        private func parseJSON(_ weatherData: Data) -> WeatherModel? {
            do {
                let decoder = JSONDecoder()
                let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
    
                let reportingStationVar = decodedData.station ?? "N/A"
                let windGustValue = decodedData.wind_gust?.value ?? 0
                let windSpeedValue = decodedData.wind_speed?.value ?? 0
                let windDirectionValue = decodedData.wind_direction?.repr ?? "N/A"
                let visibilityValue = decodedData.visibility?.repr ?? "N/A"
                let flightRulesValue = decodedData.flight_rules ?? "N/A"
                let timeReportedMETAR = decodedData.time?.repr ?? "N/A"
                let remarksReportedMETAR = decodedData.remarks ?? "N/A"
                let tempMETAR = decodedData.temperature?.repr ?? "No Data"
                let rawMETARData = decodedData.raw ?? "N/A"
                
                let weather = WeatherModel(reportingStation: reportingStationVar, windGust: windGustValue, windSpeed: windSpeedValue, windDirection: windDirectionValue, visibility: visibilityValue, flightRules: flightRulesValue, time: timeReportedMETAR, remarks: remarksReportedMETAR, temperature: tempMETAR, dewpoint: rawMETARData, rawMETAR: rawMETARData)
                
                return weather
                
            } catch {
                print(error)
                return nil
            }
        }