the making of the weather app (Pt. 3)

Click to download project open source on my Github repo :)

Pt. 3 featuring: JSONDecoder, internal and external parameter name, DispatchQueue, Protocol Extensions, CoreLocation, Property List or .plist.

About JSONDecoder

To parse the JSON data, we need to tap into a class called JSONDecoder, if you right-click on the name, you will actually see that JSONDecoder is a class we can just use right away.

In the code below, this is what’s happenin:

We use parseJSON(_:) method to parse the JSON data and create a WeatherModel object from the parsed data, it takes the JSON data as a parameter and uses JSONDecoder to decode the JSON data into a WeatherData object. Then extracts the weather condition ID, city name, and temperature from the WeatherData object, creates a WeatherModel object with the extracted data and returns the WeatherModel object. If there is an error while parsing the JSON data, it calls the didFailWithError(error:) method of the delegate to handle the error.

func parseJSON(_ weatherData: Data) -> WeatherModel? {
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
            let id = decodedData.weather[0].id
            let name = decodedData.name
            let temp = decodedData.main.temp
            
            let weather = WeatherModel(conditionId: id, cityName: name, temperature: temp)
            return weather
            
        } catch {
            delegate?.didFailWithError(error: error)
            return nil
        }
    }

Just a side note: to get the main.temp part of code, you can just click on “temp“ and copy paste the path from the JSON data link:

But in conjunction with the code above, we need to create some structs in WeatherData file to make it work:

struct WeatherData: Codable {
    let name: String
    let main: Main
    let weather: [Weather]
}

struct Main: Codable {
    let temp: Double
}

Next step is to figure out how to get a hold of the weather condition and make the icon match the fetched weather condition.

Let’s head back to the weather API site and find the list of weather condition.

Open the link for weather condition codes, you will see the condition ID, and the numbers on the left, those are the codes.

Now we can add a struct to our WeatherData file so we can get a hold of the ID. You don’t necessarily need the description, but this is just to show you that you can add whatever data you need.

struct Weather: Codable {
    let description: String
    let id: Int
}

After putting all the structs together, as well as the using switch statement to get a hold of condition name from condition ID, our WeatherModel file should include the code below:

import Foundation

struct WeatherModel {
    let conditionId: Int
    let cityName: String
    let temperature: Double
    
    var temperatureString: String {
        return String(format: "%.1f", temperature)
    }
    
    var conditionName: String {
        switch conditionId {
        case 200...232:
            return "cloud.bolt.rain"
        case 300...321:
            return "cloud.drizzle"
        case 500...513:
            return "cloud.rain"
        case 600...622:
            return "cloud.snow"
        case 701...771:
            return "cloud.fog"
        case 781:
            return "tornado"
        case 800:
            return "sun.max"
        case 801...804:
            return "cloud"
        default:
            return "sun.max"
        }
    }
}

About internal and external parameter name

Function parameters can have both internal and external names. The internal name is used within the function's implementation, while the external name is used when calling the function. This feature enhances code readability and clarity.

Syntax:

func functionName(externalName internalName: Type) {
    // function body
}

Example:

Function with Internal and External Parameter Names:

func greet(person name: String) {
    print("Hello, \(name)!")
}

// Calling the function
greet(person: "Alice") // Output: Hello, Alice!

Function with Only Internal Parameter Names:

func greet(_ name: String) {
    print("Hello, \(name)!")
}

// Calling the function
greet("Alice") // Output: Hello, Alice!

In our app, by using “_“ to omit external parameter, it allows us to call the function directly with the value “safeData“ instead of “weatherData: safeData”, see below:

About DispatchQueue

DispatchQueue is a fundamental part of Grand Central Dispatch (GCD) in Swift, which provides a way to execute tasks concurrently or serially. It helps manage the execution of work items, ensuring that tasks are performed efficiently and without blocking the main thread.

  • Concurrent vs. Serial: DispatchQueue can be either concurrent (tasks are executed in parallel) or serial (tasks are executed one at a time in the order they are added).

  • Main Queue: The main queue is a serial queue that runs on the main thread, used for updating the UI.

  • Global Queues: Global queues are concurrent queues provided by the system for executing tasks in the background.

