Skip to content

rcasanovan/weather-app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

weather-app

This is a project to create a simple weather app for iOS

🚨 Important note 🚨

This project is using cocoapods but a gitignore file is there so the third-party libraries are not part of the repo. Please be sure to run the pod install command before running the project.

If you have any doubt about cocoapods you can check the reference here.

To run the project you just need to add your APP ID in EndPoint swift file

static let appID: String = "ADD YOUR API ID HERE"

FAQS 🤔

  • Q: I want to test the project using the Xcode simulator. How could I get a location (latitude, longitude)?

You can use

LocationManager.shared.simulateLocation = true

At didFinishLaunchingWithOptions in AppDelegate file. In this case you'll use a simulated location (50.075539, 14.437800)

  • Q: What´s the logic to refresh the weather when you're using the app?

We have a few scenarios here:

  • You install the app: The app will get the wather information from the server

  • You open the app once installed: If more than 20 min has passed since the last synchronization, the app will get the weather information from the server. In any other case, it will display the information locally.

  • If the user has moved more than two kilometers since the last synchronization, the app will get the weather information from the server. In any other case, it will display the information locally.

  • Q: Wich information is shared using the app?

If the user decided to press the share option he / she will be able to share a simple text with link to this repo and a screenshot of the Today screen.

Project Architecture

alt tag

References:

How did I implement VIPER?

Basically I have a protocol file for each scene in the app. This file defines the interaction between each layer as following:

  • View - Presenter: protocols to notify changes and to inject information to the UI.
  • Presenter - Interactor: protocols to request / receive information to / from the interator.
  • Presenter - Router: protocol to define the transitions between scenes.

Whith this protocols file is really easy to know how each layer notify / request / information to the other ones so we don't have any other way to communicate all the layers.

Another important point is because I'm using protocols it's really easy to define mocks views / presenters / interactors / routers for testing.

// View / Presenter
protocol TodayViewInjection : class {
    func loadWeatherInformationWithViewModel(_ viewModel: TodayViewModel)
}

protocol TodayPresenterDelegate : class {
    func viewDidLoad()
}

// Presenter / Interactor
typealias getWeatherInteractorCompletionBlock = (_ viewModel: TodayViewModel?, _ success: Bool, _ error: ResultError?) -> Void

protocol TodayInteractorDelegate : class {
    func requestLocationAuthorizationIfNeeded()
    func getCurrentWeather(completion: @escaping getWeatherInteractorCompletionBlock)
    func getLocalWeatherInformation() -> TodayViewModel?
    func shouldGetLocalWeatherInformation() -> Bool
}

First at all. Where is the data came from?

I'm using the api from open weather map (you can check the api documentation here).

You just need to create an account to have access to the api. Once you do it you'll able to get information for movies in a JSON format like this:

{
    "cod": "200",
    "message": 0.178,
    "cnt": 40,
    "list": [
        {
            "dt": 1543687200,
            "main": {
                "temp": 8.52,
                "temp_min": 8.17,
                "temp_max": 8.52,
                "pressure": 992.89,
                "sea_level": 1034.86,
                "grnd_level": 992.89,
                "humidity": 72,
                "temp_kf": 0.35
            },
            "weather": [
                {
                    "id": 800,
                    "main": "Clear",
                    "description": "clear sky",
                    "icon": "01n"
                }
            ],
            "clouds": {
                "all": 0
            },
            "wind": {
                "speed": 0.96,
                "deg": 244.502
            },
            "sys": {
                "pod": "n"
            },
            "dt_txt": "2018-12-01 18:00:00"
        },
        ...
    ],
    "city": {
    	"id": 2509954,
        "name": "Valencia",
        "coord": {
            "lat": 39.4697,
            "lon": -0.3774
        },
        "country": "ES",
        "population": 814208
    }
}

This is an example of the api call: http://api.openweathermap.org/data/2.5/forecast?lat=50.075539&lon=14.4378&APPID=28bc6d7ace6e643065fd95756fae8b9c&units=imperial&lang=en

