This is a project to create a simple weather app for iOS
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"
- 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.
References:
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
}
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
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
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
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:
Used to store information remotely (Firebase)
Used to manage all the location stuff (request auth, get the current location)
Used to manage the reachability
Used to store the last weather information locally (Realm)
User to share the current weather using UIActivityViewController
- 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)
- Swift 4.2
- Xcode 10.1
- Cocoapods 1.5.3
- Minimun iOS version: 11.0
- 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—.
You can contact me using my email: [email protected]
Follow me @rcasanovan on twitter.