Syntax:

DispatchQueue.main.async {
    // Code to run on the main thread
}

DispatchQueue.global(qos: .background).async {
    // Code to run in the background
}

Example Usage:

Updating UI on the Main Thread:

DispatchQueue.main.async {
    // Update UI elements
    self.label.text = "Updated Text"
}

Performing a Background Task:

DispatchQueue.global(qos: .background).async {
    // Perform a time-consuming task
    let result = performComplexCalculation()
    
    // Update UI on the main thread after the task is complete
    DispatchQueue.main.async {
        self.label.text = "Calculation Result: \(result)"
    }
}

How it’s applied in our code:

extension WeatherViewController: WeatherManagerDelegate {
    
    func didUpdateWeather(_ weatherManager: WeatherManager, weather: WeatherModel) {
        DispatchQueue.main.async {
            self.temperatureLabel.text = weather.temperatureString
            self.conditionImageView.image = UIImage(systemName: weather.conditionName)
            self.cityLabel.text = weather.cityName
        }

    }
    
    func didFailWithError(error: Error) {
        print(error)
    }
    
}

About Protocol Extensions

In Swift, you can use protocol extensions to provide default implementations for methods and properties defined in a protocol. This allows you to define common behavior that can be shared across multiple types that conform to the protocol, reducing code duplication and enhancing code maintainability.

  • Protocol Extensions: Allow you to add methods and properties to any type that conforms to a protocol.

  • Default Implementations: Provide default behavior for protocol methods and properties, which can be overridden by conforming types if needed.

Syntax:

extension SomeType: SomeProtocol {
    // Add new functionality
}

Example:

protocol Greetable {
    func sayHello()
    func sayGoodbye()
}

extension Greetable {
    func sayHello() {  // Default implementation
        print("Hello!")
    }
}

struct Person: Greetable {
    func sayGoodbye() {
        print("Goodbye!")
    }
}

let person = Person()
person.sayHello()  // Output: "Hello!" (Uses the default implementation)
person.sayGoodbye() // Output: "Goodbye!"

In this example:

  • The Greetable protocol defines two methods: sayHello() and sayGoodbye().

  • An extension on Greetable provides a default implementation for sayHello().

  • The Person struct conforms to Greetable, but it only needs to implement sayGoodbye() because it can use the default implementation for sayHello().

In our WeatherViewController, we can organize our code using extensions like below:

import UIKit
import CoreLocation

class WeatherViewController: UIViewController {

    @IBOutlet weak var conditionImageView: UIImageView!
    @IBOutlet weak var temperatureLabel: UILabel!
    @IBOutlet weak var cityLabel: UILabel!
    @IBOutlet weak var searchTextField: UITextField!
    
    var weatherManager = WeatherManager()
    let locationManager = CLLocationManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization()
        locationManager.requestLocation()
        
        weatherManager.delegate = self
        searchTextField.delegate = self

    }

    @IBAction func locationPressed(_ sender: UIButton) {
        locationManager.requestLocation()
    }
}

//MARK: - UITextFieldDelegate

extension WeatherViewController: UITextFieldDelegate {
    
    @IBAction func searchPressed(_ sender: UIButton) {
        searchTextField.endEditing(true)
        print(searchTextField.text!)
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        searchTextField.endEditing(true)
        print(searchTextField.text!)
        return true
    }
    
    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        if textField.text != "" {
            return true
        } else {
            textField.placeholder = "Please enter a city"
            return false
        }
    }
    func textFieldDidEndEditing(_ textField: UITextField) {
        
        if let city = searchTextField.text {
            weatherManager.fetchWeather(cityName: city)
        }
        
        searchTextField.text = ""
    }
}

//MARK: - WeatherManagerDelegate

extension WeatherViewController: WeatherManagerDelegate {
    
    func didUpdateWeather(_ weatherManager: WeatherManager, weather: WeatherModel) {
        DispatchQueue.main.async {
            self.temperatureLabel.text = weather.temperatureString
            self.conditionImageView.image = UIImage(systemName: weather.conditionName)
            self.cityLabel.text = weather.cityName
        }

    }
    