Data models

Network data models

These includes the following models:

public struct WeatherResponse: Codable {
    let cod: String
    let message: CGFloat
    let cnt: UInt
    let list: [WeatherListResponse]
    let city: WeatherCityResponse
}

public struct WeatherListResponse: Codable {
    let dt: Double
    let main: WeatherListMainResponse
    let weather: [WeatherListWeatherResponse]
    let wind: WeatherListWindResponse
    let rain: WeatherRainResponse?
}

public struct WeatherRainResponse: Codable {
    let rain3h: CGFloat?
    
    //__ This is little trick.
    //__ The "rain" field has another field inside called "3h"
    //__ The problem is we can't process this field using Swift
    //__ so we need to create an enum like a "bridge" to process the field
    enum CodingKeys: String, CodingKey {
        case rain3h = "3h"
    }
}

public struct WeatherListMainResponse: Codable {
    let temp: CGFloat
    let temp_min: CGFloat
    let temp_max: CGFloat
    let pressure: CGFloat
    let sea_level: CGFloat
    let grnd_level: CGFloat
    let humidity: Int
    let temp_kf: CGFloat
}

public struct WeatherListWeatherResponse: Codable {
    let id: Int
    let main: String
    let description: String
    let icon: String
}

public struct WeatherListWindResponse: Codable {
    let speed: CGFloat
    let deg: CGFloat
}

public struct WeatherCityResponse: Codable {
    let id: Int64
    let name: String
    let coord: WeatherCityCoordResponse
    let country: String
    let population: Int64?
}

public struct WeatherCityCoordResponse: Codable {
    let lat: CGFloat
    let lon: CGFloat
}

I'm using a Swift Standard Library decodable functionality in order to manage a type that can decode itself from an external representation (I really ❤ this from Swift).

Are more properties there??

Obviously the response has more properties. I decided to use only these ones.

Reference: Apple documentation

Local weather data model

This model is used for save the last weather information locally:

class LocalWeather: Object {
    @objc dynamic var weatherId: String?
    @objc dynamic var weatherData: Data? = nil
    
    override class func primaryKey() -> String? {
        return "weatherId"
    }
}

And this one is used to generate a user with an unique user id:

class User: Object {
    @objc dynamic var userId: String?
    
    override class func primaryKey() -> String? {
        return "userId"
    }
}

As I'm using Realm for this it's important to define a class to manage each model in the database. In this case we only have one model (IMSearchSuggestion)

Reference: Realm

Managers

I think using managers is a good idea but be careful!. Please don't create managers as if the world were going to end tomorrow.

I'm using only 5 here:

RemoteDabaBaseManager

Used to store information remotely (Firebase)

LocationManager

Used to manage all the location stuff (request auth, get the current location)

ReachabilityManager

Used to manage the reachability

LocalWeatherManager

Used to store the last weather information locally (Realm)

ShareManager

User to share the current weather using UIActivityViewController

How it looks like?

Today weather & Forecast

alt tag alt tag

No internet connection & share option

alt tag alt tag

What's left in the demo?

  • Location permission denied: The logic to manage if the user deny the location permission is not defined. Maybe I could show a message to the user in this case.
  • Realm migration process: It would be nice to add a process to migrate the realm database to a new model (just in case you need to add a new field into the database)

Programming languages && Development tools

  • Swift 4.2
  • Xcode 10.1
  • Cocoapods 1.5.3
  • Minimun iOS version: 11.0

Third-Party Libraries

  • RealmSwift (3.7.6): A mobile database that runs directly inside phones, tablets or wearables.
  • ReachabilitySwift (4.2.1): Replacement for Apple's Reachability re-written in Swift with callbacks.
  • CollectionViewCenteredFlowLayout (1.0.1): A layout for UICollectionView that aligns the cells to the center.
  • Firebase: A Backend as a Service —BaaS—.

Support && contact

Email

You can contact me using my email: ricardo.casanova@outlook.com

Twitter

Follow me @rcasanovan on twitter.