    func didFailWithError(error: Error) {
        print(error)
    }
    
}

//MARK: - CLLocationManagerDelegate

extension WeatherViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.last {
            locationManager.stopUpdatingLocation()
            let lat = location.coordinate.latitude
            let lon = location.coordinate.longitude
            weatherManager.fetchWeather(latitude: lat, longitude: lon)
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
        print(error)
    }
}

About CoreLocation

CoreLocation is a framework provided by Apple that allows you to obtain the geographic location and orientation of a device. It provides services for determining a device's location, altitude, and orientation, as well as managing location-related events.

  • CoreLocation: Framework for obtaining the geographic location and orientation of a device.

  • Location Manager: Set up CLLocationManager to start receiving location updates.

  • Delegate Methods: Implement CLLocationManagerDelegate methods to handle location updates and errors.

  • Integration: Use the obtained location to fetch weather data or perform other location-based tasks.

Example:

Importing CoreLocation:

import CoreLocation

Setting Up Location Manager:

class LocationManager: NSObject, CLLocationManagerDelegate {
    private let locationManager = CLLocationManager()
    
    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
    }
    
    // CLLocationManagerDelegate method to handle location updates
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.last {
            print("Current location: \(location.coordinate.latitude), \(location.coordinate.longitude)")
        }
    }
    
    // CLLocationManagerDelegate method to handle errors
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("Failed to get location: \(error.localizedDescription)")
    }
}

To prompt the window that asks for permission, we need to use this code:

locationManager.requestWhenInUseAuthorization()

The result will be like:

But we are not done here without making sure that we select the privacy setting and enter what we want the user to see in the pop-up window:

Once that’s all set, we just need to add an extension for CLLocationManagerDelegate:

extension WeatherViewController: CLLocationManagerDelegate {
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        if let location = locations.last {
            locationManager.stopUpdatingLocation()
            let lat = location.coordinate.latitude
            let lon = location.coordinate.longitude
            weatherManager.fetchWeather(latitude: lat, longitude: lon)
        }
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
        print(error)
    }
}

Back to our WeatherViewController, make sure we add IBAction so that when the currently location button is pressed, we ask locationManager to requestLocation().

A side note is: locationManager.delegate = self must be before locationManager.requestLocation(), otherwise the app will crash because the delegate needs to be set before requesting the location, so that the delegate methods can be called.

import UIKit
import CoreLocation

class WeatherViewController: UIViewController {

    @IBOutlet weak var conditionImageView: UIImageView!
    @IBOutlet weak var temperatureLabel: UILabel!
    @IBOutlet weak var cityLabel: UILabel!
    @IBOutlet weak var searchTextField: UITextField!
    
    var weatherManager = WeatherManager()
    let locationManager = CLLocationManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization()
        locationManager.requestLocation()
        
        weatherManager.delegate = self
        searchTextField.delegate = self

    }

    @IBAction func locationPressed(_ sender: UIButton) {
        locationManager.requestLocation()
    }
}

About Property List or .plist

.plist is short for Property List and it’s a file that’s automatically created with every Xcode project. This file stores configuration information for the application at runtime (when the app is running). The information is stored in a format called a key-value pair. Similar to a dictionary, the key is the property name and the value is the configuration.

It’s a structured data format used by macOS and iOS applications to store configuration settings, user preferences, and other data. Property lists can store various types of data, including strings, numbers, dates, arrays, and dictionaries.

  • Format: .plist files can be in XML or binary format.

  • Data Types: Strings, numbers, dates, arrays, dictionaries, booleans, and data blobs.

  • Usage: Commonly used for storing app settings, user preferences, and configuration data.

Click here to dive deeper.

This concludes everything packed in this one app, it’s a lot and the blog just kept getting longer and longer, so I have divided them into 3 parts. It took me a while to complete this one, the journey has not been smooth, and at some point, I wanted to give up many times, but here I am! So I want to encourage whomever that might be reading this blog, you will hit walls, it’s not all fun and exciting all the time, and I know I will continue to hit more walls, but all of these efforts will pay off some day, we got this!!!

Previous
Previous

the making of Bitcoin Ticker app

Next
Next

the making of the weather app (Pt. 2)